summaryrefslogtreecommitdiffstats
path: root/accessible
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /accessible
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--accessible/android/AccessibleWrap.cpp473
-rw-r--r--accessible/android/AccessibleWrap.h68
-rw-r--r--accessible/android/ApplicationAccessibleWrap.h20
-rw-r--r--accessible/android/DocAccessibleWrap.cpp78
-rw-r--r--accessible/android/DocAccessibleWrap.h33
-rw-r--r--accessible/android/Platform.cpp233
-rw-r--r--accessible/android/RootAccessibleWrap.h22
-rw-r--r--accessible/android/SessionAccessibility.cpp941
-rw-r--r--accessible/android/SessionAccessibility.h145
-rw-r--r--accessible/android/TraversalRule.cpp288
-rw-r--r--accessible/android/TraversalRule.h58
-rw-r--r--accessible/android/moz.build35
-rw-r--r--accessible/aom/AccessibleNode.cpp170
-rw-r--r--accessible/aom/AccessibleNode.h212
-rw-r--r--accessible/aom/moz.build44
-rw-r--r--accessible/atk/AccessibleWrap.cpp1305
-rw-r--r--accessible/atk/AccessibleWrap.h93
-rw-r--r--accessible/atk/ApplicationAccessibleWrap.cpp145
-rw-r--r--accessible/atk/ApplicationAccessibleWrap.h34
-rw-r--r--accessible/atk/DOMtoATK.cpp151
-rw-r--r--accessible/atk/DOMtoATK.h152
-rw-r--r--accessible/atk/DocAccessibleWrap.cpp36
-rw-r--r--accessible/atk/DocAccessibleWrap.h33
-rw-r--r--accessible/atk/InterfaceInitFuncs.h43
-rw-r--r--accessible/atk/Platform.cpp271
-rw-r--r--accessible/atk/RootAccessibleWrap.cpp24
-rw-r--r--accessible/atk/RootAccessibleWrap.h32
-rw-r--r--accessible/atk/UtilInterface.cpp347
-rw-r--r--accessible/atk/moz.build64
-rw-r--r--accessible/atk/nsMai.h112
-rw-r--r--accessible/atk/nsMaiHyperlink.cpp216
-rw-r--r--accessible/atk/nsMaiHyperlink.h49
-rw-r--r--accessible/atk/nsMaiInterfaceAction.cpp83
-rw-r--r--accessible/atk/nsMaiInterfaceComponent.cpp186
-rw-r--r--accessible/atk/nsMaiInterfaceDocument.cpp106
-rw-r--r--accessible/atk/nsMaiInterfaceEditableText.cpp100
-rw-r--r--accessible/atk/nsMaiInterfaceHyperlinkImpl.cpp32
-rw-r--r--accessible/atk/nsMaiInterfaceHypertext.cpp63
-rw-r--r--accessible/atk/nsMaiInterfaceImage.cpp61
-rw-r--r--accessible/atk/nsMaiInterfaceSelection.cpp102
-rw-r--r--accessible/atk/nsMaiInterfaceTable.cpp264
-rw-r--r--accessible/atk/nsMaiInterfaceTableCell.cpp148
-rw-r--r--accessible/atk/nsMaiInterfaceText.cpp557
-rw-r--r--accessible/atk/nsMaiInterfaceValue.cpp98
-rw-r--r--accessible/atk/nsStateMap.h116
-rw-r--r--accessible/base/ARIAMap.cpp1687
-rw-r--r--accessible/base/ARIAMap.h335
-rw-r--r--accessible/base/ARIAStateMap.cpp334
-rw-r--r--accessible/base/ARIAStateMap.h66
-rw-r--r--accessible/base/AccAttributes.cpp270
-rw-r--r--accessible/base/AccAttributes.h293
-rw-r--r--accessible/base/AccEvent.cpp256
-rw-r--r--accessible/base/AccEvent.h562
-rw-r--r--accessible/base/AccGroupInfo.cpp397
-rw-r--r--accessible/base/AccGroupInfo.h101
-rw-r--r--accessible/base/AccIterator.cpp360
-rw-r--r--accessible/base/AccIterator.h328
-rw-r--r--accessible/base/AccTypes.h98
-rw-r--r--accessible/base/Asserts.cpp29
-rw-r--r--accessible/base/CacheConstants.h255
-rw-r--r--accessible/base/CachedTableAccessible.cpp429
-rw-r--r--accessible/base/CachedTableAccessible.h294
-rw-r--r--accessible/base/DocManager.cpp562
-rw-r--r--accessible/base/DocManager.h193
-rw-r--r--accessible/base/EmbeddedObjCollector.cpp62
-rw-r--r--accessible/base/EmbeddedObjCollector.h68
-rw-r--r--accessible/base/EventQueue.cpp436
-rw-r--r--accessible/base/EventQueue.h83
-rw-r--r--accessible/base/EventTree.cpp99
-rw-r--r--accessible/base/EventTree.h61
-rw-r--r--accessible/base/Filters.cpp25
-rw-r--r--accessible/base/Filters.h36
-rw-r--r--accessible/base/FocusManager.cpp462
-rw-r--r--accessible/base/FocusManager.h169
-rw-r--r--accessible/base/HTMLMarkupMap.h444
-rw-r--r--accessible/base/IDSet.h129
-rw-r--r--accessible/base/Logging.cpp992
-rw-r--r--accessible/base/Logging.h236
-rw-r--r--accessible/base/MathMLMarkupMap.h113
-rw-r--r--accessible/base/NotificationController.cpp1107
-rw-r--r--accessible/base/NotificationController.h396
-rw-r--r--accessible/base/Pivot.cpp331
-rw-r--r--accessible/base/Pivot.h141
-rw-r--r--accessible/base/Platform.h136
-rw-r--r--accessible/base/Relation.h105
-rw-r--r--accessible/base/RelationTypeGen.py41
-rw-r--r--accessible/base/RelationTypeMap.h90
-rw-r--r--accessible/base/RoleHGen.py42
-rw-r--r--accessible/base/RoleMap.h1546
-rw-r--r--accessible/base/SelectionManager.cpp246
-rw-r--r--accessible/base/SelectionManager.h141
-rw-r--r--accessible/base/States.h305
-rw-r--r--accessible/base/Statistics.h42
-rw-r--r--accessible/base/StyleInfo.cpp50
-rw-r--r--accessible/base/StyleInfo.h36
-rw-r--r--accessible/base/TextAttrs.cpp816
-rw-r--r--accessible/base/TextAttrs.h440
-rw-r--r--accessible/base/TextLeafRange.cpp1990
-rw-r--r--accessible/base/TextLeafRange.h360
-rw-r--r--accessible/base/TextRange-inl.h25
-rw-r--r--accessible/base/TextRange.cpp376
-rw-r--r--accessible/base/TextRange.h164
-rw-r--r--accessible/base/TextUpdater.cpp215
-rw-r--r--accessible/base/TextUpdater.h95
-rw-r--r--accessible/base/TreeWalker.cpp348
-rw-r--r--accessible/base/TreeWalker.h142
-rw-r--r--accessible/base/XULMap.h115
-rw-r--r--accessible/base/moz.build122
-rw-r--r--accessible/base/nsAccCache.h24
-rw-r--r--accessible/base/nsAccUtils.cpp626
-rw-r--r--accessible/base/nsAccUtils.h299
-rw-r--r--accessible/base/nsAccessibilityService.cpp1933
-rw-r--r--accessible/base/nsAccessibilityService.h492
-rw-r--r--accessible/base/nsCoreUtils.cpp622
-rw-r--r--accessible/base/nsCoreUtils.h329
-rw-r--r--accessible/base/nsEventShell.cpp81
-rw-r--r--accessible/base/nsEventShell.h66
-rw-r--r--accessible/base/nsTextEquivUtils.cpp360
-rw-r--r--accessible/base/nsTextEquivUtils.h182
-rw-r--r--accessible/basetypes/Accessible.cpp730
-rw-r--r--accessible/basetypes/Accessible.h741
-rw-r--r--accessible/basetypes/HyperTextAccessibleBase.cpp841
-rw-r--r--accessible/basetypes/HyperTextAccessibleBase.h317
-rw-r--r--accessible/basetypes/TableAccessible.h172
-rw-r--r--accessible/basetypes/TableCellAccessible.h68
-rw-r--r--accessible/basetypes/moz.build25
-rw-r--r--accessible/docs/Architecture.md65
-rw-r--r--accessible/docs/ColorsAndHighContrastMode.md50
-rw-r--r--accessible/docs/DocumentAccessibilityLifecycle.md104
-rw-r--r--accessible/docs/GeckoViewThreadTopography.md51
-rw-r--r--accessible/docs/HCMMediaQueries.md306
-rw-r--r--accessible/docs/HCMSettings.md45
-rw-r--r--accessible/docs/fx_view_fc.jpgbin0 -> 65120 bytes
-rw-r--r--accessible/docs/fxview.jpgbin0 -> 52575 bytes
-rw-r--r--accessible/docs/fxview_ic.jpgbin0 -> 77858 bytes
-rw-r--r--accessible/docs/index.rst17
-rw-r--r--accessible/generic/ARIAGridAccessible.cpp77
-rw-r--r--accessible/generic/ARIAGridAccessible.h35
-rw-r--r--accessible/generic/ApplicationAccessible.cpp142
-rw-r--r--accessible/generic/ApplicationAccessible.h109
-rw-r--r--accessible/generic/BaseAccessibles.cpp142
-rw-r--r--accessible/generic/BaseAccessibles.h136
-rw-r--r--accessible/generic/DocAccessible-inl.h183
-rw-r--r--accessible/generic/DocAccessible.cpp2847
-rw-r--r--accessible/generic/DocAccessible.h825
-rw-r--r--accessible/generic/FormControlAccessible.cpp83
-rw-r--r--accessible/generic/FormControlAccessible.h65
-rw-r--r--accessible/generic/HyperTextAccessible-inl.h48
-rw-r--r--accessible/generic/HyperTextAccessible.cpp1141
-rw-r--r--accessible/generic/HyperTextAccessible.h266
-rw-r--r--accessible/generic/ImageAccessible.cpp260
-rw-r--r--accessible/generic/ImageAccessible.h94
-rw-r--r--accessible/generic/LocalAccessible-inl.h107
-rw-r--r--accessible/generic/LocalAccessible.cpp4255
-rw-r--r--accessible/generic/LocalAccessible.h1028
-rw-r--r--accessible/generic/OuterDocAccessible.cpp224
-rw-r--r--accessible/generic/OuterDocAccessible.h80
-rw-r--r--accessible/generic/RootAccessible.cpp706
-rw-r--r--accessible/generic/RootAccessible.h93
-rw-r--r--accessible/generic/TextLeafAccessible.cpp42
-rw-r--r--accessible/generic/TextLeafAccessible.h46
-rw-r--r--accessible/generic/moz.build63
-rw-r--r--accessible/html/HTMLCanvasAccessible.cpp16
-rw-r--r--accessible/html/HTMLCanvasAccessible.h35
-rw-r--r--accessible/html/HTMLElementAccessibles.cpp231
-rw-r--r--accessible/html/HTMLElementAccessibles.h159
-rw-r--r--accessible/html/HTMLFormControlAccessible.cpp979
-rw-r--r--accessible/html/HTMLFormControlAccessible.h387
-rw-r--r--accessible/html/HTMLImageMapAccessible.cpp200
-rw-r--r--accessible/html/HTMLImageMapAccessible.h82
-rw-r--r--accessible/html/HTMLLinkAccessible.cpp129
-rw-r--r--accessible/html/HTMLLinkAccessible.h59
-rw-r--r--accessible/html/HTMLListAccessible.cpp112
-rw-r--r--accessible/html/HTMLListAccessible.h85
-rw-r--r--accessible/html/HTMLSelectAccessible.cpp472
-rw-r--r--accessible/html/HTMLSelectAccessible.h216
-rw-r--r--accessible/html/HTMLTableAccessible.cpp712
-rw-r--r--accessible/html/HTMLTableAccessible.h177
-rw-r--r--accessible/html/moz.build52
-rw-r--r--accessible/interfaces/ia2/IA2Typelib.idl33
-rw-r--r--accessible/interfaces/ia2/moz.build92
-rw-r--r--accessible/interfaces/moz.build45
-rw-r--r--accessible/interfaces/msaa/AccessibleMarshal.def11
-rw-r--r--accessible/interfaces/msaa/AccessibleMarshal.rc5
-rw-r--r--accessible/interfaces/msaa/AccessibleMarshalThunk.c17
-rw-r--r--accessible/interfaces/msaa/ISimpleDOM.idl22
-rw-r--r--accessible/interfaces/msaa/ISimpleDOMDocument.idl83
-rw-r--r--accessible/interfaces/msaa/ISimpleDOMNode.idl175
-rw-r--r--accessible/interfaces/msaa/ISimpleDOMText.idl78
-rw-r--r--accessible/interfaces/msaa/moz.build57
-rw-r--r--accessible/interfaces/nsIAccessibilityService.idl119
-rw-r--r--accessible/interfaces/nsIAccessible.idl351
-rw-r--r--accessible/interfaces/nsIAccessibleAnnouncementEvent.idl23
-rw-r--r--accessible/interfaces/nsIAccessibleApplication.idl34
-rw-r--r--accessible/interfaces/nsIAccessibleCaretMoveEvent.idl33
-rw-r--r--accessible/interfaces/nsIAccessibleDocument.idl70
-rw-r--r--accessible/interfaces/nsIAccessibleEditableText.idl57
-rw-r--r--accessible/interfaces/nsIAccessibleEvent.idl251
-rw-r--r--accessible/interfaces/nsIAccessibleHideEvent.idl28
-rw-r--r--accessible/interfaces/nsIAccessibleHyperLink.idl86
-rw-r--r--accessible/interfaces/nsIAccessibleHyperText.idl54
-rw-r--r--accessible/interfaces/nsIAccessibleImage.idl30
-rw-r--r--accessible/interfaces/nsIAccessibleMacInterface.idl87
-rw-r--r--accessible/interfaces/nsIAccessibleObjectAttributeChangedEvent.idl19
-rw-r--r--accessible/interfaces/nsIAccessiblePivot.idl96
-rw-r--r--accessible/interfaces/nsIAccessibleRelation.idl183
-rw-r--r--accessible/interfaces/nsIAccessibleRole.idl802
-rw-r--r--accessible/interfaces/nsIAccessibleScrollingEvent.idl34
-rw-r--r--accessible/interfaces/nsIAccessibleSelectable.idl59
-rw-r--r--accessible/interfaces/nsIAccessibleStateChangeEvent.idl29
-rw-r--r--accessible/interfaces/nsIAccessibleStates.idl76
-rw-r--r--accessible/interfaces/nsIAccessibleTable.idl234
-rw-r--r--accessible/interfaces/nsIAccessibleTableChangeEvent.idl20
-rw-r--r--accessible/interfaces/nsIAccessibleText.idl220
-rw-r--r--accessible/interfaces/nsIAccessibleTextChangeEvent.idl33
-rw-r--r--accessible/interfaces/nsIAccessibleTextLeafRange.idl43
-rw-r--r--accessible/interfaces/nsIAccessibleTextRange.idl57
-rw-r--r--accessible/interfaces/nsIAccessibleTextSelectionChangeEvent.idl21
-rw-r--r--accessible/interfaces/nsIAccessibleTypes.idl80
-rw-r--r--accessible/interfaces/nsIAccessibleValue.idl35
-rw-r--r--accessible/ipc/DocAccessibleChild.cpp431
-rw-r--r--accessible/ipc/DocAccessibleChild.h176
-rw-r--r--accessible/ipc/DocAccessibleParent.cpp1266
-rw-r--r--accessible/ipc/DocAccessibleParent.h400
-rw-r--r--accessible/ipc/DocAccessibleTypes.ipdlh19
-rw-r--r--accessible/ipc/IPCTypes.h189
-rw-r--r--accessible/ipc/PDocAccessible.ipdl164
-rw-r--r--accessible/ipc/RemoteAccessible.cpp2092
-rw-r--r--accessible/ipc/RemoteAccessible.h511
-rw-r--r--accessible/ipc/moz.build62
-rw-r--r--accessible/mac/.clang-format11
-rw-r--r--accessible/mac/AccessibleWrap.h95
-rw-r--r--accessible/mac/AccessibleWrap.mm281
-rw-r--r--accessible/mac/ApplicationAccessibleWrap.h21
-rw-r--r--accessible/mac/DocAccessibleWrap.h46
-rw-r--r--accessible/mac/DocAccessibleWrap.mm105
-rw-r--r--accessible/mac/GeckoTextMarker.h138
-rw-r--r--accessible/mac/GeckoTextMarker.mm514
-rw-r--r--accessible/mac/MOXAccessibleBase.h143
-rw-r--r--accessible/mac/MOXAccessibleBase.mm584
-rw-r--r--accessible/mac/MOXAccessibleProtocol.h538
-rw-r--r--accessible/mac/MOXLandmarkAccessibles.h15
-rw-r--r--accessible/mac/MOXLandmarkAccessibles.mm15
-rw-r--r--accessible/mac/MOXMathAccessibles.h64
-rw-r--r--accessible/mac/MOXMathAccessibles.mm117
-rw-r--r--accessible/mac/MOXSearchInfo.h43
-rw-r--r--accessible/mac/MOXSearchInfo.mm374
-rw-r--r--accessible/mac/MOXTextMarkerDelegate.h169
-rw-r--r--accessible/mac/MOXTextMarkerDelegate.mm527
-rw-r--r--accessible/mac/MOXWebAreaAccessible.h105
-rw-r--r--accessible/mac/MOXWebAreaAccessible.mm276
-rw-r--r--accessible/mac/MacUtils.h62
-rw-r--r--accessible/mac/MacUtils.mm169
-rw-r--r--accessible/mac/Platform.mm268
-rw-r--r--accessible/mac/PlatformExtTypes.h25
-rw-r--r--accessible/mac/RootAccessibleWrap.h40
-rw-r--r--accessible/mac/RootAccessibleWrap.mm51
-rw-r--r--accessible/mac/RotorRules.h144
-rw-r--r--accessible/mac/RotorRules.mm390
-rwxr-xr-xaccessible/mac/SelectorMapGen.py61
-rw-r--r--accessible/mac/moz.build70
-rw-r--r--accessible/mac/mozAccessible.h285
-rw-r--r--accessible/mac/mozAccessible.mm1003
-rw-r--r--accessible/mac/mozAccessibleProtocol.h65
-rw-r--r--accessible/mac/mozActionElements.h108
-rw-r--r--accessible/mac/mozActionElements.mm228
-rw-r--r--accessible/mac/mozHTMLAccessible.h44
-rw-r--r--accessible/mac/mozHTMLAccessible.mm83
-rw-r--r--accessible/mac/mozRootAccessible.h58
-rw-r--r--accessible/mac/mozRootAccessible.mm84
-rw-r--r--accessible/mac/mozSelectableElements.h128
-rw-r--r--accessible/mac/mozSelectableElements.mm330
-rw-r--r--accessible/mac/mozTableAccessible.h177
-rw-r--r--accessible/mac/mozTableAccessible.mm630
-rw-r--r--accessible/mac/mozTextAccessible.h114
-rw-r--r--accessible/mac/mozTextAccessible.mm423
-rw-r--r--accessible/moz.build62
-rw-r--r--accessible/other/AccessibleWrap.cpp19
-rw-r--r--accessible/other/AccessibleWrap.h28
-rw-r--r--accessible/other/ApplicationAccessibleWrap.h20
-rw-r--r--accessible/other/DocAccessibleWrap.h23
-rw-r--r--accessible/other/Platform.cpp38
-rw-r--r--accessible/other/RootAccessibleWrap.h23
-rw-r--r--accessible/other/moz.build23
-rw-r--r--accessible/tests/browser/.eslintrc.js28
-rw-r--r--accessible/tests/browser/Common.sys.mjs451
-rw-r--r--accessible/tests/browser/Layout.sys.mjs178
-rw-r--r--accessible/tests/browser/atk/a11y_setup.py64
-rw-r--r--accessible/tests/browser/atk/browser.toml16
-rw-r--r--accessible/tests/browser/atk/browser_role.js33
-rw-r--r--accessible/tests/browser/atk/browser_table.js54
-rw-r--r--accessible/tests/browser/atk/head.js18
-rw-r--r--accessible/tests/browser/bounds/browser.toml34
-rw-r--r--accessible/tests/browser/bounds/browser_accessible_moved.js49
-rw-r--r--accessible/tests/browser/bounds/browser_caret_rect.js140
-rw-r--r--accessible/tests/browser/bounds/browser_position.js135
-rw-r--r--accessible/tests/browser/bounds/browser_test_display_contents.js44
-rw-r--r--accessible/tests/browser/bounds/browser_test_iframe_transform.js209
-rw-r--r--accessible/tests/browser/bounds/browser_test_resolution.js72
-rw-r--r--accessible/tests/browser/bounds/browser_test_simple_transform.js225
-rw-r--r--accessible/tests/browser/bounds/browser_test_zoom.js65
-rw-r--r--accessible/tests/browser/bounds/browser_test_zoom_text.js86
-rw-r--r--accessible/tests/browser/bounds/browser_zero_area.js118
-rw-r--r--accessible/tests/browser/bounds/head.js19
-rw-r--r--accessible/tests/browser/browser.toml52
-rw-r--r--accessible/tests/browser/browser_shutdown_acc_reference.js64
-rw-r--r--accessible/tests/browser/browser_shutdown_doc_acc_reference.js56
-rw-r--r--accessible/tests/browser/browser_shutdown_multi_acc_reference_doc.js76
-rw-r--r--accessible/tests/browser/browser_shutdown_multi_acc_reference_obj.js76
-rw-r--r--accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.js84
-rw-r--r--accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js84
-rw-r--r--accessible/tests/browser/browser_shutdown_multi_reference.js58
-rw-r--r--accessible/tests/browser/browser_shutdown_parent_own_reference.js96
-rw-r--r--accessible/tests/browser/browser_shutdown_pref.js72
-rw-r--r--accessible/tests/browser/browser_shutdown_proxy_acc_reference.js70
-rw-r--r--accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js72
-rw-r--r--accessible/tests/browser/browser_shutdown_remote_no_reference.js135
-rw-r--r--accessible/tests/browser/browser_shutdown_remote_only.js53
-rw-r--r--accessible/tests/browser/browser_shutdown_remote_own_reference.js175
-rw-r--r--accessible/tests/browser/browser_shutdown_scope_lifecycle.js28
-rw-r--r--accessible/tests/browser/browser_shutdown_start_restart.js51
-rw-r--r--accessible/tests/browser/e10s/browser.toml125
-rw-r--r--accessible/tests/browser/e10s/browser_caching_actions.js295
-rw-r--r--accessible/tests/browser/e10s/browser_caching_attributes.js763
-rw-r--r--accessible/tests/browser/e10s/browser_caching_description.js280
-rw-r--r--accessible/tests/browser/e10s/browser_caching_document_props.js80
-rw-r--r--accessible/tests/browser/e10s/browser_caching_domnodeid.js32
-rw-r--r--accessible/tests/browser/e10s/browser_caching_hyperlink.js228
-rw-r--r--accessible/tests/browser/e10s/browser_caching_innerHTML.js48
-rw-r--r--accessible/tests/browser/e10s/browser_caching_interfaces.js59
-rw-r--r--accessible/tests/browser/e10s/browser_caching_large_update.js66
-rw-r--r--accessible/tests/browser/e10s/browser_caching_name.js542
-rw-r--r--accessible/tests/browser/e10s/browser_caching_position.js194
-rw-r--r--accessible/tests/browser/e10s/browser_caching_relations.js291
-rw-r--r--accessible/tests/browser/e10s/browser_caching_relations_002.js366
-rw-r--r--accessible/tests/browser/e10s/browser_caching_states.js552
-rw-r--r--accessible/tests/browser/e10s/browser_caching_table.js665
-rw-r--r--accessible/tests/browser/e10s/browser_caching_text_bounds.js737
-rw-r--r--accessible/tests/browser/e10s/browser_caching_uniqueid.js30
-rw-r--r--accessible/tests/browser/e10s/browser_caching_value.js457
-rw-r--r--accessible/tests/browser/e10s/browser_events_announcement.js30
-rw-r--r--accessible/tests/browser/e10s/browser_events_caretmove.js22
-rw-r--r--accessible/tests/browser/e10s/browser_events_hide.js44
-rw-r--r--accessible/tests/browser/e10s/browser_events_show.js22
-rw-r--r--accessible/tests/browser/e10s/browser_events_statechange.js71
-rw-r--r--accessible/tests/browser/e10s/browser_events_textchange.js119
-rw-r--r--accessible/tests/browser/e10s/browser_file_input.js77
-rw-r--r--accessible/tests/browser/e10s/browser_language.js29
-rw-r--r--accessible/tests/browser/e10s/browser_obj_group.js430
-rw-r--r--accessible/tests/browser/e10s/browser_obj_group_002.js390
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js45
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js457
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_canvas.js28
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_csscontentvisibility.js105
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js60
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_doc.js320
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_gencontent.js94
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_hidden.js32
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_image.js192
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_imagemap.js190
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_list.js52
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js48
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_listener.js38
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_move.js84
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_optgroup.js100
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_removal.js58
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js73
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_table.js48
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_textleaf.js38
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_visibility.js342
-rw-r--r--accessible/tests/browser/e10s/browser_treeupdate_whitespace.js69
-rw-r--r--accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html23
-rw-r--r--accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html44
-rw-r--r--accessible/tests/browser/e10s/doc_treeupdate_imagemap.html21
-rw-r--r--accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml11
-rw-r--r--accessible/tests/browser/e10s/doc_treeupdate_visibility.html78
-rw-r--r--accessible/tests/browser/e10s/doc_treeupdate_whitespace.html10
-rw-r--r--accessible/tests/browser/e10s/fonts/Ahem.sjs241
-rw-r--r--accessible/tests/browser/e10s/head.js192
-rw-r--r--accessible/tests/browser/events/browser.toml33
-rw-r--r--accessible/tests/browser/events/browser_alert.js46
-rw-r--r--accessible/tests/browser/events/browser_test_A11yUtils_announce.js57
-rw-r--r--accessible/tests/browser/events/browser_test_caret_move_granularity.js102
-rw-r--r--accessible/tests/browser/events/browser_test_docload.js128
-rw-r--r--accessible/tests/browser/events/browser_test_focus_browserui.js57
-rw-r--r--accessible/tests/browser/events/browser_test_focus_dialog.js76
-rw-r--r--accessible/tests/browser/events/browser_test_focus_urlbar.js438
-rw-r--r--accessible/tests/browser/events/browser_test_panel.js54
-rw-r--r--accessible/tests/browser/events/browser_test_scrolling.js153
-rw-r--r--accessible/tests/browser/events/browser_test_selection_urlbar.js61
-rw-r--r--accessible/tests/browser/events/browser_test_textcaret.js58
-rw-r--r--accessible/tests/browser/events/head.js18
-rw-r--r--accessible/tests/browser/fission/browser.toml24
-rw-r--r--accessible/tests/browser/fission/browser_content_tree.js75
-rw-r--r--accessible/tests/browser/fission/browser_hidden_iframe.js70
-rw-r--r--accessible/tests/browser/fission/browser_nested_iframe.js164
-rw-r--r--accessible/tests/browser/fission/browser_reframe_root.js95
-rw-r--r--accessible/tests/browser/fission/browser_reframe_visibility.js116
-rw-r--r--accessible/tests/browser/fission/browser_src_change.js62
-rw-r--r--accessible/tests/browser/fission/browser_take_focus.js73
-rw-r--r--accessible/tests/browser/fission/head.js18
-rw-r--r--accessible/tests/browser/general/browser.toml13
-rw-r--r--accessible/tests/browser/general/browser_test_doc_creation.js55
-rw-r--r--accessible/tests/browser/general/browser_test_urlbar.js40
-rw-r--r--accessible/tests/browser/general/head.js71
-rw-r--r--accessible/tests/browser/head.js120
-rw-r--r--accessible/tests/browser/hittest/browser.toml24
-rw-r--r--accessible/tests/browser/hittest/browser_test_browser.js68
-rw-r--r--accessible/tests/browser/hittest/browser_test_general.js425
-rw-r--r--accessible/tests/browser/hittest/browser_test_scroll_hittest.js105
-rw-r--r--accessible/tests/browser/hittest/browser_test_shadowroot.js60
-rw-r--r--accessible/tests/browser/hittest/browser_test_text.js84
-rw-r--r--accessible/tests/browser/hittest/browser_test_zoom.js38
-rw-r--r--accessible/tests/browser/hittest/browser_test_zoom_text.js64
-rw-r--r--accessible/tests/browser/hittest/head.js113
-rw-r--r--accessible/tests/browser/mac/browser.toml95
-rw-r--r--accessible/tests/browser/mac/browser_app.js355
-rw-r--r--accessible/tests/browser/mac/browser_aria_busy.js44
-rw-r--r--accessible/tests/browser/mac/browser_aria_controls_flowto.js92
-rw-r--r--accessible/tests/browser/mac/browser_aria_current.js58
-rw-r--r--accessible/tests/browser/mac/browser_aria_expanded.js45
-rw-r--r--accessible/tests/browser/mac/browser_aria_haspopup.js320
-rw-r--r--accessible/tests/browser/mac/browser_aria_setsize.js47
-rw-r--r--accessible/tests/browser/mac/browser_attributed_text.js165
-rw-r--r--accessible/tests/browser/mac/browser_bounds.js77
-rw-r--r--accessible/tests/browser/mac/browser_details_summary.js69
-rw-r--r--accessible/tests/browser/mac/browser_focus.js44
-rw-r--r--accessible/tests/browser/mac/browser_heading.js77
-rw-r--r--accessible/tests/browser/mac/browser_hierarchy.js75
-rw-r--r--accessible/tests/browser/mac/browser_input.js225
-rw-r--r--accessible/tests/browser/mac/browser_label_title.js111
-rw-r--r--accessible/tests/browser/mac/browser_link.js222
-rw-r--r--accessible/tests/browser/mac/browser_live_regions.js165
-rw-r--r--accessible/tests/browser/mac/browser_mathml.js146
-rw-r--r--accessible/tests/browser/mac/browser_menulist.js103
-rw-r--r--accessible/tests/browser/mac/browser_navigate.js394
-rw-r--r--accessible/tests/browser/mac/browser_outline.js566
-rw-r--r--accessible/tests/browser/mac/browser_outline_xul.js274
-rw-r--r--accessible/tests/browser/mac/browser_popupbutton.js166
-rw-r--r--accessible/tests/browser/mac/browser_radio_position.js321
-rw-r--r--accessible/tests/browser/mac/browser_range.js190
-rw-r--r--accessible/tests/browser/mac/browser_required.js175
-rw-r--r--accessible/tests/browser/mac/browser_rich_listbox.js73
-rw-r--r--accessible/tests/browser/mac/browser_roles_elements.js334
-rw-r--r--accessible/tests/browser/mac/browser_rootgroup.js246
-rw-r--r--accessible/tests/browser/mac/browser_rotor.js1752
-rw-r--r--accessible/tests/browser/mac/browser_selectables.js363
-rw-r--r--accessible/tests/browser/mac/browser_table.js629
-rw-r--r--accessible/tests/browser/mac/browser_text_basics.js426
-rw-r--r--accessible/tests/browser/mac/browser_text_input.js657
-rw-r--r--accessible/tests/browser/mac/browser_text_leaf.js83
-rw-r--r--accessible/tests/browser/mac/browser_text_selection.js187
-rw-r--r--accessible/tests/browser/mac/browser_toggle_radio_check.js304
-rw-r--r--accessible/tests/browser/mac/browser_webarea.js77
-rw-r--r--accessible/tests/browser/mac/doc_aria_tabs.html95
-rw-r--r--accessible/tests/browser/mac/doc_menulist.xhtml19
-rw-r--r--accessible/tests/browser/mac/doc_rich_listbox.xhtml22
-rw-r--r--accessible/tests/browser/mac/doc_textmarker_test.html2427
-rw-r--r--accessible/tests/browser/mac/doc_tree.xhtml59
-rw-r--r--accessible/tests/browser/mac/head.js133
-rw-r--r--accessible/tests/browser/pivot/browser.toml10
-rw-r--r--accessible/tests/browser/pivot/browser_pivot.js103
-rw-r--r--accessible/tests/browser/pivot/head.js122
-rw-r--r--accessible/tests/browser/python_runner_wsh.py88
-rw-r--r--accessible/tests/browser/role/browser.toml15
-rw-r--r--accessible/tests/browser/role/browser_computedARIARole.js88
-rw-r--r--accessible/tests/browser/role/browser_minimumRole.js59
-rw-r--r--accessible/tests/browser/role/head.js18
-rw-r--r--accessible/tests/browser/scroll/browser.toml19
-rw-r--r--accessible/tests/browser/scroll/browser_scrollToPoint.js31
-rw-r--r--accessible/tests/browser/scroll/browser_test_scrollTo.js53
-rw-r--r--accessible/tests/browser/scroll/browser_test_scroll_bounds.js662
-rw-r--r--accessible/tests/browser/scroll/browser_test_scroll_substring.js67
-rw-r--r--accessible/tests/browser/scroll/browser_test_zoom_text.js145
-rw-r--r--accessible/tests/browser/scroll/head.js18
-rw-r--r--accessible/tests/browser/selectable/browser.toml13
-rw-r--r--accessible/tests/browser/selectable/browser_test_aria_select.js164
-rw-r--r--accessible/tests/browser/selectable/browser_test_select.js329
-rw-r--r--accessible/tests/browser/selectable/head.js88
-rw-r--r--accessible/tests/browser/shared-head.js973
-rw-r--r--accessible/tests/browser/states/browser.toml22
-rw-r--r--accessible/tests/browser/states/browser_test_link.js44
-rw-r--r--accessible/tests/browser/states/browser_test_select_visibility.js76
-rw-r--r--accessible/tests/browser/states/browser_test_visibility.js181
-rw-r--r--accessible/tests/browser/states/browser_test_visibility_2.js131
-rw-r--r--accessible/tests/browser/states/head.js91
-rw-r--r--accessible/tests/browser/telemetry/browser.toml6
-rw-r--r--accessible/tests/browser/telemetry/browser_HCM_telemetry.js407
-rw-r--r--accessible/tests/browser/text/browser.toml24
-rw-r--r--accessible/tests/browser/text/browser_editabletext.js173
-rw-r--r--accessible/tests/browser/text/browser_text.js326
-rw-r--r--accessible/tests/browser/text/browser_text_caret.js452
-rw-r--r--accessible/tests/browser/text/browser_text_paragraph_boundary.js22
-rw-r--r--accessible/tests/browser/text/browser_text_selection.js344
-rw-r--r--accessible/tests/browser/text/browser_text_spelling.js151
-rw-r--r--accessible/tests/browser/text/browser_textleafpoint.js524
-rw-r--r--accessible/tests/browser/text/head.js276
-rw-r--r--accessible/tests/browser/tree/browser.toml31
-rw-r--r--accessible/tests/browser/tree/browser_aria_owns.js278
-rw-r--r--accessible/tests/browser/tree/browser_browser_element.js16
-rw-r--r--accessible/tests/browser/tree/browser_css_content_visibility.js121
-rw-r--r--accessible/tests/browser/tree/browser_general.js128
-rw-r--r--accessible/tests/browser/tree/browser_lazy_tabs.js43
-rw-r--r--accessible/tests/browser/tree/browser_link.js208
-rw-r--r--accessible/tests/browser/tree/browser_searchbar.js96
-rw-r--r--accessible/tests/browser/tree/browser_shadowdom.js98
-rw-r--r--accessible/tests/browser/tree/browser_test_nsIAccessibleDocument_URL.js54
-rw-r--r--accessible/tests/browser/tree/head.js33
-rw-r--r--accessible/tests/browser/windows/a11y_setup.py145
-rw-r--r--accessible/tests/browser/windows/a11y_setup_requirements.txt1
-rw-r--r--accessible/tests/browser/windows/ia2/browser.toml9
-rw-r--r--accessible/tests/browser/windows/ia2/browser_role.js39
-rw-r--r--accessible/tests/browser/windows/ia2/head.js18
-rw-r--r--accessible/tests/browser/windows/uia/browser.toml11
-rw-r--r--accessible/tests/browser/windows/uia/browser_controlType.js30
-rw-r--r--accessible/tests/browser/windows/uia/browser_elementFromPoint.js34
-rw-r--r--accessible/tests/browser/windows/uia/head.js18
-rw-r--r--accessible/tests/crashtests/1072792.xhtml5
-rw-r--r--accessible/tests/crashtests/1380199.html15
-rw-r--r--accessible/tests/crashtests/1402999.html11
-rw-r--r--accessible/tests/crashtests/1415667.html1
-rw-r--r--accessible/tests/crashtests/1463962.html4
-rw-r--r--accessible/tests/crashtests/1472024-1.html7
-rw-r--r--accessible/tests/crashtests/1472024-2.html20
-rw-r--r--accessible/tests/crashtests/1484778.html26
-rw-r--r--accessible/tests/crashtests/1494707.html15
-rw-r--r--accessible/tests/crashtests/1503964.html4
-rw-r--r--accessible/tests/crashtests/1572811.html9
-rw-r--r--accessible/tests/crashtests/1578282.html21
-rw-r--r--accessible/tests/crashtests/1585851.html21
-rw-r--r--accessible/tests/crashtests/1655983.html6
-rw-r--r--accessible/tests/crashtests/1838250.html6
-rw-r--r--accessible/tests/crashtests/448064.xhtml70
-rw-r--r--accessible/tests/crashtests/884202.html21
-rw-r--r--accessible/tests/crashtests/890760.html14
-rw-r--r--accessible/tests/crashtests/893515.html15
-rw-r--r--accessible/tests/crashtests/crashtests.list23
-rw-r--r--accessible/tests/crashtests/last_test_to_unload_testsuite.xhtml36
-rw-r--r--accessible/tests/mochitest/.eslintrc.js24
-rw-r--r--accessible/tests/mochitest/a11y.toml27
-rw-r--r--accessible/tests/mochitest/actions.js231
-rw-r--r--accessible/tests/mochitest/actions/a11y.toml26
-rw-r--r--accessible/tests/mochitest/actions/test_anchors.html146
-rw-r--r--accessible/tests/mochitest/actions/test_aria.html200
-rw-r--r--accessible/tests/mochitest/actions/test_controls.html107
-rw-r--r--accessible/tests/mochitest/actions/test_general.html105
-rw-r--r--accessible/tests/mochitest/actions/test_general.xhtml167
-rw-r--r--accessible/tests/mochitest/actions/test_keys.html57
-rw-r--r--accessible/tests/mochitest/actions/test_keys.xhtml126
-rw-r--r--accessible/tests/mochitest/actions/test_media.html131
-rw-r--r--accessible/tests/mochitest/actions/test_select.html67
-rw-r--r--accessible/tests/mochitest/actions/test_tree.xhtml127
-rw-r--r--accessible/tests/mochitest/actions/test_treegrid.xhtml190
-rw-r--r--accessible/tests/mochitest/aom/a11y.toml3
-rw-r--r--accessible/tests/mochitest/aom/test_general.html208
-rw-r--r--accessible/tests/mochitest/attributes.js516
-rw-r--r--accessible/tests/mochitest/attributes/a11y.toml22
-rw-r--r--accessible/tests/mochitest/attributes/test_dpub_aria_xml-roles.html120
-rw-r--r--accessible/tests/mochitest/attributes/test_graphics_aria_xml-roles.html48
-rw-r--r--accessible/tests/mochitest/attributes/test_listbox.html82
-rw-r--r--accessible/tests/mochitest/attributes/test_obj.html292
-rw-r--r--accessible/tests/mochitest/attributes/test_obj_css.html225
-rw-r--r--accessible/tests/mochitest/attributes/test_obj_group.html564
-rw-r--r--accessible/tests/mochitest/attributes/test_obj_group.xhtml215
-rw-r--r--accessible/tests/mochitest/attributes/test_obj_group_tree.xhtml84
-rw-r--r--accessible/tests/mochitest/attributes/test_tag.html80
-rw-r--r--accessible/tests/mochitest/attributes/test_xml-roles.html267
-rw-r--r--accessible/tests/mochitest/autocomplete.js198
-rw-r--r--accessible/tests/mochitest/bounds/a11y.toml4
-rw-r--r--accessible/tests/mochitest/bounds/test_list.html78
-rw-r--r--accessible/tests/mochitest/browser.js156
-rw-r--r--accessible/tests/mochitest/common.js1048
-rw-r--r--accessible/tests/mochitest/dumbfile.zipbin0 -> 22 bytes
-rw-r--r--accessible/tests/mochitest/elm/a11y.toml19
-rw-r--r--accessible/tests/mochitest/elm/test_HTMLSpec.html2029
-rw-r--r--accessible/tests/mochitest/elm/test_MathMLSpec.html616
-rw-r--r--accessible/tests/mochitest/elm/test_figure.html60
-rw-r--r--accessible/tests/mochitest/elm/test_listbox.xhtml73
-rw-r--r--accessible/tests/mochitest/elm/test_nsApplicationAcc.html67
-rw-r--r--accessible/tests/mochitest/elm/test_shadowroot.html35
-rw-r--r--accessible/tests/mochitest/elm/test_shadowroot_subframe.html68
-rw-r--r--accessible/tests/mochitest/events.js2660
-rw-r--r--accessible/tests/mochitest/events/a11y.toml128
-rw-r--r--accessible/tests/mochitest/events/docload/a11y.toml21
-rw-r--r--accessible/tests/mochitest/events/docload/docload_wnd.html37
-rw-r--r--accessible/tests/mochitest/events/docload/test_docload_aria.html75
-rw-r--r--accessible/tests/mochitest/events/docload/test_docload_busy.html83
-rw-r--r--accessible/tests/mochitest/events/docload/test_docload_embedded.html85
-rw-r--r--accessible/tests/mochitest/events/docload/test_docload_iframe.html99
-rw-r--r--accessible/tests/mochitest/events/docload/test_docload_root.html125
-rw-r--r--accessible/tests/mochitest/events/docload/test_docload_shutdown.html142
-rw-r--r--accessible/tests/mochitest/events/focus.html10
-rw-r--r--accessible/tests/mochitest/events/scroll.html181
-rw-r--r--accessible/tests/mochitest/events/slow_image.sjs55
-rw-r--r--accessible/tests/mochitest/events/test_announcement.html61
-rw-r--r--accessible/tests/mochitest/events/test_aria_alert.html84
-rw-r--r--accessible/tests/mochitest/events/test_aria_menu.html267
-rw-r--r--accessible/tests/mochitest/events/test_aria_objattr.html68
-rw-r--r--accessible/tests/mochitest/events/test_aria_owns.html122
-rw-r--r--accessible/tests/mochitest/events/test_aria_statechange.html231
-rw-r--r--accessible/tests/mochitest/events/test_attrchange.html107
-rw-r--r--accessible/tests/mochitest/events/test_attrs.html85
-rw-r--r--accessible/tests/mochitest/events/test_bug1322593-2.html77
-rw-r--r--accessible/tests/mochitest/events/test_bug1322593.html74
-rw-r--r--accessible/tests/mochitest/events/test_caretmove.html142
-rw-r--r--accessible/tests/mochitest/events/test_coalescence.html817
-rw-r--r--accessible/tests/mochitest/events/test_contextmenu.html133
-rw-r--r--accessible/tests/mochitest/events/test_descrchange.html142
-rw-r--r--accessible/tests/mochitest/events/test_dragndrop.html106
-rw-r--r--accessible/tests/mochitest/events/test_flush.html74
-rw-r--r--accessible/tests/mochitest/events/test_focus_aria_activedescendant.html327
-rw-r--r--accessible/tests/mochitest/events/test_focus_autocomplete.html83
-rw-r--r--accessible/tests/mochitest/events/test_focus_autocomplete.xhtml507
-rw-r--r--accessible/tests/mochitest/events/test_focus_canvas.html58
-rw-r--r--accessible/tests/mochitest/events/test_focus_contextmenu.xhtml98
-rw-r--r--accessible/tests/mochitest/events/test_focus_controls.html76
-rw-r--r--accessible/tests/mochitest/events/test_focus_doc.html92
-rw-r--r--accessible/tests/mochitest/events/test_focus_general.html176
-rw-r--r--accessible/tests/mochitest/events/test_focus_general.xhtml124
-rw-r--r--accessible/tests/mochitest/events/test_focus_listcontrols.xhtml153
-rw-r--r--accessible/tests/mochitest/events/test_focus_menu.xhtml117
-rw-r--r--accessible/tests/mochitest/events/test_focus_name.html116
-rw-r--r--accessible/tests/mochitest/events/test_focus_removal.html95
-rw-r--r--accessible/tests/mochitest/events/test_focus_selects.html190
-rw-r--r--accessible/tests/mochitest/events/test_focus_tabbox.xhtml102
-rw-r--r--accessible/tests/mochitest/events/test_focus_tree.xhtml117
-rw-r--r--accessible/tests/mochitest/events/test_focusable_statechange.html122
-rw-r--r--accessible/tests/mochitest/events/test_fromUserInput.html112
-rw-r--r--accessible/tests/mochitest/events/test_label.xhtml178
-rw-r--r--accessible/tests/mochitest/events/test_menu.xhtml200
-rw-r--r--accessible/tests/mochitest/events/test_mutation.html580
-rw-r--r--accessible/tests/mochitest/events/test_namechange.html185
-rw-r--r--accessible/tests/mochitest/events/test_namechange.xhtml90
-rw-r--r--accessible/tests/mochitest/events/test_scroll.xhtml107
-rw-r--r--accessible/tests/mochitest/events/test_scroll_caret.xhtml91
-rw-r--r--accessible/tests/mochitest/events/test_selection.html109
-rw-r--r--accessible/tests/mochitest/events/test_selection.xhtml254
-rw-r--r--accessible/tests/mochitest/events/test_selection_aria.html122
-rw-r--r--accessible/tests/mochitest/events/test_statechange.html565
-rw-r--r--accessible/tests/mochitest/events/test_statechange.xhtml117
-rw-r--r--accessible/tests/mochitest/events/test_text.html310
-rw-r--r--accessible/tests/mochitest/events/test_text_alg.html249
-rw-r--r--accessible/tests/mochitest/events/test_textattrchange.html107
-rw-r--r--accessible/tests/mochitest/events/test_textselchange.html82
-rw-r--r--accessible/tests/mochitest/events/test_tree.xhtml358
-rw-r--r--accessible/tests/mochitest/events/test_valuechange.html315
-rw-r--r--accessible/tests/mochitest/focus/a11y.toml10
-rw-r--r--accessible/tests/mochitest/focus/test_focus_radio.xhtml84
-rw-r--r--accessible/tests/mochitest/focus/test_focusedChild.html81
-rw-r--r--accessible/tests/mochitest/focus/test_takeFocus.html109
-rw-r--r--accessible/tests/mochitest/focus/test_takeFocus.xhtml104
-rw-r--r--accessible/tests/mochitest/formimage.pngbin0 -> 20105 bytes
-rw-r--r--accessible/tests/mochitest/grid.js142
-rw-r--r--accessible/tests/mochitest/hittest/a11y.toml19
-rw-r--r--accessible/tests/mochitest/hittest/test_browser.html61
-rw-r--r--accessible/tests/mochitest/hittest/test_general.html110
-rw-r--r--accessible/tests/mochitest/hittest/test_menu.xhtml133
-rw-r--r--accessible/tests/mochitest/hittest/test_shadowroot.html35
-rw-r--r--accessible/tests/mochitest/hittest/test_shadowroot_subframe.html57
-rw-r--r--accessible/tests/mochitest/hittest/test_zoom.html59
-rw-r--r--accessible/tests/mochitest/hittest/test_zoom_text.html57
-rw-r--r--accessible/tests/mochitest/hittest/test_zoom_tree.xhtml97
-rw-r--r--accessible/tests/mochitest/hittest/zoom_tree.xhtml18
-rw-r--r--accessible/tests/mochitest/hyperlink/a11y.toml8
-rw-r--r--accessible/tests/mochitest/hyperlink/hyperlink.js46
-rw-r--r--accessible/tests/mochitest/hyperlink/test_general.html278
-rw-r--r--accessible/tests/mochitest/hyperlink/test_general.xhtml98
-rw-r--r--accessible/tests/mochitest/hypertext/a11y.toml8
-rw-r--r--accessible/tests/mochitest/hypertext/test_general.html150
-rw-r--r--accessible/tests/mochitest/hypertext/test_update.html214
-rw-r--r--accessible/tests/mochitest/layout.js390
-rw-r--r--accessible/tests/mochitest/letters.gifbin0 -> 5596 bytes
-rw-r--r--accessible/tests/mochitest/longdesc_src.html8
-rw-r--r--accessible/tests/mochitest/moz.build35
-rw-r--r--accessible/tests/mochitest/moz.pngbin0 -> 1991 bytes
-rw-r--r--accessible/tests/mochitest/name.js38
-rw-r--r--accessible/tests/mochitest/name/a11y.toml29
-rw-r--r--accessible/tests/mochitest/name/markup.js425
-rw-r--r--accessible/tests/mochitest/name/markuprules.xml367
-rw-r--r--accessible/tests/mochitest/name/test_ARIACore_examples.html90
-rw-r--r--accessible/tests/mochitest/name/test_browserui.xhtml85
-rw-r--r--accessible/tests/mochitest/name/test_counterstyle.html150
-rw-r--r--accessible/tests/mochitest/name/test_general.html780
-rw-r--r--accessible/tests/mochitest/name/test_general.xhtml345
-rw-r--r--accessible/tests/mochitest/name/test_link.html87
-rw-r--r--accessible/tests/mochitest/name/test_list.html103
-rw-r--r--accessible/tests/mochitest/name/test_markup.html58
-rw-r--r--accessible/tests/mochitest/name/test_svg.html53
-rw-r--r--accessible/tests/mochitest/name/test_tree.xhtml207
-rw-r--r--accessible/tests/mochitest/promisified-events.js328
-rw-r--r--accessible/tests/mochitest/relations.js204
-rw-r--r--accessible/tests/mochitest/relations/a11y.toml23
-rw-r--r--accessible/tests/mochitest/relations/test_embeds.xhtml128
-rw-r--r--accessible/tests/mochitest/relations/test_general.html456
-rw-r--r--accessible/tests/mochitest/relations/test_general.xhtml237
-rw-r--r--accessible/tests/mochitest/relations/test_groupInfoUpdate.html57
-rw-r--r--accessible/tests/mochitest/relations/test_shadowdom.html58
-rw-r--r--accessible/tests/mochitest/relations/test_tabbrowser.xhtml109
-rw-r--r--accessible/tests/mochitest/relations/test_tree.xhtml105
-rw-r--r--accessible/tests/mochitest/relations/test_ui_modalprompt.html111
-rw-r--r--accessible/tests/mochitest/relations/test_update.html213
-rw-r--r--accessible/tests/mochitest/role.js198
-rw-r--r--accessible/tests/mochitest/role/a11y.toml19
-rw-r--r--accessible/tests/mochitest/role/chrome_body_role_alert.xhtml6
-rw-r--r--accessible/tests/mochitest/role/test_aria.html729
-rw-r--r--accessible/tests/mochitest/role/test_aria.xhtml65
-rw-r--r--accessible/tests/mochitest/role/test_dpub_aria.html114
-rw-r--r--accessible/tests/mochitest/role/test_general.html201
-rw-r--r--accessible/tests/mochitest/role/test_general.xhtml59
-rw-r--r--accessible/tests/mochitest/role/test_graphics_aria.html42
-rw-r--r--accessible/tests/mochitest/role/test_svg.html93
-rw-r--r--accessible/tests/mochitest/scroll/a11y.toml4
-rw-r--r--accessible/tests/mochitest/scroll/test_zoom.html145
-rw-r--r--accessible/tests/mochitest/selectable.js138
-rw-r--r--accessible/tests/mochitest/selectable/a11y.toml14
-rw-r--r--accessible/tests/mochitest/selectable/test_listbox.xhtml144
-rw-r--r--accessible/tests/mochitest/selectable/test_menu.xhtml77
-rw-r--r--accessible/tests/mochitest/selectable/test_menulist.xhtml95
-rw-r--r--accessible/tests/mochitest/selectable/test_tabs.xhtml93
-rw-r--r--accessible/tests/mochitest/selectable/test_tree.xhtml171
-rw-r--r--accessible/tests/mochitest/states.js365
-rw-r--r--accessible/tests/mochitest/states/a11y.toml60
-rw-r--r--accessible/tests/mochitest/states/test_aria.html652
-rw-r--r--accessible/tests/mochitest/states/test_aria.xhtml70
-rw-r--r--accessible/tests/mochitest/states/test_aria_imgmap.html75
-rw-r--r--accessible/tests/mochitest/states/test_aria_widgetitems.html152
-rw-r--r--accessible/tests/mochitest/states/test_buttons.html83
-rw-r--r--accessible/tests/mochitest/states/test_controls.html51
-rw-r--r--accessible/tests/mochitest/states/test_controls.xhtml153
-rw-r--r--accessible/tests/mochitest/states/test_doc.html95
-rw-r--r--accessible/tests/mochitest/states/test_doc_busy.html130
-rw-r--r--accessible/tests/mochitest/states/test_docarticle.html78
-rw-r--r--accessible/tests/mochitest/states/test_editablebody.html44
-rw-r--r--accessible/tests/mochitest/states/test_expandable.xhtml112
-rw-r--r--accessible/tests/mochitest/states/test_frames.html93
-rw-r--r--accessible/tests/mochitest/states/test_inputs.html267
-rw-r--r--accessible/tests/mochitest/states/test_link.html85
-rw-r--r--accessible/tests/mochitest/states/test_popup.xhtml54
-rw-r--r--accessible/tests/mochitest/states/test_selects.html166
-rw-r--r--accessible/tests/mochitest/states/test_stale.html108
-rw-r--r--accessible/tests/mochitest/states/test_tabs.xhtml66
-rw-r--r--accessible/tests/mochitest/states/test_textbox.xhtml78
-rw-r--r--accessible/tests/mochitest/states/test_tree.xhtml146
-rw-r--r--accessible/tests/mochitest/states/test_visibility.html75
-rw-r--r--accessible/tests/mochitest/states/test_visibility.xhtml162
-rw-r--r--accessible/tests/mochitest/states/z_frames.html11
-rw-r--r--accessible/tests/mochitest/states/z_frames_article.html11
-rw-r--r--accessible/tests/mochitest/states/z_frames_checkbox.html11
-rw-r--r--accessible/tests/mochitest/states/z_frames_textbox.html11
-rw-r--r--accessible/tests/mochitest/states/z_frames_update.html21
-rw-r--r--accessible/tests/mochitest/table.js851
-rw-r--r--accessible/tests/mochitest/table/a11y.toml42
-rw-r--r--accessible/tests/mochitest/table/test_css_tables.html114
-rw-r--r--accessible/tests/mochitest/table/test_headers_ariagrid.html183
-rw-r--r--accessible/tests/mochitest/table/test_headers_ariatable.html94
-rw-r--r--accessible/tests/mochitest/table/test_headers_table.html756
-rw-r--r--accessible/tests/mochitest/table/test_headers_tree.xhtml100
-rw-r--r--accessible/tests/mochitest/table/test_indexes_ariagrid.html159
-rw-r--r--accessible/tests/mochitest/table/test_indexes_table.html481
-rw-r--r--accessible/tests/mochitest/table/test_indexes_tree.xhtml70
-rw-r--r--accessible/tests/mochitest/table/test_layoutguess.html567
-rw-r--r--accessible/tests/mochitest/table/test_mtable.html160
-rw-r--r--accessible/tests/mochitest/table/test_sels_ariagrid.html147
-rw-r--r--accessible/tests/mochitest/table/test_sels_table.html155
-rw-r--r--accessible/tests/mochitest/table/test_sels_tree.xhtml76
-rw-r--r--accessible/tests/mochitest/table/test_struct_ariagrid.html163
-rw-r--r--accessible/tests/mochitest/table/test_struct_ariatreegrid.html74
-rw-r--r--accessible/tests/mochitest/table/test_struct_table.html217
-rw-r--r--accessible/tests/mochitest/table/test_struct_tree.xhtml73
-rw-r--r--accessible/tests/mochitest/table/test_table_1.html107
-rw-r--r--accessible/tests/mochitest/table/test_table_2.html87
-rw-r--r--accessible/tests/mochitest/table/test_table_mutation.html100
-rw-r--r--accessible/tests/mochitest/test_OuterDocAccessible.html87
-rw-r--r--accessible/tests/mochitest/test_aria_token_attrs.html417
-rw-r--r--accessible/tests/mochitest/test_bug420863.html99
-rw-r--r--accessible/tests/mochitest/test_custom_element_accessibility_defaults.html382
-rw-r--r--accessible/tests/mochitest/test_descr.html134
-rw-r--r--accessible/tests/mochitest/test_nsIAccessibleDocument.html94
-rw-r--r--accessible/tests/mochitest/test_nsIAccessibleImage.html198
-rw-r--r--accessible/tests/mochitest/text.js814
-rw-r--r--accessible/tests/mochitest/text/a11y.toml33
-rw-r--r--accessible/tests/mochitest/text/doc.html9
-rw-r--r--accessible/tests/mochitest/text/test_atcaretoffset.html425
-rw-r--r--accessible/tests/mochitest/text/test_charboundary.html138
-rw-r--r--accessible/tests/mochitest/text/test_doc.html40
-rw-r--r--accessible/tests/mochitest/text/test_dynamic.html80
-rw-r--r--accessible/tests/mochitest/text/test_general.xhtml79
-rw-r--r--accessible/tests/mochitest/text/test_gettext.html135
-rw-r--r--accessible/tests/mochitest/text/test_hypertext.html150
-rw-r--r--accessible/tests/mochitest/text/test_lineboundary.html422
-rw-r--r--accessible/tests/mochitest/text/test_paragraphboundary.html148
-rw-r--r--accessible/tests/mochitest/text/test_passwords.html72
-rw-r--r--accessible/tests/mochitest/text/test_selection.html119
-rw-r--r--accessible/tests/mochitest/text/test_settext_input_event.html38
-rw-r--r--accessible/tests/mochitest/text/test_textBounds.html36
-rw-r--r--accessible/tests/mochitest/text/test_wordboundary.html361
-rw-r--r--accessible/tests/mochitest/text/test_words.html154
-rw-r--r--accessible/tests/mochitest/textattrs/a11y.toml17
-rw-r--r--accessible/tests/mochitest/textattrs/test_general.html813
-rw-r--r--accessible/tests/mochitest/textattrs/test_general.xhtml51
-rw-r--r--accessible/tests/mochitest/textattrs/test_invalid.html59
-rw-r--r--accessible/tests/mochitest/textattrs/test_mathml.html54
-rw-r--r--accessible/tests/mochitest/textattrs/test_spelling.html52
-rw-r--r--accessible/tests/mochitest/textattrs/test_svg.html56
-rw-r--r--accessible/tests/mochitest/textcaret/a11y.toml4
-rw-r--r--accessible/tests/mochitest/textcaret/test_general.html174
-rw-r--r--accessible/tests/mochitest/textrange/a11y.toml8
-rw-r--r--accessible/tests/mochitest/textrange/test_general.html70
-rw-r--r--accessible/tests/mochitest/textrange/test_selection.html144
-rw-r--r--accessible/tests/mochitest/textselection/a11y.toml6
-rw-r--r--accessible/tests/mochitest/textselection/test_general.html221
-rw-r--r--accessible/tests/mochitest/textselection/test_userinput.html76
-rw-r--r--accessible/tests/mochitest/tree/a11y.toml105
-rw-r--r--accessible/tests/mochitest/tree/dockids.html32
-rw-r--r--accessible/tests/mochitest/tree/test_applicationacc.xhtml73
-rw-r--r--accessible/tests/mochitest/tree/test_aria_display_contents.html173
-rw-r--r--accessible/tests/mochitest/tree/test_aria_globals.html127
-rw-r--r--accessible/tests/mochitest/tree/test_aria_grid.html318
-rw-r--r--accessible/tests/mochitest/tree/test_aria_imgmap.html104
-rw-r--r--accessible/tests/mochitest/tree/test_aria_list.html90
-rw-r--r--accessible/tests/mochitest/tree/test_aria_menu.html91
-rw-r--r--accessible/tests/mochitest/tree/test_aria_owns.html197
-rw-r--r--accessible/tests/mochitest/tree/test_aria_presentation.html176
-rw-r--r--accessible/tests/mochitest/tree/test_aria_table.html101
-rw-r--r--accessible/tests/mochitest/tree/test_brokencontext.html214
-rw-r--r--accessible/tests/mochitest/tree/test_button.xhtml83
-rw-r--r--accessible/tests/mochitest/tree/test_canvas.html53
-rw-r--r--accessible/tests/mochitest/tree/test_combobox.xhtml107
-rw-r--r--accessible/tests/mochitest/tree/test_cssflexbox.html78
-rw-r--r--accessible/tests/mochitest/tree/test_cssoverflow.html135
-rw-r--r--accessible/tests/mochitest/tree/test_display_contents.html92
-rw-r--r--accessible/tests/mochitest/tree/test_divs.html351
-rw-r--r--accessible/tests/mochitest/tree/test_dochierarchy.html84
-rw-r--r--accessible/tests/mochitest/tree/test_dockids.html62
-rw-r--r--accessible/tests/mochitest/tree/test_filectrl.html47
-rw-r--r--accessible/tests/mochitest/tree/test_formctrl.html125
-rw-r--r--accessible/tests/mochitest/tree/test_formctrl.xhtml129
-rw-r--r--accessible/tests/mochitest/tree/test_gencontent.html69
-rw-r--r--accessible/tests/mochitest/tree/test_groupbox.xhtml63
-rw-r--r--accessible/tests/mochitest/tree/test_html_in_mathml.html61
-rw-r--r--accessible/tests/mochitest/tree/test_iframe.html50
-rw-r--r--accessible/tests/mochitest/tree/test_image.xhtml58
-rw-r--r--accessible/tests/mochitest/tree/test_img.html84
-rw-r--r--accessible/tests/mochitest/tree/test_invalid_img.xhtml48
-rw-r--r--accessible/tests/mochitest/tree/test_invalidationlist.html56
-rw-r--r--accessible/tests/mochitest/tree/test_list.html346
-rw-r--r--accessible/tests/mochitest/tree/test_map.html81
-rw-r--r--accessible/tests/mochitest/tree/test_media.html127
-rw-r--r--accessible/tests/mochitest/tree/test_select.html125
-rw-r--r--accessible/tests/mochitest/tree/test_svg.html127
-rw-r--r--accessible/tests/mochitest/tree/test_tabbox.xhtml108
-rw-r--r--accessible/tests/mochitest/tree/test_tabbrowser.xhtml261
-rw-r--r--accessible/tests/mochitest/tree/test_table.html507
-rw-r--r--accessible/tests/mochitest/tree/test_table_2.html242
-rw-r--r--accessible/tests/mochitest/tree/test_table_3.html244
-rw-r--r--accessible/tests/mochitest/tree/test_tree.xhtml182
-rw-r--r--accessible/tests/mochitest/tree/test_txtcntr.html234
-rw-r--r--accessible/tests/mochitest/tree/test_txtctrl.html171
-rw-r--r--accessible/tests/mochitest/tree/test_txtctrl.xhtml86
-rw-r--r--accessible/tests/mochitest/tree/wnd.xhtml8
-rw-r--r--accessible/tests/mochitest/treeupdate/a11y.toml84
-rw-r--r--accessible/tests/mochitest/treeupdate/test_ariadialog.html113
-rw-r--r--accessible/tests/mochitest/treeupdate/test_ariahidden.html118
-rw-r--r--accessible/tests/mochitest/treeupdate/test_ariaowns.html851
-rw-r--r--accessible/tests/mochitest/treeupdate/test_bug1040735.html40
-rw-r--r--accessible/tests/mochitest/treeupdate/test_bug1175913.html95
-rw-r--r--accessible/tests/mochitest/treeupdate/test_bug1189277.html82
-rw-r--r--accessible/tests/mochitest/treeupdate/test_bug1276857.html131
-rw-r--r--accessible/tests/mochitest/treeupdate/test_bug1276857_subframe.html33
-rw-r--r--accessible/tests/mochitest/treeupdate/test_bug852150.xhtml57
-rw-r--r--accessible/tests/mochitest/treeupdate/test_bug883708.xhtml31
-rw-r--r--accessible/tests/mochitest/treeupdate/test_bug884251.xhtml19
-rw-r--r--accessible/tests/mochitest/treeupdate/test_bug895082.html49
-rw-r--r--accessible/tests/mochitest/treeupdate/test_canvas.html87
-rw-r--r--accessible/tests/mochitest/treeupdate/test_contextmenu.xhtml315
-rw-r--r--accessible/tests/mochitest/treeupdate/test_cssoverflow.html149
-rw-r--r--accessible/tests/mochitest/treeupdate/test_deck.xhtml154
-rw-r--r--accessible/tests/mochitest/treeupdate/test_delayed_removal.html500
-rw-r--r--accessible/tests/mochitest/treeupdate/test_doc.html415
-rw-r--r--accessible/tests/mochitest/treeupdate/test_gencontent.html187
-rw-r--r--accessible/tests/mochitest/treeupdate/test_general.html174
-rw-r--r--accessible/tests/mochitest/treeupdate/test_hidden.html125
-rw-r--r--accessible/tests/mochitest/treeupdate/test_imagemap.html402
-rw-r--r--accessible/tests/mochitest/treeupdate/test_inert.html132
-rw-r--r--accessible/tests/mochitest/treeupdate/test_inner_reorder.html148
-rw-r--r--accessible/tests/mochitest/treeupdate/test_list.html139
-rw-r--r--accessible/tests/mochitest/treeupdate/test_list_editabledoc.html100
-rw-r--r--accessible/tests/mochitest/treeupdate/test_list_style.html181
-rw-r--r--accessible/tests/mochitest/treeupdate/test_listbox.xhtml181
-rw-r--r--accessible/tests/mochitest/treeupdate/test_menu.xhtml127
-rw-r--r--accessible/tests/mochitest/treeupdate/test_menubutton.xhtml141
-rw-r--r--accessible/tests/mochitest/treeupdate/test_optgroup.html122
-rw-r--r--accessible/tests/mochitest/treeupdate/test_recreation.html93
-rw-r--r--accessible/tests/mochitest/treeupdate/test_select.html191
-rw-r--r--accessible/tests/mochitest/treeupdate/test_shadow_slots.html554
-rw-r--r--accessible/tests/mochitest/treeupdate/test_shutdown.xhtml131
-rw-r--r--accessible/tests/mochitest/treeupdate/test_table.html74
-rw-r--r--accessible/tests/mochitest/treeupdate/test_textleaf.html167
-rw-r--r--accessible/tests/mochitest/treeupdate/test_tooltip.xhtml75
-rw-r--r--accessible/tests/mochitest/treeupdate/test_visibility.html411
-rw-r--r--accessible/tests/mochitest/treeupdate/test_whitespace.html200
-rw-r--r--accessible/tests/mochitest/treeview.css15
-rw-r--r--accessible/tests/mochitest/treeview.js273
-rw-r--r--accessible/tests/mochitest/value.js52
-rw-r--r--accessible/tests/mochitest/value/a11y.toml16
-rw-r--r--accessible/tests/mochitest/value/test_ariavalue.html85
-rw-r--r--accessible/tests/mochitest/value/test_datetime.html76
-rw-r--r--accessible/tests/mochitest/value/test_general.html159
-rw-r--r--accessible/tests/mochitest/value/test_meter.html82
-rw-r--r--accessible/tests/mochitest/value/test_number.html56
-rw-r--r--accessible/tests/mochitest/value/test_progress.html61
-rw-r--r--accessible/tests/mochitest/value/test_range.html59
-rw-r--r--accessible/windows/ia2/ia2Accessible.cpp563
-rw-r--r--accessible/windows/ia2/ia2Accessible.h120
-rw-r--r--accessible/windows/ia2/ia2AccessibleAction.cpp152
-rw-r--r--accessible/windows/ia2/ia2AccessibleAction.h85
-rw-r--r--accessible/windows/ia2/ia2AccessibleApplication.cpp94
-rw-r--r--accessible/windows/ia2/ia2AccessibleApplication.h49
-rw-r--r--accessible/windows/ia2/ia2AccessibleComponent.cpp106
-rw-r--r--accessible/windows/ia2/ia2AccessibleComponent.h40
-rw-r--r--accessible/windows/ia2/ia2AccessibleEditableText.cpp106
-rw-r--r--accessible/windows/ia2/ia2AccessibleEditableText.h59
-rw-r--r--accessible/windows/ia2/ia2AccessibleHyperlink.cpp164
-rw-r--r--accessible/windows/ia2/ia2AccessibleHyperlink.h55
-rw-r--r--accessible/windows/ia2/ia2AccessibleHypertext.cpp142
-rw-r--r--accessible/windows/ia2/ia2AccessibleHypertext.h70
-rw-r--r--accessible/windows/ia2/ia2AccessibleImage.cpp81
-rw-r--r--accessible/windows/ia2/ia2AccessibleImage.h51
-rw-r--r--accessible/windows/ia2/ia2AccessibleRelation.cpp94
-rw-r--r--accessible/windows/ia2/ia2AccessibleRelation.h80
-rw-r--r--accessible/windows/ia2/ia2AccessibleTable.cpp534
-rw-r--r--accessible/windows/ia2/ia2AccessibleTable.h178
-rw-r--r--accessible/windows/ia2/ia2AccessibleTableCell.cpp186
-rw-r--r--accessible/windows/ia2/ia2AccessibleTableCell.h71
-rw-r--r--accessible/windows/ia2/ia2AccessibleText.cpp466
-rw-r--r--accessible/windows/ia2/ia2AccessibleText.h246
-rw-r--r--accessible/windows/ia2/ia2AccessibleTextSelectionContainer.cpp126
-rw-r--r--accessible/windows/ia2/ia2AccessibleTextSelectionContainer.h43
-rw-r--r--accessible/windows/ia2/ia2AccessibleValue.cpp125
-rw-r--r--accessible/windows/ia2/ia2AccessibleValue.h44
-rw-r--r--accessible/windows/ia2/moz.build61
-rw-r--r--accessible/windows/moz.build7
-rw-r--r--accessible/windows/msaa/AccessibleWrap.cpp140
-rw-r--r--accessible/windows/msaa/AccessibleWrap.h75
-rw-r--r--accessible/windows/msaa/ApplicationAccessibleWrap.cpp43
-rw-r--r--accessible/windows/msaa/ApplicationAccessibleWrap.h31
-rw-r--r--accessible/windows/msaa/Compatibility.cpp181
-rw-r--r--accessible/windows/msaa/Compatibility.h124
-rw-r--r--accessible/windows/msaa/CompatibilityUIA.cpp347
-rw-r--r--accessible/windows/msaa/DocAccessibleWrap.cpp100
-rw-r--r--accessible/windows/msaa/DocAccessibleWrap.h39
-rw-r--r--accessible/windows/msaa/EnumVariant.cpp90
-rw-r--r--accessible/windows/msaa/EnumVariant.h62
-rw-r--r--accessible/windows/msaa/IUnknownImpl.cpp36
-rw-r--r--accessible/windows/msaa/IUnknownImpl.h173
-rw-r--r--accessible/windows/msaa/LazyInstantiator.cpp779
-rw-r--r--accessible/windows/msaa/LazyInstantiator.h142
-rw-r--r--accessible/windows/msaa/MsaaAccessible.cpp1368
-rw-r--r--accessible/windows/msaa/MsaaAccessible.h204
-rw-r--r--accessible/windows/msaa/MsaaDocAccessible.cpp133
-rw-r--r--accessible/windows/msaa/MsaaDocAccessible.h71
-rw-r--r--accessible/windows/msaa/MsaaIdGenerator.cpp87
-rw-r--r--accessible/windows/msaa/MsaaIdGenerator.h47
-rw-r--r--accessible/windows/msaa/MsaaRootAccessible.cpp67
-rw-r--r--accessible/windows/msaa/MsaaRootAccessible.h50
-rw-r--r--accessible/windows/msaa/MsaaXULMenuAccessible.cpp83
-rw-r--r--accessible/windows/msaa/MsaaXULMenuAccessible.h30
-rw-r--r--accessible/windows/msaa/NtUndoc.h85
-rw-r--r--accessible/windows/msaa/Platform.cpp264
-rw-r--r--accessible/windows/msaa/RootAccessibleWrap.cpp44
-rw-r--r--accessible/windows/msaa/RootAccessibleWrap.h33
-rw-r--r--accessible/windows/msaa/ServiceProvider.cpp106
-rw-r--r--accessible/windows/msaa/ServiceProvider.h37
-rw-r--r--accessible/windows/msaa/moz.build73
-rw-r--r--accessible/windows/msaa/nsEventMap.h57
-rw-r--r--accessible/windows/msaa/nsWinUtils.cpp167
-rw-r--r--accessible/windows/msaa/nsWinUtils.h105
-rw-r--r--accessible/windows/sdn/moz.build24
-rw-r--r--accessible/windows/sdn/sdnAccessible-inl.h49
-rw-r--r--accessible/windows/sdn/sdnAccessible.cpp522
-rw-r--r--accessible/windows/sdn/sdnAccessible.h149
-rw-r--r--accessible/windows/sdn/sdnDocAccessible.cpp117
-rw-r--r--accessible/windows/sdn/sdnDocAccessible.h52
-rw-r--r--accessible/windows/sdn/sdnTextAccessible.cpp166
-rw-r--r--accessible/windows/sdn/sdnTextAccessible.h69
-rw-r--r--accessible/windows/uia/moz.build22
-rw-r--r--accessible/windows/uia/uiaRawElmProvider.cpp206
-rw-r--r--accessible/windows/uia/uiaRawElmProvider.h75
-rwxr-xr-xaccessible/xpcom/AccEventGen.py256
-rw-r--r--accessible/xpcom/AccEvents.conf20
-rw-r--r--accessible/xpcom/moz.build80
-rw-r--r--accessible/xpcom/nsAccessibleRelation.cpp59
-rw-r--r--accessible/xpcom/nsAccessibleRelation.h46
-rw-r--r--accessible/xpcom/xpcAccessibilityService.cpp307
-rw-r--r--accessible/xpcom/xpcAccessibilityService.h68
-rw-r--r--accessible/xpcom/xpcAccessible.cpp665
-rw-r--r--accessible/xpcom/xpcAccessible.h112
-rw-r--r--accessible/xpcom/xpcAccessibleApplication.cpp60
-rw-r--r--accessible/xpcom/xpcAccessibleApplication.h47
-rw-r--r--accessible/xpcom/xpcAccessibleDocument.cpp179
-rw-r--r--accessible/xpcom/xpcAccessibleDocument.h136
-rw-r--r--accessible/xpcom/xpcAccessibleGeneric.cpp61
-rw-r--r--accessible/xpcom/xpcAccessibleGeneric.h96
-rw-r--r--accessible/xpcom/xpcAccessibleHyperLink.cpp92
-rw-r--r--accessible/xpcom/xpcAccessibleHyperLink.h48
-rw-r--r--accessible/xpcom/xpcAccessibleHyperText.cpp477
-rw-r--r--accessible/xpcom/xpcAccessibleHyperText.h57
-rw-r--r--accessible/xpcom/xpcAccessibleImage.cpp49
-rw-r--r--accessible/xpcom/xpcAccessibleImage.h40
-rw-r--r--accessible/xpcom/xpcAccessibleMacInterface.h104
-rw-r--r--accessible/xpcom/xpcAccessibleMacInterface.mm581
-rw-r--r--accessible/xpcom/xpcAccessiblePivot.cpp155
-rw-r--r--accessible/xpcom/xpcAccessiblePivot.h47
-rw-r--r--accessible/xpcom/xpcAccessibleSelectable.cpp119
-rw-r--r--accessible/xpcom/xpcAccessibleSelectable.h50
-rw-r--r--accessible/xpcom/xpcAccessibleTable.cpp362
-rw-r--r--accessible/xpcom/xpcAccessibleTable.h74
-rw-r--r--accessible/xpcom/xpcAccessibleTableCell.cpp140
-rw-r--r--accessible/xpcom/xpcAccessibleTableCell.h52
-rw-r--r--accessible/xpcom/xpcAccessibleTextLeafRange.cpp83
-rw-r--r--accessible/xpcom/xpcAccessibleTextLeafRange.h45
-rw-r--r--accessible/xpcom/xpcAccessibleTextRange.cpp125
-rw-r--r--accessible/xpcom/xpcAccessibleTextRange.h78
-rw-r--r--accessible/xpcom/xpcAccessibleValue.cpp99
-rw-r--r--accessible/xpcom/xpcAccessibleValue.h42
-rw-r--r--accessible/xul/XULAlertAccessible.cpp44
-rw-r--r--accessible/xul/XULAlertAccessible.h40
-rw-r--r--accessible/xul/XULComboboxAccessible.cpp142
-rw-r--r--accessible/xul/XULComboboxAccessible.h43
-rw-r--r--accessible/xul/XULElementAccessibles.cpp205
-rw-r--r--accessible/xul/XULElementAccessibles.h108
-rw-r--r--accessible/xul/XULFormControlAccessible.cpp446
-rw-r--r--accessible/xul/XULFormControlAccessible.h186
-rw-r--r--accessible/xul/XULListboxAccessible.cpp456
-rw-r--r--accessible/xul/XULListboxAccessible.h136
-rw-r--r--accessible/xul/XULMenuAccessible.cpp477
-rw-r--r--accessible/xul/XULMenuAccessible.h113
-rw-r--r--accessible/xul/XULSelectControlAccessible.cpp253
-rw-r--r--accessible/xul/XULSelectControlAccessible.h47
-rw-r--r--accessible/xul/XULTabAccessible.cpp217
-rw-r--r--accessible/xul/XULTabAccessible.h98
-rw-r--r--accessible/xul/XULTreeAccessible.cpp995
-rw-r--r--accessible/xul/XULTreeAccessible.h265
-rw-r--r--accessible/xul/XULTreeGridAccessible.cpp666
-rw-r--r--accessible/xul/XULTreeGridAccessible.h193
-rw-r--r--accessible/xul/moz.build56
1046 files changed, 192276 insertions, 0 deletions
diff --git a/accessible/android/AccessibleWrap.cpp b/accessible/android/AccessibleWrap.cpp
new file mode 100644
index 0000000000..4bccc2dddd
--- /dev/null
+++ b/accessible/android/AccessibleWrap.cpp
@@ -0,0 +1,473 @@
+/* -*- 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 "nsIAccessiblePivot.h"
+#include "nsAccUtils.h"
+#include "nsTextEquivUtils.h"
+#include "nsWhitespaceTokenizer.h"
+#include "RootAccessible.h"
+#include "TextLeafRange.h"
+
+#include "mozilla/a11y/PDocAccessibleChild.h"
+#include "mozilla/jni/GeckoBundleUtils.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/Maybe.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;
+using mozilla::Maybe;
+
+//-----------------------------------------------------
+// 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<AccessibleWrap*>(aEvent->GetAccessible());
+ NS_ENSURE_TRUE(accessible, NS_ERROR_FAILURE);
+
+ nsresult rv = LocalAccessible::HandleAccEvent(aEvent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ accessible->HandleLiveRegionEvent(aEvent);
+
+ 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;
+}
+
+Accessible* AccessibleWrap::ExploreByTouch(Accessible* aAccessible, float aX,
+ float aY) {
+ Accessible* root;
+ if (LocalAccessible* local = aAccessible->AsLocal()) {
+ root = local->RootAccessible();
+ } else {
+ // If this is a RemoteAccessible, 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");
+ root = doc;
+ }
+ a11y::Pivot pivot(root);
+ TraversalRule rule(java::SessionAccessibility::HTML_GRANULARITY_DEFAULT,
+ aAccessible->IsLocal());
+ Accessible* result = pivot.AtPoint(aX, aY, rule);
+ if (result == aAccessible) {
+ return nullptr;
+ }
+ return result;
+}
+
+static TextLeafPoint ToTextLeafPoint(Accessible* aAccessible, int32_t aOffset) {
+ if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
+ return ht->ToTextLeafPoint(aOffset);
+ }
+
+ return TextLeafPoint(aAccessible, aOffset);
+}
+
+Maybe<std::pair<int32_t, int32_t>> AccessibleWrap::NavigateText(
+ Accessible* aAccessible, int32_t aGranularity, int32_t aStartOffset,
+ int32_t aEndOffset, bool aForward, bool aSelect) {
+ int32_t startOffset = aStartOffset;
+ int32_t endOffset = aEndOffset;
+ if (startOffset == -1) {
+ MOZ_ASSERT(endOffset == -1,
+ "When start offset is unset, end offset should be too");
+ startOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
+ endOffset = aForward ? 0 : nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT;
+ }
+
+ // 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 (aAccessible->State() & states::EDITABLE) {
+ startOffset = endOffset = aAccessible->AsHyperTextBase()->CaretOffset();
+ }
+
+ TextLeafRange currentRange =
+ TextLeafRange(ToTextLeafPoint(aAccessible, startOffset),
+ ToTextLeafPoint(aAccessible, endOffset));
+ uint16_t startBoundaryType = nsIAccessibleText::BOUNDARY_LINE_START;
+ uint16_t endBoundaryType = nsIAccessibleText::BOUNDARY_LINE_END;
+ switch (aGranularity) {
+ case 1: // MOVEMENT_GRANULARITY_CHARACTER
+ startBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
+ endBoundaryType = nsIAccessibleText::BOUNDARY_CHAR;
+ break;
+ case 2: // MOVEMENT_GRANULARITY_WORD
+ startBoundaryType = nsIAccessibleText::BOUNDARY_WORD_START;
+ endBoundaryType = nsIAccessibleText::BOUNDARY_WORD_END;
+ break;
+ default:
+ break;
+ }
+
+ TextLeafRange resultRange;
+
+ if (aForward) {
+ resultRange.SetEnd(
+ currentRange.End().FindBoundary(endBoundaryType, eDirNext));
+ resultRange.SetStart(
+ resultRange.End().FindBoundary(startBoundaryType, eDirPrevious));
+ } else {
+ resultRange.SetStart(
+ currentRange.Start().FindBoundary(startBoundaryType, eDirPrevious));
+ resultRange.SetEnd(
+ resultRange.Start().FindBoundary(endBoundaryType, eDirNext));
+ }
+
+ if (!resultRange.Crop(aAccessible)) {
+ // If the new range does not intersect at all with the given
+ // accessible/container this navigation has failed or reached an edge.
+ return Nothing();
+ }
+
+ if (resultRange == currentRange || resultRange.Start() == resultRange.End()) {
+ // If the result range equals the current range, or if the result range is
+ // collapsed, we failed or reached an edge.
+ return Nothing();
+ }
+
+ if (HyperTextAccessibleBase* ht = aAccessible->AsHyperTextBase()) {
+ DebugOnly<bool> ok = false;
+ std::tie(ok, startOffset) = ht->TransformOffset(
+ resultRange.Start().mAcc, resultRange.Start().mOffset, false);
+ MOZ_ASSERT(ok, "Accessible of range start should be in container.");
+
+ std::tie(ok, endOffset) = ht->TransformOffset(
+ resultRange.End().mAcc, resultRange.End().mOffset, false);
+ MOZ_ASSERT(ok, "Accessible range end should be in container.");
+ } else {
+ startOffset = resultRange.Start().mOffset;
+ endOffset = resultRange.End().mOffset;
+ }
+
+ return Some(std::make_pair(startOffset, endOffset));
+}
+
+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<AccessibleWrap*>(aAccessible)->mID;
+ }
+
+ return static_cast<int32_t>(aAccessible->AsRemote()->GetWrapper());
+}
+
+void AccessibleWrap::SetVirtualViewID(Accessible* aAccessible,
+ int32_t aVirtualViewID) {
+ if (aAccessible->IsLocal()) {
+ static_cast<AccessibleWrap*>(aAccessible)->mID = aVirtualViewID;
+ } else {
+ aAccessible->AsRemote()->SetWrapper(static_cast<uintptr_t>(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<AccAttributes> 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<bool> atomic =
+ attributes->GetAttribute<bool>(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<AccessibleWrap*>(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..249c5dc14e
--- /dev/null
+++ b/accessible/android/AccessibleWrap.h
@@ -0,0 +1,68 @@
+/* -*- 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;
+
+ static Accessible* ExploreByTouch(Accessible* aAccessible, 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);
+
+ static Maybe<std::pair<int32_t, int32_t>> NavigateText(
+ Accessible* aAccessible, int32_t aGranularity, int32_t aStartOffset,
+ int32_t aEndOffset, bool aForward, bool aSelect);
+
+ protected:
+ int32_t mID;
+
+ private:
+ void GetTextEquiv(nsString& aText);
+
+ bool HandleLiveRegionEvent(AccEvent* aEvent);
+};
+
+} // 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..0e46b56649
--- /dev/null
+++ b/accessible/android/DocAccessibleWrap.cpp
@@ -0,0 +1,78 @@
+/* -*- 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"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+#define UNIQUE_ID(acc) \
+ !acc || (acc->IsDoc() && acc->AsDoc()->IPCDoc()) \
+ ? 0 \
+ : reinterpret_cast<uint64_t>(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<DocAccessibleWrap*>(aAccessible->Document());
+ while (doc && !doc->IsTopLevelContentDoc()) {
+ doc = static_cast<DocAccessibleWrap*>(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/Platform.cpp b/accessible/android/Platform.cpp
new file mode 100644
index 0000000000..02f808f8bc
--- /dev/null
+++ b/accessible/android/Platform.cpp
@@ -0,0 +1,233 @@
+/* -*- 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"
+#include "TextLeafRange.h"
+
+#define ROLE_STRINGS_URL "chrome://global/locale/AccessFu.properties"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+static nsTHashMap<nsStringHashKey, nsString> sLocalizedStrings;
+
+void a11y::PlatformInit() {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ components::StringBundle::Service();
+ if (!stringBundleService) return;
+
+ nsCOMPtr<nsIStringBundle> stringBundle;
+ nsCOMPtr<nsIStringBundleService> 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<nsString, 1> 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::PlatformEvent(Accessible* aTarget, uint32_t aEventType) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+ if (!sessionAcc) {
+ return;
+ }
+
+ switch (aEventType) {
+ case nsIAccessibleEvent::EVENT_REORDER:
+ sessionAcc->SendWindowContentChangedEvent();
+ break;
+ case nsIAccessibleEvent::EVENT_SCROLLING_START:
+ if (Accessible* result = AccessibleWrap::DoPivot(
+ aTarget, java::SessionAccessibility::HTML_GRANULARITY_DEFAULT,
+ true, true)) {
+ sessionAcc->SendAccessibilityFocusedEvent(result, false);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void a11y::PlatformStateChangeEvent(Accessible* aTarget, uint64_t aState,
+ bool aEnabled) {
+ RefPtr<SessionAccessibility> 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::PlatformFocusEvent(Accessible* aTarget,
+ const LayoutDeviceIntRect& aCaretRect) {
+ if (RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget)) {
+ sessionAcc->SendFocusEvent(aTarget);
+ }
+}
+
+void a11y::PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset,
+ bool aIsSelectionCollapsed,
+ int32_t aGranularity,
+ const LayoutDeviceIntRect& aCaretRect,
+ bool aFromUser) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+ if (!sessionAcc) {
+ return;
+ }
+
+ if (!aTarget->IsDoc() && !aFromUser && !aIsSelectionCollapsed) {
+ // Pivot to the caret's position if it has an expanded selection.
+ // This is used mostly for find in page.
+ Accessible* leaf = TextLeafPoint::GetCaret(aTarget).ActualizeCaret().mAcc;
+ MOZ_ASSERT(leaf);
+ if (leaf) {
+ if (Accessible* result = AccessibleWrap::DoPivot(
+ leaf, java::SessionAccessibility::HTML_GRANULARITY_DEFAULT, true,
+ true)) {
+ sessionAcc->SendAccessibilityFocusedEvent(result, false);
+ }
+ }
+ }
+
+ sessionAcc->SendTextSelectionChangedEvent(aTarget, aOffset);
+}
+
+void a11y::PlatformTextChangeEvent(Accessible* aTarget, const nsAString& aStr,
+ int32_t aStart, uint32_t aLen,
+ bool aIsInsert, bool aFromUser) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+
+ if (sessionAcc) {
+ sessionAcc->SendTextChangedEvent(aTarget, aStr, aStart, aLen, aIsInsert,
+ aFromUser);
+ }
+}
+
+void a11y::PlatformShowHideEvent(Accessible* aTarget, Accessible* aParent,
+ bool aInsert, bool aFromUser) {
+ // We rely on the window content changed events to be dispatched
+ // after the viewport cache is refreshed.
+}
+
+void a11y::PlatformSelectionEvent(Accessible*, Accessible*, uint32_t) {}
+
+void a11y::PlatformScrollingEvent(Accessible* aTarget, uint32_t aEventType,
+ uint32_t aScrollX, uint32_t aScrollY,
+ uint32_t aMaxScrollX, uint32_t aMaxScrollY) {
+ if (aEventType == nsIAccessibleEvent::EVENT_SCROLLING) {
+ RefPtr<SessionAccessibility> sessionAcc =
+ SessionAccessibility::GetInstanceFor(aTarget);
+
+ if (sessionAcc) {
+ sessionAcc->SendScrollingEvent(aTarget, aScrollX, aScrollY, aMaxScrollX,
+ aMaxScrollY);
+ }
+ }
+}
+
+void a11y::PlatformAnnouncementEvent(Accessible* aTarget,
+ const nsAString& aAnnouncement,
+ uint16_t aPriority) {
+ RefPtr<SessionAccessibility> 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.h b/accessible/android/RootAccessibleWrap.h
new file mode 100644
index 0000000000..4198239bad
--- /dev/null
+++ b/accessible/android/RootAccessibleWrap.h
@@ -0,0 +1,22 @@
+/* -*- 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 {
+
+using RootAccessibleWrap = RootAccessible;
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/android/SessionAccessibility.cpp b/accessible/android/SessionAccessibility.cpp
new file mode 100644
index 0000000000..aab9b7da69
--- /dev/null
+++ b/accessible/android/SessionAccessibility.cpp
@@ -0,0 +1,941 @@
+/* -*- 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 "DocAccessibleWrap.h"
+#include "JavaBuiltins.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/DocManager.h"
+#include "mozilla/a11y/HyperTextAccessibleBase.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 <android/log.h>
+# define AALOG(args...) \
+ __android_log_print(ANDROID_LOG_INFO, "GeckoAccessibilityNative", ##args)
+#else
+# define AALOG(args...) \
+ do { \
+ } while (0)
+#endif
+
+using namespace mozilla::a11y;
+
+// IDs should be a positive 32bit integer.
+IDSet sIDSet(31UL);
+
+class Settings final
+ : public mozilla::java::SessionAccessibility::Settings::Natives<Settings> {
+ public:
+ static void ToggleNativeAccessibility(bool aEnable) {
+ if (aEnable) {
+ GetOrCreateAccService();
+ } else {
+ MaybeShutdownAccService(nsAccessibilityService::ePlatformAPI);
+ }
+ }
+};
+
+SessionAccessibility::SessionAccessibility(
+ jni::NativeWeakPtr<widget::GeckoViewSupport> aWindow,
+ java::SessionAccessibility::NativeProvider::Param aSessionAccessibility)
+ : mWindow(aWindow), mSessionAccessibility(aSessionAccessibility) {
+ SetAttached(true, nullptr);
+}
+
+void SessionAccessibility::SetAttached(bool aAttached,
+ already_AddRefed<Runnable> aRunnable) {
+ if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
+ uiThread->Dispatch(NS_NewRunnableFunction(
+ "SessionAccessibility::Attach",
+ [aAttached,
+ sa = java::SessionAccessibility::NativeProvider::GlobalRef(
+ mSessionAccessibility),
+ runnable = RefPtr<Runnable>(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<SessionAccessibility> 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<SessionAccessibility> 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) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ acc->DoAction(0);
+ }
+}
+
+bool SessionAccessibility::Pivot(int32_t aID, int32_t aGranularity,
+ bool aForward, bool aInclusive) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ RefPtr<SessionAccessibility> 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->IsLocal());
+ if (Accessible* result = AccessibleWrap::DoPivot(
+ _acc, aGranularity, aForward, aInclusive)) {
+ SendAccessibilityFocusedEvent(result, true);
+ }
+ }
+ });
+ 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, true);
+ }
+ });
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void SessionAccessibility::ExploreByTouch(int32_t aID, float aX, float aY) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ RefPtr<SessionAccessibility> self(this);
+ if (Accessible* origin = GetAccessibleByID(aID)) {
+ if (origin->IsLocal()) {
+ nsAppShell::PostEvent([this, self, aID, aX, aY] {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (Accessible* origin = GetAccessibleByID(aID)) {
+ if (Accessible* result =
+ AccessibleWrap::ExploreByTouch(origin, aX, aY)) {
+ SendHoverEnterEvent(result);
+ }
+ }
+ });
+ } else {
+ if (Accessible* result = AccessibleWrap::ExploreByTouch(origin, aX, aY)) {
+ int32_t resultID = AccessibleWrap::GetVirtualViewID(result);
+ nsAppShell::PostEvent([this, self, resultID] {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (Accessible* result = GetAccessibleByID(resultID)) {
+ SendHoverEnterEvent(result);
+ }
+ });
+ }
+ }
+ }
+}
+
+static void GetSelectionOrCaret(HyperTextAccessibleBase* aHyperTextAcc,
+ int32_t* aStartOffset, int32_t* aEndOffset) {
+ if (!aHyperTextAcc->SelectionBoundsAt(0, aStartOffset, aEndOffset)) {
+ *aStartOffset = *aEndOffset = aHyperTextAcc->CaretOffset();
+ }
+}
+
+static void AdjustCaretToTextNavigation(Accessible* aAccessible,
+ int32_t aStartOffset,
+ int32_t aEndOffset, bool aForward,
+ bool aSelect) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!(aAccessible->State() & states::EDITABLE)) {
+ return;
+ }
+
+ HyperTextAccessibleBase* editable = aAccessible->AsHyperTextBase();
+ MOZ_ASSERT(editable);
+ if (!editable) {
+ return;
+ }
+
+ int32_t newOffset = aForward ? aEndOffset : aStartOffset;
+ if (aSelect) {
+ int32_t anchor = editable->CaretOffset();
+ if (editable->SelectionCount()) {
+ int32_t startSel, endSel;
+ GetSelectionOrCaret(editable, &startSel, &endSel);
+ anchor = startSel == anchor ? endSel : startSel;
+ }
+ editable->SetSelectionBoundsAt(0, anchor, newOffset);
+ } else {
+ editable->SetCaretOffset(newOffset);
+ }
+}
+
+bool SessionAccessibility::NavigateText(int32_t aID, int32_t aGranularity,
+ int32_t aStartOffset,
+ int32_t aEndOffset, bool aForward,
+ bool aSelect) {
+ MOZ_ASSERT(AndroidBridge::IsJavaUiThread());
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ RefPtr<SessionAccessibility> self(this);
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ if (acc->IsLocal()) {
+ nsAppShell::PostEvent([this, self, aID, aGranularity, aStartOffset,
+ aEndOffset, aForward, aSelect] {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (Accessible* _acc = GetAccessibleByID(aID)) {
+ auto result = AccessibleWrap::NavigateText(
+ _acc, aGranularity, aStartOffset, aEndOffset, aForward, aSelect);
+
+ if (result) {
+ SendTextTraversedEvent(_acc, result->first, result->second);
+ AdjustCaretToTextNavigation(_acc, result->first, result->second,
+ aForward, aSelect);
+ }
+ }
+ });
+ return true;
+ } else {
+ auto result = AccessibleWrap::NavigateText(
+ acc, aGranularity, aStartOffset, aEndOffset, aForward, aSelect);
+ if (result) {
+ nsAppShell::PostEvent([this, self, aID, result, aForward, aSelect] {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (Accessible* _acc = GetAccessibleByID(aID)) {
+ SendTextTraversedEvent(_acc, result->first, result->second);
+ AdjustCaretToTextNavigation(_acc, result->first, result->second,
+ aForward, aSelect);
+ }
+ });
+ }
+
+ return !!result;
+ }
+ }
+
+ return false;
+}
+
+void SessionAccessibility::SetSelection(int32_t aID, int32_t aStart,
+ int32_t aEnd) {
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ if (auto* textAcc = acc->AsHyperTextBase()) {
+ if (aStart == aEnd) {
+ textAcc->SetCaretOffset(aStart);
+ } else {
+ textAcc->SetSelectionBoundsAt(0, aStart, aEnd);
+ }
+ }
+ }
+}
+
+void SessionAccessibility::Cut(int32_t aID) {
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ if (auto* textAcc = acc->AsHyperTextBase()) {
+ int32_t startSel, endSel;
+ if (textAcc->SelectionBoundsAt(0, &startSel, &endSel)) {
+ textAcc->CutText(startSel, endSel);
+ }
+ }
+ }
+}
+
+void SessionAccessibility::Copy(int32_t aID) {
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ if (auto* textAcc = acc->AsHyperTextBase()) {
+ int32_t startSel, endSel;
+ GetSelectionOrCaret(textAcc, &startSel, &endSel);
+ textAcc->CopyText(startSel, endSel);
+ }
+ }
+}
+
+void SessionAccessibility::Paste(int32_t aID) {
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ if (auto* textAcc = acc->AsHyperTextBase()) {
+ int32_t startSel, endSel;
+ GetSelectionOrCaret(textAcc, &startSel, &endSel);
+ if (startSel != endSel) {
+ textAcc->DeleteText(startSel, endSel);
+ }
+ textAcc->PasteText(startSel);
+ }
+ }
+}
+
+RefPtr<SessionAccessibility> 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 {
+ dom::CanonicalBrowsingContext* cbc =
+ static_cast<dom::BrowserParent*>(
+ aAccessible->AsRemote()->Document()->Manager())
+ ->GetBrowsingContext()
+ ->Top();
+ dom::BrowserParent* bp = cbc->GetBrowserParent();
+ if (!bp) {
+ bp = static_cast<dom::BrowserParent*>(
+ aAccessible->AsRemote()->Document()->Manager());
+ }
+ if (auto element = bp->GetOwnerElement()) {
+ if (auto doc = element->OwnerDoc()) {
+ if (nsPresContext* presContext = doc->GetPresContext()) {
+ return GetInstanceFor(presContext->PresShell());
+ }
+ } else {
+ MOZ_ASSERT_UNREACHABLE(
+ "Browser parent's element does not have owner doc.");
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+RefPtr<SessionAccessibility> SessionAccessibility::GetInstanceFor(
+ PresShell* aPresShell) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!aPresShell) {
+ return nullptr;
+ }
+
+ nsViewManager* vm = aPresShell->GetViewManager();
+ if (!vm) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIWidget> rootWidget = vm->GetRootWidget();
+ // `rootWidget` can be one of several types. Here we make sure it is an
+ // android nsWindow.
+ if (RefPtr<nsWindow> window = nsWindow::From(rootWidget)) {
+ return window->GetSessionAccessibility();
+ }
+
+ return nullptr;
+}
+
+void SessionAccessibility::SendAccessibilityFocusedEvent(
+ Accessible* aAccessible, bool aScrollIntoView) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSessionAccessibility->SendEvent(
+ java::sdk::AccessibilityEvent::TYPE_VIEW_ACCESSIBILITY_FOCUSED,
+ AccessibleWrap::GetVirtualViewID(aAccessible),
+ AccessibleWrap::AndroidClass(aAccessible), nullptr);
+ if (aScrollIntoView) {
+ 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 (<a> 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<AccAttributes> 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<int32_t> 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<float>(minValue),
+ static_cast<float>(maxValue), static_cast<float>(curValue));
+ }
+
+ if (attributes) {
+ Maybe<int32_t> rowIndex =
+ attributes->GetAttribute<int32_t>(nsGkAtoms::posinset);
+ if (rowIndex) {
+ mSessionAccessibility->PopulateNodeCollectionItemInfo(
+ aNodeInfo, *rowIndex - 1, 1, 0, 1);
+ }
+
+ Maybe<int32_t> rowCount =
+ attributes->GetAttribute<int32_t>(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 {
+ return mIDToAccessibleMap.Get(aID);
+}
+
+#ifdef DEBUG
+static bool IsDetachedDoc(Accessible* aAccessible) {
+ if (!aAccessible->IsRemote() || !aAccessible->AsRemote()->IsDoc()) {
+ return false;
+ }
+
+ return !aAccessible->Parent() ||
+ aAccessible->Parent()->FirstChild() != aAccessible;
+}
+#endif
+
+SessionAccessibility::IDMappingEntry::IDMappingEntry(Accessible* aAccessible)
+ : mInternalID(0) {
+ *this = aAccessible;
+}
+
+SessionAccessibility::IDMappingEntry&
+SessionAccessibility::IDMappingEntry::operator=(Accessible* aAccessible) {
+ mInternalID = aAccessible->ID();
+ MOZ_ASSERT(!(mInternalID & IS_REMOTE), "First bit is used in accessible ID!");
+ if (aAccessible->IsRemote()) {
+ mInternalID |= IS_REMOTE;
+ }
+
+ Accessible* docAcc = nsAccUtils::DocumentFor(aAccessible);
+ MOZ_ASSERT(docAcc);
+ if (docAcc) {
+ MOZ_ASSERT(docAcc->IsRemote() == aAccessible->IsRemote());
+ if (docAcc->IsRemote()) {
+ mDoc = docAcc->AsRemote()->AsDoc();
+ } else {
+ mDoc = docAcc->AsLocal();
+ }
+ }
+
+ return *this;
+}
+
+SessionAccessibility::IDMappingEntry::operator Accessible*() const {
+ if (mInternalID == 0) {
+ return static_cast<LocalAccessible*>(mDoc.get());
+ }
+
+ if (mInternalID == IS_REMOTE) {
+ return static_cast<DocAccessibleParent*>(mDoc.get());
+ }
+
+ if (mInternalID & IS_REMOTE) {
+ return static_cast<DocAccessibleParent*>(mDoc.get())
+ ->GetAccessible(mInternalID & ~IS_REMOTE);
+ }
+
+ Accessible* accessible =
+ static_cast<LocalAccessible*>(mDoc.get())
+ ->AsDoc()
+ ->GetAccessibleByUniqueID(reinterpret_cast<void*>(mInternalID));
+ // If the accessible is retrievable from the DocAccessible, it can't be
+ // defunct.
+ MOZ_ASSERT(!accessible->AsLocal()->IsDefunct());
+
+ return accessible;
+}
+
+void SessionAccessibility::RegisterAccessible(Accessible* aAccessible) {
+ if (IPCAccessibilityActive()) {
+ // Don't register accessible in content process.
+ return;
+ }
+
+ nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns();
+ RefPtr<SessionAccessibility> sessionAcc = GetInstanceFor(aAccessible);
+ if (!sessionAcc) {
+ return;
+ }
+
+ bool isTopLevel = false;
+ if (aAccessible->IsLocal() && aAccessible->IsDoc()) {
+ DocAccessibleWrap* doc =
+ static_cast<DocAccessibleWrap*>(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<SessionAccessibility> sessionAcc = GetInstanceFor(aAccessible);
+ 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<SessionAccessibility> 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..deacc57150
--- /dev/null
+++ b/accessible/android/SessionAccessibility.h
@@ -0,0 +1,145 @@
+/* -*- 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"
+
+namespace mozilla {
+namespace a11y {
+
+class Accessible;
+
+class SessionAccessibility final
+ : public java::SessionAccessibility::NativeProvider::Natives<
+ SessionAccessibility> {
+ public:
+ typedef java::SessionAccessibility::NativeProvider::Natives<
+ SessionAccessibility>
+ Base;
+
+ SessionAccessibility(
+ jni::NativeWeakPtr<widget::GeckoViewSupport> aWindow,
+ java::SessionAccessibility::NativeProvider::Param aSessionAccessibility);
+
+ void OnWeakNonIntrusiveDetach(already_AddRefed<Runnable> aDisposer) {
+ SetAttached(false, std::move(aDisposer));
+ }
+
+ const java::SessionAccessibility::NativeProvider::Ref&
+ GetJavaAccessibility() {
+ return mSessionAccessibility;
+ }
+
+ static void Init();
+ static RefPtr<SessionAccessibility> GetInstanceFor(Accessible* aAccessible);
+ static RefPtr<SessionAccessibility> 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);
+ bool 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);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY 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,
+ bool aScrollIntoView);
+ 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<Runnable> aRunnable);
+
+ bool DoNavigateText(Accessible* aAccessible, int32_t aGranularity,
+ int32_t aStartOffset, int32_t aEndOffset, bool aForward,
+ bool aSelect);
+
+ jni::NativeWeakPtr<widget::GeckoViewSupport> mWindow; // Parent only
+ java::SessionAccessibility::NativeProvider::GlobalRef mSessionAccessibility;
+
+ class IDMappingEntry {
+ public:
+ explicit IDMappingEntry(Accessible* aAccessible);
+
+ IDMappingEntry& operator=(Accessible* aAccessible);
+
+ operator Accessible*() const;
+
+ private:
+ // A strong reference to a DocAccessible or DocAccessibleParent. They don't
+ // share any useful base class except nsISupports, so we use that.
+ // When we retrieve the document from this reference we cast it to
+ // LocalAccessible in the DocAccessible case because DocAccessible has
+ // multiple inheritance paths for nsISupports.
+ RefPtr<nsISupports> mDoc;
+ // The ID of the accessible as used in the internal doc mapping.
+ // We rely on this ID being pointer derived and therefore divisible by two
+ // so we can use the first bit to mark if it is remote or not.
+ uint64_t mInternalID;
+
+ static const uintptr_t IS_REMOTE = 0x1;
+ };
+
+ /*
+ * This provides a mapping from 32 bit id to accessible objects.
+ */
+ nsBaseHashtable<nsUint32HashKey, IDMappingEntry, Accessible*>
+ mIDToAccessibleMap;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/android/TraversalRule.cpp b/accessible/android/TraversalRule.cpp
new file mode 100644
index 0000000000..4203fd48c6
--- /dev/null
+++ b/accessible/android/TraversalRule.cpp
@@ -0,0 +1,288 @@
+/* -*- 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 "mozilla/a11y/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::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::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..f8a185da48
--- /dev/null
+++ b/accessible/android/moz.build
@@ -0,0 +1,35 @@
+# -*- 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",
+ "SessionAccessibility.h",
+ "TraversalRule.h",
+]
+
+SOURCES += [
+ "AccessibleWrap.cpp",
+ "DocAccessibleWrap.cpp",
+ "Platform.cpp",
+ "SessionAccessibility.cpp",
+ "TraversalRule.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/generic",
+ "/accessible/html",
+ "/accessible/ipc",
+ "/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..2aff34238c
--- /dev/null
+++ b/accessible/aom/AccessibleNode.cpp
@@ -0,0 +1,170 @@
+/* -*- 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 "LocalAccessible-inl.h"
+#include "nsAccessibilityService.h"
+#include "DocAccessible.h"
+
+#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<JSObject*> 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<nsString>& 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<nsString>& aAttributes) {
+ if (!mIntl) {
+ return;
+ }
+
+ RefPtr<AccAttributes> attrs = mIntl->Attributes();
+
+ for (const auto& iter : *attrs) {
+ aAttributes.AppendElement(nsAtomString(iter.Name()));
+ }
+}
+
+bool AccessibleNode::Is(const Sequence<nsString>& 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<nsString>& aAttributes) {
+ if (!mIntl) {
+ return false;
+ }
+ RefPtr<AccAttributes> attrs = mIntl->Attributes();
+ for (const auto& attr : aAttributes) {
+ RefPtr<nsAtom> attrAtom = NS_Atomize(attr);
+ if (!attrs->HasAttribute(attrAtom)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+void AccessibleNode::Get(JSContext* aCX, const nsAString& aAttribute,
+ JS::MutableHandle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ if (!mIntl) {
+ aRv.ThrowInvalidStateError("No attributes available");
+ return;
+ }
+
+ RefPtr<nsAtom> attrAtom = NS_Atomize(aAttribute);
+ RefPtr<AccAttributes> 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<type> Get##name() { \
+ return GetProperty(AOM##typeName##Property::e##name); \
+ } \
+ \
+ void Set##name(const dom::Nullable<type>& 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<AccessibleNode> 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<nsUint32HashKey, type> m##typeName##Properties; \
+ \
+ dom::Nullable<type> GetProperty(AOM##typeName##Property aProperty) { \
+ type value = defVal; \
+ if (m##typeName##Properties.Get(static_cast<int>(aProperty), &value)) { \
+ return dom::Nullable<type>(value); \
+ } \
+ return dom::Nullable<type>(); \
+ } \
+ \
+ void SetProperty(AOM##typeName##Property aProperty, \
+ const dom::Nullable<type>& aValue) { \
+ if (aValue.IsNull()) { \
+ m##typeName##Properties.Remove(static_cast<int>(aProperty)); \
+ } else { \
+ m##typeName##Properties.InsertOrUpdate(static_cast<int>(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<JSObject*> aGivenProto) final;
+ dom::ParentObject GetParentObject() const;
+
+ void GetComputedRole(nsAString& aRole);
+ void GetStates(nsTArray<nsString>& aStates);
+ void GetAttributes(nsTArray<nsString>& aAttributes);
+ nsINode* GetDOMNode();
+
+ bool Is(const Sequence<nsString>& aFlavors);
+ bool Has(const Sequence<nsString>& aAttributes);
+ void Get(JSContext* cx, const nsAString& aAttribute,
+ JS::MutableHandle<JS::Value> 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<int>(aProperty), &data)) {
+ SetDOMStringToNull(data);
+ }
+ aRetval = data;
+ }
+
+ void SetProperty(AOMStringProperty aProperty, const nsAString& aValue) {
+ if (DOMStringIsNull(aValue)) {
+ mStringProperties.Remove(static_cast<int>(aProperty));
+ } else {
+ nsString value(aValue);
+ mStringProperties.InsertOrUpdate(static_cast<int>(aProperty), value);
+ }
+ }
+
+ dom::Nullable<bool> GetProperty(AOMBooleanProperty aProperty) {
+ int num = static_cast<int>(aProperty);
+ if (mBooleanProperties & (1U << (2 * num))) {
+ bool data = static_cast<bool>(mBooleanProperties & (1U << (2 * num + 1)));
+ return dom::Nullable<bool>(data);
+ }
+ return dom::Nullable<bool>();
+ }
+
+ void SetProperty(AOMBooleanProperty aProperty,
+ const dom::Nullable<bool>& aValue) {
+ int num = static_cast<int>(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<AccessibleNode> GetProperty(AOMRelationProperty aProperty) {
+ return mRelationProperties.Get(static_cast<int>(aProperty));
+ }
+
+ void SetProperty(AOMRelationProperty aProperty, AccessibleNode* aValue) {
+ if (!aValue) {
+ mRelationProperties.Remove(static_cast<int>(aProperty));
+ } else {
+ mRelationProperties.InsertOrUpdate(static_cast<int>(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<nsUint32HashKey, AccessibleNode> mRelationProperties;
+ nsTHashMap<nsUint32HashKey, nsString> mStringProperties;
+
+ RefPtr<a11y::LocalAccessible> mIntl;
+ RefPtr<nsINode> mDOMNode;
+ RefPtr<dom::DOMStringList> 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..98f303ee4a
--- /dev/null
+++ b/accessible/atk/AccessibleWrap.cpp
@@ -0,0 +1,1305 @@
+/* -*- 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<AtkObject*>(g_object_new(type, nullptr));
+ if (!mAtkObject) return;
+
+ atk_object_initialize(mAtkObject, static_cast<Accessible*>(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<AtkObject*>(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<Accessible*>(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<AccAttributes> 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<gint>(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<AtkObject*> 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<AccessibleWrap*>(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<AtkObject*>(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<AtkObject*>(g_object_new(type, nullptr));
+ if (!obj) return;
+
+ atk_object_initialize(obj, static_cast<Accessible*>(aProxy));
+ obj->role = ATK_ROLE_INVALID;
+ obj->layer = ATK_LAYER_INVALID;
+ aProxy->SetWrapper(reinterpret_cast<uintptr_t>(obj));
+}
+
+void a11y::ProxyDestroyed(RemoteAccessible* aProxy) {
+ auto obj = reinterpret_cast<MaiAtkObject*>(aProxy->GetWrapper());
+ if (!obj) {
+ return;
+ }
+
+ obj->Shutdown();
+ g_object_unref(obj);
+ aProxy->SetWrapper(0);
+}
+
+void a11y::PlatformEvent(Accessible* aTarget, uint32_t aEventType) {
+ AtkObject* wrapper = GetWrapperFor(aTarget);
+
+ switch (aEventType) {
+ case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE:
+ if (aTarget->IsDoc()) {
+ g_signal_emit_by_name(wrapper, "load_complete");
+ }
+ // XXX - Handle native dialog accessibles.
+ if (!aTarget->IsRoot() && aTarget->HasARIARole() &&
+ aTarget->Role() == roles::DIALOG) {
+ guint id = g_signal_lookup("activate", MAI_TYPE_ATK_OBJECT);
+ g_signal_emit(wrapper, id, 0);
+ }
+ 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:
+ case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE:
+ if (aTarget->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*)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;
+ case nsIAccessibleEvent::EVENT_NAME_CHANGE: {
+ nsAutoString newName;
+ aTarget->Name(newName);
+ MaybeFireNameChange(wrapper, newName);
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_WINDOW_ACTIVATE: {
+ guint id = g_signal_lookup("activate", MAI_TYPE_ATK_OBJECT);
+ g_signal_emit(wrapper, 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(wrapper, id, 0);
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_WINDOW_MAXIMIZE: {
+ guint id = g_signal_lookup("maximize", MAI_TYPE_ATK_OBJECT);
+ g_signal_emit(wrapper, id, 0);
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_WINDOW_MINIMIZE: {
+ guint id = g_signal_lookup("minimize", MAI_TYPE_ATK_OBJECT);
+ g_signal_emit(wrapper, id, 0);
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_WINDOW_RESTORE: {
+ guint id = g_signal_lookup("restore", MAI_TYPE_ATK_OBJECT);
+ g_signal_emit(wrapper, id, 0);
+ break;
+ }
+ }
+}
+
+void a11y::PlatformStateChangeEvent(Accessible* aTarget, uint64_t aState,
+ bool aEnabled) {
+ MaiAtkObject* atkObj = MAI_ATK_OBJECT(GetWrapperFor(aTarget));
+ atkObj->FireStateChangeEvent(aState, aEnabled);
+}
+
+void a11y::PlatformFocusEvent(Accessible* aTarget,
+ const LayoutDeviceIntRect& aCaretRect) {
+ AtkObject* wrapper = GetWrapperFor(aTarget);
+
+ // XXX Do we really need this check? If so, do we need a similar check for
+ // RemoteAccessible?
+ if (LocalAccessible* localTarget = aTarget->AsLocal()) {
+ a11y::RootAccessible* rootAcc = localTarget->RootAccessible();
+ if (!rootAcc || !rootAcc->IsActivated()) {
+ return;
+ }
+ }
+
+ atk_focus_tracker_notify(wrapper);
+ atk_object_notify_state_change(wrapper, ATK_STATE_FOCUSED, true);
+}
+
+void a11y::PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset,
+ bool aIsSelectionCollapsed,
+ int32_t aGranularity,
+ const LayoutDeviceIntRect& aCaretRect,
+ bool aFromUser) {
+ 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<int32_t>(gAtkStateMapLen),
+ "No ATK state for internal state was found");
+ if (stateIndex < 0 || stateIndex >= static_cast<int32_t>(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::PlatformTextChangeEvent(Accessible* 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::PlatformShowHideEvent(Accessible* aTarget, Accessible* aParent,
+ bool aInsert, bool aFromUser) {
+ AtkObject* atkObj = GetWrapperFor(aTarget);
+ if (!aInsert) {
+ // XXX - Handle native dialog accessibles.
+ if (!aTarget->IsRoot() && aTarget->HasARIARole() &&
+ aTarget->Role() == roles::DIALOG) {
+ guint id = g_signal_lookup("deactivate", MAI_TYPE_ATK_OBJECT);
+ g_signal_emit(atkObj, id, 0);
+ }
+ }
+
+ MaiAtkObject* obj = MAI_ATK_OBJECT(atkObj);
+ 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) {
+ if (!aParent) {
+ // XXX ATK needs a parent for these events. However, we might have already
+ // unbound from the parent by the time we fire a hide event. Ideally, we
+ // need to find a way to keep the parent around, but ATK clients don't seem
+ // to care about these missing events.
+ MOZ_ASSERT(!aIsAdded);
+ return;
+ }
+ 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::PlatformSelectionEvent(Accessible*, Accessible* 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;<Alt>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<Accessible*, 10> 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<Accessible*, 10> 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..e12189405d
--- /dev/null
+++ b/accessible/atk/AccessibleWrap.h
@@ -0,0 +1,93 @@
+/* -*- 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;
+
+ 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..4c3e615387
--- /dev/null
+++ b/accessible/atk/ApplicationAccessibleWrap.cpp
@@ -0,0 +1,145 @@
+/* -*- 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 <gtk/gtk.h>
+#include "atk/atkobject.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+// ApplicationAccessibleWrap
+
+ApplicationAccessibleWrap::ApplicationAccessibleWrap() = default;
+
+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<GObject*>(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<gpointer>(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<gpointer>(windowAcc));
+
+ } else {
+ // Deattach the dialog accessible
+ LocalAccessible* windowAcc = reinterpret_cast<LocalAccessible*>(
+ 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<AtkObject*>(
+ g_object_new(MAI_TYPE_ATK_OBJECT, nullptr));
+ if (!mAtkObject) return;
+
+ atk_object_initialize(mAtkObject, static_cast<Accessible*>(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<int>(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<int>(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 <glib.h>
+#include <cstdint>
+#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<gunichar>(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<gunichar>(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/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 <atk/atk.h>
+
+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 <dbus/dbus.h>
+#endif
+#include <gtk/gtk.h>
+
+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<gpointer>(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<gpointer>(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<nsIGSettingsService> gsettings =
+ do_GetService(NS_GSETTINGSSERVICE_CONTRACTID);
+ nsCOMPtr<nsIGSettingsCollection> 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..41916a0055
--- /dev/null
+++ b/accessible/atk/RootAccessibleWrap.cpp
@@ -0,0 +1,24 @@
+/* -*- 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 <glib-object.h>
+
+using namespace mozilla::a11y;
+
+GtkWindowAccessible::GtkWindowAccessible(AtkObject* aAccessible) {
+ 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 <atk/atkobject.h>
+#include <atk/atkutil.h>
+#include <gtk/gtk.h>
+#include <string.h>
+
+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..092ecada57
--- /dev/null
+++ b/accessible/atk/moz.build
@@ -0,0 +1,64 @@
+# -*- 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",
+]
+
+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/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 <atk/atk.h>
+#include <glib.h>
+#include <glib-object.h>
+
+#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<AtkHyperlink*>(
+ 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<nsIURI> 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<gint>(maiLink->Acc()->EndOffset());
+}
+
+gint getStartIndexCB(AtkHyperlink* aLink) {
+ MaiHyperlink* maiLink = GetMaiHyperlink(aLink);
+ if (!maiLink) return -1;
+
+ return static_cast<gint>(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<gboolean>(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<gint>(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..81e82a8f53
--- /dev/null
+++ b/accessible/atk/nsMaiInterfaceComponent.cpp
@@ -0,0 +1,186 @@
+/* -*- 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 "nsWindow.h"
+#include "mozilla/Likely.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/a11y/RemoteAccessible.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/Document.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;
+}
+
+static double getScaleFactor(Accessible* aAccessible) {
+ DocAccessible* docAcc = nullptr;
+ if (LocalAccessible* localAcc = aAccessible->AsLocal()) {
+ docAcc = localAcc->Document();
+ } else {
+ RemoteAccessible* remote = aAccessible->AsRemote();
+ LocalAccessible* outerDoc = remote->OuterDocOfRemoteBrowser();
+ if (outerDoc) {
+ docAcc = outerDoc->Document();
+ }
+ }
+
+ if (!docAcc || !docAcc->DocumentNode()) {
+ return 1.0;
+ }
+
+ nsCOMPtr<nsIWidget> rootWidget =
+ nsContentUtils::WidgetForDocument(docAcc->DocumentNode());
+ if (!rootWidget) {
+ return 1.0;
+ }
+
+ if (RefPtr<nsWindow> window =
+ static_cast<nsWindow*>(rootWidget->GetTopLevelWidget())) {
+ return window->FractionalScaleFactor();
+ }
+
+ return 1.0;
+}
+
+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;
+ }
+
+ double scaleFactor = getScaleFactor(acc);
+
+ *aX = screenRect.x / scaleFactor;
+ *aY = screenRect.y / scaleFactor;
+ *aWidth = screenRect.width / scaleFactor;
+ *aHeight = screenRect.height / scaleFactor;
+}
+
+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<gint>(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 <atk/atkobject.h>
+#include <atk/atkselection.h>
+
+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<gint>(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<gint>(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<gint>(acc->AsTable()->RowIndexAt(aIdx));
+}
+
+static gint getColumnCountCB(AtkTable* aTable) {
+ Accessible* acc = GetInternalObj(ATK_OBJECT(aTable));
+ if (!acc) {
+ return -1;
+ }
+ return static_cast<gint>(acc->AsTable()->ColCount());
+}
+
+static gint getRowCountCB(AtkTable* aTable) {
+ Accessible* acc = GetInternalObj(ATK_OBJECT(aTable));
+ if (!acc) {
+ return -1;
+ }
+ return static_cast<gint>(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<gint>(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<gint>(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<uint32_t, 10> 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<uint32_t, 10> 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<gboolean>(acc->AsTable()->IsColSelected(aColIdx));
+}
+
+static gboolean isRowSelectedCB(AtkTable* aTable, gint aRowIdx) {
+ Accessible* acc = GetInternalObj(ATK_OBJECT(aTable));
+ if (!acc) {
+ return FALSE;
+ }
+ return static_cast<gboolean>(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<gboolean>(
+ 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..6d61344051
--- /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<gint>(acc->AsTableCell()->ColExtent());
+}
+
+static gint GetRowSpanCB(AtkTableCell* aCell) {
+ Accessible* acc = GetInternalObj(ATK_OBJECT(aCell));
+ if (!acc) {
+ return 0;
+ }
+ return static_cast<gint>(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<gint>(cell->RowIdx());
+ *aCol = static_cast<gint>(cell->ColIdx());
+ return true;
+}
+
+static gboolean GetRowColumnSpanCB(AtkTableCell* aCell, gint* aRow, gint* aCol,
+ gint* aRowExtent, gint* aColExtent) {
+ Accessible* acc = GetInternalObj(ATK_OBJECT(aCell));
+ if (!acc) {
+ return false;
+ }
+ TableCellAccessible* cellAcc = acc->AsTableCell();
+ if (!cellAcc) {
+ return false;
+ }
+ *aCol = static_cast<gint>(cellAcc->ColIdx());
+ *aRow = static_cast<gint>(cellAcc->RowIdx());
+ *aColExtent = static_cast<gint>(cellAcc->ColExtent());
+ *aRowExtent = static_cast<gint>(cellAcc->RowExtent());
+ 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<Accessible*, 10> 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<Accessible*, 10> 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 = GetRowColumnSpanCB;
+ aIface->get_table = GetTableCB;
+}
diff --git a/accessible/atk/nsMaiInterfaceText.cpp b/accessible/atk/nsMaiInterfaceText.cpp
new file mode 100644
index 0000000000..b5c0dcc38d
--- /dev/null
+++ b/accessible/atk/nsMaiInterfaceText.cpp
@@ -0,0 +1,557 @@
+/* -*- 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<Color>();
+ 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<FontSize>();
+ 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<gint>(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<AccAttributes> 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<AccAttributes> 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<gint>(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<gint>(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) {
+ Accessible* acc = GetInternalObj(ATK_OBJECT(aText));
+ if (!acc) {
+ return FALSE;
+ }
+
+ HyperTextAccessibleBase* text = acc->AsHyperTextBase();
+ if (!text) {
+ return FALSE;
+ }
+
+ text->ScrollSubstringToPoint(aStartOffset, aEndOffset, aCoords, aX, aY);
+ return TRUE;
+}
+}
+
+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<AtkTextAttribute>(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 <atk/atk.h>
+#include "AccessibleWrap.h"
+
+#include <type_traits>
+
+// 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<decltype(gAtkStateMap)>::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..01cc5d0417
--- /dev/null
+++ b/accessible/base/ARIAMap.cpp
@@ -0,0 +1,1687 @@
+/* -*- 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 "mozilla/a11y/Role.h"
+#include "States.h"
+
+#include "nsAttrName.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".
+ *
+ * Note: the list must remain alphabetically ordered to support binary search.
+ */
+
+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
+ },
+ { // definition
+ nsGkAtoms::definition,
+ roles::DEFINITION,
+ kUseMapRole,
+ eNoValue,
+ eNoAction,
+ eNoLiveAttr,
+ kGenericAccType,
+ },
+ { // 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
+ },
+ { // emphasis
+ nsGkAtoms::emphasis,
+ roles::EMPHASIS,
+ kUseMapRole,
+ eNoValue,
+ eNoAction,
+ eNoLiveAttr,
+ kGenericAccType,
+ kNoReqStates
+ },
+ { // 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
+ },
+ { // generic
+ nsGkAtoms::generic,
+ roles::SECTION,
+ kUseMapRole,
+ eNoValue,
+ eNoAction,
+ eNoLiveAttr,
+ kGenericAccType,
+ 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
+ },
+ { // image
+ nsGkAtoms::image,
+ roles::GRAPHIC,
+ 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
+ },
+ { // strong
+ nsGkAtoms::strong,
+ roles::STRONG,
+ kUseMapRole,
+ eNoValue,
+ eNoAction,
+ eNoLiveAttr,
+ 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
+ },
+ { // time
+ nsGkAtoms::time,
+ roles::TIME,
+ kUseMapRole,
+ eNoValue,
+ eNoAction,
+ eNoLiveAttr,
+ kNoReqStates
+ },
+ { // 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(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(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..30cc1f0814
--- /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<RefPtr<nsAtom>> mOverriddenAttrs;
+
+ const AttrArray* mAttrs;
+ uint32_t mAttrIdx;
+ uint32_t mAttrCount;
+ RefPtr<nsAtom> 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 <stdint.h>
+
+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..4018f09074
--- /dev/null
+++ b/accessible/base/AccAttributes.cpp
@@ -0,0 +1,270 @@
+/* -*- 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/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<nsAtom>& val) {
+ val->ToString(aValueString);
+ },
+ [&aValueString](const nsTArray<int32_t>& 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<nsString>& val) {
+ aValueString.Assign(*val);
+ },
+ [&aValueString](const RefPtr<AccAttributes>& val) {
+ aValueString.Assign(u"AccAttributes{...}");
+ },
+ [&aValueString](const uint64_t& val) { aValueString.AppendInt(val); },
+ [&aValueString](const UniquePtr<AccGroupInfo>& val) {
+ aValueString.Assign(u"AccGroupInfo{...}");
+ },
+ [&aValueString](const UniquePtr<gfx::Matrix4x4>& val) {
+ aValueString.AppendPrintf("Matrix4x4=%s", ToString(*val).c_str());
+ },
+ [&aValueString](const nsTArray<uint64_t>& 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<DeleteEntry>()) {
+ 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<UniquePtr<nsString>>()) {
+ // Because we store nsString in a UniquePtr, we must handle it specially
+ // so we compare the string and not the pointer.
+ if (!otherEntry->is<UniquePtr<nsString>>()) {
+ return false;
+ }
+ const auto& thisStr = iter.Data().as<UniquePtr<nsString>>();
+ const auto& otherStr = otherEntry->as<UniquePtr<nsString>>();
+ 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<nsAtom>& val) {
+ aDest->mData.InsertOrUpdate(iter.Key(), AsVariant(val));
+ },
+ [](const nsTArray<int32_t>& 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<nsString>& val) {
+ aDest->SetAttributeStringCopy(iter.Key(), *val);
+ },
+ [](const RefPtr<AccAttributes>& 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<AccGroupInfo>& val) {
+ MOZ_ASSERT_UNREACHABLE(
+ "Trying to copy an AccAttributes containing an AccGroupInfo");
+ },
+ [](const UniquePtr<gfx::Matrix4x4>& val) {
+ MOZ_ASSERT_UNREACHABLE(
+ "Trying to copy an AccAttributes containing a matrix");
+ },
+ [](const nsTArray<uint64_t>& 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<nsTArray<int32_t>>()) {
+ size += mValue->as<nsTArray<int32_t>>().ShallowSizeOfExcludingThis(
+ aMallocSizeOf);
+ } else if (mValue->is<UniquePtr<nsString>>()) {
+ // String data will never be shared.
+ size += mValue->as<UniquePtr<nsString>>()->SizeOfIncludingThisIfUnshared(
+ aMallocSizeOf);
+ } else if (mValue->is<RefPtr<AccAttributes>>()) {
+ size +=
+ mValue->as<RefPtr<AccAttributes>>()->SizeOfIncludingThis(aMallocSizeOf);
+ } else if (mValue->is<UniquePtr<AccGroupInfo>>()) {
+ size += mValue->as<UniquePtr<AccGroupInfo>>()->SizeOfIncludingThis(
+ aMallocSizeOf);
+ } else if (mValue->is<UniquePtr<gfx::Matrix4x4>>()) {
+ size += aMallocSizeOf(mValue->as<UniquePtr<gfx::Matrix4x4>>().get());
+ } else if (mValue->is<nsTArray<uint64_t>>()) {
+ size += mValue->as<nsTArray<uint64_t>>().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<RefPtr<nsAtom>>() || mValue->is<bool>() ||
+ mValue->is<float>() || mValue->is<double>() ||
+ mValue->is<int32_t>() || mValue->is<uint64_t>() ||
+ mValue->is<CSSCoord>() || mValue->is<FontSize>() ||
+ mValue->is<Color>() || mValue->is<DeleteEntry>());
+ }
+
+ return size;
+}
diff --git a/accessible/base/AccAttributes.h b/accessible/base/AccAttributes.h
new file mode 100644
index 0000000000..0d7610b358
--- /dev/null
+++ b/accessible/base/AccAttributes.h
@@ -0,0 +1,293 @@
+/* -*- 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 "nsStringFwd.h"
+#include "mozilla/gfx/Matrix.h"
+
+class nsVariant;
+
+namespace IPC {
+template <typename T>
+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<bool, float, double, int32_t, RefPtr<nsAtom>, nsTArray<int32_t>,
+ CSSCoord, FontSize, Color, DeleteEntry, UniquePtr<nsString>,
+ RefPtr<AccAttributes>, uint64_t, UniquePtr<AccGroupInfo>,
+ UniquePtr<gfx::Matrix4x4>, nsTArray<uint64_t>>;
+ static_assert(sizeof(AttrValueType) <= 16);
+ using AtomVariantMap = nsTHashMap<RefPtr<nsAtom>, AttrValueType>;
+
+ protected:
+ ~AccAttributes() = default;
+
+ public:
+ AccAttributes() = default;
+ AccAttributes(const AccAttributes&) = delete;
+ AccAttributes& operator=(const AccAttributes&) = delete;
+
+ NS_INLINE_DECL_REFCOUNTING(mozilla::a11y::AccAttributes)
+
+ template <typename T>
+ void SetAttribute(nsAtom* aAttrName, T&& aAttrValue) {
+ using ValType =
+ std::remove_const_t<std::remove_reference_t<decltype(aAttrValue)>>;
+ if constexpr (std::is_convertible_v<ValType, nsString>) {
+ static_assert(std::is_rvalue_reference_v<decltype(aAttrValue)>,
+ "Please only move strings into this function. To make a "
+ "copy, use SetAttributeStringCopy.");
+ UniquePtr<nsString> value = MakeUnique<nsString>(std::move(aAttrValue));
+ mData.InsertOrUpdate(aAttrName, AsVariant(std::move(value)));
+ } else if constexpr (std::is_same_v<ValType, gfx::Matrix4x4>) {
+ UniquePtr<gfx::Matrix4x4> value = MakeUnique<gfx::Matrix4x4>(aAttrValue);
+ mData.InsertOrUpdate(aAttrName, AsVariant(std::move(value)));
+ } else if constexpr (std::is_same_v<ValType, AccGroupInfo*>) {
+ UniquePtr<AccGroupInfo> value(aAttrValue);
+ mData.InsertOrUpdate(aAttrName, AsVariant(std::move(value)));
+ } else if constexpr (std::is_convertible_v<ValType, nsAtom*>) {
+ mData.InsertOrUpdate(aAttrName, AsVariant(RefPtr<nsAtom>(aAttrValue)));
+ } else {
+ mData.InsertOrUpdate(aAttrName, AsVariant(std::forward<T>(aAttrValue)));
+ }
+ }
+
+ void SetAttributeStringCopy(nsAtom* aAttrName, nsString aAttrValue) {
+ SetAttribute(aAttrName, std::move(aAttrValue));
+ }
+
+ template <typename T>
+ Maybe<const T&> GetAttribute(nsAtom* aAttrName) const {
+ if (auto value = mData.Lookup(aAttrName)) {
+ if constexpr (std::is_same_v<nsString, T>) {
+ if (value->is<UniquePtr<nsString>>()) {
+ const T& val = *(value->as<UniquePtr<nsString>>().get());
+ return SomeRef(val);
+ }
+ } else if constexpr (std::is_same_v<gfx::Matrix4x4, T>) {
+ if (value->is<UniquePtr<gfx::Matrix4x4>>()) {
+ const T& val = *(value->as<UniquePtr<gfx::Matrix4x4>>());
+ return SomeRef(val);
+ }
+ } else {
+ if (value->is<T>()) {
+ const T& val = value->as<T>();
+ return SomeRef(val);
+ }
+ }
+ }
+ return Nothing();
+ }
+
+ template <typename T>
+ RefPtr<const T> GetAttributeRefPtr(nsAtom* aAttrName) const {
+ if (auto value = mData.Lookup(aAttrName)) {
+ if (value->is<RefPtr<T>>()) {
+ RefPtr<const T> ref = value->as<RefPtr<T>>();
+ return ref;
+ }
+ }
+ return nullptr;
+ }
+
+ template <typename T>
+ Maybe<T&> GetMutableAttribute(nsAtom* aAttrName) const {
+ static_assert(std::is_same_v<nsTArray<int32_t>, T> ||
+ std::is_same_v<nsTArray<uint64_t>, T>,
+ "Only arrays should be mutable attributes");
+ if (auto value = mData.Lookup(aAttrName)) {
+ if (value->is<T>()) {
+ T& val = value->as<T>();
+ 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 <typename T>
+ Maybe<const T&> Value() const {
+ if constexpr (std::is_same_v<nsString, T>) {
+ if (mValue->is<UniquePtr<nsString>>()) {
+ const T& val = *(mValue->as<UniquePtr<nsString>>().get());
+ return SomeRef(val);
+ }
+ } else if constexpr (std::is_same_v<gfx::Matrix4x4, T>) {
+ if (mValue->is<UniquePtr<gfx::Matrix4x4>>()) {
+ const T& val = *(mValue->as<UniquePtr<gfx::Matrix4x4>>());
+ return SomeRef(val);
+ }
+ } else {
+ if (mValue->is<T>()) {
+ const T& val = mValue->as<T>();
+ 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<AccAttributes*>;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/AccEvent.cpp b/accessible/base/AccEvent.cpp
new file mode 100644
index 0000000000..1d1b4386f8
--- /dev/null
+++ b/accessible/base/AccEvent.cpp
@@ -0,0 +1,256 @@
+/* -*- 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 "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<bool>(eNoUserInput) == false &&
+ static_cast<bool>(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<uint32_t>(nsIAccessibleEvent::EVENT_TEXT_INSERTED)
+ : static_cast<uint32_t>(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<TextRange>* 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;
+ }
+}
+
+already_AddRefed<nsIAccessibleEvent> 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<nsIAccessibleEvent> 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<TextRange, 1> ranges;
+ tsc->SelectionRanges(&ranges);
+
+ nsCOMPtr<nsIMutableArray> 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::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..a4ff82916a
--- /dev/null
+++ b/accessible/base/AccEvent.h
@@ -0,0 +1,562 @@
+/* -*- 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<EIsFromUserInput>(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,
+ 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<LocalAccessible> 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<AccTreeMutationEvent> mNextEvent;
+ RefPtr<AccTreeMutationEvent> 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<LocalAccessible> mParent;
+ RefPtr<AccTextChangeEvent> 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<LocalAccessible> mNextSibling;
+ RefPtr<LocalAccessible> 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<a11y::TextRange>* aRanges) const;
+
+ private:
+ RefPtr<dom::Selection> 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<LocalAccessible> mWidget;
+ RefPtr<LocalAccessible> mItem;
+ SelChangeType mSelChangeType;
+ uint32_t mPreceedingCount;
+ AccSelChangeEvent* mPackedEvent;
+
+ friend class EventQueue;
+};
+
+/**
+ * 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<nsAtom> 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 <class Destination>
+ operator Destination*() {
+ if (!mRawPtr) return nullptr;
+
+ return mRawPtr->GetEventGroups() & (1U << Destination::kEventGroup)
+ ? static_cast<Destination*>(mRawPtr)
+ : nullptr;
+ }
+
+ private:
+ AccEvent* mRawPtr;
+};
+
+/**
+ * Return a new xpcom accessible event for the given internal one.
+ */
+already_AddRefed<nsIAccessibleEvent> 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<Accessible*>(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<Accessible*>(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<Accessible*>(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..a9afa14b8e
--- /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 "mozilla/a11y/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..badd34c0d5
--- /dev/null
+++ b/accessible/base/AccIterator.cpp
@@ -0,0 +1,360 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "LocalAccessible-inl.h"
+#include "XULTreeAccessible.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(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 <label for="[id]"> element which explicitly points to given
+ // element, or <label> ancestor which implicitly point to it.
+ LocalAccessible* label = nullptr;
+ while ((label = mRelIter.Next())) {
+ if (IsLabel(label)) {
+ return label;
+ }
+ }
+
+ // Ignore ancestor label on not widget accessible.
+ if (mLabelFilter == eSkipAncestorLabel || !mAcc->IsWidget()) return nullptr;
+
+ // Go up tree to get a name of ancestor label if there is one (an ancestor
+ // <label> implicitly points to us). Don't go up farther than form or
+ // document.
+ LocalAccessible* walkUp = mAcc->LocalParent();
+ while (walkUp && !walkUp->IsDoc()) {
+ nsIContent* walkUpEl = walkUp->GetContent();
+ if (IsLabel(walkUp) && !walkUpEl->AsElement()->HasAttr(nsGkAtoms::_for)) {
+ mLabelFilter = eSkipAncestorLabel; // prevent infinite loop
+ return walkUp;
+ }
+
+ if (walkUpEl->IsHTMLElement(nsGkAtoms::form)) break;
+
+ walkUp = walkUp->LocalParent();
+ }
+
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLOutputIterator
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLOutputIterator::HTMLOutputIterator(DocAccessible* aDocument,
+ nsIContent* aElement)
+ : mRelIter(aDocument, aElement, nsGkAtoms::_for) {}
+
+LocalAccessible* HTMLOutputIterator::Next() {
+ LocalAccessible* output = nullptr;
+ while ((output = mRelIter.Next())) {
+ if (output->GetContent()->IsHTMLElement(nsGkAtoms::output)) return output;
+ }
+
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULLabelIterator
+////////////////////////////////////////////////////////////////////////////////
+
+XULLabelIterator::XULLabelIterator(DocAccessible* aDocument,
+ nsIContent* aElement)
+ : mRelIter(aDocument, aElement, nsGkAtoms::control) {}
+
+LocalAccessible* XULLabelIterator::Next() {
+ LocalAccessible* label = nullptr;
+ while ((label = mRelIter.Next())) {
+ if (label->GetContent()->IsXULElement(nsGkAtoms::label)) return label;
+ }
+
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULDescriptionIterator
+////////////////////////////////////////////////////////////////////////////////
+
+XULDescriptionIterator::XULDescriptionIterator(DocAccessible* aDocument,
+ nsIContent* aElement)
+ : mRelIter(aDocument, aElement, nsGkAtoms::control) {}
+
+LocalAccessible* XULDescriptionIterator::Next() {
+ LocalAccessible* descr = nullptr;
+ while ((descr = mRelIter.Next())) {
+ if (descr->GetContent()->IsXULElement(nsGkAtoms::description)) return descr;
+ }
+
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// IDRefsIterator
+////////////////////////////////////////////////////////////////////////////////
+
+IDRefsIterator::IDRefsIterator(DocAccessible* aDoc, nsIContent* aContent,
+ nsAtom* aIDRefsAttr)
+ : mContent(aContent), mDoc(aDoc), mCurrIdx(0) {
+ if (mContent->IsElement()) {
+ mContent->AsElement()->GetAttr(aIDRefsAttr, mIDs);
+ }
+}
+
+const nsDependentSubstring IDRefsIterator::NextID() {
+ for (; mCurrIdx < mIDs.Length(); mCurrIdx++) {
+ if (!NS_IsAsciiWhitespace(mIDs[mCurrIdx])) break;
+ }
+
+ if (mCurrIdx >= mIDs.Length()) return nsDependentSubstring();
+
+ nsAString::index_type idStartIdx = mCurrIdx;
+ while (++mCurrIdx < mIDs.Length()) {
+ if (NS_IsAsciiWhitespace(mIDs[mCurrIdx])) break;
+ }
+
+ return Substring(mIDs, idStartIdx, mCurrIdx++ - idStartIdx);
+}
+
+nsIContent* IDRefsIterator::NextElem() {
+ while (true) {
+ const nsDependentSubstring id = NextID();
+ if (id.IsEmpty()) break;
+
+ nsIContent* refContent = GetElem(id);
+ if (refContent) return refContent;
+ }
+
+ return nullptr;
+}
+
+dom::Element* IDRefsIterator::GetElem(nsIContent* aContent,
+ const nsAString& aID) {
+ // Get elements in DOM tree by ID attribute if this is an explicit content.
+ // In case of bound element check its anonymous subtree.
+ if (!aContent->IsInNativeAnonymousSubtree()) {
+ dom::DocumentOrShadowRoot* docOrShadowRoot =
+ aContent->GetUncomposedDocOrConnectedShadowRoot();
+ if (docOrShadowRoot) {
+ dom::Element* refElm = docOrShadowRoot->GetElementById(aID);
+ if (refElm) {
+ return refElm;
+ }
+ }
+ }
+ return nullptr;
+}
+
+dom::Element* IDRefsIterator::GetElem(const nsDependentSubstring& aID) {
+ return GetElem(mContent, aID);
+}
+
+LocalAccessible* IDRefsIterator::Next() {
+ nsIContent* nextEl = nullptr;
+ while ((nextEl = NextElem())) {
+ LocalAccessible* acc = mDoc->GetAccessible(nextEl);
+ if (acc) {
+ return acc;
+ }
+ }
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// SingleAccIterator
+////////////////////////////////////////////////////////////////////////////////
+
+Accessible* SingleAccIterator::Next() {
+ Accessible* nextAcc = mAcc;
+ mAcc = nullptr;
+ if (!nextAcc) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(!nextAcc->IsLocal() || !nextAcc->AsLocal()->IsDefunct(),
+ "Iterator references defunct accessible?");
+ return nextAcc;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ItemIterator
+////////////////////////////////////////////////////////////////////////////////
+
+Accessible* ItemIterator::Next() {
+ if (mContainer) {
+ mAnchor = AccGroupInfo::FirstItemOf(mContainer);
+ mContainer = nullptr;
+ return mAnchor;
+ }
+
+ if (mAnchor) {
+ mAnchor = AccGroupInfo::NextItemTo(mAnchor);
+ }
+
+ return mAnchor;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeItemIterator
+////////////////////////////////////////////////////////////////////////////////
+
+XULTreeItemIterator::XULTreeItemIterator(const XULTreeAccessible* aXULTree,
+ nsITreeView* aTreeView,
+ int32_t aRowIdx)
+ : mXULTree(aXULTree),
+ mTreeView(aTreeView),
+ mRowCount(-1),
+ mContainerLevel(-1),
+ mCurrRowIdx(aRowIdx + 1) {
+ mTreeView->GetRowCount(&mRowCount);
+ if (aRowIdx != -1) mTreeView->GetLevel(aRowIdx, &mContainerLevel);
+}
+
+LocalAccessible* XULTreeItemIterator::Next() {
+ while (mCurrRowIdx < mRowCount) {
+ int32_t level = 0;
+ mTreeView->GetLevel(mCurrRowIdx, &level);
+
+ if (level == mContainerLevel + 1) {
+ return mXULTree->GetTreeItemAccessible(mCurrRowIdx++);
+ }
+
+ if (level <= mContainerLevel) { // got level up
+ mCurrRowIdx = mRowCount;
+ break;
+ }
+
+ mCurrRowIdx++;
+ }
+
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// RemoteAccIterator
+////////////////////////////////////////////////////////////////////////////////
+
+Accessible* RemoteAccIterator::Next() {
+ while (mIndex < mIds.Length()) {
+ uint64_t id = mIds[mIndex++];
+ Accessible* acc = mDoc->GetAccessible(id);
+ if (acc) {
+ return acc;
+ }
+ }
+ return nullptr;
+}
diff --git a/accessible/base/AccIterator.h b/accessible/base/AccIterator.h
new file mode 100644
index 0000000000..463e3e9d3e
--- /dev/null
+++ b/accessible/base/AccIterator.h
@@ -0,0 +1,328 @@
+/* -*- 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_AccIterator_h__
+#define mozilla_a11y_AccIterator_h__
+
+#include "Filters.h"
+#include "mozilla/a11y/DocAccessible.h"
+#include "nsTArray.h"
+
+#include <memory>
+
+class nsITreeView;
+
+namespace mozilla {
+namespace a11y {
+class DocAccessibleParent;
+
+/**
+ * AccIterable is a basic interface for iterators over accessibles.
+ */
+class AccIterable {
+ public:
+ virtual ~AccIterable() {}
+ virtual Accessible* Next() = 0;
+
+ private:
+ friend class Relation;
+ std::unique_ptr<AccIterable> mNextIter;
+};
+
+/**
+ * Allows to iterate through accessible children or subtree complying with
+ * filter function.
+ */
+class AccIterator : public AccIterable {
+ public:
+ AccIterator(const LocalAccessible* aRoot, filters::FilterFuncPtr aFilterFunc);
+ virtual ~AccIterator();
+
+ /**
+ * Return next accessible complying with filter function. Return the first
+ * accessible for the first time.
+ */
+ virtual LocalAccessible* Next() override;
+
+ private:
+ AccIterator();
+ AccIterator(const AccIterator&);
+ AccIterator& operator=(const AccIterator&);
+
+ struct IteratorState {
+ explicit IteratorState(const LocalAccessible* aParent,
+ IteratorState* mParentState = nullptr);
+
+ const LocalAccessible* mParent;
+ int32_t mIndex;
+ IteratorState* mParentState;
+ };
+
+ filters::FilterFuncPtr mFilterFunc;
+ IteratorState* mState;
+};
+
+/**
+ * Allows to traverse through related accessibles that are pointing to the given
+ * dependent accessible by relation attribute.
+ */
+class RelatedAccIterator : public AccIterable {
+ public:
+ /**
+ * Constructor.
+ *
+ * @param aDocument [in] the document accessible the related
+ * & accessibles belong to.
+ * @param aDependentContent [in] the content of dependent accessible that
+ * relations were requested for
+ * @param aRelAttr [in] relation attribute that relations are
+ * pointed by
+ */
+ RelatedAccIterator(DocAccessible* aDocument, nsIContent* aDependentContent,
+ nsAtom* aRelAttr);
+
+ virtual ~RelatedAccIterator() {}
+
+ /**
+ * Return next related accessible for the given dependent accessible.
+ */
+ virtual LocalAccessible* Next() override;
+
+ private:
+ RelatedAccIterator();
+ RelatedAccIterator(const RelatedAccIterator&);
+ RelatedAccIterator& operator=(const RelatedAccIterator&);
+
+ DocAccessible* mDocument;
+ nsAtom* mRelAttr;
+ DocAccessible::AttrRelProviders* mProviders;
+ uint32_t mIndex;
+};
+
+/**
+ * Used to iterate through HTML labels associated with the given accessible.
+ */
+class HTMLLabelIterator : public AccIterable {
+ public:
+ enum LabelFilter { eAllLabels, eSkipAncestorLabel };
+
+ HTMLLabelIterator(DocAccessible* aDocument,
+ const LocalAccessible* aAccessible,
+ LabelFilter aFilter = eAllLabels);
+
+ virtual ~HTMLLabelIterator() {}
+
+ /**
+ * Return next label accessible associated with the given element.
+ */
+ virtual LocalAccessible* Next() override;
+
+ private:
+ HTMLLabelIterator();
+ HTMLLabelIterator(const HTMLLabelIterator&);
+ HTMLLabelIterator& operator=(const HTMLLabelIterator&);
+
+ bool IsLabel(LocalAccessible* aLabel);
+
+ RelatedAccIterator mRelIter;
+ // XXX: replace it on weak reference (bug 678429), it's safe to use raw
+ // pointer now because iterators life cycle is short.
+ const LocalAccessible* mAcc;
+ LabelFilter mLabelFilter;
+};
+
+/**
+ * Used to iterate through HTML outputs associated with the given element.
+ */
+class HTMLOutputIterator : public AccIterable {
+ public:
+ HTMLOutputIterator(DocAccessible* aDocument, nsIContent* aElement);
+ virtual ~HTMLOutputIterator() {}
+
+ /**
+ * Return next output accessible associated with the given element.
+ */
+ virtual LocalAccessible* Next() override;
+
+ private:
+ HTMLOutputIterator();
+ HTMLOutputIterator(const HTMLOutputIterator&);
+ HTMLOutputIterator& operator=(const HTMLOutputIterator&);
+
+ RelatedAccIterator mRelIter;
+};
+
+/**
+ * Used to iterate through XUL labels associated with the given element.
+ */
+class XULLabelIterator : public AccIterable {
+ public:
+ XULLabelIterator(DocAccessible* aDocument, nsIContent* aElement);
+ virtual ~XULLabelIterator() {}
+
+ /**
+ * Return next label accessible associated with the given element.
+ */
+ virtual LocalAccessible* Next() override;
+
+ private:
+ XULLabelIterator();
+ XULLabelIterator(const XULLabelIterator&);
+ XULLabelIterator& operator=(const XULLabelIterator&);
+
+ RelatedAccIterator mRelIter;
+};
+
+/**
+ * Used to iterate through XUL descriptions associated with the given element.
+ */
+class XULDescriptionIterator : public AccIterable {
+ public:
+ XULDescriptionIterator(DocAccessible* aDocument, nsIContent* aElement);
+ virtual ~XULDescriptionIterator() {}
+
+ /**
+ * Return next description accessible associated with the given element.
+ */
+ virtual LocalAccessible* Next() override;
+
+ private:
+ XULDescriptionIterator();
+ XULDescriptionIterator(const XULDescriptionIterator&);
+ XULDescriptionIterator& operator=(const XULDescriptionIterator&);
+
+ RelatedAccIterator mRelIter;
+};
+
+/**
+ * Used to iterate through IDs, elements or accessibles pointed by IDRefs
+ * attribute. Note, any method used to iterate through IDs, elements, or
+ * accessibles moves iterator to next position.
+ */
+class IDRefsIterator : public AccIterable {
+ public:
+ IDRefsIterator(DocAccessible* aDoc, nsIContent* aContent,
+ nsAtom* aIDRefsAttr);
+ virtual ~IDRefsIterator() {}
+
+ /**
+ * Return next ID.
+ */
+ const nsDependentSubstring NextID();
+
+ /**
+ * Return next element.
+ */
+ nsIContent* NextElem();
+
+ /**
+ * Return the element with the given ID.
+ */
+ static dom::Element* GetElem(nsIContent* aContent, const nsAString& aID);
+ dom::Element* GetElem(const nsDependentSubstring& aID);
+
+ // AccIterable
+ virtual LocalAccessible* Next() override;
+
+ private:
+ IDRefsIterator();
+ IDRefsIterator(const IDRefsIterator&);
+ IDRefsIterator operator=(const IDRefsIterator&);
+
+ nsString mIDs;
+ nsIContent* mContent;
+ DocAccessible* mDoc;
+ nsAString::index_type mCurrIdx;
+};
+
+/**
+ * Iterator that points to a single accessible returning it on the first call
+ * to Next().
+ */
+class SingleAccIterator : public AccIterable {
+ public:
+ explicit SingleAccIterator(Accessible* aTarget) : mAcc(aTarget) {}
+ virtual ~SingleAccIterator() {}
+
+ virtual Accessible* Next() override;
+
+ private:
+ SingleAccIterator();
+ SingleAccIterator(const SingleAccIterator&);
+ SingleAccIterator& operator=(const SingleAccIterator&);
+
+ Accessible* mAcc;
+};
+
+/**
+ * Used to iterate items of the given item container.
+ */
+class ItemIterator : public AccIterable {
+ public:
+ explicit ItemIterator(const Accessible* aItemContainer)
+ : mContainer(aItemContainer), mAnchor(nullptr) {}
+
+ virtual Accessible* Next() override;
+
+ private:
+ ItemIterator() = delete;
+ ItemIterator(const ItemIterator&) = delete;
+ ItemIterator& operator=(const ItemIterator&) = delete;
+
+ const Accessible* mContainer;
+ Accessible* mAnchor;
+};
+
+/**
+ * Used to iterate through XUL tree items of the same level.
+ */
+class XULTreeItemIterator : public AccIterable {
+ public:
+ XULTreeItemIterator(const XULTreeAccessible* aXULTree, nsITreeView* aTreeView,
+ int32_t aRowIdx);
+ virtual ~XULTreeItemIterator() {}
+
+ virtual LocalAccessible* Next() override;
+
+ private:
+ XULTreeItemIterator() = delete;
+ XULTreeItemIterator(const XULTreeItemIterator&) = delete;
+ XULTreeItemIterator& operator=(const XULTreeItemIterator&) = delete;
+
+ const XULTreeAccessible* mXULTree;
+ nsITreeView* mTreeView;
+ int32_t mRowCount;
+ int32_t mContainerLevel;
+ int32_t mCurrRowIdx;
+};
+
+/**
+ * Used to iterate through a sequence of RemoteAccessibles supplied as an array
+ * of ids. Such id arrays are included in the RemoteAccessible cache.
+ */
+class RemoteAccIterator : public AccIterable {
+ public:
+ /**
+ * Construct with a reference to an array owned somewhere else; e.g. a
+ * RemoteAccessible cache.
+ */
+ RemoteAccIterator(const nsTArray<uint64_t>& aIds, DocAccessibleParent* aDoc)
+ : mIds(aIds), mDoc(aDoc), mIndex(0) {}
+
+ virtual ~RemoteAccIterator() = default;
+
+ virtual Accessible* Next() override;
+
+ private:
+ const nsTArray<uint64_t>& mIds;
+ DocAccessibleParent* mDoc;
+ uint32_t mIndex;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/AccTypes.h b/accessible/base/AccTypes.h
new file mode 100644
index 0000000000..3e9d88e486
--- /dev/null
+++ b/accessible/base/AccTypes.h
@@ -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/. */
+
+#ifndef mozilla_a11y_AccTypes_h
+#define mozilla_a11y_AccTypes_h
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Accessible object types. Each accessible class can have own type.
+ */
+enum AccType {
+ /**
+ * This set of types is used for accessible creation, keep them together in
+ * alphabetical order since they are used in switch statement.
+ */
+ eNoType,
+ eHTMLBRType,
+ eHTMLButtonType,
+ eHTMLCanvasType,
+ eHTMLCaptionType,
+ eHTMLCheckboxType,
+ eHTMLComboboxType,
+ eHTMLDateTimeFieldType,
+ eHTMLFileInputType,
+ eHTMLGroupboxType,
+ eHTMLHRType,
+ eHTMLImageMapType,
+ eHTMLLiType,
+ eHTMLSelectListType,
+ eHTMLMediaType,
+ eHTMLRadioButtonType,
+ eHTMLRangeType,
+ eHTMLSpinnerType,
+ eHTMLTableType,
+ eHTMLTableCellType,
+ eHTMLTableRowType,
+ eHTMLTextFieldType,
+ eHTMLTextPasswordFieldType,
+ eHyperTextType,
+ eImageType,
+ eOuterDocType,
+ eTextLeafType,
+
+ /**
+ * Other accessible types.
+ */
+ eApplicationType,
+ eHTMLLinkType,
+ eHTMLOptGroupType,
+ eImageMapType,
+ eMenuPopupType,
+ eProgressType,
+ eRootType,
+ eXULLabelType,
+ eXULListItemType,
+ eXULTabpanelsType,
+ eXULTooltipType,
+ eXULTreeType,
+
+ eLastAccType = eXULTreeType
+};
+
+/**
+ * Generic accessible type, different accessible classes can share the same
+ * type, the same accessible class can have several types.
+ */
+enum AccGenericType {
+ eAlert = 1 << 0,
+ eAutoCompletePopup = 1 << 1,
+ eButton = 1 << 2,
+ eCombobox = 1 << 3,
+ eDocument = 1 << 4,
+ eHyperText = 1 << 5,
+ eLandmark = 1 << 6,
+ eList = 1 << 7,
+ eListControl = 1 << 8,
+ eMenuButton = 1 << 9,
+ eSelect = 1 << 10,
+ eTable = 1 << 11,
+ eTableCell = 1 << 12,
+ eTableRow = 1 << 13,
+ eText = 1 << 14,
+ eNumericValue = 1 << 15,
+ eActionable = 1 << 16, // This is for remote accessibles
+
+ eLastAccGenericType = eActionable,
+ eAllGenericTypes = (eLastAccGenericType << 1) - 1
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_AccTypes_h
diff --git a/accessible/base/Asserts.cpp b/accessible/base/Asserts.cpp
new file mode 100644
index 0000000000..efdd733d9b
--- /dev/null
+++ b/accessible/base/Asserts.cpp
@@ -0,0 +1,29 @@
+/* -*- 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 "nsIAccessibleRelation.h"
+#include "nsIAccessibleRole.h"
+#include "mozilla/a11y/RelationType.h"
+#include "mozilla/a11y/Role.h"
+
+using namespace mozilla::a11y;
+
+#define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
+ msaaRole, ia2Role, androidClass, nameRule) \
+ static_assert( \
+ static_cast<uint32_t>(roles::geckoRole) == \
+ static_cast<uint32_t>(nsIAccessibleRole::ROLE_##geckoRole), \
+ "internal and xpcom roles differ!");
+#include "RoleMap.h"
+#undef ROLE
+
+#define RELATIONTYPE(geckoType, stringType, atkType, msaaType, ia2Type) \
+ static_assert( \
+ static_cast<uint32_t>(RelationType::geckoType) == \
+ static_cast<uint32_t>(nsIAccessibleRelation::RELATION_##geckoType), \
+ "internal and xpcom relations differ!");
+#include "RelationTypeMap.h"
+#undef RELATIONTYPE
diff --git a/accessible/base/CacheConstants.h b/accessible/base/CacheConstants.h
new file mode 100644
index 0000000000..eb5cc79f5e
--- /dev/null
+++ b/accessible/base/CacheConstants.h
@@ -0,0 +1,255 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set 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 _CacheConstants_h_
+#define _CacheConstants_h_
+
+#include "nsGkAtoms.h"
+#include "mozilla/a11y/RelationType.h"
+
+namespace mozilla {
+namespace a11y {
+
+class CacheDomain {
+ public:
+ static constexpr uint64_t NameAndDescription = ((uint64_t)0x1) << 0;
+ static constexpr uint64_t Value = ((uint64_t)0x1) << 1;
+ static constexpr uint64_t Bounds = ((uint64_t)0x1) << 2;
+ static constexpr uint64_t Resolution = ((uint64_t)0x1) << 3;
+ static constexpr uint64_t Text = ((uint64_t)0x1) << 4;
+ static constexpr uint64_t DOMNodeIDAndClass = ((uint64_t)0x1) << 5;
+ static constexpr uint64_t State = ((uint64_t)0x1) << 6;
+ static constexpr uint64_t GroupInfo = ((uint64_t)0x1) << 7;
+ static constexpr uint64_t Actions = ((uint64_t)0x1) << 8;
+ static constexpr uint64_t Style = ((uint64_t)0x1) << 9;
+ static constexpr uint64_t TransformMatrix = ((uint64_t)0x1) << 10;
+ static constexpr uint64_t ScrollPosition = ((uint64_t)0x1) << 11;
+ static constexpr uint64_t Table = ((uint64_t)0x1) << 12;
+ static constexpr uint64_t Spelling = ((uint64_t)0x1) << 13;
+ static constexpr uint64_t Viewport = ((uint64_t)0x1) << 14;
+ static constexpr uint64_t ARIA = ((uint64_t)0x1) << 15;
+ static constexpr uint64_t Relations = ((uint64_t)0x1) << 16;
+#ifdef XP_WIN
+ // Used for MathML.
+ static constexpr uint64_t InnerHTML = ((uint64_t)0x1) << 17;
+#endif
+ static constexpr uint64_t All = ~((uint64_t)0x0);
+};
+
+enum class CacheUpdateType {
+ /*
+ * An initial cache push of a loaded document or inserted subtree.
+ */
+ Initial,
+
+ /*
+ * An incremental cache push of one or more fields that have changed.
+ */
+ Update,
+};
+
+struct RelationData {
+ nsStaticAtom* const mAtom;
+ nsStaticAtom* const mValidTag;
+ RelationType mType;
+ RelationType mReverseType;
+};
+
+/**
+ * This array of RelationData lists our relation types (explicit and reverse)
+ * and the cache attribute atoms that store their targets. Attributes may
+ * describe different kinds of relations, depending on the element they
+ * originate on. For example, an <output> element's `for` attribute describes a
+ * CONTROLLER_FOR relation, while the `for` attribute of a <label> describes a
+ * LABEL_FOR relation. To ensure we process these attributes appropriately,
+ * RelationData.mValidTag contains the atom for the tag this attribute/relation
+ * type pairing is valid on. If the pairing is valid for all tag types, this
+ * field is null.
+ */
+static constexpr RelationData kRelationTypeAtoms[] = {
+ {nsGkAtoms::aria_labelledby, nullptr, RelationType::LABELLED_BY,
+ RelationType::LABEL_FOR},
+ {nsGkAtoms::_for, nsGkAtoms::label, RelationType::LABEL_FOR,
+ RelationType::LABELLED_BY},
+ {nsGkAtoms::aria_controls, nullptr, RelationType::CONTROLLER_FOR,
+ RelationType::CONTROLLED_BY},
+ {nsGkAtoms::_for, nsGkAtoms::output, RelationType::CONTROLLED_BY,
+ RelationType::CONTROLLER_FOR},
+ {nsGkAtoms::aria_describedby, nullptr, RelationType::DESCRIBED_BY,
+ RelationType::DESCRIPTION_FOR},
+ {nsGkAtoms::aria_flowto, nullptr, RelationType::FLOWS_TO,
+ RelationType::FLOWS_FROM},
+ {nsGkAtoms::aria_details, nullptr, RelationType::DETAILS,
+ RelationType::DETAILS_FOR},
+ {nsGkAtoms::aria_errormessage, nullptr, RelationType::ERRORMSG,
+ RelationType::ERRORMSG_FOR},
+};
+
+// The count of numbers needed to serialize an nsRect. This is used when
+// flattening character rects into an array of ints.
+constexpr int32_t kNumbersInRect = 4;
+
+/**
+ * RemoteAccessible cache keys.
+ * Cache keys are nsAtoms, but this is mostly an implementation detail. Rather
+ * than creating new atoms specific to the RemoteAccessible cache, we often
+ * reuse existing atoms which are a reasonably close match for the value we're
+ * caching, though the choices aren't always clear or intuitive. For clarity, we
+ * alias the cache keys to atoms below. Code dealing with the RemoteAccessible
+ * cache should generally use these aliases rather than using nsAtoms directly.
+ * There are two exceptions:
+ * 1. Some ARIA attributes are copied directly from the DOM node, so these
+ * aren't aliased. Specifically, aria-level, aria-posinset and aria-setsize
+ * are copied as separate cache keys as part of CacheDomain::GroupInfo.
+ * 2. Keys for relations are defined in kRelationTypeAtoms above.
+ */
+class CacheKey {
+ public:
+ // uint64_t, CacheDomain::Actions
+ // As returned by Accessible::AccessKey.
+ static constexpr nsStaticAtom* AccessKey = nsGkAtoms::accesskey;
+ // int32_t, no domain
+ static constexpr nsStaticAtom* AppUnitsPerDevPixel =
+ nsGkAtoms::_moz_device_pixel_ratio;
+ // AccAttributes, CacheDomain::ARIA
+ // ARIA attributes that are exposed as object attributes; i.e. returned in
+ // Accessible::Attributes.
+ static constexpr nsStaticAtom* ARIAAttributes = nsGkAtoms::aria;
+ // nsString, CacheUpdateType::Initial
+ // The ARIA role attribute if the role is unknown or if there are multiple
+ // roles.
+ static constexpr nsStaticAtom* ARIARole = nsGkAtoms::role;
+ // bool, CacheDomain::State
+ // The aria-selected attribute.
+ static constexpr nsStaticAtom* ARIASelected = nsGkAtoms::aria_selected;
+ // nsTArray<uint64_t>, CacheDomain::Table
+ // The explicit headers of an HTML table cell.
+ static constexpr nsStaticAtom* CellHeaders = nsGkAtoms::headers;
+ // int32_t, CacheDomain::Table
+ // The colspan of an HTML table cell.
+ static constexpr nsStaticAtom* ColSpan = nsGkAtoms::colspan;
+ // nsTArray<int32_t, 2>, CacheDomain::Bounds
+ // The offset from an OuterDocAccessible (iframe) to its embedded document.
+ static constexpr nsStaticAtom* CrossDocOffset = nsGkAtoms::crossorigin;
+ // nsAtom, CacheDomain::Style
+ // CSS display; block, inline, etc.
+ static constexpr nsStaticAtom* CSSDisplay = nsGkAtoms::display;
+ // nsAtom, CacheDomain::Style
+ // CSS overflow; e.g. hidden.
+ static constexpr nsStaticAtom* CSSOverflow = nsGkAtoms::overflow;
+ // nsAtom, CacheDomain::Style
+ // CSS position; e.g. fixed.
+ static constexpr nsStaticAtom* CssPosition = nsGkAtoms::position;
+ // nsString, CacheDomain::NameAndDescription
+ static constexpr nsStaticAtom* Description = nsGkAtoms::description;
+ // nsString, CacheDomain::Relations
+ // The "name" DOM attribute.
+ static constexpr nsStaticAtom* DOMName = nsGkAtoms::attributeName;
+ // nsAtom, CacheDomain::DOMNodeIDAndClass
+ // The "class" DOM attribute.
+ static constexpr nsStaticAtom* DOMNodeClass = nsGkAtoms::_class;
+ // nsAtom, CacheDomain::DOMNodeIDAndClass
+ static constexpr nsStaticAtom* DOMNodeID = nsGkAtoms::id;
+ // AccGroupInfo, no domain
+ static constexpr nsStaticAtom* GroupInfo = nsGkAtoms::group;
+ // nsTArray<int32_t>, no domain
+ // As returned by HyperTextAccessibleBase::CachedHyperTextOffsets.
+ static constexpr nsStaticAtom* HyperTextOffsets = nsGkAtoms::offset;
+ // bool, CacheDomain::Actions
+ // Whether this image has a longdesc.
+ static constexpr nsStaticAtom* HasLongdesc = nsGkAtoms::longdesc;
+ // nsString, CacheDomain::NameAndDescription
+ static constexpr nsStaticAtom* HTMLPlaceholder = nsGkAtoms::placeholder;
+#ifdef XP_WIN
+ // nsString, CacheDomain::InnerHTML
+ static constexpr nsStaticAtom* InnerHTML = nsGkAtoms::html;
+#endif
+ // nsAtom, CacheUpdateType::Initial
+ // The type of an <input> element; tel, email, etc.
+ static constexpr nsStaticAtom* InputType = nsGkAtoms::textInputType;
+ // bool, CacheDomain::Bounds
+ // Whether the Accessible is fully clipped.
+ static constexpr nsStaticAtom* IsClipped = nsGkAtoms::clip_rule;
+ // nsString, CacheUpdateType::Initial
+ static constexpr nsStaticAtom* MimeType = nsGkAtoms::headerContentType;
+ // double, CacheDomain::Value
+ static constexpr nsStaticAtom* MaxValue = nsGkAtoms::max;
+ // double, CacheDomain::Value
+ static constexpr nsStaticAtom* MinValue = nsGkAtoms::min;
+ // nsString, CacheDomain::NameAndDescription
+ static constexpr nsStaticAtom* Name = nsGkAtoms::name;
+ // ENameValueFlag, CacheDomain::NameAndDescription
+ // Returned by Accessible::Name.
+ static constexpr nsStaticAtom* NameValueFlag = nsGkAtoms::explicit_name;
+ // double, CacheDomain::Value
+ // The numeric value returned by Accessible::CurValue.
+ static constexpr nsStaticAtom* NumericValue = nsGkAtoms::value;
+ // float, CacheDomain::Style
+ static constexpr nsStaticAtom* Opacity = nsGkAtoms::opacity;
+ // nsTArray<int32_t, 4>, CacheDomain::Bounds
+ // The screen bounds relative to the parent Accessible
+ // as returned by LocalAccessible::ParentRelativeBounds.
+ static constexpr nsStaticAtom* ParentRelativeBounds =
+ nsGkAtoms::relativeBounds;
+ // nsAtom, CacheUpdateType::Initial
+ // The type of a popup (used for HTML popover).
+ static constexpr nsStaticAtom* PopupType = nsGkAtoms::ispopup;
+ // nsAtom, CacheDomain::Actions
+ static constexpr nsStaticAtom* PrimaryAction = nsGkAtoms::action;
+ // float, no domain
+ // Document resolution.
+ static constexpr nsStaticAtom* Resolution = nsGkAtoms::resolution;
+ // int32_t, CacheDomain::Table
+ // The rowspan of an HTML table cell.
+ static constexpr nsStaticAtom* RowSpan = nsGkAtoms::rowspan;
+ // nsTArray<int32_t, 2>, CacheDomain::ScrollPosition
+ static constexpr nsStaticAtom* ScrollPosition = nsGkAtoms::scrollPosition;
+ // nsTArray<int32_t>, CacheDomain::Spelling | CacheDomain::Text
+ // The offsets of spelling errors.
+ static constexpr nsStaticAtom* SpellingErrors = nsGkAtoms::spelling;
+ // nsString, CacheDomain::Value
+ // The src URL of images.
+ static constexpr nsStaticAtom* SrcURL = nsGkAtoms::src;
+ // uint64_t, CacheDomain::State
+ // As returned by Accessible::State.
+ static constexpr nsStaticAtom* State = nsGkAtoms::state;
+ // double, CacheDomain::Value
+ // The value step returned by Accessible::Step.
+ static constexpr nsStaticAtom* Step = nsGkAtoms::step;
+ // nsAtom, CacheUpdateType::Initial
+ // The tag name of the element.
+ static constexpr nsStaticAtom* TagName = nsGkAtoms::tag;
+ // bool, CacheDomain::Table
+ // Whether this is a layout table.
+ static constexpr nsStaticAtom* TableLayoutGuess = nsGkAtoms::layout_guess;
+ // nsString, CacheDomain::Text
+ // The text of TextLeafAccessibles.
+ static constexpr nsStaticAtom* Text = nsGkAtoms::text;
+ // AccAttributes, CacheDomain::Text
+ // Text attributes; font, etc.
+ static constexpr nsStaticAtom* TextAttributes = nsGkAtoms::style;
+ // nsTArray<int32_t, 4 * n>, CacheDomain::Text | CacheDomain::Bounds
+ // The bounds of each character in a text leaf.
+ static constexpr nsStaticAtom* TextBounds = nsGkAtoms::characterData;
+ // nsTArray<int32_t>, CacheDomain::Text | CacheDomain::Bounds
+ // The text offsets where new lines start.
+ static constexpr nsStaticAtom* TextLineStarts = nsGkAtoms::line;
+ // nsString, CacheDomain::Value
+ // The textual value returned by Accessible::Value (as opposed to
+ // the numeric value returned by Accessible::CurValue).
+ static constexpr nsStaticAtom* TextValue = nsGkAtoms::aria_valuetext;
+ // gfx::Matrix4x4, CacheDomain::TransformMatrix
+ static constexpr nsStaticAtom* TransformMatrix = nsGkAtoms::transform;
+ // nsTArray<uint64_t>, CacheDomain::Viewport
+ // The list of Accessibles in the viewport used for hit testing and on-screen
+ // determination.
+ static constexpr nsStaticAtom* Viewport = nsGkAtoms::viewport;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/CachedTableAccessible.cpp b/accessible/base/CachedTableAccessible.cpp
new file mode 100644
index 0000000000..e780bd2a89
--- /dev/null
+++ b/accessible/base/CachedTableAccessible.cpp
@@ -0,0 +1,429 @@
+/* -*- 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 "CachedTableAccessible.h"
+
+#include "AccIterator.h"
+#include "HTMLTableAccessible.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsAccUtils.h"
+#include "nsIAccessiblePivot.h"
+#include "Pivot.h"
+#include "RemoteAccessible.h"
+
+namespace mozilla::a11y {
+
+// Used to search for table descendants relevant to table structure.
+class TablePartRule : public PivotRule {
+ public:
+ virtual uint16_t Match(Accessible* aAcc) override {
+ role accRole = aAcc->Role();
+ if (accRole == roles::CAPTION || aAcc->IsTableCell()) {
+ return nsIAccessibleTraversalRule::FILTER_MATCH |
+ nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+ if (aAcc->IsTableRow()) {
+ return nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ if (aAcc->IsTable() ||
+ // Generic containers.
+ accRole == roles::TEXT || accRole == roles::TEXT_CONTAINER ||
+ accRole == roles::SECTION ||
+ // Row groups.
+ accRole == roles::GROUPING) {
+ // Walk inside these, but don't match them.
+ return nsIAccessibleTraversalRule::FILTER_IGNORE;
+ }
+ return nsIAccessibleTraversalRule::FILTER_IGNORE |
+ nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+};
+
+// The Accessible* keys should only be used for lookup. They should not be
+// dereferenced.
+using CachedTablesMap = nsTHashMap<Accessible*, CachedTableAccessible>;
+// We use a global map rather than a map in each document for three reasons:
+// 1. We don't have a common base class for local and remote documents.
+// 2. It avoids wasting memory in a document that doesn't have any tables.
+// 3. It allows the cache management to be encapsulated here in
+// CachedTableAccessible.
+static StaticAutoPtr<CachedTablesMap> sCachedTables;
+
+/* static */
+CachedTableAccessible* CachedTableAccessible::GetFrom(Accessible* aAcc) {
+ MOZ_ASSERT(aAcc->IsTable());
+ if (!sCachedTables) {
+ sCachedTables = new CachedTablesMap();
+ ClearOnShutdown(&sCachedTables);
+ }
+ return &sCachedTables->LookupOrInsertWith(
+ aAcc, [&] { return CachedTableAccessible(aAcc); });
+}
+
+/* static */
+void CachedTableAccessible::Invalidate(Accessible* aAcc) {
+ if (!sCachedTables) {
+ return;
+ }
+
+ if (Accessible* table = nsAccUtils::TableFor(aAcc)) {
+ // Destroy the instance (if any). We'll create a new one the next time it
+ // is requested.
+ sCachedTables->Remove(table);
+ }
+}
+
+CachedTableAccessible::CachedTableAccessible(Accessible* aAcc) : mAcc(aAcc) {
+ MOZ_ASSERT(mAcc);
+ // Build the cache. The cache can only be built once per instance. When it's
+ // invalidated, we just throw away the instance and create a new one when
+ // the cache is next needed.
+ int32_t rowIdx = -1;
+ uint32_t colIdx = 0;
+ // Maps a column index to the cell index of its previous implicit column
+ // header.
+ nsTHashMap<uint32_t, uint32_t> prevColHeaders;
+ Pivot pivot(mAcc);
+ TablePartRule rule;
+ for (Accessible* part = pivot.Next(mAcc, rule); part;
+ part = pivot.Next(part, rule)) {
+ role partRole = part->Role();
+ if (partRole == roles::CAPTION) {
+ // If there are multiple captions, use the first.
+ if (!mCaptionAccID) {
+ mCaptionAccID = part->ID();
+ }
+ continue;
+ }
+ if (part->IsTableRow()) {
+ ++rowIdx;
+ colIdx = 0;
+ // This might be an empty row, so ensure a row here, as our row count is
+ // based on the length of mRowColToCellIdx.
+ EnsureRow(rowIdx);
+ continue;
+ }
+ MOZ_ASSERT(part->IsTableCell());
+ if (rowIdx == -1) {
+ // We haven't created a row yet, so this cell must be outside a row.
+ continue;
+ }
+ // Check for a cell spanning multiple rows which already occupies this
+ // position. Keep incrementing until we find a vacant position.
+ for (;;) {
+ EnsureRowCol(rowIdx, colIdx);
+ if (mRowColToCellIdx[rowIdx][colIdx] == kNoCellIdx) {
+ // This position is not occupied.
+ break;
+ }
+ // This position is occupied.
+ ++colIdx;
+ }
+ // Create the cell.
+ uint32_t cellIdx = mCells.Length();
+ auto prevColHeader = prevColHeaders.MaybeGet(colIdx);
+ auto cell = mCells.AppendElement(
+ CachedTableCellAccessible(part->ID(), part, rowIdx, colIdx,
+ prevColHeader ? *prevColHeader : kNoCellIdx));
+ mAccToCellIdx.InsertOrUpdate(part, cellIdx);
+ // Update our row/col map.
+ // This cell might span multiple rows and/or columns. In that case, we need
+ // to occupy multiple coordinates in the row/col map.
+ uint32_t lastRowForCell =
+ static_cast<uint32_t>(rowIdx) + cell->RowExtent() - 1;
+ MOZ_ASSERT(lastRowForCell >= static_cast<uint32_t>(rowIdx));
+ uint32_t lastColForCell = colIdx + cell->ColExtent() - 1;
+ MOZ_ASSERT(lastColForCell >= colIdx);
+ for (uint32_t spannedRow = static_cast<uint32_t>(rowIdx);
+ spannedRow <= lastRowForCell; ++spannedRow) {
+ for (uint32_t spannedCol = colIdx; spannedCol <= lastColForCell;
+ ++spannedCol) {
+ EnsureRowCol(spannedRow, spannedCol);
+ auto& rowCol = mRowColToCellIdx[spannedRow][spannedCol];
+ // If a cell already occupies this position, it overlaps with this one;
+ // e.g. r1..2c2 and r2c1..2. In that case, we want to prefer the first
+ // cell.
+ if (rowCol == kNoCellIdx) {
+ rowCol = cellIdx;
+ }
+ }
+ }
+ if (partRole == roles::COLUMNHEADER) {
+ for (uint32_t spannedCol = colIdx; spannedCol <= lastColForCell;
+ ++spannedCol) {
+ prevColHeaders.InsertOrUpdate(spannedCol, cellIdx);
+ }
+ }
+ // Increment for the next cell.
+ colIdx = lastColForCell + 1;
+ }
+}
+
+void CachedTableAccessible::EnsureRow(uint32_t aRowIdx) {
+ if (mRowColToCellIdx.Length() <= aRowIdx) {
+ mRowColToCellIdx.AppendElements(aRowIdx - mRowColToCellIdx.Length() + 1);
+ }
+ MOZ_ASSERT(mRowColToCellIdx.Length() > aRowIdx);
+}
+
+void CachedTableAccessible::EnsureRowCol(uint32_t aRowIdx, uint32_t aColIdx) {
+ EnsureRow(aRowIdx);
+ auto& row = mRowColToCellIdx[aRowIdx];
+ if (mColCount <= aColIdx) {
+ mColCount = aColIdx + 1;
+ }
+ row.SetCapacity(mColCount);
+ for (uint32_t newCol = row.Length(); newCol <= aColIdx; ++newCol) {
+ // An entry doesn't yet exist for this column in this row.
+ row.AppendElement(kNoCellIdx);
+ }
+ MOZ_ASSERT(row.Length() > aColIdx);
+}
+
+Accessible* CachedTableAccessible::Caption() const {
+ if (mCaptionAccID) {
+ Accessible* caption = nsAccUtils::GetAccessibleByID(
+ nsAccUtils::DocumentFor(mAcc), mCaptionAccID);
+ MOZ_ASSERT(caption, "Dead caption Accessible!");
+ MOZ_ASSERT(caption->Role() == roles::CAPTION, "Caption has wrong role");
+ return caption;
+ }
+ return nullptr;
+}
+
+void CachedTableAccessible::Summary(nsString& aSummary) {
+ if (Caption()) {
+ // If there's a caption, we map caption to Name and summary to Description.
+ mAcc->Description(aSummary);
+ } else {
+ // If there's no caption, we map summary to Name.
+ mAcc->Name(aSummary);
+ }
+}
+
+Accessible* CachedTableAccessible::CellAt(uint32_t aRowIdx, uint32_t aColIdx) {
+ int32_t cellIdx = CellIndexAt(aRowIdx, aColIdx);
+ if (cellIdx == -1) {
+ return nullptr;
+ }
+ return mCells[cellIdx].Acc(mAcc);
+}
+
+bool CachedTableAccessible::IsProbablyLayoutTable() {
+ if (RemoteAccessible* remoteAcc = mAcc->AsRemote()) {
+ return remoteAcc->TableIsProbablyForLayout();
+ }
+ if (auto* localTable = HTMLTableAccessible::GetFrom(mAcc->AsLocal())) {
+ return localTable->IsProbablyLayoutTable();
+ }
+ return false;
+}
+
+/* static */
+CachedTableCellAccessible* CachedTableCellAccessible::GetFrom(
+ Accessible* aAcc) {
+ MOZ_ASSERT(aAcc->IsTableCell());
+ for (Accessible* parent = aAcc; parent; parent = parent->Parent()) {
+ if (parent->IsDoc()) {
+ break; // Never cross document boundaries.
+ }
+ TableAccessible* table = parent->AsTable();
+ if (!table) {
+ continue;
+ }
+ if (LocalAccessible* local = parent->AsLocal()) {
+ nsIContent* content = local->GetContent();
+ if (content && content->IsXULElement()) {
+ // XUL tables don't use CachedTableAccessible.
+ break;
+ }
+ }
+ // Non-XUL tables only use CachedTableAccessible.
+ auto* cachedTable = static_cast<CachedTableAccessible*>(table);
+ if (auto cellIdx = cachedTable->mAccToCellIdx.Lookup(aAcc)) {
+ return &cachedTable->mCells[*cellIdx];
+ }
+ // We found a table, but it doesn't know about this cell. This can happen
+ // if a cell is outside of a row due to authoring error. We must not search
+ // ancestor tables, since this cell's data is not valid there and vice
+ // versa.
+ break;
+ }
+ return nullptr;
+}
+
+Accessible* CachedTableCellAccessible::Acc(Accessible* aTableAcc) const {
+ Accessible* acc =
+ nsAccUtils::GetAccessibleByID(nsAccUtils::DocumentFor(aTableAcc), mAccID);
+ MOZ_DIAGNOSTIC_ASSERT(acc == mAcc, "Cell's cached mAcc is dead!");
+ return acc;
+}
+
+TableAccessible* CachedTableCellAccessible::Table() const {
+ for (const Accessible* acc = mAcc; acc; acc = acc->Parent()) {
+ // Since the caller has this cell, the table is already created, so it's
+ // okay to ignore the const restriction here.
+ if (TableAccessible* table = const_cast<Accessible*>(acc)->AsTable()) {
+ return table;
+ }
+ }
+ return nullptr;
+}
+
+uint32_t CachedTableCellAccessible::ColExtent() const {
+ if (RemoteAccessible* remoteAcc = mAcc->AsRemote()) {
+ if (remoteAcc->mCachedFields) {
+ if (auto colSpan = remoteAcc->mCachedFields->GetAttribute<int32_t>(
+ CacheKey::ColSpan)) {
+ MOZ_ASSERT(*colSpan > 0);
+ return *colSpan;
+ }
+ }
+ } else if (auto* cell = HTMLTableCellAccessible::GetFrom(mAcc->AsLocal())) {
+ // For HTML table cells, we must use the HTMLTableCellAccessible
+ // GetColExtent method rather than using the DOM attributes directly.
+ // This is because of things like rowspan="0" which depend on knowing
+ // about thead, tbody, etc., which is info we don't have in the a11y tree.
+ uint32_t colExtent = cell->ColExtent();
+ MOZ_ASSERT(colExtent > 0);
+ if (colExtent > 0) {
+ return colExtent;
+ }
+ }
+ return 1;
+}
+
+uint32_t CachedTableCellAccessible::RowExtent() const {
+ if (RemoteAccessible* remoteAcc = mAcc->AsRemote()) {
+ if (remoteAcc->mCachedFields) {
+ if (auto rowSpan = remoteAcc->mCachedFields->GetAttribute<int32_t>(
+ CacheKey::RowSpan)) {
+ MOZ_ASSERT(*rowSpan > 0);
+ return *rowSpan;
+ }
+ }
+ } else if (auto* cell = HTMLTableCellAccessible::GetFrom(mAcc->AsLocal())) {
+ // For HTML table cells, we must use the HTMLTableCellAccessible
+ // GetRowExtent method rather than using the DOM attributes directly.
+ // This is because of things like rowspan="0" which depend on knowing
+ // about thead, tbody, etc., which is info we don't have in the a11y tree.
+ uint32_t rowExtent = cell->RowExtent();
+ MOZ_ASSERT(rowExtent > 0);
+ if (rowExtent > 0) {
+ return rowExtent;
+ }
+ }
+ return 1;
+}
+
+UniquePtr<AccIterable> CachedTableCellAccessible::GetExplicitHeadersIterator() {
+ if (RemoteAccessible* remoteAcc = mAcc->AsRemote()) {
+ if (remoteAcc->mCachedFields) {
+ if (auto headers =
+ remoteAcc->mCachedFields->GetAttribute<nsTArray<uint64_t>>(
+ CacheKey::CellHeaders)) {
+ return MakeUnique<RemoteAccIterator>(*headers, remoteAcc->Document());
+ }
+ }
+ } else if (LocalAccessible* localAcc = mAcc->AsLocal()) {
+ return MakeUnique<IDRefsIterator>(
+ localAcc->Document(), localAcc->GetContent(), nsGkAtoms::headers);
+ }
+ return nullptr;
+}
+
+void CachedTableCellAccessible::ColHeaderCells(nsTArray<Accessible*>* aCells) {
+ auto* table = static_cast<CachedTableAccessible*>(Table());
+ if (!table) {
+ return;
+ }
+ if (auto iter = GetExplicitHeadersIterator()) {
+ while (Accessible* header = iter->Next()) {
+ role headerRole = header->Role();
+ if (headerRole == roles::COLUMNHEADER) {
+ aCells->AppendElement(header);
+ } else if (headerRole != roles::ROWHEADER) {
+ // Treat this cell as a column header only if it's in the same column.
+ if (auto cellIdx = table->mAccToCellIdx.Lookup(header)) {
+ CachedTableCellAccessible& cell = table->mCells[*cellIdx];
+ if (cell.ColIdx() == ColIdx()) {
+ aCells->AppendElement(header);
+ }
+ }
+ }
+ }
+ if (!aCells->IsEmpty()) {
+ return;
+ }
+ }
+ Accessible* doc = nsAccUtils::DocumentFor(table->AsAccessible());
+ // Each cell stores its previous implicit column header, effectively forming a
+ // linked list. We traverse that to get all the headers.
+ CachedTableCellAccessible* cell = this;
+ for (;;) {
+ if (cell->mPrevColHeaderCellIdx == kNoCellIdx) {
+ break; // No more headers.
+ }
+ cell = &table->mCells[cell->mPrevColHeaderCellIdx];
+ Accessible* cellAcc = nsAccUtils::GetAccessibleByID(doc, cell->mAccID);
+ aCells->AppendElement(cellAcc);
+ }
+}
+
+void CachedTableCellAccessible::RowHeaderCells(nsTArray<Accessible*>* aCells) {
+ auto* table = static_cast<CachedTableAccessible*>(Table());
+ if (!table) {
+ return;
+ }
+ if (auto iter = GetExplicitHeadersIterator()) {
+ while (Accessible* header = iter->Next()) {
+ role headerRole = header->Role();
+ if (headerRole == roles::ROWHEADER) {
+ aCells->AppendElement(header);
+ } else if (headerRole != roles::COLUMNHEADER) {
+ // Treat this cell as a row header only if it's in the same row.
+ if (auto cellIdx = table->mAccToCellIdx.Lookup(header)) {
+ CachedTableCellAccessible& cell = table->mCells[*cellIdx];
+ if (cell.RowIdx() == RowIdx()) {
+ aCells->AppendElement(header);
+ }
+ }
+ }
+ }
+ if (!aCells->IsEmpty()) {
+ return;
+ }
+ }
+ Accessible* doc = nsAccUtils::DocumentFor(table->AsAccessible());
+ // We don't cache implicit row headers because there are usually not that many
+ // cells per row. Get all the row headers on the row before this cell.
+ uint32_t row = RowIdx();
+ uint32_t thisCol = ColIdx();
+ for (uint32_t col = thisCol - 1; col < thisCol; --col) {
+ int32_t cellIdx = table->CellIndexAt(row, col);
+ if (cellIdx == -1) {
+ continue;
+ }
+ CachedTableCellAccessible& cell = table->mCells[cellIdx];
+ Accessible* cellAcc = nsAccUtils::GetAccessibleByID(doc, cell.mAccID);
+ MOZ_ASSERT(cellAcc);
+ // cell might span multiple columns. We don't want to visit it multiple
+ // times, so ensure col is set to cell's starting column.
+ col = cell.ColIdx();
+ if (cellAcc->Role() != roles::ROWHEADER) {
+ continue;
+ }
+ aCells->AppendElement(cellAcc);
+ }
+}
+
+bool CachedTableCellAccessible::Selected() {
+ return mAcc->State() & states::SELECTED;
+}
+
+} // namespace mozilla::a11y
diff --git a/accessible/base/CachedTableAccessible.h b/accessible/base/CachedTableAccessible.h
new file mode 100644
index 0000000000..7803343070
--- /dev/null
+++ b/accessible/base/CachedTableAccessible.h
@@ -0,0 +1,294 @@
+/* -*- 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 CACHED_TABLE_ACCESSIBLE_H
+#define CACHED_TABLE_ACCESSIBLE_H
+
+#include "mozilla/a11y/TableAccessible.h"
+#include "mozilla/a11y/TableCellAccessible.h"
+#include "mozilla/UniquePtr.h"
+#include "nsTHashMap.h"
+
+namespace mozilla::a11y {
+
+const uint32_t kNoCellIdx = UINT32_MAX;
+
+class AccIterable;
+
+class CachedTableAccessible;
+
+class CachedTableCellAccessible final : public TableCellAccessible {
+ public:
+ static CachedTableCellAccessible* GetFrom(Accessible* aAcc);
+
+ virtual TableAccessible* Table() const override;
+
+ virtual uint32_t ColIdx() const override {
+ return static_cast<int32_t>(mColIdx);
+ }
+
+ virtual uint32_t RowIdx() const override {
+ return static_cast<int32_t>(mRowIdx);
+ }
+
+ virtual uint32_t ColExtent() const override;
+
+ virtual uint32_t RowExtent() const override;
+
+ virtual void ColHeaderCells(nsTArray<Accessible*>* aCells) override;
+
+ virtual void RowHeaderCells(nsTArray<Accessible*>* aCells) override;
+
+ virtual bool Selected() override;
+
+ private:
+ CachedTableCellAccessible(uint64_t aAccID, Accessible* aAcc, uint32_t aRowIdx,
+ uint32_t aColIdx, uint32_t aPrevColHeaderCellIdx)
+ : mAccID(aAccID),
+ mAcc(aAcc),
+ mRowIdx(aRowIdx),
+ mColIdx(aColIdx),
+ mPrevColHeaderCellIdx(aPrevColHeaderCellIdx) {}
+
+ // Get the Accessible for this table cell given its ancestor table Accessible,
+ // verifying that the Accessible is valid.
+ Accessible* Acc(Accessible* aTableAcc) const;
+
+ UniquePtr<AccIterable> GetExplicitHeadersIterator();
+
+ uint64_t mAccID;
+ // CachedTableAccessible methods which fetch a cell should retrieve the
+ // Accessible using Acc() rather than using mAcc. We need mAcc for some
+ // methods because we can't fetch a document by id. It's okay to use mAcc in
+ // these methods because the caller has to hold the Accessible in order to
+ // call them.
+ Accessible* mAcc;
+ uint32_t mRowIdx;
+ uint32_t mColIdx;
+ // The cell index of the previous implicit column header.
+ uint32_t mPrevColHeaderCellIdx;
+ friend class CachedTableAccessible;
+};
+
+/**
+ * TableAccessible implementation which builds and queries a cache.
+ */
+class CachedTableAccessible final : public TableAccessible {
+ public:
+ static CachedTableAccessible* GetFrom(Accessible* aAcc);
+
+ /**
+ * This must be called whenever a table is destroyed or the structure of a
+ * table changes; e.g. cells wer added or removed. It can be called with
+ * either a table or a cell.
+ */
+ static void Invalidate(Accessible* aAcc);
+
+ virtual Accessible* Caption() const override;
+ virtual void Summary(nsString& aSummary) override;
+
+ virtual uint32_t ColCount() const override { return mColCount; }
+
+ virtual uint32_t RowCount() override { return mRowColToCellIdx.Length(); }
+
+ virtual int32_t ColIndexAt(uint32_t aCellIdx) override {
+ if (aCellIdx < mCells.Length()) {
+ return static_cast<int32_t>(mCells[aCellIdx].mColIdx);
+ }
+ return -1;
+ }
+
+ virtual int32_t RowIndexAt(uint32_t aCellIdx) override {
+ if (aCellIdx < mCells.Length()) {
+ return static_cast<int32_t>(mCells[aCellIdx].mRowIdx);
+ }
+ return -1;
+ }
+
+ virtual void RowAndColIndicesAt(uint32_t aCellIdx, int32_t* aRowIdx,
+ int32_t* aColIdx) override {
+ if (aCellIdx < mCells.Length()) {
+ CachedTableCellAccessible& cell = mCells[aCellIdx];
+ *aRowIdx = static_cast<int32_t>(cell.mRowIdx);
+ *aColIdx = static_cast<int32_t>(cell.mColIdx);
+ return;
+ }
+ *aRowIdx = -1;
+ *aColIdx = -1;
+ }
+
+ virtual uint32_t ColExtentAt(uint32_t aRowIdx, uint32_t aColIdx) override {
+ int32_t cellIdx = CellIndexAt(aRowIdx, aColIdx);
+ if (cellIdx == -1) {
+ return 0;
+ }
+ // Verify that the cell's Accessible is valid.
+ mCells[cellIdx].Acc(mAcc);
+ return mCells[cellIdx].ColExtent();
+ }
+
+ virtual uint32_t RowExtentAt(uint32_t aRowIdx, uint32_t aColIdx) override {
+ int32_t cellIdx = CellIndexAt(aRowIdx, aColIdx);
+ if (cellIdx == -1) {
+ return 0;
+ }
+ // Verify that the cell's Accessible is valid.
+ mCells[cellIdx].Acc(mAcc);
+ return mCells[cellIdx].RowExtent();
+ }
+
+ virtual int32_t CellIndexAt(uint32_t aRowIdx, uint32_t aColIdx) override {
+ if (aRowIdx < mRowColToCellIdx.Length()) {
+ auto& row = mRowColToCellIdx[aRowIdx];
+ if (aColIdx < row.Length()) {
+ uint32_t cellIdx = row[aColIdx];
+ if (cellIdx != kNoCellIdx) {
+ return static_cast<int32_t>(cellIdx);
+ }
+ }
+ }
+ return -1;
+ }
+
+ virtual Accessible* CellAt(uint32_t aRowIdx, uint32_t aColIdx) override;
+
+ virtual bool IsColSelected(uint32_t aColIdx) override {
+ bool selected = false;
+ for (uint32_t row = 0; row < RowCount(); ++row) {
+ selected = IsCellSelected(row, aColIdx);
+ if (!selected) {
+ break;
+ }
+ }
+ return selected;
+ }
+
+ virtual bool IsRowSelected(uint32_t aRowIdx) override {
+ bool selected = false;
+ for (uint32_t col = 0; col < mColCount; ++col) {
+ selected = IsCellSelected(aRowIdx, col);
+ if (!selected) {
+ break;
+ }
+ }
+ return selected;
+ }
+
+ virtual bool IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) override {
+ int32_t cellIdx = CellIndexAt(aRowIdx, aColIdx);
+ if (cellIdx == -1) {
+ return false;
+ }
+ // Verify that the cell's Accessible is valid.
+ mCells[cellIdx].Acc(mAcc);
+ return mCells[cellIdx].Selected();
+ }
+
+ virtual uint32_t SelectedCellCount() override {
+ uint32_t count = 0;
+ for (auto& cell : mCells) {
+ // Verify that the cell's Accessible is valid.
+ cell.Acc(mAcc);
+ if (cell.Selected()) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ virtual uint32_t SelectedColCount() override {
+ uint32_t count = 0;
+ for (uint32_t col = 0; col < mColCount; ++col) {
+ if (IsColSelected(col)) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ virtual uint32_t SelectedRowCount() override {
+ uint32_t count = 0;
+ for (uint32_t row = 0; row < RowCount(); ++row) {
+ if (IsRowSelected(row)) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ virtual void SelectedCells(nsTArray<Accessible*>* aCells) override {
+ for (auto& cell : mCells) {
+ // Verify that the cell's Accessible is valid.
+ Accessible* acc = cell.Acc(mAcc);
+ if (cell.Selected()) {
+ aCells->AppendElement(acc);
+ }
+ }
+ }
+
+ virtual void SelectedCellIndices(nsTArray<uint32_t>* aCells) override {
+ for (uint32_t idx = 0; idx < mCells.Length(); ++idx) {
+ CachedTableCellAccessible& cell = mCells[idx];
+ // Verify that the cell's Accessible is valid.
+ cell.Acc(mAcc);
+ if (cell.Selected()) {
+ aCells->AppendElement(idx);
+ }
+ }
+ }
+
+ virtual void SelectedColIndices(nsTArray<uint32_t>* aCols) override {
+ for (uint32_t col = 0; col < mColCount; ++col) {
+ if (IsColSelected(col)) {
+ aCols->AppendElement(col);
+ }
+ }
+ }
+
+ virtual void SelectedRowIndices(nsTArray<uint32_t>* aRows) override {
+ for (uint32_t row = 0; row < RowCount(); ++row) {
+ if (IsRowSelected(row)) {
+ aRows->AppendElement(row);
+ }
+ }
+ }
+
+ virtual Accessible* AsAccessible() override { return mAcc; }
+
+ virtual bool IsProbablyLayoutTable() override;
+
+ private:
+ explicit CachedTableAccessible(Accessible* aAcc);
+
+ // Ensure that the given row exists in our data structure, creating array
+ // elements as needed.
+ void EnsureRow(uint32_t aRowIdx);
+
+ // Ensure that the given row and column coordinate exists in our data
+ // structure, creating array elements as needed. A newly created coordinate
+ // will be set to kNoCellIdx.
+ void EnsureRowCol(uint32_t aRowIdx, uint32_t aColIdx);
+
+ Accessible* mAcc; // The table Accessible.
+ // We track the column count because it might not be uniform across rows in
+ // malformed tables.
+ uint32_t mColCount = 0;
+ // An array of cell instances. A cell index is an index into this array.
+ nsTArray<CachedTableCellAccessible> mCells;
+ // Maps row and column coordinates to cell indices.
+ nsTArray<nsTArray<uint32_t>> mRowColToCellIdx;
+ // Maps Accessibles to cell indexes to facilitate retrieval of a cell
+ // instance from a cell Accessible. The Accessible* keys should only be used
+ // for lookup. They should not be dereferenced.
+ nsTHashMap<Accessible*, uint32_t> mAccToCellIdx;
+ uint64_t mCaptionAccID = 0;
+
+ friend class CachedTableCellAccessible;
+};
+
+} // namespace mozilla::a11y
+
+#endif
diff --git a/accessible/base/DocManager.cpp b/accessible/base/DocManager.cpp
new file mode 100644
index 0000000000..b7a5203e40
--- /dev/null
+++ b/accessible/base/DocManager.cpp
@@ -0,0 +1,562 @@
+/* -*- 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/. */
+
+#include "DocManager.h"
+
+#include "ApplicationAccessible.h"
+#include "DocAccessible-inl.h"
+#include "DocAccessibleParent.h"
+#include "nsAccessibilityService.h"
+#include "Platform.h"
+#include "RootAccessibleWrap.h"
+
+#ifdef A11Y_LOG
+# include "Logging.h"
+#endif
+
+#include "mozilla/Components.h"
+#include "mozilla/EventListenerManager.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Event.h" // for Event
+#include "nsContentUtils.h"
+#include "nsDocShellLoadTypes.h"
+#include "nsIChannel.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIWebNavigation.h"
+#include "nsIWebProgress.h"
+#include "nsCoreUtils.h"
+#include "xpcAccessibleDocument.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+using namespace mozilla::dom;
+
+StaticAutoPtr<nsTArray<DocAccessibleParent*>> DocManager::sRemoteDocuments;
+StaticAutoPtr<nsRefPtrHashtable<nsPtrHashKey<const DocAccessibleParent>,
+ xpcAccessibleDocument>>
+ DocManager::sRemoteXPCDocumentCache;
+
+////////////////////////////////////////////////////////////////////////////////
+// DocManager
+////////////////////////////////////////////////////////////////////////////////
+
+DocManager::DocManager() : mDocAccessibleCache(2), mXPCDocumentCache(0) {}
+
+////////////////////////////////////////////////////////////////////////////////
+// DocManager public
+
+DocAccessible* DocManager::GetDocAccessible(Document* aDocument) {
+ if (!aDocument) return nullptr;
+
+ DocAccessible* docAcc = GetExistingDocAccessible(aDocument);
+ if (docAcc) return docAcc;
+
+ return CreateDocOrRootAccessible(aDocument);
+}
+
+DocAccessible* DocManager::GetDocAccessible(const PresShell* aPresShell) {
+ if (!aPresShell) {
+ return nullptr;
+ }
+
+ DocAccessible* doc = aPresShell->GetDocAccessible();
+ if (doc) {
+ return doc;
+ }
+
+ return GetDocAccessible(aPresShell->GetDocument());
+}
+
+LocalAccessible* DocManager::FindAccessibleInCache(nsINode* aNode) const {
+ for (const auto& docAccessible : mDocAccessibleCache.Values()) {
+ NS_ASSERTION(docAccessible,
+ "No doc accessible for the object in doc accessible cache!");
+
+ if (docAccessible) {
+ LocalAccessible* accessible = docAccessible->GetAccessible(aNode);
+ if (accessible) {
+ return accessible;
+ }
+ }
+ }
+ return nullptr;
+}
+
+void DocManager::RemoveFromXPCDocumentCache(DocAccessible* aDocument,
+ bool aAllowServiceShutdown) {
+ xpcAccessibleDocument* xpcDoc = mXPCDocumentCache.GetWeak(aDocument);
+ if (xpcDoc) {
+ xpcDoc->Shutdown();
+ mXPCDocumentCache.Remove(aDocument);
+
+ if (aAllowServiceShutdown && !HasXPCDocuments()) {
+ MaybeShutdownAccService(nsAccessibilityService::eXPCOM);
+ }
+ }
+}
+
+void DocManager::NotifyOfDocumentShutdown(DocAccessible* aDocument,
+ Document* aDOMDocument,
+ bool aAllowServiceShutdown) {
+ // We need to remove listeners in both cases, when document is being shutdown
+ // or when accessibility service is being shut down as well.
+ RemoveListeners(aDOMDocument);
+
+ // Document will already be removed when accessibility service is shutting
+ // down so we do not need to remove it twice.
+ if (nsAccessibilityService::IsShutdown()) {
+ return;
+ }
+
+ RemoveFromXPCDocumentCache(aDocument, aAllowServiceShutdown);
+ mDocAccessibleCache.Remove(aDOMDocument);
+}
+
+void DocManager::RemoveFromRemoteXPCDocumentCache(DocAccessibleParent* aDoc) {
+ xpcAccessibleDocument* doc = GetCachedXPCDocument(aDoc);
+ if (doc) {
+ doc->Shutdown();
+ sRemoteXPCDocumentCache->Remove(aDoc);
+ }
+
+ if (sRemoteXPCDocumentCache && sRemoteXPCDocumentCache->Count() == 0) {
+ MaybeShutdownAccService(nsAccessibilityService::eXPCOM);
+ }
+}
+
+void DocManager::NotifyOfRemoteDocShutdown(DocAccessibleParent* aDoc) {
+ RemoveFromRemoteXPCDocumentCache(aDoc);
+}
+
+xpcAccessibleDocument* DocManager::GetXPCDocument(DocAccessible* aDocument) {
+ if (!aDocument) return nullptr;
+
+ return mXPCDocumentCache.GetOrInsertNew(aDocument, aDocument);
+}
+
+xpcAccessibleDocument* DocManager::GetXPCDocument(DocAccessibleParent* aDoc) {
+ xpcAccessibleDocument* doc = GetCachedXPCDocument(aDoc);
+ if (doc) {
+ return doc;
+ }
+
+ if (!sRemoteXPCDocumentCache) {
+ sRemoteXPCDocumentCache =
+ new nsRefPtrHashtable<nsPtrHashKey<const DocAccessibleParent>,
+ xpcAccessibleDocument>;
+ ClearOnShutdown(&sRemoteXPCDocumentCache);
+ }
+
+ MOZ_ASSERT(!aDoc->IsShutdown(), "Adding a shutdown doc to remote XPC cache");
+ doc = new xpcAccessibleDocument(aDoc);
+ sRemoteXPCDocumentCache->InsertOrUpdate(aDoc, RefPtr{doc});
+
+ return doc;
+}
+
+#ifdef DEBUG
+bool DocManager::IsProcessingRefreshDriverNotification() const {
+ for (const auto& entry : mDocAccessibleCache) {
+ DocAccessible* docAccessible = entry.GetWeak();
+ NS_ASSERTION(docAccessible,
+ "No doc accessible for the object in doc accessible cache!");
+
+ if (docAccessible && docAccessible->mNotificationController &&
+ docAccessible->mNotificationController->IsUpdating()) {
+ return true;
+ }
+ }
+ return false;
+}
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+// DocManager protected
+
+bool DocManager::Init() {
+ nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service();
+
+ if (!progress) return false;
+
+ progress->AddProgressListener(static_cast<nsIWebProgressListener*>(this),
+ nsIWebProgress::NOTIFY_STATE_DOCUMENT);
+
+ return true;
+}
+
+void DocManager::Shutdown() {
+ nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service();
+
+ if (progress) {
+ progress->RemoveProgressListener(
+ static_cast<nsIWebProgressListener*>(this));
+ }
+
+ ClearDocCache();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_IMPL_ISUPPORTS(DocManager, nsIWebProgressListener, nsIDOMEventListener,
+ nsISupportsWeakReference)
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIWebProgressListener
+
+NS_IMETHODIMP
+DocManager::OnStateChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
+ uint32_t aStateFlags, nsresult aStatus) {
+ NS_ASSERTION(aStateFlags & STATE_IS_DOCUMENT, "Other notifications excluded");
+
+ if (nsAccessibilityService::IsShutdown() || !aWebProgress ||
+ (aStateFlags & (STATE_START | STATE_STOP)) == 0) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<mozIDOMWindowProxy> DOMWindow;
+ aWebProgress->GetDOMWindow(getter_AddRefs(DOMWindow));
+ NS_ENSURE_STATE(DOMWindow);
+
+ nsPIDOMWindowOuter* piWindow = nsPIDOMWindowOuter::From(DOMWindow);
+ MOZ_ASSERT(piWindow);
+
+ nsCOMPtr<Document> document = piWindow->GetDoc();
+ NS_ENSURE_STATE(document);
+
+ // Document was loaded.
+ if (aStateFlags & STATE_STOP) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocLoad)) {
+ logging::DocLoad("document loaded", aWebProgress, aRequest, aStateFlags);
+ }
+#endif
+
+ // Figure out an event type to notify the document has been loaded.
+ uint32_t eventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_STOPPED;
+
+ // Some XUL documents get start state and then stop state with failure
+ // status when everything is ok. Fire document load complete event in this
+ // case.
+ if (NS_SUCCEEDED(aStatus) || !document->IsContentDocument()) {
+ eventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE;
+ }
+
+ // If end consumer has been retargeted for loaded content then do not fire
+ // any event because it means no new document has been loaded, for example,
+ // it happens when user clicks on file link.
+ if (aRequest) {
+ uint32_t loadFlags = 0;
+ aRequest->GetLoadFlags(&loadFlags);
+ if (loadFlags & nsIChannel::LOAD_RETARGETED_DOCUMENT_URI) eventType = 0;
+ }
+
+ HandleDOMDocumentLoad(document, eventType);
+ return NS_OK;
+ }
+
+ // Document loading was started.
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocLoad)) {
+ logging::DocLoad("start document loading", aWebProgress, aRequest,
+ aStateFlags);
+ }
+#endif
+
+ DocAccessible* docAcc = GetExistingDocAccessible(document);
+ if (!docAcc) return NS_OK;
+
+ nsCOMPtr<nsIWebNavigation> webNav(do_GetInterface(DOMWindow));
+ nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(webNav));
+ NS_ENSURE_STATE(docShell);
+
+ bool isReloading = false;
+ uint32_t loadType;
+ docShell->GetLoadType(&loadType);
+ if (loadType == LOAD_RELOAD_NORMAL || loadType == LOAD_RELOAD_BYPASS_CACHE ||
+ loadType == LOAD_RELOAD_BYPASS_PROXY ||
+ loadType == LOAD_RELOAD_BYPASS_PROXY_AND_CACHE) {
+ isReloading = true;
+ }
+
+ docAcc->NotifyOfLoading(isReloading);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DocManager::OnProgressChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
+ int32_t aCurSelfProgress, int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DocManager::OnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
+ nsIURI* aLocation, uint32_t aFlags) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DocManager::OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
+ nsresult aStatus, const char16_t* aMessage) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DocManager::OnSecurityChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
+ uint32_t aState) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DocManager::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aEvent) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIDOMEventListener
+
+NS_IMETHODIMP
+DocManager::HandleEvent(Event* aEvent) {
+ nsAutoString type;
+ aEvent->GetType(type);
+
+ nsCOMPtr<Document> document = do_QueryInterface(aEvent->GetTarget());
+ NS_ASSERTION(document, "pagehide or DOMContentLoaded for non document!");
+ if (!document) return NS_OK;
+
+ if (type.EqualsLiteral("pagehide")) {
+ // 'pagehide' event is registered on every DOM document we create an
+ // accessible for, process the event for the target. This document
+ // accessible and all its sub document accessible are shutdown as result of
+ // processing.
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocDestroy)) {
+ logging::DocDestroy("received 'pagehide' event", document);
+ }
+#endif
+
+ // Shutdown this one and sub document accessibles.
+
+ // We're allowed to not remove listeners when accessible document is
+ // shutdown since we don't keep strong reference on chrome event target and
+ // listeners are removed automatically when chrome event target goes away.
+ DocAccessible* docAccessible = GetExistingDocAccessible(document);
+ if (docAccessible) docAccessible->Shutdown();
+
+ return NS_OK;
+ }
+
+ // XXX: handle error pages loading separately since they get neither
+ // webprogress notifications nor 'pageshow' event.
+ if (type.EqualsLiteral("DOMContentLoaded") &&
+ nsCoreUtils::IsErrorPage(document)) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocLoad)) {
+ logging::DocLoad("handled 'DOMContentLoaded' event", document);
+ }
+#endif
+
+ HandleDOMDocumentLoad(document,
+ nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE);
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DocManager private
+
+void DocManager::HandleDOMDocumentLoad(Document* aDocument,
+ uint32_t aLoadEventType) {
+ // Document accessible can be created before we were notified the DOM document
+ // was loaded completely. However if it's not created yet then create it.
+ DocAccessible* docAcc = GetExistingDocAccessible(aDocument);
+ if (!docAcc) {
+ docAcc = CreateDocOrRootAccessible(aDocument);
+ if (!docAcc) return;
+ }
+
+ docAcc->NotifyOfLoad(aLoadEventType);
+}
+
+void DocManager::AddListeners(Document* aDocument,
+ bool aAddDOMContentLoadedListener) {
+ nsPIDOMWindowOuter* window = aDocument->GetWindow();
+ EventTarget* target = window->GetChromeEventHandler();
+ EventListenerManager* elm = target->GetOrCreateListenerManager();
+ elm->AddEventListenerByType(this, u"pagehide"_ns, TrustedEventsAtCapture());
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocCreate)) {
+ logging::Text("added 'pagehide' listener");
+ }
+#endif
+
+ if (aAddDOMContentLoadedListener) {
+ elm->AddEventListenerByType(this, u"DOMContentLoaded"_ns,
+ TrustedEventsAtCapture());
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocCreate)) {
+ logging::Text("added 'DOMContentLoaded' listener");
+ }
+#endif
+ }
+}
+
+void DocManager::RemoveListeners(Document* aDocument) {
+ nsPIDOMWindowOuter* window = aDocument->GetWindow();
+ if (!window) return;
+
+ EventTarget* target = window->GetChromeEventHandler();
+ if (!target) return;
+
+ EventListenerManager* elm = target->GetOrCreateListenerManager();
+ elm->RemoveEventListenerByType(this, u"pagehide"_ns,
+ TrustedEventsAtCapture());
+
+ elm->RemoveEventListenerByType(this, u"DOMContentLoaded"_ns,
+ TrustedEventsAtCapture());
+}
+
+DocAccessible* DocManager::CreateDocOrRootAccessible(Document* aDocument) {
+ // Ignore hidden documents, resource documents, static clone
+ // (printing) documents and documents without a docshell.
+ if (!nsCoreUtils::IsDocumentVisibleConsideringInProcessAncestors(aDocument) ||
+ aDocument->IsResourceDoc() || aDocument->IsStaticDocument() ||
+ !aDocument->IsActive()) {
+ return nullptr;
+ }
+
+ nsIDocShell* docShell = aDocument->GetDocShell();
+ if (!docShell || docShell->IsInvisible()) {
+ return nullptr;
+ }
+
+ nsIWidget* widget = nsContentUtils::WidgetForDocument(aDocument);
+ if (!widget || widget->GetWindowType() == widget::WindowType::Invisible) {
+ return nullptr;
+ }
+
+ // Ignore documents without presshell. We must not ignore documents with no
+ // root frame because DOM focus can hit such documents and ignoring them would
+ // prevent a11y focus.
+ PresShell* presShell = aDocument->GetPresShell();
+ if (!presShell || presShell->IsDestroying()) {
+ return nullptr;
+ }
+
+ bool isRootDoc = nsCoreUtils::IsRootDocument(aDocument);
+
+ DocAccessible* parentDocAcc = nullptr;
+ if (!isRootDoc) {
+ // XXXaaronl: ideally we would traverse the presshell chain. Since there's
+ // no easy way to do that, we cheat and use the document hierarchy.
+ parentDocAcc = GetDocAccessible(aDocument->GetInProcessParentDocument());
+ NS_ASSERTION(parentDocAcc, "Can't create an accessible for the document!");
+ if (!parentDocAcc) return nullptr;
+ }
+
+ // We only create root accessibles for the true root, otherwise create a
+ // doc accessible.
+ RefPtr<DocAccessible> docAcc =
+ isRootDoc ? new RootAccessibleWrap(aDocument, presShell)
+ : new DocAccessibleWrap(aDocument, presShell);
+
+ // Cache the document accessible into document cache.
+ mDocAccessibleCache.InsertOrUpdate(aDocument, RefPtr{docAcc});
+
+ // Initialize the document accessible.
+ docAcc->Init();
+
+ // Bind the document to the tree.
+ if (isRootDoc) {
+ if (!ApplicationAcc()->AppendChild(docAcc)) {
+ docAcc->Shutdown();
+ return nullptr;
+ }
+
+ // Fire reorder event to notify new accessible document has been attached to
+ // the tree. The reorder event is delivered after the document tree is
+ // constructed because event processing and tree construction are done by
+ // the same document.
+ // Note: don't use AccReorderEvent to avoid coalsecense and special reorder
+ // events processing.
+ docAcc->FireDelayedEvent(nsIAccessibleEvent::EVENT_REORDER,
+ ApplicationAcc());
+
+ } else {
+ parentDocAcc->BindChildDocument(docAcc);
+ }
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocCreate)) {
+ logging::DocCreate("document creation finished", aDocument);
+ logging::Stack();
+ }
+#endif
+
+ AddListeners(aDocument, isRootDoc);
+ return docAcc;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DocManager static
+
+void DocManager::ClearDocCache() {
+ while (mDocAccessibleCache.Count() > 0) {
+ auto iter = mDocAccessibleCache.Iter();
+ MOZ_ASSERT(!iter.Done());
+ DocAccessible* docAcc = iter.UserData();
+ NS_ASSERTION(docAcc,
+ "No doc accessible for the object in doc accessible cache!");
+ if (docAcc) {
+ docAcc->Shutdown();
+ }
+
+ iter.Remove();
+ }
+
+ // Ensure that all xpcom accessible documents are shut down as well.
+ while (mXPCDocumentCache.Count() > 0) {
+ auto iter = mXPCDocumentCache.Iter();
+ MOZ_ASSERT(!iter.Done());
+ xpcAccessibleDocument* xpcDoc = iter.UserData();
+ NS_ASSERTION(xpcDoc, "No xpc doc for the object in xpc doc cache!");
+
+ if (xpcDoc) {
+ xpcDoc->Shutdown();
+ }
+
+ iter.Remove();
+ }
+}
+
+void DocManager::RemoteDocAdded(DocAccessibleParent* aDoc) {
+ if (!sRemoteDocuments) {
+ sRemoteDocuments = new nsTArray<DocAccessibleParent*>;
+ ClearOnShutdown(&sRemoteDocuments);
+ }
+
+ MOZ_ASSERT(!sRemoteDocuments->Contains(aDoc),
+ "How did we already have the doc!");
+ sRemoteDocuments->AppendElement(aDoc);
+ ProxyCreated(aDoc);
+}
+
+DocAccessible* mozilla::a11y::GetExistingDocAccessible(
+ const dom::Document* aDocument) {
+ PresShell* presShell = aDocument->GetPresShell();
+ return presShell ? presShell->GetDocAccessible() : nullptr;
+}
diff --git a/accessible/base/DocManager.h b/accessible/base/DocManager.h
new file mode 100644
index 0000000000..94da5d0a24
--- /dev/null
+++ b/accessible/base/DocManager.h
@@ -0,0 +1,193 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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_a11_DocManager_h_
+#define mozilla_a11_DocManager_h_
+
+#include "mozilla/ClearOnShutdown.h"
+#include "nsIDOMEventListener.h"
+#include "nsRefPtrHashtable.h"
+#include "nsIWebProgressListener.h"
+#include "nsWeakReference.h"
+#include "mozilla/StaticPtr.h"
+#include "nsINode.h"
+
+namespace mozilla::dom {
+class Document;
+}
+
+namespace mozilla {
+class PresShell;
+
+namespace a11y {
+
+class LocalAccessible;
+class DocAccessible;
+class xpcAccessibleDocument;
+class DocAccessibleParent;
+
+/**
+ * Manage the document accessible life cycle.
+ */
+class DocManager : public nsIWebProgressListener,
+ public nsIDOMEventListener,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIWEBPROGRESSLISTENER
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ /**
+ * Return document accessible for the given DOM node.
+ */
+ DocAccessible* GetDocAccessible(dom::Document* aDocument);
+
+ /**
+ * Return document accessible for the given presshell.
+ */
+ DocAccessible* GetDocAccessible(const PresShell* aPresShell);
+
+ /**
+ * Search through all document accessibles for an accessible with the given
+ * unique id.
+ */
+ LocalAccessible* FindAccessibleInCache(nsINode* aNode) const;
+
+ /**
+ * Called by document accessible when it gets shutdown.
+ * @param aAllowServiceShutdown true to shut down nsAccessibilityService
+ * if it is no longer required, false to prevent it.
+ */
+ void NotifyOfDocumentShutdown(DocAccessible* aDocument,
+ dom::Document* aDOMDocument,
+ bool aAllowServiceShutdown = true);
+
+ void RemoveFromXPCDocumentCache(DocAccessible* aDocument,
+ bool aAllowServiceShutdown = true);
+
+ /**
+ * Return XPCOM accessible document.
+ */
+ xpcAccessibleDocument* GetXPCDocument(DocAccessible* aDocument);
+ xpcAccessibleDocument* GetCachedXPCDocument(DocAccessible* aDocument) const {
+ return mXPCDocumentCache.GetWeak(aDocument);
+ }
+
+ /*
+ * Notification that a top level document in a content process has gone away.
+ */
+ static void RemoteDocShutdown(DocAccessibleParent* aDoc) {
+ DebugOnly<bool> result = sRemoteDocuments->RemoveElement(aDoc);
+ MOZ_ASSERT(result, "Why didn't we find the document!");
+ }
+
+ /*
+ * Notify of a new top level document in a content process.
+ */
+ static void RemoteDocAdded(DocAccessibleParent* aDoc);
+
+ static const nsTArray<DocAccessibleParent*>* TopLevelRemoteDocs() {
+ return sRemoteDocuments;
+ }
+
+ /**
+ * Remove the xpc document for a remote document if there is one.
+ */
+ static void NotifyOfRemoteDocShutdown(DocAccessibleParent* adoc);
+
+ static void RemoveFromRemoteXPCDocumentCache(DocAccessibleParent* aDoc);
+
+ /**
+ * Get a XPC document for a remote document.
+ */
+ static xpcAccessibleDocument* GetXPCDocument(DocAccessibleParent* aDoc);
+ static xpcAccessibleDocument* GetCachedXPCDocument(
+ const DocAccessibleParent* aDoc) {
+ return sRemoteXPCDocumentCache ? sRemoteXPCDocumentCache->GetWeak(aDoc)
+ : nullptr;
+ }
+
+#ifdef DEBUG
+ bool IsProcessingRefreshDriverNotification() const;
+#endif
+
+ protected:
+ DocManager();
+ virtual ~DocManager() = default;
+
+ /**
+ * Initialize the manager.
+ */
+ bool Init();
+
+ /**
+ * Shutdown the manager.
+ */
+ void Shutdown();
+
+ bool HasXPCDocuments() {
+ return mXPCDocumentCache.Count() > 0 ||
+ (sRemoteXPCDocumentCache && sRemoteXPCDocumentCache->Count() > 0);
+ }
+
+ private:
+ DocManager(const DocManager&);
+ DocManager& operator=(const DocManager&);
+
+ private:
+ /**
+ * Create an accessible document if it was't created and fire accessibility
+ * events if needed.
+ *
+ * @param aDocument [in] loaded DOM document
+ * @param aLoadEventType [in] specifies the event type to fire load event,
+ * if 0 then no event is fired
+ */
+ void HandleDOMDocumentLoad(dom::Document* aDocument, uint32_t aLoadEventType);
+
+ /**
+ * Add/remove 'pagehide' and 'DOMContentLoaded' event listeners.
+ */
+ void AddListeners(dom::Document* aDocument, bool aAddPageShowListener);
+ void RemoveListeners(dom::Document* aDocument);
+
+ /**
+ * Create document or root accessible.
+ */
+ DocAccessible* CreateDocOrRootAccessible(dom::Document* aDocument);
+
+ /**
+ * Clear the cache and shutdown the document accessibles.
+ */
+ void ClearDocCache();
+
+ typedef nsRefPtrHashtable<nsPtrHashKey<const dom::Document>, DocAccessible>
+ DocAccessibleHashtable;
+ DocAccessibleHashtable mDocAccessibleCache;
+
+ typedef nsRefPtrHashtable<nsPtrHashKey<const DocAccessible>,
+ xpcAccessibleDocument>
+ XPCDocumentHashtable;
+ XPCDocumentHashtable mXPCDocumentCache;
+ static StaticAutoPtr<nsRefPtrHashtable<
+ nsPtrHashKey<const DocAccessibleParent>, xpcAccessibleDocument>>
+ sRemoteXPCDocumentCache;
+
+ /*
+ * The list of remote top level documents.
+ */
+ static StaticAutoPtr<nsTArray<DocAccessibleParent*>> sRemoteDocuments;
+};
+
+/**
+ * Return the existing document accessible for the document if any.
+ * Note this returns the doc accessible for the primary pres shell if there is
+ * more than one.
+ */
+DocAccessible* GetExistingDocAccessible(const dom::Document* aDocument);
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11_DocManager_h_
diff --git a/accessible/base/EmbeddedObjCollector.cpp b/accessible/base/EmbeddedObjCollector.cpp
new file mode 100644
index 0000000000..cba5f2d9ba
--- /dev/null
+++ b/accessible/base/EmbeddedObjCollector.cpp
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EmbeddedObjCollector.h"
+
+#include "LocalAccessible.h"
+
+using namespace mozilla::a11y;
+
+uint32_t EmbeddedObjCollector::Count() {
+ EnsureNGetIndex(nullptr);
+ return mObjects.Length();
+}
+
+LocalAccessible* EmbeddedObjCollector::GetAccessibleAt(uint32_t aIndex) {
+ LocalAccessible* accessible = mObjects.SafeElementAt(aIndex, nullptr);
+ if (accessible) return accessible;
+
+ return EnsureNGetObject(aIndex);
+}
+
+LocalAccessible* EmbeddedObjCollector::EnsureNGetObject(uint32_t aIndex) {
+ uint32_t childCount = mRoot->ChildCount();
+ while (mRootChildIdx < childCount) {
+ LocalAccessible* child = mRoot->LocalChildAt(mRootChildIdx++);
+ if (child->IsText()) continue;
+
+ AppendObject(child);
+ if (mObjects.Length() - 1 == aIndex) return mObjects[aIndex];
+ }
+
+ return nullptr;
+}
+
+int32_t EmbeddedObjCollector::EnsureNGetIndex(LocalAccessible* aAccessible) {
+ uint32_t childCount = mRoot->ChildCount();
+ while (mRootChildIdx < childCount) {
+ LocalAccessible* child = mRoot->LocalChildAt(mRootChildIdx++);
+ if (child->IsText()) continue;
+
+ AppendObject(child);
+ if (child == aAccessible) return mObjects.Length() - 1;
+ }
+
+ return -1;
+}
+
+int32_t EmbeddedObjCollector::GetIndexAt(LocalAccessible* aAccessible) {
+ if (aAccessible->mParent != mRoot) return -1;
+
+ if (aAccessible->mIndexOfEmbeddedChild != -1) {
+ return aAccessible->mIndexOfEmbeddedChild;
+ }
+
+ return !aAccessible->IsText() ? EnsureNGetIndex(aAccessible) : -1;
+}
+
+void EmbeddedObjCollector::AppendObject(LocalAccessible* aAccessible) {
+ aAccessible->mIndexOfEmbeddedChild = mObjects.Length();
+ mObjects.AppendElement(aAccessible);
+}
diff --git a/accessible/base/EmbeddedObjCollector.h b/accessible/base/EmbeddedObjCollector.h
new file mode 100644
index 0000000000..a1d29e458f
--- /dev/null
+++ b/accessible/base/EmbeddedObjCollector.h
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_a11y_EmbeddedObjCollector_h__
+#define mozilla_a11y_EmbeddedObjCollector_h__
+
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace a11y {
+
+class LocalAccessible;
+
+/**
+ * Collect embedded objects. Provide quick access to accessible by index and
+ * vice versa.
+ */
+class EmbeddedObjCollector final {
+ public:
+ ~EmbeddedObjCollector() {}
+
+ /**
+ * Return index of the given accessible within the collection.
+ */
+ int32_t GetIndexAt(LocalAccessible* aAccessible);
+
+ /**
+ * Return accessible count within the collection.
+ */
+ uint32_t Count();
+
+ /**
+ * Return an accessible from the collection at the given index.
+ */
+ LocalAccessible* GetAccessibleAt(uint32_t aIndex);
+
+ protected:
+ /**
+ * Ensure accessible at the given index is stored and return it.
+ */
+ LocalAccessible* EnsureNGetObject(uint32_t aIndex);
+
+ /**
+ * Ensure index for the given accessible is stored and return it.
+ */
+ int32_t EnsureNGetIndex(LocalAccessible* aAccessible);
+
+ // Make sure it's used by LocalAccessible class only.
+ explicit EmbeddedObjCollector(LocalAccessible* aRoot)
+ : mRoot(aRoot), mRootChildIdx(0) {}
+
+ /**
+ * Append the object to collection.
+ */
+ void AppendObject(LocalAccessible* aAccessible);
+
+ friend class LocalAccessible;
+
+ LocalAccessible* mRoot;
+ uint32_t mRootChildIdx;
+ nsTArray<LocalAccessible*> mObjects;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/EventQueue.cpp b/accessible/base/EventQueue.cpp
new file mode 100644
index 0000000000..8a5e22cd48
--- /dev/null
+++ b/accessible/base/EventQueue.cpp
@@ -0,0 +1,436 @@
+/* -*- 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 "EventQueue.h"
+
+#include "LocalAccessible-inl.h"
+#include "nsEventShell.h"
+#include "DocAccessibleChild.h"
+#include "nsTextEquivUtils.h"
+#ifdef A11Y_LOG
+# include "Logging.h"
+#endif
+#include "Relation.h"
+
+namespace mozilla {
+namespace a11y {
+
+// Defines the number of selection add/remove events in the queue when they
+// aren't packed into single selection within event.
+const unsigned int kSelChangeCountToPack = 5;
+
+////////////////////////////////////////////////////////////////////////////////
+// EventQueue
+////////////////////////////////////////////////////////////////////////////////
+
+bool EventQueue::PushEvent(AccEvent* aEvent) {
+ NS_ASSERTION((aEvent->mAccessible && aEvent->mAccessible->IsApplication()) ||
+ aEvent->Document() == mDocument,
+ "Queued event belongs to another document!");
+
+ if (aEvent->mEventType == nsIAccessibleEvent::EVENT_FOCUS) {
+ mFocusEvent = aEvent;
+ return true;
+ }
+
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier, or change the return type to void.
+ mEvents.AppendElement(aEvent);
+
+ // Filter events.
+ CoalesceEvents();
+
+ if (aEvent->mEventRule != AccEvent::eDoNotEmit &&
+ (aEvent->mEventType == nsIAccessibleEvent::EVENT_NAME_CHANGE ||
+ aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_REMOVED ||
+ aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED)) {
+ PushNameOrDescriptionChange(aEvent);
+ }
+ return true;
+}
+
+bool EventQueue::PushNameOrDescriptionChange(AccEvent* aOrigEvent) {
+ // Fire name/description change event on parent or related LocalAccessible
+ // being labelled/described given that this event hasn't been coalesced, the
+ // dependent's name/description was calculated from this subtree, and the
+ // subtree was changed.
+ LocalAccessible* target = aOrigEvent->mAccessible;
+ // If the text of a text leaf changed without replacing the leaf, the only
+ // event we get is text inserted on the container. In this case, we might
+ // need to fire a name change event on the target itself.
+ const bool maybeTargetNameChanged =
+ (aOrigEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_REMOVED ||
+ aOrigEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED) &&
+ nsTextEquivUtils::HasNameRule(target, eNameFromSubtreeRule);
+ const bool doName = target->HasNameDependent() || maybeTargetNameChanged;
+ const bool doDesc = target->HasDescriptionDependent();
+ if (!doName && !doDesc) {
+ return false;
+ }
+ bool pushed = false;
+ bool nameCheckAncestor = true;
+ // Only continue traversing up the tree if it's possible that the parent
+ // LocalAccessible's name (or a LocalAccessible being labelled by this
+ // LocalAccessible or an ancestor) can depend on this LocalAccessible's name.
+ LocalAccessible* parent = target;
+ do {
+ // Test possible name dependent parent.
+ if (doName) {
+ if (nameCheckAncestor && (maybeTargetNameChanged || parent != target) &&
+ nsTextEquivUtils::HasNameRule(parent, eNameFromSubtreeRule)) {
+ nsAutoString name;
+ ENameValueFlag nameFlag = parent->Name(name);
+ // If name is obtained from subtree, fire name change event.
+ // HTML file inputs always get part of their name from the subtree, even
+ // if the author provided a name.
+ if (nameFlag == eNameFromSubtree || parent->IsHTMLFileInput()) {
+ RefPtr<AccEvent> nameChangeEvent =
+ new AccEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, parent);
+ pushed |= PushEvent(nameChangeEvent);
+ }
+ nameCheckAncestor = false;
+ }
+
+ Relation rel = parent->RelationByType(RelationType::LABEL_FOR);
+ while (LocalAccessible* relTarget = rel.LocalNext()) {
+ RefPtr<AccEvent> nameChangeEvent =
+ new AccEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, relTarget);
+ pushed |= PushEvent(nameChangeEvent);
+ }
+ }
+
+ if (doDesc) {
+ Relation rel = parent->RelationByType(RelationType::DESCRIPTION_FOR);
+ while (LocalAccessible* relTarget = rel.LocalNext()) {
+ RefPtr<AccEvent> descChangeEvent = new AccEvent(
+ nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, relTarget);
+ pushed |= PushEvent(descChangeEvent);
+ }
+ }
+
+ if (parent->IsDoc()) {
+ // Never cross document boundaries.
+ break;
+ }
+ parent = parent->LocalParent();
+ } while (parent &&
+ nsTextEquivUtils::HasNameRule(parent, eNameFromSubtreeIfReqRule));
+
+ return pushed;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// EventQueue: private
+
+void EventQueue::CoalesceEvents() {
+ NS_ASSERTION(mEvents.Length(), "There should be at least one pending event!");
+ uint32_t tail = mEvents.Length() - 1;
+ AccEvent* tailEvent = mEvents[tail];
+
+ switch (tailEvent->mEventRule) {
+ case AccEvent::eCoalesceReorder: {
+ DebugOnly<LocalAccessible*> target = tailEvent->mAccessible.get();
+ MOZ_ASSERT(
+ target->IsApplication() || target->IsOuterDoc() ||
+ target->IsXULTree(),
+ "Only app or outerdoc accessible reorder events are in the queue");
+ MOZ_ASSERT(tailEvent->GetEventType() == nsIAccessibleEvent::EVENT_REORDER,
+ "only reorder events should be queued");
+ break; // case eCoalesceReorder
+ }
+
+ case AccEvent::eCoalesceOfSameType: {
+ // Coalesce old events by newer event.
+ for (uint32_t index = tail - 1; index < tail; index--) {
+ AccEvent* accEvent = mEvents[index];
+ if (accEvent->mEventType == tailEvent->mEventType &&
+ accEvent->mEventRule == tailEvent->mEventRule) {
+ accEvent->mEventRule = AccEvent::eDoNotEmit;
+ return;
+ }
+ }
+ break; // case eCoalesceOfSameType
+ }
+
+ case AccEvent::eCoalesceSelectionChange: {
+ AccSelChangeEvent* tailSelChangeEvent = downcast_accEvent(tailEvent);
+ for (uint32_t index = tail - 1; index < tail; index--) {
+ AccEvent* thisEvent = mEvents[index];
+ if (thisEvent->mEventRule == tailEvent->mEventRule) {
+ AccSelChangeEvent* thisSelChangeEvent = downcast_accEvent(thisEvent);
+
+ // Coalesce selection change events within same control.
+ if (tailSelChangeEvent->mWidget == thisSelChangeEvent->mWidget) {
+ CoalesceSelChangeEvents(tailSelChangeEvent, thisSelChangeEvent,
+ index);
+ return;
+ }
+ }
+ }
+ break; // eCoalesceSelectionChange
+ }
+
+ case AccEvent::eCoalesceStateChange: {
+ // If state change event is duped then ignore previous event. If state
+ // change event is opposite to previous event then no event is emitted
+ // (accessible state wasn't changed).
+ for (uint32_t index = tail - 1; index < tail; index--) {
+ AccEvent* thisEvent = mEvents[index];
+ if (thisEvent->mEventRule != AccEvent::eDoNotEmit &&
+ thisEvent->mEventType == tailEvent->mEventType &&
+ thisEvent->mAccessible == tailEvent->mAccessible) {
+ AccStateChangeEvent* thisSCEvent = downcast_accEvent(thisEvent);
+ AccStateChangeEvent* tailSCEvent = downcast_accEvent(tailEvent);
+ if (thisSCEvent->mState == tailSCEvent->mState) {
+ thisEvent->mEventRule = AccEvent::eDoNotEmit;
+ if (thisSCEvent->mIsEnabled != tailSCEvent->mIsEnabled) {
+ tailEvent->mEventRule = AccEvent::eDoNotEmit;
+ }
+ }
+ }
+ }
+ break; // eCoalesceStateChange
+ }
+
+ case AccEvent::eCoalesceTextSelChange: {
+ // Coalesce older event by newer event for the same selection or target.
+ // Events for same selection may have different targets and vice versa one
+ // target may be pointed by different selections (for latter see
+ // bug 927159).
+ for (uint32_t index = tail - 1; index < tail; index--) {
+ AccEvent* thisEvent = mEvents[index];
+ if (thisEvent->mEventRule != AccEvent::eDoNotEmit &&
+ thisEvent->mEventType == tailEvent->mEventType) {
+ AccTextSelChangeEvent* thisTSCEvent = downcast_accEvent(thisEvent);
+ AccTextSelChangeEvent* tailTSCEvent = downcast_accEvent(tailEvent);
+ if (thisTSCEvent->mSel == tailTSCEvent->mSel ||
+ thisEvent->mAccessible == tailEvent->mAccessible) {
+ thisEvent->mEventRule = AccEvent::eDoNotEmit;
+ }
+ }
+ }
+ break; // eCoalesceTextSelChange
+ }
+
+ case AccEvent::eRemoveDupes: {
+ // Check for repeat events, coalesce newly appended event by more older
+ // event.
+ for (uint32_t index = tail - 1; index < tail; index--) {
+ AccEvent* accEvent = mEvents[index];
+ if (accEvent->mEventType == tailEvent->mEventType &&
+ accEvent->mEventRule == tailEvent->mEventRule &&
+ accEvent->mAccessible == tailEvent->mAccessible) {
+ tailEvent->mEventRule = AccEvent::eDoNotEmit;
+ return;
+ }
+ }
+ break; // case eRemoveDupes
+ }
+
+ default:
+ break; // case eAllowDupes, eDoNotEmit
+ } // switch
+}
+
+void EventQueue::CoalesceSelChangeEvents(AccSelChangeEvent* aTailEvent,
+ AccSelChangeEvent* aThisEvent,
+ uint32_t aThisIndex) {
+ aTailEvent->mPreceedingCount = aThisEvent->mPreceedingCount + 1;
+
+ // Pack all preceding events into single selection within event
+ // when we receive too much selection add/remove events.
+ if (aTailEvent->mPreceedingCount >= kSelChangeCountToPack) {
+ aTailEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION_WITHIN;
+ aTailEvent->mAccessible = aTailEvent->mWidget;
+ aThisEvent->mEventRule = AccEvent::eDoNotEmit;
+
+ // Do not emit any preceding selection events for same widget if they
+ // weren't coalesced yet.
+ if (aThisEvent->mEventType != nsIAccessibleEvent::EVENT_SELECTION_WITHIN) {
+ for (uint32_t jdx = aThisIndex - 1; jdx < aThisIndex; jdx--) {
+ AccEvent* prevEvent = mEvents[jdx];
+ if (prevEvent->mEventRule == aTailEvent->mEventRule) {
+ AccSelChangeEvent* prevSelChangeEvent = downcast_accEvent(prevEvent);
+ if (prevSelChangeEvent->mWidget == aTailEvent->mWidget) {
+ prevSelChangeEvent->mEventRule = AccEvent::eDoNotEmit;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+ // Pack sequential selection remove and selection add events into
+ // single selection change event.
+ if (aTailEvent->mPreceedingCount == 1 &&
+ aTailEvent->mItem != aThisEvent->mItem) {
+ if (aTailEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd &&
+ aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionRemove) {
+ aThisEvent->mEventRule = AccEvent::eDoNotEmit;
+ aTailEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION;
+ aTailEvent->mPackedEvent = aThisEvent;
+ return;
+ }
+
+ if (aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd &&
+ aTailEvent->mSelChangeType == AccSelChangeEvent::eSelectionRemove) {
+ aTailEvent->mEventRule = AccEvent::eDoNotEmit;
+ aThisEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION;
+ aThisEvent->mPackedEvent = aTailEvent;
+ return;
+ }
+ }
+
+ // Unpack the packed selection change event because we've got one
+ // more selection add/remove.
+ if (aThisEvent->mEventType == nsIAccessibleEvent::EVENT_SELECTION) {
+ if (aThisEvent->mPackedEvent) {
+ aThisEvent->mPackedEvent->mEventType =
+ aThisEvent->mPackedEvent->mSelChangeType ==
+ AccSelChangeEvent::eSelectionAdd
+ ? nsIAccessibleEvent::EVENT_SELECTION_ADD
+ : nsIAccessibleEvent::EVENT_SELECTION_REMOVE;
+
+ aThisEvent->mPackedEvent->mEventRule = AccEvent::eCoalesceSelectionChange;
+
+ aThisEvent->mPackedEvent = nullptr;
+ }
+
+ aThisEvent->mEventType =
+ aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd
+ ? nsIAccessibleEvent::EVENT_SELECTION_ADD
+ : nsIAccessibleEvent::EVENT_SELECTION_REMOVE;
+
+ return;
+ }
+
+ // Convert into selection add since control has single selection but other
+ // selection events for this control are queued.
+ if (aTailEvent->mEventType == nsIAccessibleEvent::EVENT_SELECTION) {
+ aTailEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION_ADD;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// EventQueue: event queue
+
+void EventQueue::ProcessEventQueue() {
+ // Process only currently queued events.
+ const nsTArray<RefPtr<AccEvent> > events = std::move(mEvents);
+ nsTArray<uint64_t> selectedIDs;
+ nsTArray<uint64_t> unselectedIDs;
+
+ uint32_t eventCount = events.Length();
+#ifdef A11Y_LOG
+ if ((eventCount > 0 || mFocusEvent) && logging::IsEnabled(logging::eEvents)) {
+ logging::MsgBegin("EVENTS", "events processing");
+ logging::Address("document", mDocument);
+ logging::MsgEnd();
+ }
+#endif
+
+ if (mFocusEvent) {
+ // Always fire a pending focus event before all other events. We do this for
+ // two reasons:
+ // 1. It prevents extraneous screen reader speech if the name, states, etc.
+ // of the currently focused item change at the same time as another item is
+ // focused. If aria-activedescendant is used, even setting
+ // aria-activedescendant before changing other properties results in the
+ // property change events being queued before the focus event because we
+ // process aria-activedescendant async.
+ // 2. It improves perceived performance. Focus changes should be reported as
+ // soon as possible, so clients should handle focus events before they spend
+ // time on anything else.
+ RefPtr<AccEvent> event = std::move(mFocusEvent);
+ if (!event->mAccessible->IsDefunct()) {
+ FocusMgr()->ProcessFocusEvent(event);
+ }
+ }
+
+ for (uint32_t idx = 0; idx < eventCount; idx++) {
+ AccEvent* event = events[idx];
+ uint32_t eventType = event->mEventType;
+ LocalAccessible* target = event->GetAccessible();
+ if (!target || target->IsDefunct()) {
+ continue;
+ }
+
+ // Collect select changes
+ if (IPCAccessibilityActive()) {
+ if ((event->mEventRule == AccEvent::eDoNotEmit &&
+ (eventType == nsIAccessibleEvent::EVENT_SELECTION_ADD ||
+ eventType == nsIAccessibleEvent::EVENT_SELECTION_REMOVE ||
+ eventType == nsIAccessibleEvent::EVENT_SELECTION)) ||
+ eventType == nsIAccessibleEvent::EVENT_SELECTION_WITHIN) {
+ // The selection even was either dropped or morphed to a
+ // selection-within. We need to collect the items from all these events
+ // and manually push their new state to the parent process.
+ AccSelChangeEvent* selChangeEvent = downcast_accEvent(event);
+ LocalAccessible* item = selChangeEvent->mItem;
+ if (!item->IsDefunct()) {
+ uint64_t itemID =
+ item->IsDoc() ? 0 : reinterpret_cast<uint64_t>(item->UniqueID());
+ bool selected = selChangeEvent->mSelChangeType ==
+ AccSelChangeEvent::eSelectionAdd;
+ if (selected) {
+ selectedIDs.AppendElement(itemID);
+ } else {
+ unselectedIDs.AppendElement(itemID);
+ }
+ }
+ }
+ }
+
+ if (event->mEventRule == AccEvent::eDoNotEmit) {
+ continue;
+ }
+
+ // Dispatch caret moved and text selection change events.
+ if (eventType == nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED) {
+ SelectionMgr()->ProcessTextSelChangeEvent(event);
+ continue;
+ }
+
+ // Fire selected state change events in support to selection events.
+ if (eventType == nsIAccessibleEvent::EVENT_SELECTION_ADD) {
+ nsEventShell::FireEvent(event->mAccessible, states::SELECTED, true,
+ event->mIsFromUserInput);
+
+ } else if (eventType == nsIAccessibleEvent::EVENT_SELECTION_REMOVE) {
+ nsEventShell::FireEvent(event->mAccessible, states::SELECTED, false,
+ event->mIsFromUserInput);
+
+ } else if (eventType == nsIAccessibleEvent::EVENT_SELECTION) {
+ AccSelChangeEvent* selChangeEvent = downcast_accEvent(event);
+ nsEventShell::FireEvent(
+ event->mAccessible, states::SELECTED,
+ (selChangeEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd),
+ event->mIsFromUserInput);
+
+ if (selChangeEvent->mPackedEvent) {
+ nsEventShell::FireEvent(selChangeEvent->mPackedEvent->mAccessible,
+ states::SELECTED,
+ (selChangeEvent->mPackedEvent->mSelChangeType ==
+ AccSelChangeEvent::eSelectionAdd),
+ selChangeEvent->mPackedEvent->mIsFromUserInput);
+ }
+ }
+
+ nsEventShell::FireEvent(event);
+
+ if (!mDocument) {
+ return;
+ }
+ }
+
+ if (mDocument && IPCAccessibilityActive() &&
+ (!selectedIDs.IsEmpty() || !unselectedIDs.IsEmpty())) {
+ DocAccessibleChild* ipcDoc = mDocument->IPCDoc();
+ ipcDoc->SendSelectedAccessiblesChanged(selectedIDs, unselectedIDs);
+ }
+}
+
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/base/EventQueue.h b/accessible/base/EventQueue.h
new file mode 100644
index 0000000000..87bcf89340
--- /dev/null
+++ b/accessible/base/EventQueue.h
@@ -0,0 +1,83 @@
+/* -*- 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_EventQueue_h_
+#define mozilla_a11y_EventQueue_h_
+
+#include "AccEvent.h"
+#include "mozilla/Assertions.h"
+
+namespace mozilla {
+namespace a11y {
+
+class DocAccessible;
+
+/**
+ * Used to organize and coalesce pending events.
+ */
+class EventQueue {
+ protected:
+ explicit EventQueue(DocAccessible* aDocument) : mDocument(aDocument) {
+ MOZ_ASSERT(mDocument,
+ "There's no point creating an event queue for a null document");
+ }
+
+ /**
+ * Put an accessible event into the queue to process it later.
+ */
+ bool PushEvent(AccEvent* aEvent);
+
+ /**
+ * Puts name and/or description change events into the queue, if needed.
+ */
+ bool PushNameOrDescriptionChange(AccEvent* aOrigEvent);
+
+ /**
+ * Process events from the queue and fires events.
+ */
+ void ProcessEventQueue();
+
+ private:
+ EventQueue(const EventQueue&) = delete;
+ EventQueue& operator=(const EventQueue&) = delete;
+
+ // Event queue processing
+ /**
+ * Coalesce redundant events from the queue.
+ */
+ void CoalesceEvents();
+
+ /**
+ * Coalesce events from the same subtree.
+ */
+ void CoalesceReorderEvents(AccEvent* aTailEvent);
+
+ /**
+ * Coalesce two selection change events within the same select control.
+ */
+ void CoalesceSelChangeEvents(AccSelChangeEvent* aTailEvent,
+ AccSelChangeEvent* aThisEvent,
+ uint32_t aThisIndex);
+
+ protected:
+ /**
+ * The document accessible reference owning this queue.
+ */
+ DocAccessible* mDocument;
+
+ /**
+ * Pending events array. Don't make this an AutoTArray; we use
+ * SwapElements() on it.
+ */
+ nsTArray<RefPtr<AccEvent>> mEvents;
+
+ // Pending focus event.
+ RefPtr<AccEvent> mFocusEvent;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_EventQueue_h_
diff --git a/accessible/base/EventTree.cpp b/accessible/base/EventTree.cpp
new file mode 100644
index 0000000000..29cf66e493
--- /dev/null
+++ b/accessible/base/EventTree.cpp
@@ -0,0 +1,99 @@
+/* -*- 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 "EventTree.h"
+
+#include "EmbeddedObjCollector.h"
+#include "NotificationController.h"
+#ifdef A11Y_LOG
+# include "Logging.h"
+#endif
+
+#include "mozilla/UniquePtr.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// TreeMutation class
+
+TreeMutation::TreeMutation(LocalAccessible* aParent, bool aNoEvents)
+ : mParent(aParent),
+ mStartIdx(UINT32_MAX),
+ mStateFlagsCopy(mParent->mStateFlags),
+ mQueueEvents(!aNoEvents) {
+#ifdef DEBUG
+ mIsDone = false;
+#endif
+
+ mParent->mStateFlags |= LocalAccessible::eKidsMutating;
+}
+
+TreeMutation::~TreeMutation() {
+ MOZ_ASSERT(mIsDone, "Done() must be called explicitly");
+}
+
+void TreeMutation::AfterInsertion(LocalAccessible* aChild) {
+ MOZ_ASSERT(aChild->LocalParent() == mParent);
+
+ if (static_cast<uint32_t>(aChild->mIndexInParent) < mStartIdx) {
+ mStartIdx = aChild->mIndexInParent + 1;
+ }
+
+ if (!mQueueEvents) {
+ return;
+ }
+
+ RefPtr<AccShowEvent> ev = new AccShowEvent(aChild);
+ DebugOnly<bool> added = Controller()->QueueMutationEvent(ev);
+ MOZ_ASSERT(added);
+ aChild->SetShowEventTarget(true);
+}
+
+void TreeMutation::BeforeRemoval(LocalAccessible* aChild, bool aNoShutdown) {
+ MOZ_ASSERT(aChild->LocalParent() == mParent);
+
+ if (static_cast<uint32_t>(aChild->mIndexInParent) < mStartIdx) {
+ mStartIdx = aChild->mIndexInParent;
+ }
+
+ if (!mQueueEvents) {
+ return;
+ }
+
+ RefPtr<AccHideEvent> ev = new AccHideEvent(aChild, !aNoShutdown);
+ if (Controller()->QueueMutationEvent(ev)) {
+ aChild->SetHideEventTarget(true);
+ }
+}
+
+void TreeMutation::Done() {
+ MOZ_ASSERT(mParent->mStateFlags & LocalAccessible::eKidsMutating);
+ mParent->mStateFlags &= ~LocalAccessible::eKidsMutating;
+
+ uint32_t length = mParent->mChildren.Length();
+#ifdef DEBUG
+ for (uint32_t idx = 0; idx < mStartIdx && idx < length; idx++) {
+ MOZ_ASSERT(
+ mParent->mChildren[idx]->mIndexInParent == static_cast<int32_t>(idx),
+ "Wrong index detected");
+ }
+#endif
+
+ for (uint32_t idx = mStartIdx; idx < length; idx++) {
+ mParent->mChildren[idx]->mIndexOfEmbeddedChild = -1;
+ }
+
+ for (uint32_t idx = 0; idx < length; idx++) {
+ mParent->mChildren[idx]->mStateFlags |= LocalAccessible::eGroupInfoDirty;
+ }
+
+ mParent->mEmbeddedObjCollector = nullptr;
+ mParent->mStateFlags |= mStateFlagsCopy & LocalAccessible::eKidsMutating;
+
+#ifdef DEBUG
+ mIsDone = true;
+#endif
+}
diff --git a/accessible/base/EventTree.h b/accessible/base/EventTree.h
new file mode 100644
index 0000000000..bd9976b4fb
--- /dev/null
+++ b/accessible/base/EventTree.h
@@ -0,0 +1,61 @@
+/* -*- 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_EventTree_h_
+#define mozilla_a11y_EventTree_h_
+
+#include "AccEvent.h"
+#include "LocalAccessible.h"
+
+#include "mozilla/a11y/DocAccessible.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+namespace a11y {
+
+class NotificationController;
+
+/**
+ * This class makes sure required tasks are done before and after tree
+ * mutations. Currently this only includes group info invalidation. You must
+ * have an object of this class on the stack when calling methods that mutate
+ * the accessible tree.
+ */
+class TreeMutation final {
+ public:
+ static const bool kNoEvents = true;
+ static const bool kNoShutdown = true;
+
+ explicit TreeMutation(LocalAccessible* aParent, bool aNoEvents = false);
+ ~TreeMutation();
+
+ void AfterInsertion(LocalAccessible* aChild);
+ void BeforeRemoval(LocalAccessible* aChild, bool aNoShutdown = false);
+ void Done();
+
+ private:
+ NotificationController* Controller() const {
+ return mParent->Document()->Controller();
+ }
+
+ LocalAccessible* mParent;
+ uint32_t mStartIdx;
+ uint32_t mStateFlagsCopy;
+
+ /*
+ * True if mutation events should be queued.
+ */
+ bool mQueueEvents;
+
+#ifdef DEBUG
+ bool mIsDone;
+#endif
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_EventQueue_h_
diff --git a/accessible/base/Filters.cpp b/accessible/base/Filters.cpp
new file mode 100644
index 0000000000..3ef58dc4fe
--- /dev/null
+++ b/accessible/base/Filters.cpp
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "Filters.h"
+
+#include "LocalAccessible-inl.h"
+#include "States.h"
+
+using namespace mozilla::a11y;
+using namespace mozilla::a11y::filters;
+
+uint32_t filters::GetSelected(LocalAccessible* aAccessible) {
+ if (aAccessible->State() & states::SELECTED) return eMatch | eSkipSubtree;
+
+ return eSkip;
+}
+
+uint32_t filters::GetSelectable(LocalAccessible* aAccessible) {
+ if (aAccessible->InteractiveState() & states::SELECTABLE) {
+ return eMatch | eSkipSubtree;
+ }
+
+ return eSkip;
+}
diff --git a/accessible/base/Filters.h b/accessible/base/Filters.h
new file mode 100644
index 0000000000..486beb5c83
--- /dev/null
+++ b/accessible/base/Filters.h
@@ -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/. */
+
+#ifndef mozilla_a11y_Filters_h__
+#define mozilla_a11y_Filters_h__
+
+#include <stdint.h>
+
+/**
+ * Predefined filters used for nsAccIterator and nsAccCollector.
+ */
+namespace mozilla {
+namespace a11y {
+
+class LocalAccessible;
+
+namespace filters {
+
+enum EResult { eSkip = 0, eMatch = 1, eSkipSubtree = 2 };
+
+/**
+ * Return true if the traversed accessible complies with filter.
+ */
+typedef uint32_t (*FilterFuncPtr)(LocalAccessible*);
+
+/**
+ * Matches selected/selectable accessibles in subtree.
+ */
+uint32_t GetSelected(LocalAccessible* aAccessible);
+uint32_t GetSelectable(LocalAccessible* aAccessible);
+} // namespace filters
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/FocusManager.cpp b/accessible/base/FocusManager.cpp
new file mode 100644
index 0000000000..57cfd7034e
--- /dev/null
+++ b/accessible/base/FocusManager.cpp
@@ -0,0 +1,462 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "FocusManager.h"
+
+#include "LocalAccessible-inl.h"
+#include "DocAccessible-inl.h"
+#include "nsAccessibilityService.h"
+#include "nsEventShell.h"
+
+#include "nsFocusManager.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/BrowserParent.h"
+
+namespace mozilla {
+namespace a11y {
+
+FocusManager::FocusManager() {}
+
+FocusManager::~FocusManager() {}
+
+LocalAccessible* FocusManager::FocusedLocalAccessible() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mActiveItem) {
+ if (mActiveItem->IsDefunct()) {
+ MOZ_ASSERT_UNREACHABLE("Stored active item is unbound from document");
+ return nullptr;
+ }
+
+ return mActiveItem;
+ }
+
+ if (nsAccessibilityService::IsShutdown()) {
+ // We might try to get or create a DocAccessible below, which isn't safe (or
+ // useful) if the accessibility service is shutting down.
+ return nullptr;
+ }
+
+ nsINode* focusedNode = FocusedDOMNode();
+ if (focusedNode) {
+ DocAccessible* doc =
+ GetAccService()->GetDocAccessible(focusedNode->OwnerDoc());
+ return doc ? doc->GetAccessibleEvenIfNotInMapOrContainer(focusedNode)
+ : nullptr;
+ }
+
+ return nullptr;
+}
+
+Accessible* FocusManager::FocusedAccessible() const {
+#if defined(ANDROID)
+ // It's not safe to call FocusedLocalAccessible() except on the main thread.
+ // Android might query RemoteAccessibles on the UI thread, which might call
+ // FocusedAccessible(). Never try to get the focused LocalAccessible in this
+ // case.
+ if (NS_IsMainThread()) {
+ if (Accessible* focusedAcc = FocusedLocalAccessible()) {
+ return focusedAcc;
+ }
+ } else {
+ nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns();
+ }
+ return mFocusedRemoteDoc ? mFocusedRemoteDoc->GetFocusedAcc() : nullptr;
+#else
+ if (Accessible* focusedAcc = FocusedLocalAccessible()) {
+ return focusedAcc;
+ }
+
+ if (!XRE_IsParentProcess()) {
+ // DocAccessibleParent's don't exist in the content
+ // process, so we can't return anything useful if this
+ // is the case.
+ return nullptr;
+ }
+
+ nsFocusManager* focusManagerDOM = nsFocusManager::GetFocusManager();
+ if (!focusManagerDOM) {
+ return nullptr;
+ }
+
+ // If we call GetFocusedBrowsingContext from the chrome process
+ // it returns the BrowsingContext for the focused _window_, which
+ // is not helpful here. Instead use GetFocusedBrowsingContextInChrome
+ // which returns the content BrowsingContext that has focus.
+ dom::BrowsingContext* focusedContext =
+ focusManagerDOM->GetFocusedBrowsingContextInChrome();
+
+ DocAccessibleParent* focusedDoc =
+ DocAccessibleParent::GetFrom(focusedContext);
+ return focusedDoc ? focusedDoc->GetFocusedAcc() : nullptr;
+#endif // defined(ANDROID)
+}
+
+bool FocusManager::IsFocusWithin(const Accessible* aContainer) const {
+ Accessible* child = FocusedAccessible();
+ while (child) {
+ if (child == aContainer) return true;
+
+ child = child->Parent();
+ }
+ return false;
+}
+
+FocusManager::FocusDisposition FocusManager::IsInOrContainsFocus(
+ const LocalAccessible* aAccessible) const {
+ LocalAccessible* focus = FocusedLocalAccessible();
+ if (!focus) return eNone;
+
+ // If focused.
+ if (focus == aAccessible) return eFocused;
+
+ // If contains the focus.
+ LocalAccessible* child = focus->LocalParent();
+ while (child) {
+ if (child == aAccessible) return eContainsFocus;
+
+ child = child->LocalParent();
+ }
+
+ // If contained by focus.
+ child = aAccessible->LocalParent();
+ while (child) {
+ if (child == focus) return eContainedByFocus;
+
+ child = child->LocalParent();
+ }
+
+ return eNone;
+}
+
+bool FocusManager::WasLastFocused(const LocalAccessible* aAccessible) const {
+ return mLastFocus == aAccessible;
+}
+
+void FocusManager::NotifyOfDOMFocus(nsISupports* aTarget) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::FocusNotificationTarget("DOM focus", "Target", aTarget);
+ }
+#endif
+
+ mActiveItem = nullptr;
+
+ nsCOMPtr<nsINode> targetNode(do_QueryInterface(aTarget));
+ if (targetNode) {
+ DocAccessible* document =
+ GetAccService()->GetDocAccessible(targetNode->OwnerDoc());
+ if (document) {
+ // Set selection listener for focused element.
+ if (targetNode->IsElement()) {
+ SelectionMgr()->SetControlSelectionListener(targetNode->AsElement());
+ }
+
+ document->HandleNotification<FocusManager, nsINode>(
+ this, &FocusManager::ProcessDOMFocus, targetNode);
+ }
+ }
+}
+
+void FocusManager::NotifyOfDOMBlur(nsISupports* aTarget) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::FocusNotificationTarget("DOM blur", "Target", aTarget);
+ }
+#endif
+
+ mActiveItem = nullptr;
+
+ // If DOM document stays focused then fire accessible focus event to process
+ // the case when no element within this DOM document will be focused.
+ nsCOMPtr<nsINode> targetNode(do_QueryInterface(aTarget));
+ if (targetNode && targetNode->OwnerDoc() == FocusedDOMDocument()) {
+ dom::Document* DOMDoc = targetNode->OwnerDoc();
+ DocAccessible* document = GetAccService()->GetDocAccessible(DOMDoc);
+ if (document) {
+ // Clear selection listener for previously focused element.
+ if (targetNode->IsElement()) {
+ SelectionMgr()->ClearControlSelectionListener();
+ }
+
+ document->HandleNotification<FocusManager, nsINode>(
+ this, &FocusManager::ProcessDOMFocus, DOMDoc);
+ }
+ }
+}
+
+void FocusManager::ActiveItemChanged(LocalAccessible* aItem,
+ bool aCheckIfActive) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::FocusNotificationTarget("active item changed", "Item", aItem);
+ }
+#endif
+
+ // Nothing changed, happens for XUL trees and HTML selects.
+ if (aItem && aItem == mActiveItem) {
+ return;
+ }
+
+ mActiveItem = nullptr;
+
+ if (aItem && aCheckIfActive) {
+ LocalAccessible* widget = aItem->ContainerWidget();
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) logging::ActiveWidget(widget);
+#endif
+ if (!widget || !widget->IsActiveWidget() || !widget->AreItemsOperable()) {
+ return;
+ }
+ }
+ mActiveItem = aItem;
+
+ // If mActiveItem is null we may need to shift a11y focus back to a remote
+ // document. For example, when combobox popup is closed, then
+ // the focus should be moved back to the combobox.
+ if (!mActiveItem && XRE_IsParentProcess()) {
+ dom::BrowserParent* browser = dom::BrowserParent::GetFocused();
+ if (browser) {
+ a11y::DocAccessibleParent* dap = browser->GetTopLevelDocAccessible();
+ if (dap) {
+ Unused << dap->SendRestoreFocus();
+ }
+ }
+ }
+
+ // If active item is changed then fire accessible focus event on it, otherwise
+ // if there's no an active item then fire focus event to accessible having
+ // DOM focus.
+ LocalAccessible* target = FocusedLocalAccessible();
+ if (target) {
+ DispatchFocusEvent(target->Document(), target);
+ }
+}
+
+void FocusManager::ForceFocusEvent() {
+ nsINode* focusedNode = FocusedDOMNode();
+ if (focusedNode) {
+ DocAccessible* document =
+ GetAccService()->GetDocAccessible(focusedNode->OwnerDoc());
+ if (document) {
+ document->HandleNotification<FocusManager, nsINode>(
+ this, &FocusManager::ProcessDOMFocus, focusedNode);
+ }
+ }
+}
+
+void FocusManager::DispatchFocusEvent(DocAccessible* aDocument,
+ LocalAccessible* aTarget) {
+ MOZ_ASSERT(aDocument, "No document for focused accessible!");
+ if (aDocument) {
+ RefPtr<AccEvent> event =
+ new AccEvent(nsIAccessibleEvent::EVENT_FOCUS, aTarget, eAutoDetect,
+ AccEvent::eCoalesceOfSameType);
+ aDocument->FireDelayedEvent(event);
+ mLastFocus = aTarget;
+ if (mActiveItem != aTarget) {
+ // This new focus overrides the stored active item, so clear the active
+ // item. Among other things, the old active item might die.
+ mActiveItem = nullptr;
+ }
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) logging::FocusDispatched(aTarget);
+#endif
+ }
+}
+
+void FocusManager::ProcessDOMFocus(nsINode* aTarget) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::FocusNotificationTarget("process DOM focus", "Target", aTarget);
+ }
+#endif
+
+ DocAccessible* document =
+ GetAccService()->GetDocAccessible(aTarget->OwnerDoc());
+ if (!document) return;
+
+ LocalAccessible* target =
+ document->GetAccessibleEvenIfNotInMapOrContainer(aTarget);
+ if (target) {
+ // Check if still focused. Otherwise we can end up with storing the active
+ // item for control that isn't focused anymore.
+ nsINode* focusedNode = FocusedDOMNode();
+ if (!focusedNode) return;
+
+ LocalAccessible* DOMFocus =
+ document->GetAccessibleEvenIfNotInMapOrContainer(focusedNode);
+ if (target != DOMFocus) return;
+
+ LocalAccessible* activeItem = target->CurrentItem();
+ if (activeItem) {
+ mActiveItem = activeItem;
+ target = activeItem;
+ }
+
+ DispatchFocusEvent(document, target);
+ }
+}
+
+void FocusManager::ProcessFocusEvent(AccEvent* aEvent) {
+ MOZ_ASSERT(aEvent->GetEventType() == nsIAccessibleEvent::EVENT_FOCUS,
+ "Focus event is expected!");
+
+ // Emit focus event if event target is the active item. Otherwise then check
+ // if it's still focused and then update active item and emit focus event.
+ LocalAccessible* target = aEvent->GetAccessible();
+ MOZ_ASSERT(!target->IsDefunct());
+ if (target != mActiveItem) {
+ // Check if still focused. Otherwise we can end up with storing the active
+ // item for control that isn't focused anymore.
+ DocAccessible* document = aEvent->Document();
+ nsINode* focusedNode = FocusedDOMNode();
+ if (!focusedNode) return;
+
+ LocalAccessible* DOMFocus =
+ document->GetAccessibleEvenIfNotInMapOrContainer(focusedNode);
+ if (target != DOMFocus) return;
+
+ LocalAccessible* activeItem = target->CurrentItem();
+ if (activeItem) {
+ mActiveItem = activeItem;
+ target = activeItem;
+ MOZ_ASSERT(!target->IsDefunct());
+ }
+ }
+
+ // Fire menu start/end events for ARIA menus.
+ if (target->IsARIARole(nsGkAtoms::menuitem)) {
+ // The focus was moved into menu.
+ LocalAccessible* ARIAMenubar = nullptr;
+ for (LocalAccessible* parent = target->LocalParent(); parent;
+ parent = parent->LocalParent()) {
+ if (parent->IsARIARole(nsGkAtoms::menubar)) {
+ ARIAMenubar = parent;
+ break;
+ }
+
+ // Go up in the parent chain of the menu hierarchy.
+ if (!parent->IsARIARole(nsGkAtoms::menuitem) &&
+ !parent->IsARIARole(nsGkAtoms::menu)) {
+ break;
+ }
+ }
+
+ if (ARIAMenubar != mActiveARIAMenubar) {
+ // Leaving ARIA menu. Fire menu_end event on current menubar.
+ if (mActiveARIAMenubar) {
+ RefPtr<AccEvent> menuEndEvent =
+ new AccEvent(nsIAccessibleEvent::EVENT_MENU_END, mActiveARIAMenubar,
+ aEvent->FromUserInput());
+ nsEventShell::FireEvent(menuEndEvent);
+ }
+
+ mActiveARIAMenubar = ARIAMenubar;
+
+ // Entering ARIA menu. Fire menu_start event.
+ if (mActiveARIAMenubar) {
+ RefPtr<AccEvent> menuStartEvent =
+ new AccEvent(nsIAccessibleEvent::EVENT_MENU_START,
+ mActiveARIAMenubar, aEvent->FromUserInput());
+ nsEventShell::FireEvent(menuStartEvent);
+ }
+ }
+ } else if (mActiveARIAMenubar) {
+ // Focus left a menu. Fire menu_end event.
+ RefPtr<AccEvent> menuEndEvent =
+ new AccEvent(nsIAccessibleEvent::EVENT_MENU_END, mActiveARIAMenubar,
+ aEvent->FromUserInput());
+ nsEventShell::FireEvent(menuEndEvent);
+
+ mActiveARIAMenubar = nullptr;
+ }
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::FocusNotificationTarget("fire focus event", "Target", target);
+ }
+#endif
+
+ // Reset cached caret value. The cache will be updated upon processing the
+ // next caret move event. This ensures that we will return the correct caret
+ // offset before the caret move event is handled.
+ SelectionMgr()->ResetCaretOffset();
+
+ RefPtr<AccEvent> focusEvent = new AccEvent(nsIAccessibleEvent::EVENT_FOCUS,
+ target, aEvent->FromUserInput());
+ nsEventShell::FireEvent(focusEvent);
+
+ if (NS_WARN_IF(target->IsDefunct())) {
+ // target died during nsEventShell::FireEvent.
+ return;
+ }
+
+ // Fire scrolling_start event when the document receives the focus if it has
+ // an anchor jump. If an accessible within the document receive the focus
+ // then null out the anchor jump because it no longer applies.
+ DocAccessible* targetDocument = target->Document();
+ MOZ_ASSERT(targetDocument);
+ LocalAccessible* anchorJump = targetDocument->AnchorJump();
+ if (anchorJump) {
+ if (target == targetDocument || target->IsNonInteractive()) {
+ // XXX: bug 625699, note in some cases the node could go away before we
+ // we receive focus event, for example if the node is removed from DOM.
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SCROLLING_START,
+ anchorJump, aEvent->FromUserInput());
+ }
+ targetDocument->SetAnchorJump(nullptr);
+ }
+}
+
+nsINode* FocusManager::FocusedDOMNode() const {
+ nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager();
+ nsIContent* focusedElm = DOMFocusManager->GetFocusedElement();
+ nsIFrame* focusedFrame = focusedElm ? focusedElm->GetPrimaryFrame() : nullptr;
+ // DOM elements retain their focused state when they get styled as display:
+ // none/content or visibility: hidden. We should treat those cases as if those
+ // elements were removed, and focus on doc.
+ if (focusedFrame && focusedFrame->StyleVisibility()->IsVisible()) {
+ // Print preview documents don't get DocAccessibles, but we still want a11y
+ // focus to go somewhere useful. Therefore, we allow a11y focus to land on
+ // the OuterDocAccessible in this case.
+ // Note that this code only handles remote print preview documents.
+ if (EventStateManager::IsTopLevelRemoteTarget(focusedElm) &&
+ focusedElm->AsElement()->HasAttribute(u"printpreview"_ns)) {
+ return focusedElm;
+ }
+ // No focus on remote target elements like xul:browser having DOM focus and
+ // residing in chrome process because it means an element in content process
+ // keeps the focus. Similarly, suppress focus on OOP iframes because an
+ // element in another content process should now have the focus.
+ if (EventStateManager::IsRemoteTarget(focusedElm)) {
+ return nullptr;
+ }
+ return focusedElm;
+ }
+
+ // Otherwise the focus can be on DOM document.
+ dom::BrowsingContext* context = DOMFocusManager->GetFocusedBrowsingContext();
+ if (context) {
+ // GetDocShell will return null if the document isn't in our process.
+ nsIDocShell* shell = context->GetDocShell();
+ if (shell) {
+ return shell->GetDocument();
+ }
+ }
+
+ // Focus isn't in this process.
+ return nullptr;
+}
+
+dom::Document* FocusManager::FocusedDOMDocument() const {
+ nsINode* focusedNode = FocusedDOMNode();
+ return focusedNode ? focusedNode->OwnerDoc() : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/base/FocusManager.h b/accessible/base/FocusManager.h
new file mode 100644
index 0000000000..7460a21f9a
--- /dev/null
+++ b/accessible/base/FocusManager.h
@@ -0,0 +1,169 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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_FocusManager_h_
+#define mozilla_a11y_FocusManager_h_
+
+#include "mozilla/RefPtr.h"
+
+class nsINode;
+class nsISupports;
+
+namespace mozilla {
+namespace dom {
+class Document;
+}
+
+namespace a11y {
+
+class Accessible;
+class AccEvent;
+class LocalAccessible;
+class DocAccessible;
+class DocAccessibleParent;
+
+/**
+ * Manage the accessible focus. Used to fire and process accessible events.
+ */
+class FocusManager {
+ public:
+ virtual ~FocusManager();
+
+ /**
+ * Return the currently focused LocalAccessible. If a remote document has
+ * focus, this will return null.
+ */
+ LocalAccessible* FocusedLocalAccessible() const;
+
+ /**
+ * Return the currently focused Accessible, local or remote.
+ */
+ Accessible* FocusedAccessible() const;
+
+ /**
+ * Return true if given accessible is focused.
+ */
+ bool IsFocused(const Accessible* aAccessible) const {
+ return FocusedAccessible() == aAccessible;
+ }
+
+ /**
+ * Return true if the given accessible is an active item, i.e. an item that
+ * is current within the active widget.
+ */
+ inline bool IsActiveItem(const LocalAccessible* aAccessible) {
+ return aAccessible == mActiveItem;
+ }
+
+ /**
+ * Return DOM node having DOM focus.
+ */
+ nsINode* FocusedDOMNode() const;
+
+ /**
+ * Return true if given DOM node has DOM focus.
+ */
+ inline bool HasDOMFocus(const nsINode* aNode) const {
+ return aNode == FocusedDOMNode();
+ }
+
+ /**
+ * Return true if focused accessible is within the given container.
+ */
+ bool IsFocusWithin(const Accessible* aContainer) const;
+
+ /**
+ * Return whether the given accessible is focused or contains the focus or
+ * contained by focused accessible.
+ */
+ enum FocusDisposition { eNone, eFocused, eContainsFocus, eContainedByFocus };
+ FocusDisposition IsInOrContainsFocus(
+ const LocalAccessible* aAccessible) const;
+
+ /**
+ * Return true if the given accessible was the last accessible focused.
+ * This is useful to detect the case where the last focused accessible was
+ * removed before something else was focused. This can happen in one of two
+ * ways:
+ * 1. The DOM focus was removed. DOM doesn't fire a blur event when this
+ * happens; see bug 559561.
+ * 2. The accessibility focus was an active item (e.g. aria-activedescendant)
+ * and that item was removed.
+ */
+ bool WasLastFocused(const LocalAccessible* aAccessible) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Focus notifications and processing (don't use until you know what you do).
+
+ /**
+ * Called when DOM focus event is fired.
+ */
+ void NotifyOfDOMFocus(nsISupports* aTarget);
+
+ /**
+ * Called when DOM blur event is fired.
+ */
+ void NotifyOfDOMBlur(nsISupports* aTarget);
+
+ /**
+ * Called when active item is changed. Note: must be called when accessible
+ * tree is up to date.
+ */
+ void ActiveItemChanged(LocalAccessible* aItem, bool aCheckIfActive = true);
+
+ /**
+ * Dispatch delayed focus event for the current focus accessible.
+ */
+ void ForceFocusEvent();
+
+ /**
+ * Dispatch delayed focus event for the given target.
+ */
+ void DispatchFocusEvent(DocAccessible* aDocument, LocalAccessible* aTarget);
+
+ /**
+ * Process DOM focus notification.
+ */
+ void ProcessDOMFocus(nsINode* aTarget);
+
+ /**
+ * Process the delayed accessible event.
+ * do.
+ */
+ void ProcessFocusEvent(AccEvent* aEvent);
+
+#ifdef ANDROID
+ void SetFocusedRemoteDoc(DocAccessibleParent* aDoc) {
+ mFocusedRemoteDoc = aDoc;
+ }
+ bool IsFocusedRemoteDoc(DocAccessibleParent* aDoc) {
+ return mFocusedRemoteDoc == aDoc;
+ }
+#endif
+
+ protected:
+ FocusManager();
+
+ private:
+ FocusManager(const FocusManager&);
+ FocusManager& operator=(const FocusManager&);
+
+ /**
+ * Return DOM document having DOM focus.
+ */
+ dom::Document* FocusedDOMDocument() const;
+
+ private:
+ RefPtr<LocalAccessible> mActiveItem;
+ RefPtr<LocalAccessible> mLastFocus;
+ RefPtr<LocalAccessible> mActiveARIAMenubar;
+#ifdef ANDROID
+ DocAccessibleParent* mFocusedRemoteDoc = nullptr;
+#endif
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/HTMLMarkupMap.h b/accessible/base/HTMLMarkupMap.h
new file mode 100644
index 0000000000..b903097ea0
--- /dev/null
+++ b/accessible/base/HTMLMarkupMap.h
@@ -0,0 +1,444 @@
+/* -*- 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(
+ a,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ // An anchor element without an href attribute and without a click
+ // listener should be a generic.
+ if (!aElement->HasAttr(nsGkAtoms::href) &&
+ !nsCoreUtils::HasClickListener(aElement)) {
+ return new HyperTextAccessible(aElement, aContext->Document());
+ }
+ // Only some roles truly enjoy life as HTMLLinkAccessibles, for
+ // details see closed bug 494807.
+ const nsRoleMapEntry* roleMapEntry = aria::GetRoleMap(aElement);
+ if (roleMapEntry && roleMapEntry->role != roles::NOTHING &&
+ roleMapEntry->role != roles::LINK) {
+ return new HyperTextAccessible(aElement, aContext->Document());
+ }
+
+ return new HTMLLinkAccessible(aElement, aContext->Document());
+ },
+ 0)
+
+MARKUPMAP(abbr, New_HyperText, 0)
+
+MARKUPMAP(acronym, New_HyperText, 0)
+
+MARKUPMAP(address, New_HyperText, roles::GROUPING)
+
+MARKUPMAP(article, New_HyperText, roles::ARTICLE, Attr(xmlroles, article))
+
+MARKUPMAP(aside, New_HyperText, roles::LANDMARK)
+
+MARKUPMAP(blockquote, New_HyperText, roles::BLOCKQUOTE)
+
+MARKUPMAP(
+ button,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLButtonAccessible(aElement, aContext->Document());
+ },
+ 0)
+
+MARKUPMAP(
+ caption,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ if (aContext->IsTable()) {
+ dom::HTMLTableElement* tableEl =
+ dom::HTMLTableElement::FromNode(aContext->GetContent());
+ if (tableEl && tableEl == aElement->GetParent() &&
+ tableEl->GetCaption() == aElement) {
+ return new HTMLCaptionAccessible(aElement, aContext->Document());
+ }
+ }
+ return nullptr;
+ },
+ 0)
+
+MARKUPMAP(code, New_HyperText, roles::CODE)
+
+MARKUPMAP(dd, New_HTMLDtOrDd<HyperTextAccessible>, roles::DEFINITION)
+
+MARKUPMAP(del, New_HyperText, roles::CONTENT_DELETION)
+
+MARKUPMAP(details, New_HyperText, roles::DETAILS)
+
+MARKUPMAP(dfn, New_HyperText, roles::TERM)
+
+MARKUPMAP(dialog, New_HyperText, roles::DIALOG)
+
+MARKUPMAP(
+ div,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ // Never create an accessible if we're part of an anonymous
+ // subtree.
+ if (aElement->IsInNativeAnonymousSubtree()) {
+ return nullptr;
+ }
+ // Always create an accessible if the div has an id.
+ if (aElement->HasAttr(nsGkAtoms::id)) {
+ return new HyperTextAccessible(aElement, aContext->Document());
+ }
+ // Never create an accessible if the div is not display:block; or
+ // display:inline-block or the like.
+ nsIFrame* f = aElement->GetPrimaryFrame();
+ if (!f || !f->IsBlockFrameOrSubclass()) {
+ return nullptr;
+ }
+ // Check for various conditions to determine if this is a block
+ // break and needs to be rendered.
+ // If its previous sibling is an inline element, we probably want
+ // to break, so render.
+ // FIXME: This looks extremely incorrect in presence of shadow DOM,
+ // display: contents, and what not.
+ nsIContent* prevSibling = aElement->GetPreviousSibling();
+ if (prevSibling) {
+ nsIFrame* prevSiblingFrame = prevSibling->GetPrimaryFrame();
+ if (prevSiblingFrame && prevSiblingFrame->IsInlineOutside()) {
+ return new HyperTextAccessible(aElement, aContext->Document());
+ }
+ }
+ // Now, check the children.
+ nsIContent* firstChild = aElement->GetFirstChild();
+ if (firstChild) {
+ nsIFrame* firstChildFrame = firstChild->GetPrimaryFrame();
+ if (!firstChildFrame) {
+ // The first child is invisible, but this might be due to an
+ // invisible text node. Try the next.
+ firstChild = firstChild->GetNextSibling();
+ if (!firstChild) {
+ // If there's no next sibling, there's only one child, so there's
+ // nothing more we can do.
+ return nullptr;
+ }
+ firstChildFrame = firstChild->GetPrimaryFrame();
+ }
+ // Check to see if first child has an inline frame.
+ if (firstChildFrame && firstChildFrame->IsInlineOutside()) {
+ return new HyperTextAccessible(aElement, aContext->Document());
+ }
+ nsIContent* lastChild = aElement->GetLastChild();
+ MOZ_ASSERT(lastChild);
+ if (lastChild != firstChild) {
+ nsIFrame* lastChildFrame = lastChild->GetPrimaryFrame();
+ if (!lastChildFrame) {
+ // The last child is invisible, but this might be due to an
+ // invisible text node. Try the next.
+ lastChild = lastChild->GetPreviousSibling();
+ MOZ_ASSERT(lastChild);
+ if (lastChild == firstChild) {
+ return nullptr;
+ }
+ lastChildFrame = lastChild->GetPrimaryFrame();
+ }
+ // Check to see if last child has an inline frame.
+ if (lastChildFrame && lastChildFrame->IsInlineOutside()) {
+ return new HyperTextAccessible(aElement, aContext->Document());
+ }
+ }
+ }
+ return nullptr;
+ },
+ roles::SECTION)
+
+MARKUPMAP(
+ dl,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLListAccessible(aElement, aContext->Document());
+ },
+ roles::DEFINITION_LIST)
+
+MARKUPMAP(dt, New_HTMLDtOrDd<HTMLLIAccessible>, roles::TERM)
+
+MARKUPMAP(em, New_HyperText, roles::EMPHASIS)
+
+MARKUPMAP(
+ figcaption,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLFigcaptionAccessible(aElement, aContext->Document());
+ },
+ roles::CAPTION)
+
+MARKUPMAP(
+ figure,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLFigureAccessible(aElement, aContext->Document());
+ },
+ roles::FIGURE, Attr(xmlroles, figure))
+
+MARKUPMAP(
+ fieldset,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLGroupboxAccessible(aElement, aContext->Document());
+ },
+ 0)
+
+MARKUPMAP(
+ form,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLFormAccessible(aElement, aContext->Document());
+ },
+ 0)
+
+MARKUPMAP(
+ footer,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLHeaderOrFooterAccessible(aElement, aContext->Document());
+ },
+ 0)
+
+MARKUPMAP(
+ header,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLHeaderOrFooterAccessible(aElement, aContext->Document());
+ },
+ 0)
+
+MARKUPMAP(h1, New_HyperText, roles::HEADING)
+
+MARKUPMAP(h2, New_HyperText, roles::HEADING)
+
+MARKUPMAP(h3, New_HyperText, roles::HEADING)
+
+MARKUPMAP(h4, New_HyperText, roles::HEADING)
+
+MARKUPMAP(h5, New_HyperText, roles::HEADING)
+
+MARKUPMAP(h6, New_HyperText, roles::HEADING)
+
+MARKUPMAP(hgroup, New_HyperText, roles::GROUPING)
+
+MARKUPMAP(
+ hr,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLHRAccessible(aElement, aContext->Document());
+ },
+ 0)
+
+MARKUPMAP(
+ input,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ // TODO(emilio): This would be faster if it used
+ // HTMLInputElement's already-parsed representation.
+ if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::checkbox, eIgnoreCase)) {
+ return new CheckboxAccessible(aElement, aContext->Document());
+ }
+ if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::image, eIgnoreCase)) {
+ return new HTMLButtonAccessible(aElement, aContext->Document());
+ }
+ if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::radio, eIgnoreCase)) {
+ return new HTMLRadioButtonAccessible(aElement, aContext->Document());
+ }
+ if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::time, eIgnoreCase)) {
+ return new HTMLDateTimeAccessible<roles::TIME_EDITOR>(
+ aElement, aContext->Document());
+ }
+ if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::date, eIgnoreCase) ||
+ aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::datetime_local, eIgnoreCase)) {
+ return new HTMLDateTimeAccessible<roles::DATE_EDITOR>(
+ aElement, aContext->Document());
+ }
+ return nullptr;
+ },
+ 0)
+
+MARKUPMAP(ins, New_HyperText, roles::CONTENT_INSERTION)
+
+MARKUPMAP(
+ label,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLLabelAccessible(aElement, aContext->Document());
+ },
+ roles::LABEL)
+
+MARKUPMAP(
+ legend,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLLegendAccessible(aElement, aContext->Document());
+ },
+ roles::LABEL)
+
+MARKUPMAP(
+ li,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ // If list item is a child of accessible list then create an
+ // accessible for it unconditionally by tag name. nsBlockFrame
+ // creates the list item accessible for other elements styled as
+ // list items.
+ if (aContext->IsList() &&
+ aContext->GetContent() == aElement->GetParent()) {
+ return new HTMLLIAccessible(aElement, aContext->Document());
+ }
+
+ return nullptr;
+ },
+ 0)
+
+MARKUPMAP(main, New_HyperText, roles::LANDMARK)
+
+MARKUPMAP(map, nullptr, roles::TEXT_CONTAINER)
+
+MARKUPMAP(mark, New_HyperText, roles::MARK, Attr(xmlroles, mark))
+
+MARKUPMAP(
+ menu,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLListAccessible(aElement, aContext->Document());
+ },
+ roles::LIST)
+
+MARKUPMAP(nav, New_HyperText, roles::LANDMARK)
+
+MARKUPMAP(
+ ol,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLListAccessible(aElement, aContext->Document());
+ },
+ roles::LIST)
+
+MARKUPMAP(
+ option,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLSelectOptionAccessible(aElement, aContext->Document());
+ },
+ 0)
+
+MARKUPMAP(
+ optgroup,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLSelectOptGroupAccessible(aElement, aContext->Document());
+ },
+ 0)
+
+MARKUPMAP(
+ output,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLOutputAccessible(aElement, aContext->Document());
+ },
+ roles::STATUSBAR, Attr(aria_live, polite))
+
+MARKUPMAP(p, nullptr, roles::PARAGRAPH)
+
+MARKUPMAP(
+ progress,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLProgressAccessible(aElement, aContext->Document());
+ },
+ 0)
+
+MARKUPMAP(q, New_HyperText, 0)
+
+MARKUPMAP(s, New_HyperText, roles::CONTENT_DELETION)
+
+MARKUPMAP(
+ section,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLSectionAccessible(aElement, aContext->Document());
+ },
+ 0)
+
+MARKUPMAP(strong, New_HyperText, roles::STRONG)
+
+MARKUPMAP(sub, New_HyperText, roles::SUBSCRIPT)
+
+MARKUPMAP(
+ summary,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLSummaryAccessible(aElement, aContext->Document());
+ },
+ roles::SUMMARY)
+
+MARKUPMAP(sup, New_HyperText, roles::SUPERSCRIPT)
+
+MARKUPMAP(
+ table,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new HTMLTableAccessible(aElement, aContext->Document());
+ },
+ roles::TABLE)
+
+MARKUPMAP(time, New_HyperText, roles::TIME, Attr(xmlroles, time),
+ AttrFromDOM(datetime, datetime))
+
+MARKUPMAP(tbody, nullptr, roles::GROUPING)
+
+MARKUPMAP(
+ td,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ if (!aContext->IsHTMLTableRow()) {
+ return nullptr;
+ }
+ if (aElement->HasAttr(nsGkAtoms::scope)) {
+ return new HTMLTableHeaderCellAccessible(aElement,
+ aContext->Document());
+ }
+ return new HTMLTableCellAccessible(aElement, aContext->Document());
+ },
+ 0)
+
+MARKUPMAP(tfoot, nullptr, roles::GROUPING)
+
+MARKUPMAP(
+ th,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ if (!aContext->IsHTMLTableRow()) {
+ return nullptr;
+ }
+ return new HTMLTableHeaderCellAccessible(aElement, aContext->Document());
+ },
+ 0)
+
+MARKUPMAP(thead, nullptr, roles::GROUPING)
+
+MARKUPMAP(
+ tr,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ if (aContext->IsTableRow()) {
+ // A <tr> within a row isn't valid.
+ return nullptr;
+ }
+ const nsRoleMapEntry* roleMapEntry = aria::GetRoleMap(aElement);
+ if (roleMapEntry && roleMapEntry->role != roles::NOTHING &&
+ roleMapEntry->role != roles::ROW) {
+ // There is a valid ARIA role which isn't "row". Don't treat this as an
+ // HTML table row.
+ return nullptr;
+ }
+ // Check if this <tr> is within a table. We check the grandparent because
+ // it might be inside a rowgroup. We don't specifically check for an HTML
+ // table because there are cases where there is a <tr> inside a
+ // <div role="table"> 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)
+
+MARKUPMAP(search, New_HyperText, roles::LANDMARK)
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<BitSetElt> {
+ 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<BitSetElt, BitSetElt> 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..67bf76ee5b
--- /dev/null
+++ b/accessible/base/Logging.cpp
@@ -0,0 +1,992 @@
+/* -*- 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 "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<nsIDocShell> 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<nsIDocShellTreeItem> treeItem(aDocumentNode->GetDocShell());
+ if (!treeItem) {
+ printf("in-process docshell hierarchy, null docshell;");
+ return;
+ }
+ nsCOMPtr<nsIDocShellTreeItem> parentTreeItem;
+ treeItem->GetInProcessParent(getter_AddRefs(parentTreeItem));
+ nsCOMPtr<nsIDocShellTreeItem> rootTreeItem;
+ treeItem->GetInProcessRootTreeItem(getter_AddRefs(rootTreeItem));
+ printf(
+ "in-process docshell hierarchy, parent: %p, root: %p, "
+ "is top level: %s;",
+ static_cast<void*>(parentTreeItem), static_cast<void*>(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<void*>(presShell));
+
+ nsIScrollableFrame* sf = nullptr;
+ if (presShell) {
+ printf(", is %s destroying", (presShell->IsDestroying() ? "" : "not"));
+ sf = presShell->GetRootScrollFrameAsScrollable();
+ }
+ printf(", root scroll frame: %p", static_cast<void*>(sf));
+}
+
+static void LogDocLoadGroup(dom::Document* aDocumentNode) {
+ nsCOMPtr<nsILoadGroup> loadGroup = aDocumentNode->GetDocumentLoadGroup();
+ printf("load group: %p", static_cast<void*>(loadGroup));
+}
+
+static void LogDocParent(dom::Document* aDocumentNode) {
+ dom::Document* parentDoc = aDocumentNode->GetInProcessParentDocument();
+ printf("parent DOM document: %p", static_cast<void*>(parentDoc));
+ if (parentDoc) {
+ printf(", parent acc document: %p",
+ static_cast<void*>(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<void*>(aDocumentNode), static_cast<void*>(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<mozIDOMWindowProxy> DOMWindow;
+ aWebProgress->GetDOMWindow(getter_AddRefs(DOMWindow));
+ nsPIDOMWindowOuter* window = nsPIDOMWindowOuter::From(DOMWindow);
+ if (!window) {
+ MsgEnd();
+ return;
+ }
+
+ nsCOMPtr<dom::Document> documentNode = window->GetDoc();
+ if (!documentNode) {
+ MsgEnd();
+ return;
+ }
+
+ DocAccessible* document = GetExistingDocAccessible(documentNode);
+
+ LogDocInfo(documentNode, document);
+
+ nsCOMPtr<nsIDocShell> 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<void*>(aDocument->DocumentNode()),
+ static_cast<void*>(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<nsINode> targetNode(do_QueryInterface(aTargetThing));
+ if (targetNode) {
+ AccessibleNNode(aTargetDescr, targetNode);
+ } else {
+ printf(" %s: %p, window\n", aTargetDescr,
+ static_cast<void*>(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<int32_t>(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<int32_t>(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<int32_t>(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<int32_t>(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<void*>(aAcc), static_cast<void*>(aAcc->GetNode()));
+ }
+
+ DocAccessible* doc = aAcc->Document();
+ dom::Document* docNode = doc->DocumentNode();
+ printf(" document: %p, node: %p\n", static_cast<void*>(doc),
+ static_cast<void*>(docNode));
+
+ printf(" ");
+ LogDocURI(docNode);
+ printf("\n");
+}
+
+void logging::Node(const char* aDescr, nsINode* aNode) {
+ Maybe<uint32_t> 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<void*>(aDocument),
+ static_cast<void*>(aDocument->DocumentNode()));
+
+ printf(" Document ");
+ LogDocURI(aDocument->DocumentNode());
+ printf("\n");
+}
+
+void logging::AccessibleInfo(const char* aDescr, LocalAccessible* aAccessible) {
+ printf(" %s: %p; ", aDescr, static_cast<void*>(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<void*>(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..63786861f1
--- /dev/null
+++ b/accessible/base/NotificationController.cpp
@@ -0,0 +1,1107 @@
+/* -*- 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 "LocalAccessible-inl.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<nsCOMPtr<nsIContent>>* 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;
+
+ mTextArray.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;
+ }
+
+ // Don't queue a hide event on an accessible that's already being moved. It
+ // or an ancestor should already have a hide event queued.
+ if (mDocument &&
+ mDocument->IsAccessibleBeingMoved(aEvent->GetAccessible())) {
+ 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<AccReorderEvent> 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) {
+ const uint32_t eventType = aEvent->GetEventType();
+ MOZ_ASSERT(eventType != nsIAccessibleEvent::EVENT_INNER_REORDER,
+ "Inner reorder has already been dropped, cannot drop again");
+ if (eventType == 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;
+ }
+ if (eventType == nsIAccessibleEvent::EVENT_SHOW) {
+ // unset the event bits since the event isn't being fired any more.
+ aEvent->GetAccessible()->SetShowEventTarget(false);
+ } else if (eventType == nsIAccessibleEvent::EVENT_HIDE) {
+ // 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());
+ }
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Mutation event has non-mutation event type");
+ }
+
+ // 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<nsCOMPtr<nsIContent>>& 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 ||
+ !mTextArray.IsEmpty() ||
+ !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<AccTreeMutationEvent> firstEvent = mFirstMutationEvent;
+ mFirstMutationEvent = mLastMutationEvent = nullptr;
+ mMutationMap.Clear();
+ mEventGeneration = 0;
+
+ // Group the show events by the parent of their target.
+ nsTHashMap<nsPtrHashKey<LocalAccessible>, nsTArray<AccTreeMutationEvent*>>
+ 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 && (a == b || aIdx != bIdx));
+ return aIdx < bIdx;
+ }
+ bool Equals(const AccTreeMutationEvent* a,
+ const AccTreeMutationEvent* b) const {
+ DebugOnly<int32_t> aIdx = a->GetAccessible()->IndexInParent();
+ DebugOnly<int32_t> bIdx = b->GetAccessible()->IndexInParent();
+ MOZ_ASSERT(aIdx >= 0 && bIdx >= 0 && (a == b || aIdx != bIdx));
+ return a == b;
+ }
+ };
+
+ nsTArray<AccTreeMutationEvent*>& 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<AccTreeMutationEvent*>& 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<AccTreeMutationEvent> event = firstEvent;
+ while (event) {
+ RefPtr<AccTreeMutationEvent> 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<Telemetry::A11Y_TREE_UPDATE_TIMING_MS> 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. Even though we want to process
+ // them in the order in which they were queued, we still want to avoid
+ // duplicates.
+ nsTHashSet<nsIContent*> textHash;
+ for (nsIContent* textNode : mTextArray) {
+ if (!textHash.EnsureInserted(textNode)) {
+ continue; // Already processed.
+ }
+ 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<nsCOMPtr<nsIContent>>* list =
+ mContentInsertions.GetOrInsertNew(container);
+ list->AppendElement(textNode);
+ }
+ }
+ }
+ textHash.Clear();
+ mTextArray.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<RefPtr<DocAccessible>> 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<RefPtr<Notification>> 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<uintptr_t>(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<nsIBrowserChild> browserChild =
+ do_GetInterface(mDocument->DocumentNode()->GetDocShell());
+ if (browserChild) {
+ static_cast<BrowserChild*>(browserChild.get())
+ ->SendPDocAccessibleConstructor(
+ ipcDoc, parentIPCDoc, id,
+ childDoc->DocumentNode()->GetBrowsingContext());
+ }
+ }
+ }
+
+ 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() && mTextArray.IsEmpty() &&
+ 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<uintptr_t>(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<uintptr_t>(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<uintptr_t>(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..137963f117
--- /dev/null
+++ b/accessible/base/NotificationController.h
@@ -0,0 +1,396 @@
+/* -*- 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 <utility>
+
+#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 Class, class... Args>
+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<Args...>{});
+ }
+
+ private:
+ TNotification(const TNotification&);
+ TNotification& operator=(const TNotification&);
+
+ template <size_t... Indices>
+ void ProcessHelper(std::index_sequence<Indices...>) {
+ (mInstance->*mCallback)(std::get<Indices>(mArgs)...);
+ }
+
+ Class* mInstance;
+ Callback mCallback;
+ std::tuple<RefPtr<Args>...> 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");
+
+ mTextArray.AppendElement(aTextNode);
+
+ ScheduleProcessing();
+ }
+
+ /**
+ * Pend accessible tree update for content insertion.
+ */
+ void ScheduleContentInsertion(LocalAccessible* aContainer,
+ nsTArray<nsCOMPtr<nsIContent>>& 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 <class Class, class... Args>
+ inline void HandleNotification(
+ Class* aInstance,
+ typename TNotification<Class, Args...>::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> notification =
+ new TNotification<Class, Args...>(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 <class Class>
+ inline void ScheduleNotification(
+ Class* aInstance, typename TNotification<Class>::Callback aMethod) {
+ RefPtr<Notification> notification =
+ new TNotification<Class>(aInstance, aMethod);
+ if (notification) {
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mNotifications.AppendElement(notification);
+ ScheduleProcessing();
+ }
+ }
+
+ template <class Class, class Arg>
+ inline void ScheduleNotification(
+ Class* aInstance, typename TNotification<Class, Arg>::Callback aMethod,
+ Arg* aArg) {
+ RefPtr<Notification> notification =
+ new TNotification<Class, Arg>(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<RefPtr<DocAccessible>> mHangingChildDocuments;
+
+ /**
+ * Pending accessible tree update notifications for content insertions.
+ */
+ nsClassHashtable<nsRefPtrHashKey<LocalAccessible>,
+ nsTArray<nsCOMPtr<nsIContent>>>
+ mContentInsertions;
+
+ template <class T>
+ class nsCOMPtrHashKey : public PLDHashEntryHdr {
+ public:
+ typedef T* KeyType;
+ typedef const T* KeyTypePointer;
+
+ explicit nsCOMPtrHashKey(const T* aKey) : mKey(const_cast<T*>(aKey)) {}
+ nsCOMPtrHashKey(nsCOMPtrHashKey<T>&& 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<T> mKey;
+ };
+
+ /**
+ * Pending accessible tree update notifications for rendered text changes.
+ * When there are a lot of nearby text insertions (e.g. during a reflow), it
+ * is much more performant to process them in order because we then benefit
+ * from the layout line cursor. Therefore, we use an array here.
+ */
+ nsTArray<nsCOMPtr<nsIContent>> mTextArray;
+
+ /**
+ * Other notifications like DOM events. Don't make this an AutoTArray; we
+ * use SwapElements() on it.
+ */
+ nsTArray<RefPtr<Notification>> mNotifications;
+
+ /**
+ * Holds all scheduled relocations.
+ */
+ nsTArray<RefPtr<LocalAccessible>> 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<AccTreeMutationEvent> mFirstMutationEvent;
+ RefPtr<AccTreeMutationEvent> 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<nsUint64HashKey, AccTreeMutationEvent> 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..146d9207cf
--- /dev/null
+++ b/accessible/base/Pivot.cpp
@@ -0,0 +1,331 @@
+/* -*- 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 "nsAccUtils.h"
+#include "nsIAccessiblePivot.h"
+
+#include "mozilla/a11y/Accessible.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::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::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..bd2814f48e
--- /dev/null
+++ b/accessible/base/Pivot.h
@@ -0,0 +1,141 @@
+/* -*- 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 <stdint.h>
+#include "mozilla/a11y/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 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);
+
+ 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..23f214246f
--- /dev/null
+++ b/accessible/base/Platform.h
@@ -0,0 +1,136 @@
+/* -*- 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 <stdint.h>
+#include "nsStringFwd.h"
+#include "Units.h"
+
+#if defined(ANDROID)
+# include "nsTArray.h"
+# include "nsRect.h"
+#endif
+
+#ifdef MOZ_WIDGET_COCOA
+# include "mozilla/a11y/Role.h"
+# include "nsTArray.h"
+#endif
+
+namespace mozilla {
+namespace a11y {
+
+class Accessible;
+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*);
+
+/**
+ * Called when an event is fired on an Accessible so that platforms may fire
+ * events if appropriate.
+ */
+void PlatformEvent(Accessible* aTarget, uint32_t aEventType);
+void PlatformStateChangeEvent(Accessible* aTarget, uint64_t aState,
+ bool aEnabled);
+
+void PlatformFocusEvent(Accessible* aTarget,
+ const LayoutDeviceIntRect& aCaretRect);
+void PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset,
+ bool aIsSelectionCollapsed, int32_t aGranularity,
+ const LayoutDeviceIntRect& aCaretRect,
+ bool aFromUser);
+void PlatformTextChangeEvent(Accessible* aTarget, const nsAString& aStr,
+ int32_t aStart, uint32_t aLen, bool aIsInsert,
+ bool aFromUser);
+void PlatformShowHideEvent(Accessible* aTarget, Accessible* aParent,
+ bool aInsert, bool aFromUser);
+void PlatformSelectionEvent(Accessible* aTarget, Accessible* aWidget,
+ uint32_t aType);
+
+#if defined(ANDROID)
+void PlatformScrollingEvent(Accessible* aTarget, uint32_t aEventType,
+ uint32_t aScrollX, uint32_t aScrollY,
+ uint32_t aMaxScrollX, uint32_t aMaxScrollY);
+
+void PlatformAnnouncementEvent(Accessible* aTarget,
+ const nsAString& aAnnouncement,
+ uint16_t aPriority);
+
+bool LocalizeString(const nsAString& aToken, nsAString& aLocalized);
+#endif
+
+#ifdef MOZ_WIDGET_COCOA
+class TextRange;
+void PlatformTextSelectionChangeEvent(Accessible* aTarget,
+ const nsTArray<TextRange>& aSelection);
+
+void PlatformRoleChangedEvent(Accessible* 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 <memory>
+
+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<AccIterable> mFirstIter;
+ AccIterable* mLastIter;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/RelationTypeGen.py b/accessible/base/RelationTypeGen.py
new file mode 100644
index 0000000000..8d9a0f91bf
--- /dev/null
+++ b/accessible/base/RelationTypeGen.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env 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/.
+
+import re
+
+
+def generate(relH, relIdl):
+ input = open(relIdl, "rt").read()
+ relations = re.findall(
+ r"const unsigned long RELATION_([A-Z_]+) = ([x0-9a-f]+);", input
+ )
+
+ relH.write(
+ "/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n"
+ "/* Relations are defined in accessible/interfaces/nsIAccessibleRelation.idl */\n\n"
+ "#ifndef mozilla_a11y_relationtype_h_\n"
+ "#define mozilla_a11y_relationtype_h_\n\n"
+ "namespace mozilla {\n"
+ "namespace a11y {\n\n"
+ "enum class RelationType {\n"
+ )
+ for name, num in relations:
+ relH.write(f" {name} = {num},\n")
+ lastName = relations[-1][0]
+ relH.write(
+ f" LAST = {lastName}\n"
+ "};\n\n"
+ "} // namespace a11y\n"
+ "} // namespace mozilla\n\n"
+ "#endif\n"
+ )
+
+
+# For debugging
+if __name__ == "__main__":
+ import sys
+
+ generate(sys.stdout, "accessible/interfaces/nsIAccessibleRelation.idl")
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/RoleHGen.py b/accessible/base/RoleHGen.py
new file mode 100644
index 0000000000..374d2f66a9
--- /dev/null
+++ b/accessible/base/RoleHGen.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env 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/.
+
+import re
+
+
+def generate(roleH, roleIdl):
+ input = open(roleIdl, "rt").read()
+ roles = re.findall(r"const unsigned long ROLE_([A-Z_]+) = (\d+);", input)
+
+ roleH.write(
+ "/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n"
+ "/* Roles are defined in accessible/interfaces/nsIAccessibleRole.idl */\n\n"
+ "#ifndef _role_h_\n"
+ "#define _role_h_\n\n"
+ "namespace mozilla {\n"
+ "namespace a11y {\n"
+ "namespace roles {\n\n"
+ "enum Role {\n"
+ )
+ for name, num in roles:
+ roleH.write(f" {name} = {num},\n")
+ lastName = roles[-1][0]
+ roleH.write(
+ f" LAST_ROLE = {lastName}\n"
+ "};\n\n"
+ "} // namespace roles\n\n"
+ "typedef enum mozilla::a11y::roles::Role role;\n\n"
+ "} // namespace a11y\n"
+ "} // namespace mozilla\n\n"
+ "#endif\n"
+ )
+
+
+# For debugging
+if __name__ == "__main__":
+ import sys
+
+ generate(sys.stdout, "accessible/interfaces/nsIAccessibleRole.idl")
diff --git a/accessible/base/RoleMap.h b/accessible/base/RoleMap.h
new file mode 100644
index 0000000000..ce82000188
--- /dev/null
+++ b/accessible/base/RoleMap.h
@@ -0,0 +1,1546 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// clang-format off
+/**
+ * Usage: declare the macro ROLE()with the following arguments:
+ * ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, msaaRole, ia2Role, nameRule)
+ */
+
+ROLE(NOTHING,
+ "nothing",
+ nullptr,
+ ATK_ROLE_UNKNOWN,
+ NSAccessibilityUnknownRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_CLIENT,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(MENUBAR,
+ "menubar",
+ nsGkAtoms::menubar,
+ ATK_ROLE_MENU_BAR,
+ NSAccessibilityMenuBarRole, //Irrelevant on OS X; the menubar will always be native and on the top of the screen.
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_MENUBAR,
+ ROLE_SYSTEM_MENUBAR,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(SCROLLBAR,
+ "scrollbar",
+ nsGkAtoms::scrollbar,
+ ATK_ROLE_SCROLL_BAR,
+ NSAccessibilityScrollBarRole, //We might need to make this its own mozAccessible, to support the children objects (valueindicator, down/up buttons).
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_SCROLLBAR,
+ ROLE_SYSTEM_SCROLLBAR,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromValueRule)
+
+ROLE(ALERT,
+ "alert",
+ nsGkAtoms::alert,
+ ATK_ROLE_ALERT,
+ NSAccessibilityGroupRole,
+ @"AXApplicationAlert",
+ ROLE_SYSTEM_ALERT,
+ ROLE_SYSTEM_ALERT,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(INTERNAL_FRAME,
+ "internal frame",
+ nullptr,
+ ATK_ROLE_INTERNAL_FRAME,
+ NSAccessibilityScrollAreaRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_INTERNAL_FRAME,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MENUPOPUP,
+ "menupopup",
+ nsGkAtoms::menu,
+ ATK_ROLE_MENU,
+ NSAccessibilityMenuRole, //The parent of menuitems.
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_MENUPOPUP,
+ ROLE_SYSTEM_MENUPOPUP,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MENUITEM,
+ "menuitem",
+ nsGkAtoms::menuitem,
+ ATK_ROLE_MENU_ITEM,
+ NSAccessibilityMenuItemRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_MENUITEM,
+ ROLE_SYSTEM_MENUITEM,
+ java::SessionAccessibility::CLASSNAME_MENUITEM,
+ eNameFromSubtreeRule)
+
+ROLE(TOOLTIP,
+ "tooltip",
+ nsGkAtoms::tooltip,
+ ATK_ROLE_TOOL_TIP,
+ NSAccessibilityGroupRole,
+ @"AXUserInterfaceTooltip",
+ ROLE_SYSTEM_TOOLTIP,
+ ROLE_SYSTEM_TOOLTIP,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(APPLICATION,
+ "application",
+ nsGkAtoms::application,
+ ATK_ROLE_EMBEDDED,
+ NSAccessibilityGroupRole, //Unused on OS X. the system will take care of this.
+ @"AXLandmarkApplication",
+ ROLE_SYSTEM_APPLICATION,
+ ROLE_SYSTEM_APPLICATION,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(DOCUMENT,
+ "document",
+ nsGkAtoms::document,
+ ATK_ROLE_DOCUMENT_WEB,
+ @"AXWebArea",
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_DOCUMENT,
+ ROLE_SYSTEM_DOCUMENT,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+/**
+ * msaa comment:
+ * We used to map to ROLE_SYSTEM_PANE, but JAWS would
+ * not read the accessible name for the contaning pane.
+ * However, JAWS will read the accessible name for a groupbox.
+ * By mapping a PANE to a GROUPING, we get no undesirable effects,
+ * but fortunately JAWS will then read the group's label,
+ * when an inner control gets focused.
+ */
+ROLE(PANE,
+ "pane",
+ nullptr,
+ ATK_ROLE_PANEL,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ ROLE_SYSTEM_GROUPING,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(DIALOG,
+ "dialog",
+ nsGkAtoms::dialog,
+ ATK_ROLE_DIALOG,
+ NSAccessibilityGroupRole, //There's a dialog subrole.
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_DIALOG,
+ ROLE_SYSTEM_DIALOG,
+ java::SessionAccessibility::CLASSNAME_DIALOG,
+ eNoNameRule)
+
+ROLE(GROUPING,
+ "grouping",
+ nsGkAtoms::group,
+ ATK_ROLE_PANEL,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ ROLE_SYSTEM_GROUPING,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(SEPARATOR,
+ "separator",
+ nsGkAtoms::separator_,
+ ATK_ROLE_SEPARATOR,
+ NSAccessibilitySplitterRole,
+ @"AXContentSeparator",
+ ROLE_SYSTEM_SEPARATOR,
+ ROLE_SYSTEM_SEPARATOR,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(TOOLBAR,
+ "toolbar",
+ nsGkAtoms::toolbar,
+ ATK_ROLE_TOOL_BAR,
+ NSAccessibilityToolbarRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_TOOLBAR,
+ ROLE_SYSTEM_TOOLBAR,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(STATUSBAR,
+ "statusbar",
+ nsGkAtoms::status,
+ ATK_ROLE_STATUSBAR,
+ NSAccessibilityGroupRole,
+ @"AXApplicationStatus",
+ ROLE_SYSTEM_STATUSBAR,
+ ROLE_SYSTEM_STATUSBAR,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(TABLE,
+ "table",
+ nsGkAtoms::table,
+ ATK_ROLE_TABLE,
+ NSAccessibilityTableRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_TABLE,
+ ROLE_SYSTEM_TABLE,
+ java::SessionAccessibility::CLASSNAME_GRIDVIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(COLUMNHEADER,
+ "columnheader",
+ nsGkAtoms::columnheader,
+ ATK_ROLE_COLUMN_HEADER,
+ NSAccessibilityCellRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_COLUMNHEADER,
+ ROLE_SYSTEM_COLUMNHEADER,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(ROWHEADER,
+ "rowheader",
+ nsGkAtoms::rowheader,
+ ATK_ROLE_ROW_HEADER,
+ NSAccessibilityCellRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_ROWHEADER,
+ ROLE_SYSTEM_ROWHEADER,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(ROW,
+ "row",
+ nsGkAtoms::row,
+ ATK_ROLE_TABLE_ROW,
+ NSAccessibilityRowRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_ROW,
+ ROLE_SYSTEM_ROW,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(CELL,
+ "cell",
+ nsGkAtoms::cell,
+ ATK_ROLE_TABLE_CELL,
+ NSAccessibilityCellRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_CELL,
+ ROLE_SYSTEM_CELL,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(LINK,
+ "link",
+ nsGkAtoms::link,
+ ATK_ROLE_LINK,
+ NSAccessibilityLinkRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_LINK,
+ ROLE_SYSTEM_LINK,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(LIST,
+ "list",
+ nsGkAtoms::list_,
+ ATK_ROLE_LIST,
+ NSAccessibilityListRole,
+ NSAccessibilityContentListSubrole,
+ ROLE_SYSTEM_LIST,
+ ROLE_SYSTEM_LIST,
+ java::SessionAccessibility::CLASSNAME_LISTVIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(LISTITEM,
+ "listitem",
+ nsGkAtoms::listitem,
+ ATK_ROLE_LIST_ITEM,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_LISTITEM,
+ ROLE_SYSTEM_LISTITEM,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(OUTLINE,
+ "outline",
+ nsGkAtoms::tree,
+ ATK_ROLE_TREE,
+ NSAccessibilityOutlineRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_OUTLINE,
+ ROLE_SYSTEM_OUTLINE,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(OUTLINEITEM,
+ "outlineitem",
+ nsGkAtoms::treeitem,
+ ATK_ROLE_TREE_ITEM,
+ NSAccessibilityRowRole,
+ NSAccessibilityOutlineRowSubrole,
+ ROLE_SYSTEM_OUTLINEITEM,
+ ROLE_SYSTEM_OUTLINEITEM,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(PAGETAB,
+ "pagetab",
+ nsGkAtoms::tab,
+ ATK_ROLE_PAGE_TAB,
+ NSAccessibilityRadioButtonRole,
+ @"AXTabButton", // Can be upgraded to NSAccessibilityTabButtonSubrole in 10.13
+ ROLE_SYSTEM_PAGETAB,
+ ROLE_SYSTEM_PAGETAB,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(PROPERTYPAGE,
+ "propertypage",
+ nsGkAtoms::tabpanel,
+ ATK_ROLE_SCROLL_PANE,
+ NSAccessibilityGroupRole,
+ @"AXTabPanel",
+ ROLE_SYSTEM_PROPERTYPAGE,
+ ROLE_SYSTEM_PROPERTYPAGE,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(GRAPHIC,
+ "graphic",
+ nsGkAtoms::image,
+ ATK_ROLE_IMAGE,
+ NSAccessibilityImageRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GRAPHIC,
+ ROLE_SYSTEM_GRAPHIC,
+ java::SessionAccessibility::CLASSNAME_IMAGE,
+ eNoNameRule)
+
+ROLE(STATICTEXT,
+ "statictext",
+ nullptr,
+ ATK_ROLE_UNKNOWN,
+ NSAccessibilityStaticTextRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_STATICTEXT,
+ ROLE_SYSTEM_STATICTEXT,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(TEXT_LEAF,
+ "text leaf",
+ nullptr,
+ ATK_ROLE_UNKNOWN,
+ NSAccessibilityStaticTextRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_TEXT,
+ ROLE_SYSTEM_TEXT,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(PUSHBUTTON,
+ "pushbutton",
+ nsGkAtoms::button,
+ ATK_ROLE_PUSH_BUTTON,
+ NSAccessibilityButtonRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_PUSHBUTTON,
+ ROLE_SYSTEM_PUSHBUTTON,
+ java::SessionAccessibility::CLASSNAME_BUTTON,
+ eNameFromSubtreeRule)
+
+ROLE(CHECKBUTTON,
+ "checkbutton",
+ nsGkAtoms::checkbox,
+ ATK_ROLE_CHECK_BOX,
+ NSAccessibilityCheckBoxRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_CHECKBUTTON,
+ ROLE_SYSTEM_CHECKBUTTON,
+ java::SessionAccessibility::CLASSNAME_CHECKBOX,
+ eNameFromSubtreeRule)
+
+ROLE(RADIOBUTTON,
+ "radiobutton",
+ nsGkAtoms::radio,
+ ATK_ROLE_RADIO_BUTTON,
+ NSAccessibilityRadioButtonRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_RADIOBUTTON,
+ ROLE_SYSTEM_RADIOBUTTON,
+ java::SessionAccessibility::CLASSNAME_RADIOBUTTON,
+ eNameFromSubtreeRule)
+
+// Equivalent of HTML select element with size="1". See also EDITCOMBOBOX.
+ROLE(COMBOBOX,
+ "combobox",
+ nsGkAtoms::combobox,
+ ATK_ROLE_COMBO_BOX,
+ NSAccessibilityPopUpButtonRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_COMBOBOX,
+ ROLE_SYSTEM_COMBOBOX,
+ java::SessionAccessibility::CLASSNAME_SPINNER,
+ eNameFromValueRule)
+
+ROLE(PROGRESSBAR,
+ "progressbar",
+ nsGkAtoms::progressbar,
+ ATK_ROLE_PROGRESS_BAR,
+ NSAccessibilityProgressIndicatorRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_PROGRESSBAR,
+ ROLE_SYSTEM_PROGRESSBAR,
+ java::SessionAccessibility::CLASSNAME_PROGRESSBAR,
+ eNameFromValueRule)
+
+ROLE(SLIDER,
+ "slider",
+ nsGkAtoms::slider,
+ ATK_ROLE_SLIDER,
+ NSAccessibilitySliderRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_SLIDER,
+ ROLE_SYSTEM_SLIDER,
+ java::SessionAccessibility::CLASSNAME_SEEKBAR,
+ eNameFromValueRule)
+
+ROLE(SPINBUTTON,
+ "spinbutton",
+ nsGkAtoms::spinbutton,
+ ATK_ROLE_SPIN_BUTTON,
+ NSAccessibilityIncrementorRole, //Subroles: Increment/Decrement.
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_SPINBUTTON,
+ ROLE_SYSTEM_SPINBUTTON,
+ java::SessionAccessibility::CLASSNAME_EDITTEXT,
+ eNameFromValueRule)
+
+ROLE(DIAGRAM,
+ "diagram",
+ nsGkAtoms::graphicsDocument,
+ ATK_ROLE_IMAGE,
+ NSAccessibilityImageRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_DIAGRAM,
+ ROLE_SYSTEM_DIAGRAM,
+ java::SessionAccessibility::CLASSNAME_IMAGE,
+ eNoNameRule)
+
+ROLE(ANIMATION,
+ "animation",
+ nsGkAtoms::marquee,
+ ATK_ROLE_ANIMATION,
+ NSAccessibilityUnknownRole,
+ @"AXApplicationMarquee",
+ ROLE_SYSTEM_ANIMATION,
+ ROLE_SYSTEM_ANIMATION,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(BUTTONDROPDOWN,
+ "buttondropdown",
+ nullptr,
+ ATK_ROLE_PUSH_BUTTON,
+ NSAccessibilityPopUpButtonRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_BUTTONDROPDOWN,
+ ROLE_SYSTEM_BUTTONDROPDOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(BUTTONMENU,
+ "buttonmenu",
+ nsGkAtoms::button,
+ ATK_ROLE_PUSH_BUTTON,
+ NSAccessibilityMenuButtonRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_BUTTONMENU,
+ ROLE_SYSTEM_BUTTONMENU,
+ java::SessionAccessibility::CLASSNAME_SPINNER,
+ eNameFromSubtreeRule)
+
+ROLE(WHITESPACE,
+ "whitespace",
+ nullptr,
+ ATK_ROLE_UNKNOWN,
+ NSAccessibilityUnknownRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_WHITESPACE,
+ ROLE_SYSTEM_WHITESPACE,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(PAGETABLIST,
+ "pagetablist",
+ nsGkAtoms::tablist,
+ ATK_ROLE_PAGE_TAB_LIST,
+ NSAccessibilityTabGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_PAGETABLIST,
+ ROLE_SYSTEM_PAGETABLIST,
+ java::SessionAccessibility::CLASSNAME_TABWIDGET,
+ eNoNameRule)
+
+ROLE(CANVAS,
+ "canvas",
+ nullptr,
+ ATK_ROLE_CANVAS,
+ NSAccessibilityImageRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GRAPHIC,
+ IA2_ROLE_CANVAS,
+ java::SessionAccessibility::CLASSNAME_IMAGE,
+ eNoNameRule)
+
+ROLE(CHECK_MENU_ITEM,
+ "check menu item",
+ nsGkAtoms::menuitemcheckbox,
+ ATK_ROLE_CHECK_MENU_ITEM,
+ NSAccessibilityMenuItemRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_MENUITEM,
+ IA2_ROLE_CHECK_MENU_ITEM,
+ java::SessionAccessibility::CLASSNAME_MENUITEM,
+ eNameFromSubtreeRule)
+
+ROLE(DATE_EDITOR,
+ "date editor",
+ nullptr,
+ ATK_ROLE_DATE_EDITOR,
+ @"AXGroup",
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_DATE_EDITOR,
+ java::SessionAccessibility::CLASSNAME_SPINNER,
+ eNoNameRule)
+
+ROLE(CHROME_WINDOW,
+ "chrome window",
+ nullptr,
+ ATK_ROLE_FRAME,
+ NSAccessibilityGroupRole, //Contains the main Firefox UI
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_APPLICATION,
+ IA2_ROLE_FRAME,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(LABEL,
+ "label",
+ nullptr,
+ ATK_ROLE_LABEL,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_STATICTEXT,
+ IA2_ROLE_LABEL,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(PASSWORD_TEXT,
+ "password text",
+ nullptr,
+ ATK_ROLE_PASSWORD_TEXT,
+ NSAccessibilityTextFieldRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_TEXT,
+ ROLE_SYSTEM_TEXT,
+ java::SessionAccessibility::CLASSNAME_EDITTEXT,
+ eNoNameRule)
+
+ROLE(RADIO_MENU_ITEM,
+ "radio menu item",
+ nsGkAtoms::menuitemradio,
+ ATK_ROLE_RADIO_MENU_ITEM,
+ NSAccessibilityMenuItemRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_MENUITEM,
+ IA2_ROLE_RADIO_MENU_ITEM,
+ java::SessionAccessibility::CLASSNAME_MENUITEM,
+ eNameFromSubtreeRule)
+
+ROLE(TEXT_CONTAINER,
+ "text container",
+ nsGkAtoms::generic,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_TEXT_FRAME,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(TOGGLE_BUTTON,
+ "toggle button",
+ nsGkAtoms::button,
+ ATK_ROLE_TOGGLE_BUTTON,
+ NSAccessibilityCheckBoxRole,
+ NSAccessibilityToggleSubrole,
+ ROLE_SYSTEM_PUSHBUTTON,
+ IA2_ROLE_TOGGLE_BUTTON,
+ java::SessionAccessibility::CLASSNAME_TOGGLEBUTTON,
+ eNameFromSubtreeRule)
+
+ROLE(TREE_TABLE,
+ "tree table",
+ nsGkAtoms::treegrid,
+ ATK_ROLE_TREE_TABLE,
+ NSAccessibilityTableRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_OUTLINE,
+ ROLE_SYSTEM_OUTLINE,
+ java::SessionAccessibility::CLASSNAME_GRIDVIEW,
+ eNoNameRule)
+
+ROLE(PARAGRAPH,
+ "paragraph",
+ nsGkAtoms::paragraph,
+ ATK_ROLE_PARAGRAPH,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_PARAGRAPH,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(ENTRY,
+ "entry",
+ nsGkAtoms::textbox,
+ ATK_ROLE_ENTRY,
+ NSAccessibilityTextFieldRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_TEXT,
+ ROLE_SYSTEM_TEXT,
+ java::SessionAccessibility::CLASSNAME_EDITTEXT,
+ eNameFromValueRule)
+
+ROLE(CAPTION,
+ "caption",
+ nsGkAtoms::caption,
+ ATK_ROLE_CAPTION,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_CAPTION,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(NON_NATIVE_DOCUMENT,
+ "non-native document",
+ nsGkAtoms::document,
+ ATK_ROLE_DOCUMENT_FRAME,
+ NSAccessibilityGroupRole,
+ @"AXDocument",
+ ROLE_SYSTEM_DOCUMENT,
+ ROLE_SYSTEM_DOCUMENT,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(HEADING,
+ "heading",
+ nsGkAtoms::heading,
+ ATK_ROLE_HEADING,
+ @"AXHeading",
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_HEADING,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(SECTION,
+ "section",
+ nsGkAtoms::generic,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_SECTION,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(FORM,
+ "form",
+ nsGkAtoms::form,
+ ATK_ROLE_FORM,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_FORM,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(APP_ROOT,
+ "app root",
+ nullptr,
+ ATK_ROLE_APPLICATION,
+ NSAccessibilityUnknownRole, //Unused on OS X
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_APPLICATION,
+ ROLE_SYSTEM_APPLICATION,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(PARENT_MENUITEM,
+ "parent menuitem",
+ nsGkAtoms::menuitem,
+ ATK_ROLE_MENU,
+ NSAccessibilityMenuItemRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_MENUITEM,
+ ROLE_SYSTEM_MENUITEM,
+ java::SessionAccessibility::CLASSNAME_MENUITEM,
+ eNameFromSubtreeRule)
+
+ROLE(COMBOBOX_LIST,
+ "combobox list",
+ nsGkAtoms::listbox,
+ ATK_ROLE_MENU,
+ NSAccessibilityMenuRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_LIST,
+ ROLE_SYSTEM_LIST,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(COMBOBOX_OPTION,
+ "combobox option",
+ nsGkAtoms::option,
+ ATK_ROLE_MENU_ITEM,
+ NSAccessibilityMenuItemRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_LISTITEM,
+ ROLE_SYSTEM_LISTITEM,
+ java::SessionAccessibility::CLASSNAME_MENUITEM,
+ eNameFromSubtreeRule)
+
+ROLE(IMAGE_MAP,
+ "image map",
+ nsGkAtoms::img,
+ ATK_ROLE_IMAGE,
+ @"AXImageMap",
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GRAPHIC,
+ ROLE_SYSTEM_GRAPHIC,
+ java::SessionAccessibility::CLASSNAME_IMAGE,
+ eNoNameRule)
+
+ROLE(OPTION,
+ "listbox option",
+ nsGkAtoms::option,
+ ATK_ROLE_LIST_ITEM,
+ NSAccessibilityStaticTextRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_LISTITEM,
+ ROLE_SYSTEM_LISTITEM,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(RICH_OPTION,
+ "listbox rich option",
+ nullptr,
+ ATK_ROLE_LIST_ITEM,
+ NSAccessibilityRowRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_LISTITEM,
+ ROLE_SYSTEM_LISTITEM,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(LISTBOX,
+ "listbox",
+ nsGkAtoms::listbox,
+ ATK_ROLE_LIST_BOX,
+ NSAccessibilityListRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_LIST,
+ ROLE_SYSTEM_LIST,
+ java::SessionAccessibility::CLASSNAME_LISTVIEW,
+ eNoNameRule)
+
+ROLE(FLAT_EQUATION,
+ "flat equation",
+ nsGkAtoms::math,
+ ATK_ROLE_UNKNOWN,
+ NSAccessibilityUnknownRole,
+ @"AXDocumentMath",
+ ROLE_SYSTEM_EQUATION,
+ ROLE_SYSTEM_EQUATION,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(GRID_CELL,
+ "gridcell",
+ nsGkAtoms::gridcell,
+ ATK_ROLE_TABLE_CELL,
+ NSAccessibilityCellRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_CELL,
+ ROLE_SYSTEM_CELL,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(NOTE,
+ "note",
+ nsGkAtoms::note_,
+ ATK_ROLE_COMMENT,
+ NSAccessibilityGroupRole,
+ @"AXDocumentNote",
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_NOTE,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(FIGURE,
+ "figure",
+ nsGkAtoms::figure,
+ ATK_ROLE_PANEL,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ ROLE_SYSTEM_GROUPING,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(CHECK_RICH_OPTION,
+ "check rich option",
+ nullptr,
+ ATK_ROLE_CHECK_BOX,
+ NSAccessibilityCheckBoxRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_CHECKBUTTON,
+ ROLE_SYSTEM_CHECKBUTTON,
+ java::SessionAccessibility::CLASSNAME_CHECKBOX,
+ eNameFromSubtreeRule)
+
+ROLE(DEFINITION_LIST,
+ "definitionlist",
+ nullptr,
+ ATK_ROLE_LIST,
+ NSAccessibilityListRole,
+ @"AXDescriptionList",
+ ROLE_SYSTEM_LIST,
+ ROLE_SYSTEM_LIST,
+ java::SessionAccessibility::CLASSNAME_LISTVIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(TERM,
+ "term",
+ nsGkAtoms::term,
+ ATK_ROLE_DESCRIPTION_TERM,
+ NSAccessibilityGroupRole,
+ @"AXTerm",
+ ROLE_SYSTEM_LISTITEM,
+ ROLE_SYSTEM_LISTITEM,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(DEFINITION,
+ "definition",
+ nsGkAtoms::definition,
+ ATK_ROLE_PARAGRAPH,
+ NSAccessibilityGroupRole,
+ @"AXDescription",
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_PARAGRAPH,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(KEY,
+ "key",
+ nullptr,
+ ATK_ROLE_PUSH_BUTTON,
+ NSAccessibilityButtonRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_PUSHBUTTON,
+ ROLE_SYSTEM_PUSHBUTTON,
+ java::SessionAccessibility::CLASSNAME_BUTTON,
+ eNameFromSubtreeRule)
+
+ROLE(SWITCH,
+ "switch",
+ nsGkAtoms::svgSwitch,
+ ATK_ROLE_TOGGLE_BUTTON,
+ NSAccessibilityCheckBoxRole,
+ NSAccessibilitySwitchSubrole,
+ ROLE_SYSTEM_CHECKBUTTON,
+ IA2_ROLE_TOGGLE_BUTTON,
+ java::SessionAccessibility::CLASSNAME_CHECKBOX,
+ eNameFromSubtreeRule)
+
+ROLE(MATHML_MATH,
+ "math",
+ nsGkAtoms::math,
+ ATK_ROLE_MATH,
+ NSAccessibilityGroupRole,
+ @"AXDocumentMath",
+ ROLE_SYSTEM_EQUATION,
+ ROLE_SYSTEM_EQUATION,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_IDENTIFIER,
+ "mathml identifier",
+ nullptr,
+ ATK_ROLE_STATIC,
+ NSAccessibilityGroupRole,
+ @"AXMathIdentifier",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(MATHML_NUMBER,
+ "mathml number",
+ nullptr,
+ ATK_ROLE_STATIC,
+ NSAccessibilityGroupRole,
+ @"AXMathNumber",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(MATHML_OPERATOR,
+ "mathml operator",
+ nullptr,
+ ATK_ROLE_STATIC,
+ NSAccessibilityGroupRole,
+ @"AXMathOperator",
+ // XXX: NSAccessibility also uses subroles AXMathSeparatorOperator and
+ // AXMathFenceOperator. We should use the NS_MATHML_OPERATOR_FENCE and
+ // NS_MATHML_OPERATOR_SEPARATOR bits of nsOperatorFlags, but currently they
+ // are only available from the MathML layout code. Hence we just fallback
+ // to subrole AXMathOperator for now.
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(MATHML_TEXT,
+ "mathml text",
+ nullptr,
+ ATK_ROLE_STATIC,
+ NSAccessibilityGroupRole,
+ @"AXMathRoot",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(MATHML_STRING_LITERAL,
+ "mathml string literal",
+ nullptr,
+ ATK_ROLE_STATIC,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeRule)
+
+ROLE(MATHML_GLYPH,
+ "mathml glyph",
+ nullptr,
+ ATK_ROLE_IMAGE,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_IMAGE,
+ eNameFromSubtreeRule)
+
+ROLE(MATHML_ROW,
+ "mathml row",
+ nullptr,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ @"AXMathRow",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_FRACTION,
+ "mathml fraction",
+ nullptr,
+ ATK_ROLE_MATH_FRACTION,
+ NSAccessibilityGroupRole,
+ @"AXMathFraction",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_SQUARE_ROOT,
+ "mathml square root",
+ nullptr,
+ ATK_ROLE_MATH_ROOT,
+ NSAccessibilityGroupRole,
+ @"AXMathSquareRoot",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_ROOT,
+ "mathml root",
+ nullptr,
+ ATK_ROLE_MATH_ROOT,
+ NSAccessibilityGroupRole,
+ @"AXMathRoot",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_ENCLOSED,
+ "mathml enclosed",
+ nullptr,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_STYLE,
+ "mathml style",
+ nullptr,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ @"AXMathRow",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_SUB,
+ "mathml sub",
+ nullptr,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ @"AXMathSubscriptSuperscript",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_SUP,
+ "mathml sup",
+ nullptr,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ @"AXMathSubscriptSuperscript",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_SUB_SUP,
+ "mathml sub sup",
+ nullptr,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ @"AXMathSubscriptSuperscript",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_UNDER,
+ "mathml under",
+ nullptr,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ @"AXMathUnderOver",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_OVER,
+ "mathml over",
+ nullptr,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ @"AXMathUnderOver",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_UNDER_OVER,
+ "mathml under over",
+ nullptr,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ @"AXMathUnderOver",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_MULTISCRIPTS,
+ "mathml multiscripts",
+ nullptr,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ @"AXMathMultiscript",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_TABLE,
+ "mathml table",
+ nullptr,
+ ATK_ROLE_TABLE,
+ NSAccessibilityGroupRole,
+ @"AXMathTable",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_GRIDVIEW,
+ eNoNameRule)
+
+ROLE(MATHML_LABELED_ROW,
+ "mathml labeled row",
+ nullptr,
+ ATK_ROLE_TABLE_ROW,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_TABLE_ROW,
+ "mathml table row",
+ nullptr,
+ ATK_ROLE_TABLE_ROW,
+ NSAccessibilityGroupRole,
+ @"AXMathTableRow",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_CELL,
+ "mathml cell",
+ nullptr,
+ ATK_ROLE_TABLE_CELL,
+ NSAccessibilityGroupRole,
+ @"AXMathTableCell",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_ACTION,
+ "mathml action",
+ nullptr,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_ERROR,
+ "mathml error",
+ nullptr,
+ ATK_ROLE_SECTION,
+ NSAccessibilityGroupRole,
+ @"AXMathRow",
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_STACK,
+ "mathml stack",
+ nullptr,
+ ATK_ROLE_UNKNOWN,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_LONG_DIVISION,
+ "mathml long division",
+ nullptr,
+ ATK_ROLE_UNKNOWN,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_STACK_GROUP,
+ "mathml stack group",
+ nullptr,
+ ATK_ROLE_UNKNOWN,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_STACK_ROW,
+ "mathml stack row",
+ nullptr,
+ ATK_ROLE_UNKNOWN,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_STACK_CARRIES,
+ "mathml stack carries",
+ nullptr,
+ ATK_ROLE_UNKNOWN,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_STACK_CARRY,
+ "mathml stack carry",
+ nullptr,
+ ATK_ROLE_UNKNOWN,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MATHML_STACK_LINE,
+ "mathml stack line",
+ nullptr,
+ ATK_ROLE_UNKNOWN,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ 0,
+ IA2_ROLE_UNKNOWN,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(RADIO_GROUP,
+ "grouping",
+ nsGkAtoms::radiogroup,
+ ATK_ROLE_PANEL,
+ NSAccessibilityRadioGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ ROLE_SYSTEM_GROUPING,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(TEXT,
+ "text",
+ nsGkAtoms::generic,
+ ATK_ROLE_STATIC,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_TEXT_FRAME,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(DETAILS,
+ "details",
+ nsGkAtoms::group,
+ ATK_ROLE_PANEL,
+ NSAccessibilityGroupRole,
+ @"AXDetails",
+ ROLE_SYSTEM_GROUPING,
+ ROLE_SYSTEM_GROUPING,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(SUMMARY,
+ "summary",
+ nullptr,
+ ATK_ROLE_PUSH_BUTTON,
+ NSAccessibilityButtonRole,
+ @"AXSummary",
+ ROLE_SYSTEM_PUSHBUTTON,
+ ROLE_SYSTEM_PUSHBUTTON,
+ java::SessionAccessibility::CLASSNAME_BUTTON,
+ eNameFromSubtreeRule)
+
+ROLE(LANDMARK,
+ "landmark",
+ nullptr,
+ ATK_ROLE_LANDMARK,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_LANDMARK,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(NAVIGATION,
+ "navigation",
+ nullptr,
+ ATK_ROLE_LANDMARK,
+ NSAccessibilityGroupRole,
+ @"AXLandmarkNavigation",
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_LANDMARK,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(FOOTNOTE,
+ "footnote",
+ nullptr,
+ ATK_ROLE_FOOTNOTE,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_FOOTNOTE,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(ARTICLE,
+ "article",
+ nsGkAtoms::article,
+ ATK_ROLE_ARTICLE,
+ NSAccessibilityGroupRole,
+ @"AXDocumentArticle",
+ ROLE_SYSTEM_DOCUMENT,
+ ROLE_SYSTEM_DOCUMENT,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(REGION,
+ "region",
+ nsGkAtoms::region,
+ ATK_ROLE_LANDMARK,
+ NSAccessibilityGroupRole,
+ @"AXLandmarkRegion",
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_LANDMARK,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+// A composite widget with a text input and popup. Used for ARIA role combobox.
+// See also COMBOBOX.
+ROLE(EDITCOMBOBOX,
+ "editcombobox",
+ nsGkAtoms::combobox,
+ ATK_ROLE_COMBO_BOX,
+ NSAccessibilityComboBoxRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_COMBOBOX,
+ ROLE_SYSTEM_COMBOBOX,
+ java::SessionAccessibility::CLASSNAME_EDITTEXT,
+ eNameFromValueRule)
+
+ROLE(BLOCKQUOTE,
+ "blockquote",
+ nsGkAtoms::blockquote,
+ ATK_ROLE_BLOCK_QUOTE,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_BLOCK_QUOTE,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(CONTENT_DELETION,
+ "content deletion",
+ nsGkAtoms::deletion,
+ ATK_ROLE_CONTENT_DELETION,
+ NSAccessibilityGroupRole,
+ @"AXDeleteStyleGroup",
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_CONTENT_DELETION,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(CONTENT_INSERTION,
+ "content insertion",
+ nsGkAtoms::insertion,
+ ATK_ROLE_CONTENT_INSERTION,
+ NSAccessibilityGroupRole,
+ @"AXInsertStyleGroup",
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_CONTENT_INSERTION,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(FORM_LANDMARK,
+ "form",
+ nsGkAtoms::form,
+ ATK_ROLE_LANDMARK,
+ NSAccessibilityGroupRole,
+ @"AXLandmarkForm",
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_FORM,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(MARK,
+ "mark",
+ nsGkAtoms::mark,
+ ATK_ROLE_MARK,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_MARK,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(SUGGESTION,
+ "suggestion",
+ nsGkAtoms::suggestion,
+ ATK_ROLE_SUGGESTION,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_SUGGESTION,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(COMMENT,
+ "comment",
+ nsGkAtoms::comment,
+ ATK_ROLE_COMMENT,
+ NSAccessibilityGroupRole,
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_COMMENT,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(CODE,
+ "code",
+ nsGkAtoms::code,
+ ATK_ROLE_STATIC,
+ NSAccessibilityGroupRole,
+ @"AXCodeStyleGroup",
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_TEXT_FRAME,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(TIME_EDITOR,
+ "time editor",
+ nullptr,
+ ATK_ROLE_PANEL,
+ @"AXTimeField",
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_GROUPING,
+ ROLE_SYSTEM_GROUPING,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(LISTITEM_MARKER,
+ "list item marker",
+ nullptr,
+ ATK_ROLE_UNKNOWN,
+ @"AXListMarker",
+ NSAccessibilityUnknownSubrole,
+ ROLE_SYSTEM_STATICTEXT,
+ ROLE_SYSTEM_STATICTEXT,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNoNameRule)
+
+ROLE(METER,
+ "meter",
+ nsGkAtoms::meter,
+ ATK_ROLE_LEVEL_BAR,
+ NSAccessibilityLevelIndicatorRole,
+ @"AXMeter",
+ ROLE_SYSTEM_PROGRESSBAR,
+ ROLE_SYSTEM_PROGRESSBAR,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromValueRule)
+
+ROLE(SUBSCRIPT,
+ "subscript",
+ nsGkAtoms::subscript,
+ ATK_ROLE_SUBSCRIPT,
+ NSAccessibilityGroupRole,
+ @"AXSubscriptStyleGroup",
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_TEXT_FRAME,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(SUPERSCRIPT,
+ "superscript",
+ nsGkAtoms::superscript,
+ ATK_ROLE_SUPERSCRIPT,
+ NSAccessibilityGroupRole,
+ @"AXSuperscriptStyleGroup",
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_TEXT_FRAME,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(EMPHASIS,
+ "emphasis",
+ nsGkAtoms::emphasis,
+ ATK_ROLE_STATIC,
+ NSAccessibilityGroupRole,
+ @"AXEmphasisStyleGroup",
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_TEXT_FRAME,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(STRONG,
+ "strong",
+ nsGkAtoms::strong,
+ ATK_ROLE_STATIC,
+ NSAccessibilityGroupRole,
+ @"AXStrongStyleGroup",
+ ROLE_SYSTEM_GROUPING,
+ IA2_ROLE_TEXT_FRAME,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+ROLE(TIME,
+ "time",
+ nsGkAtoms::time,
+ ATK_ROLE_STATIC,
+ NSAccessibilityGroupRole,
+ @"AXTimeGroup",
+ ROLE_SYSTEM_GROUPING,
+ ROLE_SYSTEM_GROUPING,
+ java::SessionAccessibility::CLASSNAME_VIEW,
+ eNameFromSubtreeIfReqRule)
+
+// clang-format on
diff --git a/accessible/base/SelectionManager.cpp b/accessible/base/SelectionManager.cpp
new file mode 100644
index 0000000000..97721bb439
--- /dev/null
+++ b/accessible/base/SelectionManager.cpp
@@ -0,0 +1,246 @@
+/* -*- Mode: C++; tab-width: 4; 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 "mozilla/a11y/SelectionManager.h"
+
+#include "DocAccessible-inl.h"
+#include "HyperTextAccessible.h"
+#include "HyperTextAccessible-inl.h"
+#include "nsAccUtils.h"
+#include "nsCoreUtils.h"
+#include "nsEventShell.h"
+#include "nsFrameSelection.h"
+#include "TextLeafRange.h"
+
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/Element.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+using mozilla::dom::Selection;
+
+struct mozilla::a11y::SelData final {
+ SelData(Selection* aSel, int32_t aReason, int32_t aGranularity)
+ : mSel(aSel), mReason(aReason), mGranularity(aGranularity) {}
+
+ RefPtr<Selection> mSel;
+ int16_t mReason;
+ int32_t mGranularity;
+
+ NS_INLINE_DECL_REFCOUNTING(SelData)
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~SelData() {}
+};
+
+SelectionManager::SelectionManager()
+ : mCaretOffset(-1), mAccWithCaret(nullptr) {}
+
+void SelectionManager::ClearControlSelectionListener() {
+ // Remove 'this' registered as selection listener for the normal selection.
+ if (mCurrCtrlNormalSel) {
+ mCurrCtrlNormalSel->RemoveSelectionListener(this);
+ mCurrCtrlNormalSel = nullptr;
+ }
+
+ // Remove 'this' registered as selection listener for the spellcheck
+ // selection.
+ if (mCurrCtrlSpellSel) {
+ mCurrCtrlSpellSel->RemoveSelectionListener(this);
+ mCurrCtrlSpellSel = nullptr;
+ }
+}
+
+void SelectionManager::SetControlSelectionListener(dom::Element* aFocusedElm) {
+ // When focus moves such that the caret is part of a new frame selection
+ // this removes the old selection listener and attaches a new one for
+ // the current focus.
+ ClearControlSelectionListener();
+
+ nsIFrame* controlFrame = aFocusedElm->GetPrimaryFrame();
+ if (!controlFrame) return;
+
+ const nsFrameSelection* frameSel = controlFrame->GetConstFrameSelection();
+ NS_ASSERTION(frameSel, "No frame selection for focused element!");
+ if (!frameSel) return;
+
+ // Register 'this' as selection listener for the normal selection.
+ Selection* normalSel = frameSel->GetSelection(SelectionType::eNormal);
+ normalSel->AddSelectionListener(this);
+ mCurrCtrlNormalSel = normalSel;
+
+ // Register 'this' as selection listener for the spell check selection.
+ Selection* spellSel = frameSel->GetSelection(SelectionType::eSpellCheck);
+ spellSel->AddSelectionListener(this);
+ mCurrCtrlSpellSel = spellSel;
+}
+
+void SelectionManager::AddDocSelectionListener(PresShell* aPresShell) {
+ const nsFrameSelection* frameSel = aPresShell->ConstFrameSelection();
+
+ // Register 'this' as selection listener for the normal selection.
+ Selection* normalSel = frameSel->GetSelection(SelectionType::eNormal);
+ normalSel->AddSelectionListener(this);
+
+ // Register 'this' as selection listener for the spell check selection.
+ Selection* spellSel = frameSel->GetSelection(SelectionType::eSpellCheck);
+ spellSel->AddSelectionListener(this);
+}
+
+void SelectionManager::RemoveDocSelectionListener(PresShell* aPresShell) {
+ const nsFrameSelection* frameSel = aPresShell->ConstFrameSelection();
+
+ // Remove 'this' registered as selection listener for the normal selection.
+ Selection* normalSel = frameSel->GetSelection(SelectionType::eNormal);
+ normalSel->RemoveSelectionListener(this);
+
+ // Remove 'this' registered as selection listener for the spellcheck
+ // selection.
+ Selection* spellSel = frameSel->GetSelection(SelectionType::eSpellCheck);
+ spellSel->RemoveSelectionListener(this);
+
+ if (mCurrCtrlNormalSel) {
+ if (mCurrCtrlNormalSel->GetPresShell() == aPresShell) {
+ // Remove 'this' registered as selection listener for the normal selection
+ // if we are removing listeners for its PresShell.
+ mCurrCtrlNormalSel->RemoveSelectionListener(this);
+ mCurrCtrlNormalSel = nullptr;
+ }
+ }
+
+ if (mCurrCtrlSpellSel) {
+ if (mCurrCtrlSpellSel->GetPresShell() == aPresShell) {
+ // Remove 'this' registered as selection listener for the spellcheck
+ // selection if we are removing listeners for its PresShell.
+ mCurrCtrlSpellSel->RemoveSelectionListener(this);
+ mCurrCtrlSpellSel = nullptr;
+ }
+ }
+}
+
+void SelectionManager::ProcessTextSelChangeEvent(AccEvent* aEvent) {
+ // Fire selection change event if it's not pure caret-move selection change,
+ // i.e. the accessible has or had not collapsed selection. Also, it must not
+ // be a collapsed selection on the container of a focused text field, since
+ // the text field has an independent selection and will thus fire its own
+ // selection events.
+ AccTextSelChangeEvent* event = downcast_accEvent(aEvent);
+ if (!event->IsCaretMoveOnly() &&
+ !(event->mSel->IsCollapsed() && event->mSel != mCurrCtrlNormalSel &&
+ FocusMgr() && FocusMgr()->FocusedLocalAccessible() &&
+ FocusMgr()->FocusedLocalAccessible()->IsTextField())) {
+ nsEventShell::FireEvent(aEvent);
+ }
+
+ // Fire caret move event if there's a caret in the selection.
+ nsINode* caretCntrNode = nsCoreUtils::GetDOMNodeFromDOMPoint(
+ event->mSel->GetFocusNode(), event->mSel->FocusOffset());
+ if (!caretCntrNode) return;
+
+ HyperTextAccessible* caretCntr = nsAccUtils::GetTextContainer(caretCntrNode);
+ NS_ASSERTION(
+ caretCntr,
+ "No text container for focus while there's one for common ancestor?!");
+ if (!caretCntr) return;
+
+ Selection* selection = caretCntr->DOMSelection();
+
+ // XXX Sometimes we can't get a selection for caretCntr, in that case assume
+ // event->mSel is correct.
+ if (!selection) selection = event->mSel;
+
+ mCaretOffset = caretCntr->DOMPointToOffset(selection->GetFocusNode(),
+ selection->FocusOffset());
+ mAccWithCaret = caretCntr;
+ if (mCaretOffset != -1) {
+ RefPtr<AccCaretMoveEvent> caretMoveEvent =
+ new AccCaretMoveEvent(caretCntr, mCaretOffset, selection->IsCollapsed(),
+ caretCntr->IsCaretAtEndOfLine(),
+ event->GetGranularity(), aEvent->FromUserInput());
+ nsEventShell::FireEvent(caretMoveEvent);
+ }
+}
+
+NS_IMETHODIMP
+SelectionManager::NotifySelectionChanged(dom::Document* aDocument,
+ Selection* aSelection, int16_t aReason,
+ int32_t aAmount) {
+ if (NS_WARN_IF(!aDocument) || NS_WARN_IF(!aSelection)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ DocAccessible* document = GetAccService()->GetDocAccessible(aDocument);
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eSelection)) {
+ logging::SelChange(aSelection, document, aReason);
+ }
+#endif
+
+ if (document) {
+ // Selection manager has longer lifetime than any document accessible,
+ // so that we are guaranteed that the notification is processed before
+ // the selection manager is destroyed.
+ RefPtr<SelData> selData = new SelData(aSelection, aReason, aAmount);
+ document->HandleNotification<SelectionManager, SelData>(
+ this, &SelectionManager::ProcessSelectionChanged, selData);
+ }
+
+ return NS_OK;
+}
+
+void SelectionManager::ProcessSelectionChanged(SelData* aSelData) {
+ Selection* selection = aSelData->mSel;
+ if (!selection->GetPresShell()) return;
+
+ const nsRange* range = selection->GetAnchorFocusRange();
+ nsINode* cntrNode = nullptr;
+ if (range) {
+ cntrNode = range->GetClosestCommonInclusiveAncestor();
+ }
+
+ if (!cntrNode) {
+ cntrNode = selection->GetFrameSelection()->GetAncestorLimiter();
+ if (!cntrNode) {
+ cntrNode = selection->GetPresShell()->GetDocument();
+ NS_ASSERTION(aSelData->mSel->GetPresShell()->ConstFrameSelection() ==
+ selection->GetFrameSelection(),
+ "Wrong selection container was used!");
+ }
+ }
+
+ HyperTextAccessible* text = nsAccUtils::GetTextContainer(cntrNode);
+ if (!text) {
+ // FIXME bug 1126649
+ NS_ERROR("We must reach document accessible implementing text interface!");
+ return;
+ }
+
+ if (selection->GetType() == SelectionType::eNormal) {
+ RefPtr<AccEvent> event = new AccTextSelChangeEvent(
+ text, selection, aSelData->mReason, aSelData->mGranularity);
+ text->Document()->FireDelayedEvent(event);
+
+ } else if (selection->GetType() == SelectionType::eSpellCheck) {
+ // XXX: fire an event for container accessible of the focus/anchor range
+ // of the spelcheck selection.
+ text->Document()->FireDelayedEvent(
+ nsIAccessibleEvent::EVENT_TEXT_ATTRIBUTE_CHANGED, text);
+ }
+}
+
+void SelectionManager::SpellCheckRangeChanged(const nsRange& aRange) {
+ // Events are fired in SelectionManager::NotifySelectionChanged. This is only
+ // used to push cache updates.
+ if (IPCAccessibilityActive()) {
+ dom::Document* doc = aRange.GetStartContainer()->OwnerDoc();
+ MOZ_ASSERT(doc);
+ TextLeafPoint::UpdateCachedSpellingError(doc, aRange);
+ }
+}
+
+SelectionManager::~SelectionManager() = default;
diff --git a/accessible/base/SelectionManager.h b/accessible/base/SelectionManager.h
new file mode 100644
index 0000000000..1dba086036
--- /dev/null
+++ b/accessible/base/SelectionManager.h
@@ -0,0 +1,141 @@
+/* -*- Mode: C++; tab-width: 4; 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_SelectionManager_h__
+#define mozilla_a11y_SelectionManager_h__
+
+#include "nsISelectionListener.h"
+#include "mozilla/WeakPtr.h"
+
+class nsRange;
+
+namespace mozilla {
+
+class PresShell;
+
+namespace dom {
+class Element;
+class Selection;
+} // namespace dom
+
+namespace a11y {
+
+class AccEvent;
+class HyperTextAccessible;
+
+/**
+ * This special accessibility class is for the caret and selection management.
+ * There is only 1 visible caret per top level window. However, there may be
+ * several visible selections.
+ *
+ * The important selections are the one owned by each document, and the one in
+ * the currently focused control.
+ *
+ * On Windows this class is used to move an invisible system caret that
+ * shadows the Mozilla caret. Windows will also automatically map this to
+ * the MSAA caret accessible object (via OBJID_CARET) (as opposed to the root
+ * accessible tree for a window which is retrieved with OBJID_CLIENT).
+ *
+ * For ATK and IAccessible2, this class is used to fire caret move and
+ * selection change events.
+ */
+
+struct SelData;
+
+class SelectionManager : public nsISelectionListener {
+ public:
+ // nsISupports
+ // implemented by derived nsAccessibilityService
+
+ // nsISelectionListener
+ NS_DECL_NSISELECTIONLISTENER
+
+ // SelectionManager
+ void Shutdown() { ClearControlSelectionListener(); }
+
+ /**
+ * Listen to selection events on the focused control.
+ *
+ * Note: only one control's selection events are listened to at a time. This
+ * will remove the previous control's selection listener.
+ */
+ void SetControlSelectionListener(dom::Element* aFocusedElm);
+
+ /**
+ * Stop listening to selection events on the control.
+ */
+ void ClearControlSelectionListener();
+
+ /**
+ * Listen to selection events on the document.
+ */
+ void AddDocSelectionListener(PresShell* aPresShell);
+
+ /**
+ * Stop listening to selection events for a given document
+ */
+ void RemoveDocSelectionListener(PresShell* aPresShell);
+
+ /**
+ * Process delayed event, results in caret move and text selection change
+ * events.
+ */
+ void ProcessTextSelChangeEvent(AccEvent* aEvent);
+
+ /**
+ * Gets the current caret offset/hypertext accessible pair. If there is no
+ * current pair, then returns -1 for the offset and a nullptr for the
+ * accessible.
+ */
+ inline HyperTextAccessible* AccessibleWithCaret(int32_t* aCaret) {
+ if (aCaret) *aCaret = mCaretOffset;
+
+ return mAccWithCaret;
+ }
+
+ /**
+ * Update caret offset when it doesn't go through a caret move event.
+ */
+ inline void UpdateCaretOffset(HyperTextAccessible* aItem, int32_t aOffset) {
+ mAccWithCaret = aItem;
+ mCaretOffset = aOffset;
+ }
+
+ inline void ResetCaretOffset() {
+ mCaretOffset = -1;
+ mAccWithCaret = nullptr;
+ }
+
+ /**
+ * Called by mozInlineSpellChecker when a spell check range is added/removed.
+ * nsISelectionListener isn't sufficient for spelling errors, since it only
+ * tells us that there was a change, not which range changed. We don't want
+ * to unnecessarily push a cache update for all Accessibles in the entire
+ * selection.
+ */
+ void SpellCheckRangeChanged(const nsRange& aRange);
+
+ ~SelectionManager();
+
+ protected:
+ SelectionManager();
+
+ /**
+ * Process DOM selection change. Fire selection and caret move events.
+ */
+ void ProcessSelectionChanged(SelData* aSelData);
+
+ private:
+ // Currently focused control.
+ int32_t mCaretOffset;
+ HyperTextAccessible* mAccWithCaret;
+ WeakPtr<dom::Selection> mCurrCtrlNormalSel;
+ WeakPtr<dom::Selection> mCurrCtrlSpellSel;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/States.h b/accessible/base/States.h
new file mode 100644
index 0000000000..5588568d89
--- /dev/null
+++ b/accessible/base/States.h
@@ -0,0 +1,305 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set 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 _states_h_
+#define _states_h_
+
+#include <stdint.h>
+
+namespace mozilla {
+namespace a11y {
+namespace states {
+
+/**
+ * The object is disabled, opposite to enabled and sensitive.
+ */
+const uint64_t UNAVAILABLE = ((uint64_t)0x1) << 0;
+
+/**
+ * The object is selected.
+ */
+const uint64_t SELECTED = ((uint64_t)0x1) << 1;
+
+/**
+ * The object has the keyboard focus.
+ */
+const uint64_t FOCUSED = ((uint64_t)0x1) << 2;
+
+/**
+ * The object is pressed.
+ */
+const uint64_t PRESSED = ((uint64_t)0x1) << 3;
+
+/**
+ * The checkable object is checked, applied to check box controls,
+ * @see CHECKABLE and MIXED states.
+ */
+const uint64_t CHECKED = ((uint64_t)0x1) << 4;
+
+/**
+ * Indicates that the state of a three-state check box or tool bar button is
+ * undetermined. The check box is neither checked or unchecked, and is
+ * in the third or mixed state.
+ */
+const uint64_t MIXED = ((uint64_t)0x1) << 5;
+
+/**
+ * The object is designated read-only, so it can't be edited.
+ */
+const uint64_t READONLY = ((uint64_t)0x1) << 6;
+
+/**
+ * The object is hot-tracked by the mouse, which means that its appearance
+ * has changed to indicate that the mouse pointer is located over it.
+ *
+ * This is currently unused.
+ */
+const uint64_t HOTTRACKED = ((uint64_t)0x1) << 7;
+
+/**
+ * This object is the default button in a window.
+ */
+const uint64_t DEFAULT = ((uint64_t)0x1) << 8;
+
+/**
+ * The expandable object's children are displayed, the opposite of collapsed,
+ * applied to trees, list and other controls.
+ * @see COLLAPSED state
+ */
+const uint64_t EXPANDED = ((uint64_t)0x1) << 9;
+
+/**
+ * The expandable object's children are not displayed, the opposite of
+ * expanded, applied to tree lists and other controls,
+ * @see EXPANDED state.
+ */
+const uint64_t COLLAPSED = ((uint64_t)0x1) << 10;
+
+/**
+ * The control or document can not accept input at this time.
+ */
+const uint64_t BUSY = ((uint64_t)0x1) << 11;
+
+/**
+ * The object is out of normal flow, may be outside of boundaries of its
+ * parent.
+ */
+const uint64_t FLOATING = ((uint64_t)0x1) << 12;
+
+/**
+ * The object can be checked.
+ */
+const uint64_t CHECKABLE = ((uint64_t)0x1) << 13;
+
+/**
+ * This object is a graphic which is rapidly changing appearance.
+ */
+const uint64_t ANIMATED = ((uint64_t)0x1) << 14;
+
+/**
+ * The object is programmatically hidden.
+ * So user action like scrolling or switching tabs won't make this visible.
+ */
+const uint64_t INVISIBLE = ((uint64_t)0x1) << 15;
+
+/**
+ * The object is scrolled off screen.
+ * User action such as scrolling or changing tab may make the object
+ * visible.
+ */
+const uint64_t OFFSCREEN = ((uint64_t)0x1) << 16;
+
+/**
+ * The object can be resized.
+ */
+const uint64_t SIZEABLE = ((uint64_t)0x1) << 17;
+
+/**
+ * The object can be moved to a different position.
+ */
+const uint64_t MOVEABLE = ((uint64_t)0x1) << 18;
+
+/**
+ * The object describes itself with speech.
+ * Other speech related assistive technology may want to avoid speaking
+ * information about this object, because the object is already doing this.
+ */
+const uint64_t SELFVOICING = ((uint64_t)0x1) << 19;
+
+/**
+ * The object can have the focus and become focused.
+ */
+const uint64_t FOCUSABLE = ((uint64_t)0x1) << 20;
+
+/**
+ * The object can be selected.
+ */
+const uint64_t SELECTABLE = ((uint64_t)0x1) << 21;
+
+/**
+ * This object is a link.
+ */
+const uint64_t LINKED = ((uint64_t)0x1) << 22;
+
+/**
+ * This is used for links that have been traversed
+ * i.e. the linked page has been visited.
+ */
+const uint64_t TRAVERSED = ((uint64_t)0x1) << 23;
+
+/**
+ * Supports multiple selection.
+ */
+const uint64_t MULTISELECTABLE = ((uint64_t)0x1) << 24;
+
+/**
+ * Supports extended selection.
+ * All objects supporting this are also multipselectable.
+ * This only makes sense for msaa see bug 635690.
+ */
+const uint64_t EXTSELECTABLE = ((uint64_t)0x1) << 25;
+
+/**
+ * The user is required to interact with this object.
+ */
+const uint64_t REQUIRED = ((uint64_t)0x1) << 26;
+
+/**
+ * The object is an alert, notifying the user of something important.
+ */
+const uint64_t ALERT = ((uint64_t)0x1) << 27;
+
+/**
+ * Used for text fields containing invalid values.
+ */
+const uint64_t INVALID = ((uint64_t)0x1) << 28;
+
+/**
+ * The controls value can not be obtained, and is returned as a set of "*"s.
+ */
+const uint64_t PROTECTED = ((uint64_t)0x1) << 29;
+
+/**
+ * The object can be invoked to show a pop up menu or window.
+ */
+const uint64_t HASPOPUP = ((uint64_t)0x1) << 30;
+
+/**
+ * The editable area has some kind of autocompletion.
+ */
+const uint64_t SUPPORTS_AUTOCOMPLETION = ((uint64_t)0x1) << 31;
+
+/**
+ * The object is no longer available to be queried.
+ */
+const uint64_t DEFUNCT = ((uint64_t)0x1) << 32;
+
+/**
+ * The text is selectable, the object must implement the text interface.
+ */
+const uint64_t SELECTABLE_TEXT = ((uint64_t)0x1) << 33;
+
+/**
+ * The text in this object can be edited.
+ */
+const uint64_t EDITABLE = ((uint64_t)0x1) << 34;
+
+/**
+ * This window is currently the active window.
+ */
+const uint64_t ACTIVE = ((uint64_t)0x1) << 35;
+
+/**
+ * Indicates that the object is modal. Modal objects have the behavior
+ * that something must be done with the object before the user can
+ * interact with an object in a different window.
+ */
+const uint64_t MODAL = ((uint64_t)0x1) << 36;
+
+/**
+ * Edit control that can take multiple lines.
+ */
+const uint64_t MULTI_LINE = ((uint64_t)0x1) << 37;
+
+/**
+ * Uses horizontal layout.
+ */
+const uint64_t HORIZONTAL = ((uint64_t)0x1) << 38;
+
+/**
+ * Indicates this object paints every pixel within its rectangular region.
+ */
+const uint64_t OPAQUE1 = ((uint64_t)0x1) << 39;
+
+/**
+ * This text object can only contain 1 line of text.
+ */
+const uint64_t SINGLE_LINE = ((uint64_t)0x1) << 40;
+
+/**
+ * The parent object manages descendants, and this object may only exist
+ * while it is visible or has focus.
+ * For example the focused cell of a table or the current element of a list box
+ * may have this state.
+ */
+const uint64_t TRANSIENT = ((uint64_t)0x1) << 41;
+
+/**
+ * Uses vertical layout.
+ * Especially used for sliders and scrollbars.
+ */
+const uint64_t VERTICAL = ((uint64_t)0x1) << 42;
+
+/**
+ * Object not dead, but not up-to-date either.
+ */
+const uint64_t STALE = ((uint64_t)0x1) << 43;
+
+/**
+ * A widget that is not unavailable.
+ */
+const uint64_t ENABLED = ((uint64_t)0x1) << 44;
+
+/**
+ * Same as ENABLED state for now see bug 636158
+ */
+const uint64_t SENSITIVE = ((uint64_t)0x1) << 45;
+
+/**
+ * The object is expandable, provides a UI to expand/collapse its children
+ * @see EXPANDED and COLLAPSED states.
+ */
+const uint64_t EXPANDABLE = ((uint64_t)0x1) << 46;
+
+/**
+ * The object is pinned, usually indicating it is fixed in place and has
+ * permanence.
+ */
+const uint64_t PINNED = ((uint64_t)0x1) << 47;
+
+/**
+ * The object is the current item within a container or set of related elements.
+ */
+const uint64_t CURRENT = ((uint64_t)0x1) << 48;
+
+/**
+ * Not a real state, used for static assertions.
+ */
+const uint64_t LAST_ENTRY = CURRENT;
+
+} // namespace states
+
+/**
+ * States that must be calculated by RemoteAccessible and are thus not cached.
+ */
+const uint64_t kRemoteCalculatedStates =
+ states::FOCUSED | states::INVISIBLE | states::OFFSCREEN | states::ENABLED |
+ states::SENSITIVE | states::COLLAPSED | states::OPAQUE1;
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/Statistics.h b/accessible/base/Statistics.h
new file mode 100644
index 0000000000..19f7166317
--- /dev/null
+++ b/accessible/base/Statistics.h
@@ -0,0 +1,42 @@
+/* -*- 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 A11Y_STATISTICS_H_
+#define A11Y_STATISTICS_H_
+
+#include "mozilla/Telemetry.h"
+
+namespace mozilla {
+namespace a11y {
+namespace statistics {
+
+inline void A11yInitialized() {
+ Telemetry::Accumulate(Telemetry::A11Y_INSTANTIATED_FLAG, true);
+}
+
+inline void A11yConsumers(uint32_t aConsumer) {
+ Telemetry::Accumulate(Telemetry::A11Y_CONSUMERS, aConsumer);
+}
+
+/**
+ * Report that ISimpleDOM* has been used.
+ */
+inline void ISimpleDOMUsed() {
+ Telemetry::Accumulate(Telemetry::A11Y_ISIMPLEDOM_USAGE_FLAG, true);
+}
+
+/**
+ * Report that IAccessibleTable has been used.
+ */
+inline void IAccessibleTableUsed() {
+ Telemetry::Accumulate(Telemetry::A11Y_IATABLE_USAGE_FLAG, true);
+}
+
+} // namespace statistics
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/StyleInfo.cpp b/accessible/base/StyleInfo.cpp
new file mode 100644
index 0000000000..2673e9684d
--- /dev/null
+++ b/accessible/base/StyleInfo.cpp
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set 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 "StyleInfo.h"
+
+#include "nsStyleConsts.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+void StyleInfo::FormatColor(const nscolor& aValue, nsAString& aFormattedValue) {
+ // Combine the string like rgb(R, G, B) from nscolor.
+ // FIXME: What about the alpha channel?
+ aFormattedValue.AppendLiteral("rgb(");
+ aFormattedValue.AppendInt(NS_GET_R(aValue));
+ aFormattedValue.AppendLiteral(", ");
+ aFormattedValue.AppendInt(NS_GET_G(aValue));
+ aFormattedValue.AppendLiteral(", ");
+ aFormattedValue.AppendInt(NS_GET_B(aValue));
+ aFormattedValue.Append(')');
+}
+
+already_AddRefed<nsAtom> StyleInfo::TextDecorationStyleToAtom(
+ StyleTextDecorationStyle aValue) {
+ // TODO: When these are enum classes that rust also understands we should just
+ // make an FFI call here.
+ // TODO: These should probably be static atoms.
+ switch (aValue) {
+ case StyleTextDecorationStyle::None:
+ return NS_Atomize("-moz-none");
+ case StyleTextDecorationStyle::Solid:
+ return NS_Atomize("solid");
+ case StyleTextDecorationStyle::Double:
+ return NS_Atomize("double");
+ case StyleTextDecorationStyle::Dotted:
+ return NS_Atomize("dotted");
+ case StyleTextDecorationStyle::Dashed:
+ return NS_Atomize("dashed");
+ case StyleTextDecorationStyle::Wavy:
+ return NS_Atomize("wavy");
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown decoration style");
+ break;
+ }
+
+ return nullptr;
+}
diff --git a/accessible/base/StyleInfo.h b/accessible/base/StyleInfo.h
new file mode 100644
index 0000000000..e078a86834
--- /dev/null
+++ b/accessible/base/StyleInfo.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set 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_style_h_
+#define _mozilla_a11y_style_h_
+
+#include "mozilla/gfx/Types.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "nsStringFwd.h"
+#include "nsColor.h"
+
+class nsAtom;
+
+namespace mozilla {
+
+enum class StyleTextDecorationStyle : uint8_t;
+
+namespace dom {
+class Element;
+} // namespace dom
+namespace a11y {
+
+class StyleInfo {
+ public:
+ static void FormatColor(const nscolor& aValue, nsAString& aFormattedValue);
+ static already_AddRefed<nsAtom> TextDecorationStyleToAtom(
+ StyleTextDecorationStyle aValue);
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/TextAttrs.cpp b/accessible/base/TextAttrs.cpp
new file mode 100644
index 0000000000..e1ca62e549
--- /dev/null
+++ b/accessible/base/TextAttrs.cpp
@@ -0,0 +1,816 @@
+/* -*- 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 "TextAttrs.h"
+
+#include "AccAttributes.h"
+#include "nsAccUtils.h"
+#include "nsCoreUtils.h"
+#include "StyleInfo.h"
+
+#include "gfxTextRun.h"
+#include "nsFontMetrics.h"
+#include "nsLayoutUtils.h"
+#include "nsContainerFrame.h"
+#include "HyperTextAccessible.h"
+#include "mozilla/AppUnits.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// TextAttrsMgr
+////////////////////////////////////////////////////////////////////////////////
+
+void TextAttrsMgr::GetAttributes(AccAttributes* aAttributes,
+ uint32_t* aStartOffset, uint32_t* aEndOffset) {
+ // 1. Hyper text accessible must be specified always.
+ // 2. Offset accessible must be specified in
+ // the case of text attributes. Result hyper text offsets are optional if you
+ // just want the attributes for a single text Accessible.
+ // 3. Offset accessible and result hyper text offsets must not be specified
+ // but include default text attributes flag and attributes list must be
+ // specified in the case of default text attributes.
+ MOZ_ASSERT(
+ mHyperTextAcc && ((mOffsetAcc && mOffsetAccIdx != -1) ||
+ (!mOffsetAcc && mOffsetAccIdx == -1 && !aStartOffset &&
+ !aEndOffset && mIncludeDefAttrs && aAttributes)),
+ "Wrong usage of TextAttrsMgr!");
+
+ // Embedded objects are combined into own range with empty attributes set.
+ if (mOffsetAcc && !mOffsetAcc->IsText()) {
+ if (!aStartOffset) {
+ return;
+ }
+ for (int32_t childIdx = mOffsetAccIdx - 1; childIdx >= 0; childIdx--) {
+ LocalAccessible* currAcc = mHyperTextAcc->LocalChildAt(childIdx);
+ if (currAcc->IsText()) break;
+
+ (*aStartOffset)--;
+ }
+
+ uint32_t childCount = mHyperTextAcc->ChildCount();
+ for (uint32_t childIdx = mOffsetAccIdx + 1; childIdx < childCount;
+ childIdx++) {
+ LocalAccessible* currAcc = mHyperTextAcc->LocalChildAt(childIdx);
+ if (currAcc->IsText()) break;
+
+ (*aEndOffset)++;
+ }
+
+ return;
+ }
+
+ // Get the content and frame of the accessible. In the case of document
+ // accessible it's role content and root frame.
+ nsIContent* hyperTextElm = mHyperTextAcc->GetContent();
+ if (!hyperTextElm) {
+ return; // XXX: we don't support text attrs on document with no body
+ }
+
+ nsIFrame* rootFrame = mHyperTextAcc->GetFrame();
+ if (!rootFrame) {
+ return;
+ }
+
+ nsIContent *offsetNode = nullptr, *offsetElm = nullptr;
+ nsIFrame* frame = nullptr;
+ if (mOffsetAcc) {
+ offsetNode = mOffsetAcc->GetContent();
+ offsetElm = nsCoreUtils::GetDOMElementFor(offsetNode);
+ MOZ_ASSERT(offsetElm, "No element for offset accessible!");
+ if (!offsetElm) return;
+
+ frame = offsetElm->GetPrimaryFrame();
+ }
+
+ // "language" text attribute
+ LangTextAttr langTextAttr(mHyperTextAcc, hyperTextElm, offsetNode);
+
+ // "aria-invalid" text attribute
+ InvalidTextAttr invalidTextAttr(hyperTextElm, offsetNode);
+
+ // "background-color" text attribute
+ BGColorTextAttr bgColorTextAttr(rootFrame, frame);
+
+ // "color" text attribute
+ ColorTextAttr colorTextAttr(rootFrame, frame);
+
+ // "font-family" text attribute
+ FontFamilyTextAttr fontFamilyTextAttr(rootFrame, frame);
+
+ // "font-size" text attribute
+ FontSizeTextAttr fontSizeTextAttr(rootFrame, frame);
+
+ // "font-style" text attribute
+ FontStyleTextAttr fontStyleTextAttr(rootFrame, frame);
+
+ // "font-weight" text attribute
+ FontWeightTextAttr fontWeightTextAttr(rootFrame, frame);
+
+ // "auto-generated" text attribute
+ AutoGeneratedTextAttr autoGenTextAttr(mHyperTextAcc, mOffsetAcc);
+
+ // "text-underline(line-through)-style(color)" text attributes
+ TextDecorTextAttr textDecorTextAttr(rootFrame, frame);
+
+ // "text-position" text attribute
+ TextPosTextAttr textPosTextAttr(rootFrame, frame, hyperTextElm, offsetNode);
+
+ TextAttr* attrArray[] = {
+ &langTextAttr, &invalidTextAttr, &bgColorTextAttr,
+ &colorTextAttr, &fontFamilyTextAttr, &fontSizeTextAttr,
+ &fontStyleTextAttr, &fontWeightTextAttr, &autoGenTextAttr,
+ &textDecorTextAttr, &textPosTextAttr};
+
+ // Expose text attributes if applicable.
+ if (aAttributes) {
+ for (uint32_t idx = 0; idx < ArrayLength(attrArray); idx++) {
+ attrArray[idx]->Expose(aAttributes, mIncludeDefAttrs);
+ }
+ }
+
+ // Expose text attributes range where they are applied if applicable.
+ if (aStartOffset) {
+ GetRange(attrArray, ArrayLength(attrArray), aStartOffset, aEndOffset);
+ }
+}
+
+void TextAttrsMgr::GetRange(TextAttr* aAttrArray[], uint32_t aAttrArrayLen,
+ uint32_t* aStartOffset, uint32_t* aEndOffset) {
+ // Navigate backward from anchor accessible to find start offset.
+ for (int32_t childIdx = mOffsetAccIdx - 1; childIdx >= 0; childIdx--) {
+ LocalAccessible* currAcc = mHyperTextAcc->LocalChildAt(childIdx);
+
+ // Stop on embedded accessible since embedded accessibles are combined into
+ // own range.
+ if (!currAcc->IsText()) break;
+
+ MOZ_ASSERT(nsCoreUtils::GetDOMElementFor(currAcc->GetContent()),
+ "Text accessible has to have an associated DOM element");
+
+ bool offsetFound = false;
+ for (uint32_t attrIdx = 0; attrIdx < aAttrArrayLen; attrIdx++) {
+ TextAttr* textAttr = aAttrArray[attrIdx];
+ if (!textAttr->Equal(currAcc)) {
+ offsetFound = true;
+ break;
+ }
+ }
+
+ if (offsetFound) break;
+
+ *(aStartOffset) -= nsAccUtils::TextLength(currAcc);
+ }
+
+ // Navigate forward from anchor accessible to find end offset.
+ uint32_t childLen = mHyperTextAcc->ChildCount();
+ for (uint32_t childIdx = mOffsetAccIdx + 1; childIdx < childLen; childIdx++) {
+ LocalAccessible* currAcc = mHyperTextAcc->LocalChildAt(childIdx);
+ if (!currAcc->IsText()) break;
+
+ MOZ_ASSERT(nsCoreUtils::GetDOMElementFor(currAcc->GetContent()),
+ "Text accessible has to have an associated DOM element");
+
+ bool offsetFound = false;
+ for (uint32_t attrIdx = 0; attrIdx < aAttrArrayLen; attrIdx++) {
+ TextAttr* textAttr = aAttrArray[attrIdx];
+
+ // Alter the end offset when text attribute changes its value and stop
+ // the search.
+ if (!textAttr->Equal(currAcc)) {
+ offsetFound = true;
+ break;
+ }
+ }
+
+ if (offsetFound) break;
+
+ (*aEndOffset) += nsAccUtils::TextLength(currAcc);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LangTextAttr
+////////////////////////////////////////////////////////////////////////////////
+
+TextAttrsMgr::LangTextAttr::LangTextAttr(HyperTextAccessible* aRoot,
+ nsIContent* aRootElm, nsIContent* aElm)
+ : TTextAttr<nsString>(!aElm), mRootContent(aRootElm) {
+ aRoot->Language(mRootNativeValue);
+ mIsRootDefined = !mRootNativeValue.IsEmpty();
+
+ if (aElm) {
+ nsCoreUtils::GetLanguageFor(aElm, mRootContent, mNativeValue);
+ mIsDefined = !mNativeValue.IsEmpty();
+ }
+}
+
+TextAttrsMgr::LangTextAttr::~LangTextAttr() {}
+
+bool TextAttrsMgr::LangTextAttr::GetValueFor(LocalAccessible* aAccessible,
+ nsString* aValue) {
+ nsCoreUtils::GetLanguageFor(aAccessible->GetContent(), mRootContent, *aValue);
+ return !aValue->IsEmpty();
+}
+
+void TextAttrsMgr::LangTextAttr::ExposeValue(AccAttributes* aAttributes,
+ const nsString& aValue) {
+ RefPtr<nsAtom> lang = NS_Atomize(aValue);
+ aAttributes->SetAttribute(nsGkAtoms::language, lang);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// InvalidTextAttr
+////////////////////////////////////////////////////////////////////////////////
+
+TextAttrsMgr::InvalidTextAttr::InvalidTextAttr(nsIContent* aRootElm,
+ nsIContent* aElm)
+ : TTextAttr<uint32_t>(!aElm), mRootElm(aRootElm) {
+ mIsRootDefined = GetValue(mRootElm, &mRootNativeValue);
+ if (aElm) mIsDefined = GetValue(aElm, &mNativeValue);
+}
+
+bool TextAttrsMgr::InvalidTextAttr::GetValueFor(LocalAccessible* aAccessible,
+ uint32_t* aValue) {
+ nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent());
+ return elm ? GetValue(elm, aValue) : false;
+}
+
+void TextAttrsMgr::InvalidTextAttr::ExposeValue(AccAttributes* aAttributes,
+ const uint32_t& aValue) {
+ switch (aValue) {
+ case eFalse:
+ aAttributes->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::_false);
+ break;
+
+ case eGrammar:
+ aAttributes->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::grammar);
+ break;
+
+ case eSpelling:
+ aAttributes->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::spelling);
+ break;
+
+ case eTrue:
+ aAttributes->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::_true);
+ break;
+ }
+}
+
+bool TextAttrsMgr::InvalidTextAttr::GetValue(nsIContent* aElm,
+ uint32_t* aValue) {
+ nsIContent* elm = aElm;
+ do {
+ if (nsAccUtils::HasDefinedARIAToken(elm, nsGkAtoms::aria_invalid)) {
+ static dom::Element::AttrValuesArray tokens[] = {
+ nsGkAtoms::_false, nsGkAtoms::grammar, nsGkAtoms::spelling, nullptr};
+
+ int32_t idx = nsAccUtils::FindARIAAttrValueIn(
+ elm->AsElement(), nsGkAtoms::aria_invalid, tokens, eCaseMatters);
+ switch (idx) {
+ case 0:
+ *aValue = eFalse;
+ return true;
+ case 1:
+ *aValue = eGrammar;
+ return true;
+ case 2:
+ *aValue = eSpelling;
+ return true;
+ default:
+ *aValue = eTrue;
+ return true;
+ }
+ }
+ } while ((elm = elm->GetParent()) && elm != mRootElm);
+
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// BGColorTextAttr
+////////////////////////////////////////////////////////////////////////////////
+
+TextAttrsMgr::BGColorTextAttr::BGColorTextAttr(nsIFrame* aRootFrame,
+ nsIFrame* aFrame)
+ : TTextAttr<nscolor>(!aFrame), mRootFrame(aRootFrame) {
+ mIsRootDefined = GetColor(mRootFrame, &mRootNativeValue);
+ if (aFrame) mIsDefined = GetColor(aFrame, &mNativeValue);
+}
+
+bool TextAttrsMgr::BGColorTextAttr::GetValueFor(LocalAccessible* aAccessible,
+ nscolor* aValue) {
+ nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent());
+ if (elm) {
+ nsIFrame* frame = elm->GetPrimaryFrame();
+ if (frame) {
+ return GetColor(frame, aValue);
+ }
+ }
+ return false;
+}
+
+void TextAttrsMgr::BGColorTextAttr::ExposeValue(AccAttributes* aAttributes,
+ const nscolor& aValue) {
+ aAttributes->SetAttribute(nsGkAtoms::backgroundColor, Color{aValue});
+}
+
+bool TextAttrsMgr::BGColorTextAttr::GetColor(nsIFrame* aFrame,
+ nscolor* aColor) {
+ nscolor backgroundColor = aFrame->StyleBackground()->BackgroundColor(aFrame);
+ if (NS_GET_A(backgroundColor) > 0) {
+ *aColor = backgroundColor;
+ return true;
+ }
+
+ nsContainerFrame* parentFrame = aFrame->GetParent();
+ if (!parentFrame) {
+ *aColor = aFrame->PresContext()->DefaultBackgroundColor();
+ return true;
+ }
+
+ // Each frame of parents chain for the initially passed 'aFrame' has
+ // transparent background color. So background color isn't changed from
+ // 'mRootFrame' to initially passed 'aFrame'.
+ if (parentFrame == mRootFrame) return false;
+
+ return GetColor(parentFrame, aColor);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ColorTextAttr
+////////////////////////////////////////////////////////////////////////////////
+
+TextAttrsMgr::ColorTextAttr::ColorTextAttr(nsIFrame* aRootFrame,
+ nsIFrame* aFrame)
+ : TTextAttr<nscolor>(!aFrame) {
+ mRootNativeValue = aRootFrame->StyleText()->mColor.ToColor();
+ mIsRootDefined = true;
+
+ if (aFrame) {
+ mNativeValue = aFrame->StyleText()->mColor.ToColor();
+ mIsDefined = true;
+ }
+}
+
+bool TextAttrsMgr::ColorTextAttr::GetValueFor(LocalAccessible* aAccessible,
+ nscolor* aValue) {
+ nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent());
+ if (elm) {
+ if (nsIFrame* frame = elm->GetPrimaryFrame()) {
+ *aValue = frame->StyleText()->mColor.ToColor();
+ return true;
+ }
+ }
+ return false;
+}
+
+void TextAttrsMgr::ColorTextAttr::ExposeValue(AccAttributes* aAttributes,
+ const nscolor& aValue) {
+ aAttributes->SetAttribute(nsGkAtoms::color, Color{aValue});
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// FontFamilyTextAttr
+////////////////////////////////////////////////////////////////////////////////
+
+TextAttrsMgr::FontFamilyTextAttr::FontFamilyTextAttr(nsIFrame* aRootFrame,
+ nsIFrame* aFrame)
+ : TTextAttr<nsString>(!aFrame) {
+ mIsRootDefined = GetFontFamily(aRootFrame, mRootNativeValue);
+
+ if (aFrame) mIsDefined = GetFontFamily(aFrame, mNativeValue);
+}
+
+bool TextAttrsMgr::FontFamilyTextAttr::GetValueFor(LocalAccessible* aAccessible,
+ nsString* aValue) {
+ nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent());
+ if (elm) {
+ nsIFrame* frame = elm->GetPrimaryFrame();
+ if (frame) {
+ return GetFontFamily(frame, *aValue);
+ }
+ }
+ return false;
+}
+
+void TextAttrsMgr::FontFamilyTextAttr::ExposeValue(AccAttributes* aAttributes,
+ const nsString& aValue) {
+ RefPtr<nsAtom> family = NS_Atomize(aValue);
+ aAttributes->SetAttribute(nsGkAtoms::font_family, family);
+}
+
+bool TextAttrsMgr::FontFamilyTextAttr::GetFontFamily(nsIFrame* aFrame,
+ nsString& aFamily) {
+ RefPtr<nsFontMetrics> fm =
+ nsLayoutUtils::GetFontMetricsForFrame(aFrame, 1.0f);
+
+ gfxFontGroup* fontGroup = fm->GetThebesFontGroup();
+ RefPtr<gfxFont> font = fontGroup->GetFirstValidFont();
+ gfxFontEntry* fontEntry = font->GetFontEntry();
+ aFamily.Append(NS_ConvertUTF8toUTF16(fontEntry->FamilyName()));
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// FontSizeTextAttr
+////////////////////////////////////////////////////////////////////////////////
+
+TextAttrsMgr::FontSizeTextAttr::FontSizeTextAttr(nsIFrame* aRootFrame,
+ nsIFrame* aFrame)
+ : TTextAttr<nscoord>(!aFrame) {
+ mDC = aRootFrame->PresContext()->DeviceContext();
+
+ mRootNativeValue = aRootFrame->StyleFont()->mSize.ToAppUnits();
+ mIsRootDefined = true;
+
+ if (aFrame) {
+ mNativeValue = aFrame->StyleFont()->mSize.ToAppUnits();
+ mIsDefined = true;
+ }
+}
+
+bool TextAttrsMgr::FontSizeTextAttr::GetValueFor(LocalAccessible* aAccessible,
+ nscoord* aValue) {
+ nsIContent* el = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent());
+ if (el) {
+ nsIFrame* frame = el->GetPrimaryFrame();
+ if (frame) {
+ *aValue = frame->StyleFont()->mSize.ToAppUnits();
+ return true;
+ }
+ }
+ return false;
+}
+
+void TextAttrsMgr::FontSizeTextAttr::ExposeValue(AccAttributes* aAttributes,
+ const nscoord& aValue) {
+ // Convert from nscoord to pt.
+ //
+ // Note: according to IA2, "The conversion doesn't have to be exact.
+ // The intent is to give the user a feel for the size of the text."
+ //
+ // ATK does not specify a unit and will likely follow IA2 here.
+ //
+ // XXX todo: consider sharing this code with layout module? (bug 474621)
+ float px = NSAppUnitsToFloatPixels(aValue, mozilla::AppUnitsPerCSSPixel());
+ // Each pt is 4/3 of a CSS pixel.
+ FontSize fontSize{NS_lround(px * 3 / 4)};
+
+ aAttributes->SetAttribute(nsGkAtoms::font_size, fontSize);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// FontStyleTextAttr
+////////////////////////////////////////////////////////////////////////////////
+
+TextAttrsMgr::FontStyleTextAttr::FontStyleTextAttr(nsIFrame* aRootFrame,
+ nsIFrame* aFrame)
+ : TTextAttr<FontSlantStyle>(!aFrame) {
+ mRootNativeValue = aRootFrame->StyleFont()->mFont.style;
+ mIsRootDefined = true;
+
+ if (aFrame) {
+ mNativeValue = aFrame->StyleFont()->mFont.style;
+ mIsDefined = true;
+ }
+}
+
+bool TextAttrsMgr::FontStyleTextAttr::GetValueFor(LocalAccessible* aAccessible,
+ FontSlantStyle* aValue) {
+ nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent());
+ if (elm) {
+ nsIFrame* frame = elm->GetPrimaryFrame();
+ if (frame) {
+ *aValue = frame->StyleFont()->mFont.style;
+ return true;
+ }
+ }
+ return false;
+}
+
+void TextAttrsMgr::FontStyleTextAttr::ExposeValue(
+ AccAttributes* aAttributes, const FontSlantStyle& aValue) {
+ if (aValue.IsNormal()) {
+ aAttributes->SetAttribute(nsGkAtoms::font_style, nsGkAtoms::normal);
+ } else if (aValue.IsItalic()) {
+ RefPtr<nsAtom> atom = NS_Atomize("italic");
+ aAttributes->SetAttribute(nsGkAtoms::font_style, atom);
+ } else {
+ nsAutoCString s;
+ aValue.ToString(s);
+ nsString wide;
+ CopyUTF8toUTF16(s, wide);
+ aAttributes->SetAttribute(nsGkAtoms::font_style, std::move(wide));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// FontWeightTextAttr
+////////////////////////////////////////////////////////////////////////////////
+
+TextAttrsMgr::FontWeightTextAttr::FontWeightTextAttr(nsIFrame* aRootFrame,
+ nsIFrame* aFrame)
+ : TTextAttr<FontWeight>(!aFrame) {
+ mRootNativeValue = GetFontWeight(aRootFrame);
+ mIsRootDefined = true;
+
+ if (aFrame) {
+ mNativeValue = GetFontWeight(aFrame);
+ mIsDefined = true;
+ }
+}
+
+bool TextAttrsMgr::FontWeightTextAttr::GetValueFor(LocalAccessible* aAccessible,
+ FontWeight* aValue) {
+ nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent());
+ if (elm) {
+ nsIFrame* frame = elm->GetPrimaryFrame();
+ if (frame) {
+ *aValue = GetFontWeight(frame);
+ return true;
+ }
+ }
+ return false;
+}
+
+void TextAttrsMgr::FontWeightTextAttr::ExposeValue(AccAttributes* aAttributes,
+ const FontWeight& aValue) {
+ int value = aValue.ToIntRounded();
+ aAttributes->SetAttribute(nsGkAtoms::fontWeight, value);
+}
+
+FontWeight TextAttrsMgr::FontWeightTextAttr::GetFontWeight(nsIFrame* aFrame) {
+ // nsFont::width isn't suitable here because it's necessary to expose real
+ // value of font weight (used font might not have some font weight values).
+ RefPtr<nsFontMetrics> fm =
+ nsLayoutUtils::GetFontMetricsForFrame(aFrame, 1.0f);
+
+ gfxFontGroup* fontGroup = fm->GetThebesFontGroup();
+ RefPtr<gfxFont> font = fontGroup->GetFirstValidFont();
+
+ // When there doesn't exist a bold font in the family and so the rendering of
+ // a non-bold font face is changed so that the user sees what looks like a
+ // bold font, i.e. synthetic bolding is used. (Simply returns false on any
+ // platforms that don't use the multi-strike synthetic bolding.)
+ if (font->ApplySyntheticBold()) {
+ return FontWeight::BOLD;
+ }
+
+ // On Windows, font->GetStyle()->weight will give the same weight as
+ // fontEntry->Weight(), the weight of the first font in the font group,
+ // which may not be the weight of the font face used to render the
+ // characters. On Mac, font->GetStyle()->weight will just give the same
+ // number as getComputedStyle(). fontEntry->Weight() will give the weight
+ // range supported by the font face used, so we clamp the weight that was
+ // requested by style to what is actually supported by the font.
+ gfxFontEntry* fontEntry = font->GetFontEntry();
+ return fontEntry->Weight().Clamp(font->GetStyle()->weight);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// AutoGeneratedTextAttr
+////////////////////////////////////////////////////////////////////////////////
+TextAttrsMgr::AutoGeneratedTextAttr::AutoGeneratedTextAttr(
+ HyperTextAccessible* aHyperTextAcc, LocalAccessible* aAccessible)
+ : TTextAttr<bool>(!aAccessible) {
+ mRootNativeValue = false;
+ mIsRootDefined = false;
+
+ if (aAccessible) {
+ mIsDefined = mNativeValue =
+ ((aAccessible->NativeRole() == roles::STATICTEXT) ||
+ (aAccessible->NativeRole() == roles::LISTITEM_MARKER));
+ }
+}
+
+bool TextAttrsMgr::AutoGeneratedTextAttr::GetValueFor(
+ LocalAccessible* aAccessible, bool* aValue) {
+ return *aValue = (aAccessible->NativeRole() == roles::STATICTEXT);
+}
+
+void TextAttrsMgr::AutoGeneratedTextAttr::ExposeValue(
+ AccAttributes* aAttributes, const bool& aValue) {
+ aAttributes->SetAttribute(nsGkAtoms::auto_generated, aValue);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// TextDecorTextAttr
+////////////////////////////////////////////////////////////////////////////////
+
+TextAttrsMgr::TextDecorValue::TextDecorValue(nsIFrame* aFrame) {
+ const nsStyleTextReset* textReset = aFrame->StyleTextReset();
+ mStyle = textReset->mTextDecorationStyle;
+ mColor = textReset->mTextDecorationColor.CalcColor(aFrame);
+ mLine =
+ textReset->mTextDecorationLine & (StyleTextDecorationLine::UNDERLINE |
+ StyleTextDecorationLine::LINE_THROUGH);
+}
+
+TextAttrsMgr::TextDecorTextAttr::TextDecorTextAttr(nsIFrame* aRootFrame,
+ nsIFrame* aFrame)
+ : TTextAttr<TextDecorValue>(!aFrame) {
+ mRootNativeValue = TextDecorValue(aRootFrame);
+ mIsRootDefined = mRootNativeValue.IsDefined();
+
+ if (aFrame) {
+ mNativeValue = TextDecorValue(aFrame);
+ mIsDefined = mNativeValue.IsDefined();
+ }
+}
+
+bool TextAttrsMgr::TextDecorTextAttr::GetValueFor(LocalAccessible* aAccessible,
+ TextDecorValue* aValue) {
+ nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent());
+ if (elm) {
+ nsIFrame* frame = elm->GetPrimaryFrame();
+ if (frame) {
+ *aValue = TextDecorValue(frame);
+ return aValue->IsDefined();
+ }
+ }
+ return false;
+}
+
+void TextAttrsMgr::TextDecorTextAttr::ExposeValue(
+ AccAttributes* aAttributes, const TextDecorValue& aValue) {
+ if (aValue.IsUnderline()) {
+ RefPtr<nsAtom> underlineStyle =
+ StyleInfo::TextDecorationStyleToAtom(aValue.Style());
+ aAttributes->SetAttribute(nsGkAtoms::textUnderlineStyle, underlineStyle);
+
+ aAttributes->SetAttribute(nsGkAtoms::textUnderlineColor,
+ Color{aValue.Color()});
+ return;
+ }
+
+ if (aValue.IsLineThrough()) {
+ RefPtr<nsAtom> lineThroughStyle =
+ StyleInfo::TextDecorationStyleToAtom(aValue.Style());
+ aAttributes->SetAttribute(nsGkAtoms::textLineThroughStyle,
+ lineThroughStyle);
+
+ aAttributes->SetAttribute(nsGkAtoms::textLineThroughColor,
+ Color{aValue.Color()});
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// TextPosTextAttr
+////////////////////////////////////////////////////////////////////////////////
+
+TextAttrsMgr::TextPosTextAttr::TextPosTextAttr(nsIFrame* aRootFrame,
+ nsIFrame* aFrame,
+ nsIContent* aRootElm,
+ nsIContent* aElm)
+ : TTextAttr<Maybe<TextPosValue>>(!aFrame && !aElm), mRootElm(aRootElm) {
+ // Get the text-position values for the roots and children.
+ // If we find an ARIA text-position value on a DOM element - searching up
+ // from the supplied root DOM element - use the associated frame as the root
+ // frame. This ensures that we're using the proper root frame for comparison.
+ nsIFrame* ariaFrame = nullptr;
+ Maybe<TextPosValue> rootAria = GetAriaTextPosValue(aRootElm, ariaFrame);
+ if (rootAria && ariaFrame) {
+ aRootFrame = ariaFrame;
+ }
+ Maybe<TextPosValue> rootLayout = GetLayoutTextPosValue(aRootFrame);
+ Maybe<TextPosValue> childLayout;
+ Maybe<TextPosValue> childAria;
+ if (aFrame) {
+ childLayout = GetLayoutTextPosValue(aFrame);
+ }
+ if (aElm) {
+ childAria = GetAriaTextPosValue(aElm);
+ }
+
+ // Aria values take precedence over layout values.
+ mIsRootDefined = rootAria || rootLayout;
+ mRootNativeValue = rootAria ? rootAria : rootLayout;
+ mIsDefined = childAria || childLayout;
+ mNativeValue = childAria ? childAria : childLayout;
+
+ // If there's no child text-position information from ARIA, and the child
+ // layout info is equivalent to the root layout info (i.e., it's inherited),
+ // then we should prefer the root information.
+ if (!childAria && childLayout == rootLayout) {
+ mIsDefined = false;
+ }
+}
+
+bool TextAttrsMgr::TextPosTextAttr::GetValueFor(LocalAccessible* aAccessible,
+ Maybe<TextPosValue>* aValue) {
+ nsIContent* elm = nsCoreUtils::GetDOMElementFor(aAccessible->GetContent());
+ if (elm) {
+ nsIFrame* frame = elm->GetPrimaryFrame();
+ if (frame) {
+ Maybe<TextPosValue> layoutValue = GetLayoutTextPosValue(frame);
+ Maybe<TextPosValue> ariaValue = GetAriaTextPosValue(elm);
+
+ *aValue = ariaValue ? ariaValue : layoutValue;
+ return aValue->isSome();
+ }
+ }
+ return false;
+}
+
+void TextAttrsMgr::TextPosTextAttr::ExposeValue(
+ AccAttributes* aAttributes, const Maybe<TextPosValue>& aValue) {
+ if (aValue.isNothing()) {
+ return;
+ }
+
+ RefPtr<nsAtom> atom = nullptr;
+ switch (*aValue) {
+ case eTextPosBaseline:
+ atom = nsGkAtoms::baseline;
+ break;
+
+ case eTextPosSub:
+ atom = nsGkAtoms::sub;
+ break;
+
+ case eTextPosSuper:
+ atom = NS_Atomize("super");
+ break;
+ }
+
+ if (atom) {
+ aAttributes->SetAttribute(nsGkAtoms::textPosition, atom);
+ }
+}
+
+Maybe<TextAttrsMgr::TextPosValue>
+TextAttrsMgr::TextPosTextAttr::GetAriaTextPosValue(nsIContent* aElm) const {
+ nsIFrame* ariaFrame = nullptr;
+ return GetAriaTextPosValue(aElm, ariaFrame);
+}
+
+Maybe<TextAttrsMgr::TextPosValue>
+TextAttrsMgr::TextPosTextAttr::GetAriaTextPosValue(nsIContent* aElm,
+ nsIFrame*& ariaFrame) const {
+ // Search for the superscript and subscript roles that imply text-position.
+ const nsIContent* elm = aElm;
+ do {
+ if (elm->IsElement()) {
+ const mozilla::dom::Element* domElm = elm->AsElement();
+ static const dom::Element::AttrValuesArray tokens[] = {
+ nsGkAtoms::subscript, nsGkAtoms::superscript, nullptr};
+ const int32_t valueIdx = domElm->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::role, tokens, eCaseMatters);
+ ariaFrame = domElm->GetPrimaryFrame();
+ if (valueIdx == 0) {
+ return Some(eTextPosSub);
+ }
+ if (valueIdx == 1) {
+ return Some(eTextPosSuper);
+ }
+ }
+ } while ((elm = elm->GetParent()) && elm != mRootElm);
+
+ ariaFrame = nullptr;
+ return Nothing{};
+}
+
+Maybe<TextAttrsMgr::TextPosValue>
+TextAttrsMgr::TextPosTextAttr::GetLayoutTextPosValue(nsIFrame* aFrame) const {
+ const auto& verticalAlign = aFrame->StyleDisplay()->mVerticalAlign;
+ if (verticalAlign.IsKeyword()) {
+ switch (verticalAlign.AsKeyword()) {
+ case StyleVerticalAlignKeyword::Baseline:
+ return Some(eTextPosBaseline);
+ case StyleVerticalAlignKeyword::Sub:
+ return Some(eTextPosSub);
+ case StyleVerticalAlignKeyword::Super:
+ return Some(eTextPosSuper);
+ // No good guess for the rest, so do not expose value of text-position
+ // attribute.
+ default:
+ return Nothing{};
+ }
+ }
+
+ const auto& length = verticalAlign.AsLength();
+ if (length.ConvertsToPercentage()) {
+ const float percentValue = length.ToPercentage();
+ return percentValue > 0 ? Some(eTextPosSuper)
+ : (percentValue < 0 ? Some(eTextPosSub)
+ : Some(eTextPosBaseline));
+ }
+
+ if (length.ConvertsToLength()) {
+ const nscoord coordValue = length.ToLength();
+ return coordValue > 0
+ ? Some(eTextPosSuper)
+ : (coordValue < 0 ? Some(eTextPosSub) : Some(eTextPosBaseline));
+ }
+
+ if (const nsIContent* content = aFrame->GetContent()) {
+ if (content->IsHTMLElement(nsGkAtoms::sup)) return Some(eTextPosSuper);
+ if (content->IsHTMLElement(nsGkAtoms::sub)) return Some(eTextPosSub);
+ }
+
+ return Nothing{};
+}
diff --git a/accessible/base/TextAttrs.h b/accessible/base/TextAttrs.h
new file mode 100644
index 0000000000..031e114273
--- /dev/null
+++ b/accessible/base/TextAttrs.h
@@ -0,0 +1,440 @@
+/* -*- 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 nsTextAttrs_h_
+#define nsTextAttrs_h_
+
+#include "mozilla/FontPropertyTypes.h"
+#include "nsCOMPtr.h"
+#include "nsColor.h"
+#include "nsString.h"
+#include "nsStyleConsts.h"
+
+class nsIFrame;
+class nsIContent;
+class nsDeviceContext;
+
+namespace mozilla {
+namespace a11y {
+
+class AccAttributes;
+class LocalAccessible;
+class HyperTextAccessible;
+
+/**
+ * Used to expose text attributes for the hyper text accessible (see
+ * HyperTextAccessible class).
+ *
+ * @note "invalid: spelling" text attribute is implemented entirely in
+ * HyperTextAccessible class.
+ */
+class TextAttrsMgr {
+ public:
+ /**
+ * Constructor. Used to expose default text attributes.
+ */
+ explicit TextAttrsMgr(HyperTextAccessible* aHyperTextAcc)
+ : mOffsetAcc(nullptr),
+ mHyperTextAcc(aHyperTextAcc),
+ mOffsetAccIdx(-1),
+ mIncludeDefAttrs(true) {}
+
+ /**
+ * Constructor. Used to expose text attributes at the given offset.
+ *
+ * @param aHyperTextAcc [in] hyper text accessible text attributes are
+ * calculated for
+ * @param aIncludeDefAttrs [optional] indicates whether default text
+ * attributes should be included into list of exposed
+ * text attributes
+ * @param oOffsetAcc [optional] offset an accessible the text attributes
+ * should be calculated for
+ * @param oOffsetAccIdx [optional] index in parent of offset accessible
+ */
+ TextAttrsMgr(HyperTextAccessible* aHyperTextAcc, bool aIncludeDefAttrs,
+ LocalAccessible* aOffsetAcc, int32_t aOffsetAccIdx)
+ : mOffsetAcc(aOffsetAcc),
+ mHyperTextAcc(aHyperTextAcc),
+ mOffsetAccIdx(aOffsetAccIdx),
+ mIncludeDefAttrs(aIncludeDefAttrs) {}
+
+ /*
+ * Return text attributes and hyper text offsets where these attributes are
+ * applied. Offsets are calculated in the case of non default attributes.
+ *
+ * @note In the case of default attributes pointers on hyper text offsets
+ * must be skipped.
+ *
+ * @param aAttributes [in, out] text attributes list
+ * @param aStartHTOffset [out, optional] start hyper text offset
+ * @param aEndHTOffset [out, optional] end hyper text offset
+ */
+ void GetAttributes(AccAttributes* aAttributes,
+ uint32_t* aStartHTOffset = nullptr,
+ uint32_t* aEndHTOffset = nullptr);
+
+ protected:
+ /**
+ * Calculates range (start and end offsets) of text where the text attributes
+ * are stretched. New offsets may be smaller if one of text attributes changes
+ * its value before or after the given offsets.
+ *
+ * @param aTextAttrArray [in] text attributes array
+ * @param aAttrArrayLen [in] text attributes array length
+ * @param aStartHTOffset [in, out] the start offset
+ * @param aEndHTOffset [in, out] the end offset
+ */
+ class TextAttr;
+ void GetRange(TextAttr* aAttrArray[], uint32_t aAttrArrayLen,
+ uint32_t* aStartOffset, uint32_t* aEndOffset);
+
+ private:
+ LocalAccessible* mOffsetAcc;
+ HyperTextAccessible* mHyperTextAcc;
+ int32_t mOffsetAccIdx;
+ bool mIncludeDefAttrs;
+
+ protected:
+ /**
+ * Interface class of text attribute class implementations.
+ */
+ class TextAttr {
+ public:
+ /**
+ * Expose the text attribute to the given attribute set.
+ *
+ * @param aAttributes [in] the given attribute set
+ * @param aIncludeDefAttrValue [in] if true then attribute is exposed even
+ * if its value is the same as default one
+ */
+ virtual void Expose(AccAttributes* aAttributes,
+ bool aIncludeDefAttrValue) = 0;
+
+ /**
+ * Return true if the text attribute value on the given element equals with
+ * predefined attribute value.
+ */
+ virtual bool Equal(LocalAccessible* aAccessible) = 0;
+ };
+
+ /**
+ * Base class to work with text attributes. See derived classes below.
+ */
+ template <class T>
+ class TTextAttr : public TextAttr {
+ public:
+ explicit TTextAttr(bool aGetRootValue) : mGetRootValue(aGetRootValue) {}
+
+ // TextAttr
+ virtual void Expose(AccAttributes* aAttributes,
+ bool aIncludeDefAttrValue) override {
+ if (mGetRootValue) {
+ if (mIsRootDefined) ExposeValue(aAttributes, mRootNativeValue);
+ return;
+ }
+
+ if (mIsDefined) {
+ if (aIncludeDefAttrValue || mRootNativeValue != mNativeValue) {
+ ExposeValue(aAttributes, mNativeValue);
+ }
+ return;
+ }
+
+ if (aIncludeDefAttrValue && mIsRootDefined) {
+ ExposeValue(aAttributes, mRootNativeValue);
+ }
+ }
+
+ virtual bool Equal(LocalAccessible* aAccessible) override {
+ T nativeValue;
+ bool isDefined = GetValueFor(aAccessible, &nativeValue);
+
+ if (!mIsDefined && !isDefined) return true;
+
+ if (mIsDefined && isDefined) return nativeValue == mNativeValue;
+
+ if (mIsDefined) return mNativeValue == mRootNativeValue;
+
+ return nativeValue == mRootNativeValue;
+ }
+
+ protected:
+ // Expose the text attribute with the given value to attribute set.
+ virtual void ExposeValue(AccAttributes* aAttributes, const T& aValue) = 0;
+
+ // Return native value for the given DOM element.
+ virtual bool GetValueFor(LocalAccessible* aAccessible, T* aValue) = 0;
+
+ // Indicates if root value should be exposed.
+ bool mGetRootValue;
+
+ // Native value and flag indicating if the value is defined (initialized in
+ // derived classes). Note, undefined native value means it is inherited
+ // from root.
+ MOZ_INIT_OUTSIDE_CTOR T mNativeValue;
+ MOZ_INIT_OUTSIDE_CTOR bool mIsDefined;
+
+ // Native root value and flag indicating if the value is defined
+ // (initialized in derived classes).
+ MOZ_INIT_OUTSIDE_CTOR T mRootNativeValue;
+ MOZ_INIT_OUTSIDE_CTOR bool mIsRootDefined;
+ };
+
+ /**
+ * Class is used for the work with 'language' text attribute.
+ */
+ class LangTextAttr : public TTextAttr<nsString> {
+ public:
+ LangTextAttr(HyperTextAccessible* aRoot, nsIContent* aRootElm,
+ nsIContent* aElm);
+ virtual ~LangTextAttr();
+
+ protected:
+ // TextAttr
+ virtual bool GetValueFor(LocalAccessible* aAccessible,
+ nsString* aValue) override;
+ virtual void ExposeValue(AccAttributes* aAttributes,
+ const nsString& aValue) override;
+
+ private:
+ nsCOMPtr<nsIContent> mRootContent;
+ };
+
+ /**
+ * Class is used for the 'invalid' text attribute. Note, it calculated
+ * the attribute from aria-invalid attribute only; invalid:spelling attribute
+ * calculated from misspelled text in the editor is managed by
+ * HyperTextAccessible and applied on top of the value from aria-invalid.
+ */
+ class InvalidTextAttr : public TTextAttr<uint32_t> {
+ public:
+ InvalidTextAttr(nsIContent* aRootElm, nsIContent* aElm);
+ virtual ~InvalidTextAttr(){};
+
+ protected:
+ enum { eFalse, eGrammar, eSpelling, eTrue };
+
+ // TextAttr
+ virtual bool GetValueFor(LocalAccessible* aAccessible,
+ uint32_t* aValue) override;
+ virtual void ExposeValue(AccAttributes* aAttributes,
+ const uint32_t& aValue) override;
+
+ private:
+ bool GetValue(nsIContent* aElm, uint32_t* aValue);
+ nsIContent* mRootElm;
+ };
+
+ /**
+ * Class is used for the work with 'background-color' text attribute.
+ */
+ class BGColorTextAttr : public TTextAttr<nscolor> {
+ public:
+ BGColorTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame);
+ virtual ~BGColorTextAttr() {}
+
+ protected:
+ // TextAttr
+ virtual bool GetValueFor(LocalAccessible* aAccessible,
+ nscolor* aValue) override;
+ virtual void ExposeValue(AccAttributes* aAttributes,
+ const nscolor& aValue) override;
+
+ private:
+ bool GetColor(nsIFrame* aFrame, nscolor* aColor);
+ nsIFrame* mRootFrame;
+ };
+
+ /**
+ * Class is used for the work with 'color' text attribute.
+ */
+ class ColorTextAttr : public TTextAttr<nscolor> {
+ public:
+ ColorTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame);
+ virtual ~ColorTextAttr() {}
+
+ protected:
+ // TTextAttr
+ virtual bool GetValueFor(LocalAccessible* aAccessible,
+ nscolor* aValue) override;
+ virtual void ExposeValue(AccAttributes* aAttributes,
+ const nscolor& aValue) override;
+ };
+
+ /**
+ * Class is used for the work with "font-family" text attribute.
+ */
+ class FontFamilyTextAttr : public TTextAttr<nsString> {
+ public:
+ FontFamilyTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame);
+ virtual ~FontFamilyTextAttr() {}
+
+ protected:
+ // TTextAttr
+ virtual bool GetValueFor(LocalAccessible* aAccessible,
+ nsString* aValue) override;
+ virtual void ExposeValue(AccAttributes* aAttributes,
+ const nsString& aValue) override;
+
+ private:
+ bool GetFontFamily(nsIFrame* aFrame, nsString& aFamily);
+ };
+
+ /**
+ * Class is used for the work with "font-size" text attribute.
+ */
+ class FontSizeTextAttr : public TTextAttr<nscoord> {
+ public:
+ FontSizeTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame);
+ virtual ~FontSizeTextAttr() {}
+
+ protected:
+ // TTextAttr
+ virtual bool GetValueFor(LocalAccessible* aAccessible,
+ nscoord* aValue) override;
+ virtual void ExposeValue(AccAttributes* aAttributes,
+ const nscoord& aValue) override;
+
+ private:
+ nsDeviceContext* mDC;
+ };
+
+ /**
+ * Class is used for the work with "font-style" text attribute.
+ */
+ class FontStyleTextAttr : public TTextAttr<mozilla::FontSlantStyle> {
+ public:
+ FontStyleTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame);
+ virtual ~FontStyleTextAttr() {}
+
+ protected:
+ // TTextAttr
+ virtual bool GetValueFor(LocalAccessible* aContent,
+ mozilla::FontSlantStyle* aValue) override;
+ virtual void ExposeValue(AccAttributes* aAttributes,
+ const mozilla::FontSlantStyle& aValue) override;
+ };
+
+ /**
+ * Class is used for the work with "font-weight" text attribute.
+ */
+ class FontWeightTextAttr : public TTextAttr<mozilla::FontWeight> {
+ public:
+ FontWeightTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame);
+ virtual ~FontWeightTextAttr() {}
+
+ protected:
+ // TTextAttr
+ virtual bool GetValueFor(LocalAccessible* aAccessible,
+ mozilla::FontWeight* aValue) override;
+ virtual void ExposeValue(AccAttributes* aAttributes,
+ const mozilla::FontWeight& aValue) override;
+
+ private:
+ mozilla::FontWeight GetFontWeight(nsIFrame* aFrame);
+ };
+
+ /**
+ * Class is used for the work with 'auto-generated' text attribute.
+ */
+ class AutoGeneratedTextAttr : public TTextAttr<bool> {
+ public:
+ AutoGeneratedTextAttr(HyperTextAccessible* aHyperTextAcc,
+ LocalAccessible* aAccessible);
+ virtual ~AutoGeneratedTextAttr() {}
+
+ protected:
+ // TextAttr
+ virtual bool GetValueFor(LocalAccessible* aAccessible,
+ bool* aValue) override;
+ virtual void ExposeValue(AccAttributes* aAttributes,
+ const bool& aValue) override;
+ };
+
+ /**
+ * TextDecorTextAttr class is used for the work with
+ * "text-line-through-style", "text-line-through-color",
+ * "text-underline-style" and "text-underline-color" text attributes.
+ */
+
+ class TextDecorValue {
+ public:
+ TextDecorValue()
+ : mColor{0},
+ mLine{StyleTextDecorationLine::NONE},
+ mStyle{StyleTextDecorationStyle::None} {}
+ explicit TextDecorValue(nsIFrame* aFrame);
+
+ nscolor Color() const { return mColor; }
+ mozilla::StyleTextDecorationStyle Style() const { return mStyle; }
+
+ bool IsDefined() const { return IsUnderline() || IsLineThrough(); }
+ bool IsUnderline() const {
+ return bool(mLine & mozilla::StyleTextDecorationLine::UNDERLINE);
+ }
+ bool IsLineThrough() const {
+ return bool(mLine & mozilla::StyleTextDecorationLine::LINE_THROUGH);
+ }
+
+ bool operator==(const TextDecorValue& aValue) const {
+ return mColor == aValue.mColor && mLine == aValue.mLine &&
+ mStyle == aValue.mStyle;
+ }
+ bool operator!=(const TextDecorValue& aValue) const {
+ return !(*this == aValue);
+ }
+
+ private:
+ nscolor mColor;
+ mozilla::StyleTextDecorationLine mLine;
+ mozilla::StyleTextDecorationStyle mStyle;
+ };
+
+ class TextDecorTextAttr : public TTextAttr<TextDecorValue> {
+ public:
+ TextDecorTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame);
+ virtual ~TextDecorTextAttr() {}
+
+ protected:
+ // TextAttr
+ virtual bool GetValueFor(LocalAccessible* aAccessible,
+ TextDecorValue* aValue) override;
+ virtual void ExposeValue(AccAttributes* aAttributes,
+ const TextDecorValue& aValue) override;
+ };
+
+ /**
+ * Class is used for the work with "text-position" text attribute.
+ */
+
+ enum TextPosValue { eTextPosBaseline, eTextPosSub, eTextPosSuper };
+
+ class TextPosTextAttr : public TTextAttr<Maybe<TextPosValue>> {
+ public:
+ TextPosTextAttr(nsIFrame* aRootFrame, nsIFrame* aFrame,
+ nsIContent* aRootElm, nsIContent* aElm);
+ virtual ~TextPosTextAttr() {}
+
+ protected:
+ // TextAttr
+ virtual bool GetValueFor(LocalAccessible* aAccessible,
+ Maybe<TextPosValue>* aValue) override;
+ virtual void ExposeValue(AccAttributes* aAttributes,
+ const Maybe<TextPosValue>& aValue) override;
+
+ private:
+ Maybe<TextPosValue> GetAriaTextPosValue(nsIContent* aElm) const;
+ Maybe<TextPosValue> GetAriaTextPosValue(nsIContent* aElm,
+ nsIFrame*& ariaFrame) const;
+ Maybe<TextPosValue> GetLayoutTextPosValue(nsIFrame* aFrame) const;
+ nsIContent* mRootElm;
+ };
+
+}; // TextAttrMgr
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/TextLeafRange.cpp b/accessible/base/TextLeafRange.cpp
new file mode 100644
index 0000000000..9b658e12af
--- /dev/null
+++ b/accessible/base/TextLeafRange.cpp
@@ -0,0 +1,1990 @@
+/* -*- 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 "TextLeafRange.h"
+
+#include "HyperTextAccessible-inl.h"
+#include "mozilla/a11y/Accessible.h"
+#include "mozilla/a11y/CacheConstants.h"
+#include "mozilla/a11y/DocAccessible.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/a11y/LocalAccessible.h"
+#include "mozilla/BinarySearch.h"
+#include "mozilla/Casting.h"
+#include "mozilla/dom/CharacterData.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/intl/Segmenter.h"
+#include "mozilla/intl/WordBreaker.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/TextEditor.h"
+#include "nsAccUtils.h"
+#include "nsBlockFrame.h"
+#include "nsFrameSelection.h"
+#include "nsIAccessiblePivot.h"
+#include "nsILineIterator.h"
+#include "nsINode.h"
+#include "nsRange.h"
+#include "nsStyleStructInlines.h"
+#include "nsTArray.h"
+#include "nsTextFrame.h"
+#include "nsUnicharUtils.h"
+#include "Pivot.h"
+#include "TextAttrs.h"
+
+using mozilla::intl::WordBreaker;
+using FindWordOptions = mozilla::intl::WordBreaker::FindWordOptions;
+
+namespace mozilla::a11y {
+
+/*** Helpers ***/
+
+/**
+ * These two functions convert between rendered and content text offsets.
+ * When text DOM nodes are rendered, the rendered text often does not contain
+ * all the whitespace from the source. For example, by default, the text
+ * "a b" will be rendered as "a b"; i.e. multiple spaces are compressed to
+ * one. TextLeafAccessibles contain rendered text, but when we query layout, we
+ * need to provide offsets into the original content text. Similarly, layout
+ * returns content offsets, but we need to convert them to rendered offsets to
+ * map them to TextLeafAccessibles.
+ */
+
+static int32_t RenderedToContentOffset(LocalAccessible* aAcc,
+ uint32_t aRenderedOffset) {
+ nsTextFrame* frame = do_QueryFrame(aAcc->GetFrame());
+ if (!frame) {
+ MOZ_ASSERT(!aAcc->HasOwnContent() || aAcc->IsHTMLBr(),
+ "No text frame because this is a XUL label[value] text leaf or "
+ "a BR element.");
+ return static_cast<int32_t>(aRenderedOffset);
+ }
+
+ if (frame->StyleText()->WhiteSpaceIsSignificant() &&
+ frame->StyleText()->NewlineIsSignificant(frame)) {
+ // Spaces and new lines aren't altered, so the content and rendered offsets
+ // are the same. This happens in pre-formatted text and text fields.
+ return static_cast<int32_t>(aRenderedOffset);
+ }
+
+ nsIFrame::RenderedText text =
+ frame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1,
+ nsIFrame::TextOffsetType::OffsetsInRenderedText,
+ nsIFrame::TrailingWhitespace::DontTrim);
+ return text.mOffsetWithinNodeText;
+}
+
+static uint32_t ContentToRenderedOffset(LocalAccessible* aAcc,
+ int32_t aContentOffset) {
+ nsTextFrame* frame = do_QueryFrame(aAcc->GetFrame());
+ if (!frame) {
+ MOZ_ASSERT(!aAcc->HasOwnContent(),
+ "No text frame because this is a XUL label[value] text leaf.");
+ return aContentOffset;
+ }
+
+ if (frame->StyleText()->WhiteSpaceIsSignificant() &&
+ frame->StyleText()->NewlineIsSignificant(frame)) {
+ // Spaces and new lines aren't altered, so the content and rendered offsets
+ // are the same. This happens in pre-formatted text and text fields.
+ return aContentOffset;
+ }
+
+ nsIFrame::RenderedText text =
+ frame->GetRenderedText(aContentOffset, aContentOffset + 1,
+ nsIFrame::TextOffsetType::OffsetsInContentText,
+ nsIFrame::TrailingWhitespace::DontTrim);
+ return text.mOffsetWithinNodeRenderedText;
+}
+
+class LeafRule : public PivotRule {
+ public:
+ explicit LeafRule(bool aIgnoreListItemMarker)
+ : mIgnoreListItemMarker(aIgnoreListItemMarker) {}
+
+ virtual uint16_t Match(Accessible* aAcc) override {
+ if (aAcc->IsOuterDoc()) {
+ // Treat an embedded doc as a single character in this document, but do
+ // not descend inside it.
+ return nsIAccessibleTraversalRule::FILTER_MATCH |
+ nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+
+ if (mIgnoreListItemMarker && aAcc->Role() == roles::LISTITEM_MARKER) {
+ // Ignore list item markers if configured to do so.
+ return nsIAccessibleTraversalRule::FILTER_IGNORE;
+ }
+
+ // We deliberately include Accessibles such as empty input elements and
+ // empty containers, as these can be at the start of a line.
+ if (!aAcc->HasChildren()) {
+ return nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ return nsIAccessibleTraversalRule::FILTER_IGNORE;
+ }
+
+ private:
+ bool mIgnoreListItemMarker;
+};
+
+static HyperTextAccessible* HyperTextFor(LocalAccessible* aAcc) {
+ for (LocalAccessible* acc = aAcc; acc; acc = acc->LocalParent()) {
+ if (HyperTextAccessible* ht = acc->AsHyperText()) {
+ return ht;
+ }
+ }
+ return nullptr;
+}
+
+static Accessible* NextLeaf(Accessible* aOrigin, bool aIsEditable = false,
+ bool aIgnoreListItemMarker = false) {
+ MOZ_ASSERT(aOrigin);
+ Accessible* doc = nsAccUtils::DocumentFor(aOrigin);
+ Pivot pivot(doc);
+ auto rule = LeafRule(aIgnoreListItemMarker);
+ Accessible* leaf = pivot.Next(aOrigin, rule);
+ if (aIsEditable && leaf) {
+ return leaf->Parent() && (leaf->Parent()->State() & states::EDITABLE)
+ ? leaf
+ : nullptr;
+ }
+ return leaf;
+}
+
+static Accessible* PrevLeaf(Accessible* aOrigin, bool aIsEditable = false,
+ bool aIgnoreListItemMarker = false) {
+ MOZ_ASSERT(aOrigin);
+ Accessible* doc = nsAccUtils::DocumentFor(aOrigin);
+ Pivot pivot(doc);
+ auto rule = LeafRule(aIgnoreListItemMarker);
+ Accessible* leaf = pivot.Prev(aOrigin, rule);
+ if (aIsEditable && leaf) {
+ return leaf->Parent() && (leaf->Parent()->State() & states::EDITABLE)
+ ? leaf
+ : nullptr;
+ }
+ return leaf;
+}
+
+static nsIFrame* GetFrameInBlock(const LocalAccessible* aAcc) {
+ dom::HTMLInputElement* input =
+ dom::HTMLInputElement::FromNodeOrNull(aAcc->GetContent());
+ if (!input) {
+ if (LocalAccessible* parent = aAcc->LocalParent()) {
+ input = dom::HTMLInputElement::FromNodeOrNull(parent->GetContent());
+ }
+ }
+
+ if (input) {
+ // If this is a single line input (or a leaf of an input) we want to return
+ // the top frame of the input element and not the text leaf's frame because
+ // the leaf may be inside of an embedded block frame in the input's shadow
+ // DOM that we aren't interested in.
+ return input->GetPrimaryFrame();
+ }
+
+ return aAcc->GetFrame();
+}
+
+static bool IsLocalAccAtLineStart(LocalAccessible* aAcc) {
+ if (aAcc->NativeRole() == roles::LISTITEM_MARKER) {
+ // A bullet always starts a line.
+ return true;
+ }
+ // Splitting of content across lines is handled by layout.
+ // nsIFrame::IsLogicallyAtLineEdge queries whether a frame is the first frame
+ // on its line. However, we can't use that because the first frame on a line
+ // might not be included in the a11y tree; e.g. an empty span, or space
+ // in the DOM after a line break which is stripped when rendered. Instead, we
+ // get the line number for this Accessible's frame and the line number for the
+ // previous leaf Accessible's frame and compare them.
+ Accessible* prev = PrevLeaf(aAcc);
+ LocalAccessible* prevLocal = prev ? prev->AsLocal() : nullptr;
+ if (!prevLocal) {
+ // There's nothing before us, so this is the start of the first line.
+ return true;
+ }
+ if (prevLocal->NativeRole() == roles::LISTITEM_MARKER) {
+ // If there is a bullet immediately before us and we're inside the same
+ // list item, this is not the start of a line.
+ LocalAccessible* listItem = prevLocal->LocalParent();
+ MOZ_ASSERT(listItem);
+ LocalAccessible* doc = listItem->Document();
+ MOZ_ASSERT(doc);
+ for (LocalAccessible* parent = aAcc->LocalParent(); parent && parent != doc;
+ parent = parent->LocalParent()) {
+ if (parent == listItem) {
+ return false;
+ }
+ }
+ }
+
+ nsIFrame* thisFrame = GetFrameInBlock(aAcc);
+ if (!thisFrame) {
+ return false;
+ }
+
+ nsIFrame* prevFrame = GetFrameInBlock(prevLocal);
+ if (!prevFrame) {
+ return false;
+ }
+
+ auto [thisBlock, thisLineFrame] = thisFrame->GetContainingBlockForLine(
+ /* aLockScroll */ false);
+ if (!thisBlock) {
+ // We couldn't get the containing block for this frame. In that case, we
+ // play it safe and assume this is the beginning of a new line.
+ return true;
+ }
+
+ // The previous leaf might cross lines. We want to compare against the last
+ // line.
+ prevFrame = prevFrame->LastContinuation();
+ auto [prevBlock, prevLineFrame] = prevFrame->GetContainingBlockForLine(
+ /* aLockScroll */ false);
+ if (thisBlock != prevBlock) {
+ // If the blocks are different, that means there's nothing before us on the
+ // same line, so we're at the start.
+ return true;
+ }
+ if (nsBlockFrame* block = do_QueryFrame(thisBlock)) {
+ // If we have a block frame, it's faster for us to use
+ // BlockInFlowLineIterator because it uses the line cursor.
+ bool found = false;
+ block->SetupLineCursorForQuery();
+ nsBlockInFlowLineIterator prevIt(block, prevLineFrame, &found);
+ if (!found) {
+ // Error; play it safe.
+ return true;
+ }
+ found = false;
+ nsBlockInFlowLineIterator thisIt(block, thisLineFrame, &found);
+ // if the lines are different, that means there's nothing before us on the
+ // same line, so we're at the start.
+ return !found || prevIt.GetLine() != thisIt.GetLine();
+ }
+ AutoAssertNoDomMutations guard;
+ nsILineIterator* it = prevBlock->GetLineIterator();
+ MOZ_ASSERT(it, "GetLineIterator impl in line-container blocks is infallible");
+ int32_t prevLineNum = it->FindLineContaining(prevLineFrame);
+ if (prevLineNum < 0) {
+ // Error; play it safe.
+ return true;
+ }
+ int32_t thisLineNum = it->FindLineContaining(thisLineFrame, prevLineNum);
+ // if the blocks and line numbers are different, that means there's nothing
+ // before us on the same line, so we're at the start.
+ return thisLineNum != prevLineNum;
+}
+
+/**
+ * There are many kinds of word break, but we only need to treat punctuation and
+ * space specially.
+ */
+enum WordBreakClass { eWbcSpace = 0, eWbcPunct, eWbcOther };
+
+static WordBreakClass GetWordBreakClass(char16_t aChar) {
+ // Based on IsSelectionInlineWhitespace and IsSelectionNewline in
+ // layout/generic/nsTextFrame.cpp.
+ const char16_t kCharNbsp = 0xA0;
+ switch (aChar) {
+ case ' ':
+ case kCharNbsp:
+ case '\t':
+ case '\f':
+ case '\n':
+ case '\r':
+ return eWbcSpace;
+ default:
+ break;
+ }
+ return mozilla::IsPunctuationForWordSelect(aChar) ? eWbcPunct : eWbcOther;
+}
+
+/**
+ * Words can cross Accessibles. To work out whether we're at the start of a
+ * word, we might have to check the previous leaf. This class handles querying
+ * the previous WordBreakClass, crossing Accessibles if necessary.
+ */
+class PrevWordBreakClassWalker {
+ public:
+ PrevWordBreakClassWalker(Accessible* aAcc, const nsAString& aText,
+ int32_t aOffset)
+ : mAcc(aAcc), mText(aText), mOffset(aOffset) {
+ mClass = GetWordBreakClass(mText.CharAt(mOffset));
+ }
+
+ WordBreakClass CurClass() { return mClass; }
+
+ Maybe<WordBreakClass> PrevClass() {
+ for (;;) {
+ if (!PrevChar()) {
+ return Nothing();
+ }
+ WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset));
+ if (curClass != mClass) {
+ mClass = curClass;
+ return Some(curClass);
+ }
+ }
+ MOZ_ASSERT_UNREACHABLE();
+ return Nothing();
+ }
+
+ bool IsStartOfGroup() {
+ if (!PrevChar()) {
+ // There are no characters before us.
+ return true;
+ }
+ WordBreakClass curClass = GetWordBreakClass(mText.CharAt(mOffset));
+ // We wanted to peek at the previous character, not really move to it.
+ ++mOffset;
+ return curClass != mClass;
+ }
+
+ private:
+ bool PrevChar() {
+ if (mOffset > 0) {
+ --mOffset;
+ return true;
+ }
+ if (!mAcc) {
+ // PrevChar was called already and failed.
+ return false;
+ }
+ mAcc = PrevLeaf(mAcc);
+ if (!mAcc) {
+ return false;
+ }
+ mText.Truncate();
+ mAcc->AppendTextTo(mText);
+ mOffset = static_cast<int32_t>(mText.Length()) - 1;
+ return true;
+ }
+
+ Accessible* mAcc;
+ nsAutoString mText;
+ int32_t mOffset;
+ WordBreakClass mClass;
+};
+
+/**
+ * WordBreaker breaks at all space, punctuation, etc. We want to emulate
+ * layout, so that's not what we want. This function determines whether this
+ * is acceptable as the start of a word for our purposes.
+ */
+static bool IsAcceptableWordStart(Accessible* aAcc, const nsAutoString& aText,
+ int32_t aOffset) {
+ PrevWordBreakClassWalker walker(aAcc, aText, aOffset);
+ if (!walker.IsStartOfGroup()) {
+ // If we're not at the start of a WordBreaker group, this can't be the
+ // start of a word.
+ return false;
+ }
+ WordBreakClass curClass = walker.CurClass();
+ if (curClass == eWbcSpace) {
+ // Space isn't the start of a word.
+ return false;
+ }
+ Maybe<WordBreakClass> prevClass = walker.PrevClass();
+ if (curClass == eWbcPunct && (!prevClass || prevClass.value() != eWbcSpace)) {
+ // Punctuation isn't the start of a word (unless it is after space).
+ return false;
+ }
+ if (!prevClass || prevClass.value() != eWbcPunct) {
+ // If there's nothing before this or the group before this isn't
+ // punctuation, this is the start of a word.
+ return true;
+ }
+ // At this point, we know the group before this is punctuation.
+ if (!StaticPrefs::layout_word_select_stop_at_punctuation()) {
+ // When layout.word_select.stop_at_punctuation is false (defaults to true),
+ // if there is punctuation before this, this is not the start of a word.
+ return false;
+ }
+ Maybe<WordBreakClass> prevPrevClass = walker.PrevClass();
+ if (!prevPrevClass || prevPrevClass.value() == eWbcSpace) {
+ // If there is punctuation before this and space (or nothing) before the
+ // punctuation, this is not the start of a word.
+ return false;
+ }
+ return true;
+}
+
+class BlockRule : public PivotRule {
+ public:
+ virtual uint16_t Match(Accessible* aAcc) override {
+ if (RefPtr<nsAtom>(aAcc->DisplayStyle()) == nsGkAtoms::block ||
+ aAcc->IsHTMLListItem() || aAcc->IsTableRow() || aAcc->IsTableCell()) {
+ return nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ return nsIAccessibleTraversalRule::FILTER_IGNORE;
+ }
+};
+
+/**
+ * Find spelling error DOM ranges overlapping the requested LocalAccessible and
+ * offsets. This includes ranges that begin or end outside of the given
+ * LocalAccessible. Note that the offset arguments are rendered offsets, but
+ * because the returned ranges are DOM ranges, those offsets are content
+ * offsets. See the documentation for dom::Selection::GetRangesForIntervalArray
+ * for information about the aAllowAdjacent argument.
+ */
+static nsTArray<nsRange*> FindDOMSpellingErrors(LocalAccessible* aAcc,
+ int32_t aRenderedStart,
+ int32_t aRenderedEnd,
+ bool aAllowAdjacent = false) {
+ if (!aAcc->IsTextLeaf() || !aAcc->HasOwnContent()) {
+ return {};
+ }
+ nsIFrame* frame = aAcc->GetFrame();
+ RefPtr<nsFrameSelection> frameSel =
+ frame ? frame->GetFrameSelection() : nullptr;
+ dom::Selection* domSel =
+ frameSel ? frameSel->GetSelection(SelectionType::eSpellCheck) : nullptr;
+ if (!domSel) {
+ return {};
+ }
+ nsINode* node = aAcc->GetNode();
+ uint32_t contentStart = RenderedToContentOffset(aAcc, aRenderedStart);
+ uint32_t contentEnd =
+ aRenderedEnd == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
+ ? dom::CharacterData::FromNode(node)->TextLength()
+ : RenderedToContentOffset(aAcc, aRenderedEnd);
+ nsTArray<nsRange*> domRanges;
+ domSel->GetDynamicRangesForIntervalArray(node, contentStart, node, contentEnd,
+ aAllowAdjacent, &domRanges);
+ return domRanges;
+}
+
+/**
+ * Given two DOM nodes get DOM Selection object that is common
+ * to both of them.
+ */
+static dom::Selection* GetDOMSelection(const nsIContent* aStartContent,
+ const nsIContent* aEndContent) {
+ nsIFrame* startFrame = aStartContent->GetPrimaryFrame();
+ const nsFrameSelection* startFrameSel =
+ startFrame ? startFrame->GetConstFrameSelection() : nullptr;
+ nsIFrame* endFrame = aEndContent->GetPrimaryFrame();
+ const nsFrameSelection* endFrameSel =
+ endFrame ? endFrame->GetConstFrameSelection() : nullptr;
+
+ if (startFrameSel != endFrameSel) {
+ // Start and end point don't share the same selection state.
+ // This could happen when both points aren't in the same editable.
+ return nullptr;
+ }
+
+ return startFrameSel ? startFrameSel->GetSelection(SelectionType::eNormal)
+ : nullptr;
+}
+
+std::pair<nsIContent*, int32_t> TextLeafPoint::ToDOMPoint(
+ bool aIncludeGenerated) const {
+ if (!(*this) || !mAcc->IsLocal()) {
+ MOZ_ASSERT_UNREACHABLE("Invalid point");
+ return {nullptr, 0};
+ }
+
+ nsIContent* content = mAcc->AsLocal()->GetContent();
+ nsIFrame* frame = content ? content->GetPrimaryFrame() : nullptr;
+ MOZ_ASSERT(frame);
+
+ if (!aIncludeGenerated && frame && frame->IsGeneratedContentFrame()) {
+ // List markers accessibles represent the generated content element,
+ // before/after text accessibles represent the child text nodes.
+ auto generatedElement = content->IsGeneratedContentContainerForMarker()
+ ? content
+ : content->GetParentElement();
+ auto parent = generatedElement ? generatedElement->GetParent() : nullptr;
+ MOZ_ASSERT(parent);
+ if (parent) {
+ if (generatedElement->IsGeneratedContentContainerForAfter()) {
+ // Use the end offset of the parent element for trailing generated
+ // content.
+ return {parent, parent->GetChildCount()};
+ }
+
+ if (generatedElement->IsGeneratedContentContainerForBefore() ||
+ generatedElement->IsGeneratedContentContainerForMarker()) {
+ // Use the start offset of the parent element for leading generated
+ // content.
+ return {parent, 0};
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Unknown generated content type!");
+ }
+ }
+
+ if (!mAcc->IsTextLeaf() && !mAcc->IsHTMLBr() && !mAcc->HasChildren()) {
+ // If this is not a text leaf it can be an empty editable container,
+ // whitespace, or an empty doc. In any case, the offset inside should be 0.
+ MOZ_ASSERT(mOffset == 0);
+
+ if (RefPtr<TextControlElement> textControlElement =
+ TextControlElement::FromNodeOrNull(content)) {
+ // This is an empty input, use the shadow root's element.
+ if (RefPtr<TextEditor> textEditor = textControlElement->GetTextEditor()) {
+ if (textEditor->IsEmpty()) {
+ MOZ_ASSERT(mOffset == 0);
+ return {textEditor->GetRoot(), 0};
+ }
+ }
+ }
+
+ return {content, 0};
+ }
+
+ return {content, RenderedToContentOffset(mAcc->AsLocal(), mOffset)};
+}
+
+/*** TextLeafPoint ***/
+
+TextLeafPoint::TextLeafPoint(Accessible* aAcc, int32_t aOffset) {
+ if (!aAcc) {
+ // Construct an invalid point.
+ mAcc = nullptr;
+ mOffset = 0;
+ return;
+ }
+
+ // Even though an OuterDoc contains a document, we treat it as a leaf because
+ // we don't want to move into another document.
+ if (aOffset != nsIAccessibleText::TEXT_OFFSET_CARET && !aAcc->IsOuterDoc() &&
+ aAcc->HasChildren()) {
+ // Find a leaf. This might not necessarily be a TextLeafAccessible; it
+ // could be an empty container.
+ auto GetChild = [&aOffset](Accessible* acc) -> Accessible* {
+ if (acc->IsOuterDoc()) {
+ return nullptr;
+ }
+ return aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
+ ? acc->FirstChild()
+ : acc->LastChild();
+ };
+
+ for (Accessible* acc = GetChild(aAcc); acc; acc = GetChild(acc)) {
+ mAcc = acc;
+ }
+ mOffset = aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
+ ? 0
+ : nsAccUtils::TextLength(mAcc);
+ return;
+ }
+ mAcc = aAcc;
+ mOffset = aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT
+ ? aOffset
+ : nsAccUtils::TextLength(mAcc);
+}
+
+bool TextLeafPoint::operator<(const TextLeafPoint& aPoint) const {
+ if (mAcc == aPoint.mAcc) {
+ return mOffset < aPoint.mOffset;
+ }
+ return mAcc->IsBefore(aPoint.mAcc);
+}
+
+bool TextLeafPoint::operator<=(const TextLeafPoint& aPoint) const {
+ return *this == aPoint || *this < aPoint;
+}
+
+bool TextLeafPoint::IsDocEdge(nsDirection aDirection) const {
+ if (aDirection == eDirPrevious) {
+ return mOffset == 0 && !PrevLeaf(mAcc);
+ }
+
+ return mOffset == static_cast<int32_t>(nsAccUtils::TextLength(mAcc)) &&
+ !NextLeaf(mAcc);
+}
+
+bool TextLeafPoint::IsLeafAfterListItemMarker() const {
+ Accessible* prev = PrevLeaf(mAcc);
+ return prev && prev->Role() == roles::LISTITEM_MARKER &&
+ prev->Parent()->IsAncestorOf(mAcc);
+}
+
+bool TextLeafPoint::IsEmptyLastLine() const {
+ if (mAcc->IsHTMLBr() && mOffset == 1) {
+ return true;
+ }
+ if (!mAcc->IsTextLeaf()) {
+ return false;
+ }
+ if (mOffset < static_cast<int32_t>(nsAccUtils::TextLength(mAcc))) {
+ return false;
+ }
+ nsAutoString text;
+ mAcc->AppendTextTo(text, mOffset - 1, 1);
+ return text.CharAt(0) == '\n';
+}
+
+char16_t TextLeafPoint::GetChar() const {
+ nsAutoString text;
+ mAcc->AppendTextTo(text, mOffset, 1);
+ return text.CharAt(0);
+}
+
+TextLeafPoint TextLeafPoint::FindPrevLineStartSameLocalAcc(
+ bool aIncludeOrigin) const {
+ LocalAccessible* acc = mAcc->AsLocal();
+ MOZ_ASSERT(acc);
+ if (mOffset == 0) {
+ if (aIncludeOrigin && IsLocalAccAtLineStart(acc)) {
+ return *this;
+ }
+ return TextLeafPoint();
+ }
+ nsIFrame* frame = acc->GetFrame();
+ if (!frame) {
+ // This can happen if this is an empty element with display: contents. In
+ // that case, this Accessible contains no lines.
+ return TextLeafPoint();
+ }
+ if (!frame->IsTextFrame()) {
+ if (IsLocalAccAtLineStart(acc)) {
+ return TextLeafPoint(acc, 0);
+ }
+ return TextLeafPoint();
+ }
+ // Each line of a text node is rendered as a continuation frame. Get the
+ // continuation containing the origin.
+ int32_t origOffset = mOffset;
+ origOffset = RenderedToContentOffset(acc, origOffset);
+ nsTextFrame* continuation = nullptr;
+ int32_t unusedOffsetInContinuation = 0;
+ frame->GetChildFrameContainingOffset(
+ origOffset, true, &unusedOffsetInContinuation, (nsIFrame**)&continuation);
+ MOZ_ASSERT(continuation);
+ int32_t lineStart = continuation->GetContentOffset();
+ if (!aIncludeOrigin && lineStart > 0 && lineStart == origOffset) {
+ // A line starts at the origin, but the caller doesn't want this included.
+ // Go back one more.
+ continuation = continuation->GetPrevContinuation();
+ MOZ_ASSERT(continuation);
+ lineStart = continuation->GetContentOffset();
+ }
+ MOZ_ASSERT(lineStart >= 0);
+ if (lineStart == 0 && !IsLocalAccAtLineStart(acc)) {
+ // This is the first line of this text node, but there is something else
+ // on the same line before this text node, so don't return this as a line
+ // start.
+ return TextLeafPoint();
+ }
+ lineStart = static_cast<int32_t>(ContentToRenderedOffset(acc, lineStart));
+ return TextLeafPoint(acc, lineStart);
+}
+
+TextLeafPoint TextLeafPoint::FindNextLineStartSameLocalAcc(
+ bool aIncludeOrigin) const {
+ LocalAccessible* acc = mAcc->AsLocal();
+ MOZ_ASSERT(acc);
+ if (aIncludeOrigin && mOffset == 0 && IsLocalAccAtLineStart(acc)) {
+ return *this;
+ }
+ nsIFrame* frame = acc->GetFrame();
+ if (!frame) {
+ // This can happen if this is an empty element with display: contents. In
+ // that case, this Accessible contains no lines.
+ return TextLeafPoint();
+ }
+ if (!frame->IsTextFrame()) {
+ // There can't be multiple lines in a non-text leaf.
+ return TextLeafPoint();
+ }
+ // Each line of a text node is rendered as a continuation frame. Get the
+ // continuation containing the origin.
+ int32_t origOffset = mOffset;
+ origOffset = RenderedToContentOffset(acc, origOffset);
+ nsTextFrame* continuation = nullptr;
+ int32_t unusedOffsetInContinuation = 0;
+ frame->GetChildFrameContainingOffset(
+ origOffset, true, &unusedOffsetInContinuation, (nsIFrame**)&continuation);
+ MOZ_ASSERT(continuation);
+ if (
+ // A line starts at the origin and the caller wants this included.
+ aIncludeOrigin && continuation->GetContentOffset() == origOffset &&
+ // If this is the first line of this text node (offset 0), don't treat it
+ // as a line start if there's something else on the line before this text
+ // node.
+ !(origOffset == 0 && !IsLocalAccAtLineStart(acc))) {
+ return *this;
+ }
+ continuation = continuation->GetNextContinuation();
+ if (!continuation) {
+ return TextLeafPoint();
+ }
+ int32_t lineStart = continuation->GetContentOffset();
+ lineStart = static_cast<int32_t>(ContentToRenderedOffset(acc, lineStart));
+ return TextLeafPoint(acc, lineStart);
+}
+
+TextLeafPoint TextLeafPoint::FindLineStartSameRemoteAcc(
+ nsDirection aDirection, bool aIncludeOrigin) const {
+ RemoteAccessible* acc = mAcc->AsRemote();
+ MOZ_ASSERT(acc);
+ auto lines = acc->GetCachedTextLines();
+ if (!lines) {
+ return TextLeafPoint();
+ }
+ size_t index;
+ // If BinarySearch returns true, mOffset is in the array and index points at
+ // it. If BinarySearch returns false, mOffset is not in the array and index
+ // points at the next line start after mOffset.
+ if (BinarySearch(*lines, 0, lines->Length(), mOffset, &index)) {
+ if (aIncludeOrigin) {
+ return *this;
+ }
+ if (aDirection == eDirNext) {
+ // We don't want to include the origin. Get the next line start.
+ ++index;
+ }
+ }
+ MOZ_ASSERT(index <= lines->Length());
+ if ((aDirection == eDirNext && index == lines->Length()) ||
+ (aDirection == eDirPrevious && index == 0)) {
+ return TextLeafPoint();
+ }
+ // index points at the line start after mOffset.
+ if (aDirection == eDirPrevious) {
+ --index;
+ }
+ return TextLeafPoint(mAcc, lines->ElementAt(index));
+}
+
+TextLeafPoint TextLeafPoint::FindLineStartSameAcc(
+ nsDirection aDirection, bool aIncludeOrigin,
+ bool aIgnoreListItemMarker) const {
+ TextLeafPoint boundary;
+ if (aIgnoreListItemMarker && aIncludeOrigin && mOffset == 0 &&
+ IsLeafAfterListItemMarker()) {
+ // If:
+ // (1) we are ignoring list markers
+ // (2) we should include origin
+ // (3) we are at the start of a leaf that follows a list item marker
+ // ...then return this point.
+ return *this;
+ }
+
+ if (mAcc->IsLocal()) {
+ boundary = aDirection == eDirNext
+ ? FindNextLineStartSameLocalAcc(aIncludeOrigin)
+ : FindPrevLineStartSameLocalAcc(aIncludeOrigin);
+ } else {
+ boundary = FindLineStartSameRemoteAcc(aDirection, aIncludeOrigin);
+ }
+
+ if (aIgnoreListItemMarker && aDirection == eDirPrevious && !boundary &&
+ mOffset != 0 && IsLeafAfterListItemMarker()) {
+ // If:
+ // (1) we are ignoring list markers
+ // (2) we are searching backwards in accessible
+ // (3) we did not find a line start before this point
+ // (4) we are in a leaf that follows a list item marker
+ // ...then return the first point in this accessible.
+ boundary = TextLeafPoint(mAcc, 0);
+ }
+
+ return boundary;
+}
+
+TextLeafPoint TextLeafPoint::FindPrevWordStartSameAcc(
+ bool aIncludeOrigin) const {
+ if (mOffset == 0 && !aIncludeOrigin) {
+ // We can't go back any further and the caller doesn't want the origin
+ // included, so there's nothing more to do.
+ return TextLeafPoint();
+ }
+ nsAutoString text;
+ mAcc->AppendTextTo(text);
+ TextLeafPoint lineStart = *this;
+ if (!aIncludeOrigin || (lineStart.mOffset == 1 && text.Length() == 1 &&
+ text.CharAt(0) == '\n')) {
+ // We're not interested in a line that starts here, either because
+ // aIncludeOrigin is false or because we're at the end of a line break
+ // node.
+ --lineStart.mOffset;
+ }
+ // A word never starts with a line feed character. If there are multiple
+ // consecutive line feed characters and we're after the first of them, the
+ // previous line start will be a line feed character. Skip this and any prior
+ // consecutive line feed first.
+ for (; lineStart.mOffset >= 0 && text.CharAt(lineStart.mOffset) == '\n';
+ --lineStart.mOffset) {
+ }
+ if (lineStart.mOffset < 0) {
+ // There's no line start for our purposes.
+ lineStart = TextLeafPoint();
+ } else {
+ lineStart =
+ lineStart.FindLineStartSameAcc(eDirPrevious, /* aIncludeOrigin */ true);
+ }
+ // Keep walking backward until we find an acceptable word start.
+ intl::WordRange word;
+ if (mOffset == 0) {
+ word.mBegin = 0;
+ } else if (mOffset == static_cast<int32_t>(text.Length())) {
+ word = WordBreaker::FindWord(
+ text, mOffset - 1,
+ StaticPrefs::layout_word_select_stop_at_punctuation()
+ ? FindWordOptions::StopAtPunctuation
+ : FindWordOptions::None);
+ } else {
+ word = WordBreaker::FindWord(
+ text, mOffset,
+ StaticPrefs::layout_word_select_stop_at_punctuation()
+ ? FindWordOptions::StopAtPunctuation
+ : FindWordOptions::None);
+ }
+ for (;; word = WordBreaker::FindWord(
+ text, word.mBegin - 1,
+ StaticPrefs::layout_word_select_stop_at_punctuation()
+ ? FindWordOptions::StopAtPunctuation
+ : FindWordOptions::None)) {
+ if (!aIncludeOrigin && static_cast<int32_t>(word.mBegin) == mOffset) {
+ // A word possibly starts at the origin, but the caller doesn't want this
+ // included.
+ MOZ_ASSERT(word.mBegin != 0);
+ continue;
+ }
+ if (lineStart && static_cast<int32_t>(word.mBegin) < lineStart.mOffset) {
+ // A line start always starts a new word.
+ return lineStart;
+ }
+ if (IsAcceptableWordStart(mAcc, text, static_cast<int32_t>(word.mBegin))) {
+ break;
+ }
+ if (word.mBegin == 0) {
+ // We can't go back any further.
+ if (lineStart) {
+ // A line start always starts a new word.
+ return lineStart;
+ }
+ return TextLeafPoint();
+ }
+ }
+ return TextLeafPoint(mAcc, static_cast<int32_t>(word.mBegin));
+}
+
+TextLeafPoint TextLeafPoint::FindNextWordStartSameAcc(
+ bool aIncludeOrigin) const {
+ nsAutoString text;
+ mAcc->AppendTextTo(text);
+ int32_t wordStart = mOffset;
+ if (aIncludeOrigin) {
+ if (wordStart == 0) {
+ if (IsAcceptableWordStart(mAcc, text, 0)) {
+ return *this;
+ }
+ } else {
+ // The origin might start a word, so search from just before it.
+ --wordStart;
+ }
+ }
+ TextLeafPoint lineStart = FindLineStartSameAcc(eDirNext, aIncludeOrigin);
+ if (lineStart) {
+ // A word never starts with a line feed character. If there are multiple
+ // consecutive line feed characters, lineStart will point at the second of
+ // them. Skip this and any subsequent consecutive line feed.
+ for (; lineStart.mOffset < static_cast<int32_t>(text.Length()) &&
+ text.CharAt(lineStart.mOffset) == '\n';
+ ++lineStart.mOffset) {
+ }
+ if (lineStart.mOffset == static_cast<int32_t>(text.Length())) {
+ // There's no line start for our purposes.
+ lineStart = TextLeafPoint();
+ }
+ }
+ // Keep walking forward until we find an acceptable word start.
+ intl::WordBreakIteratorUtf16 wordBreakIter(text);
+ int32_t previousPos = wordStart;
+ Maybe<uint32_t> nextBreak = wordBreakIter.Seek(wordStart);
+ for (;;) {
+ if (!nextBreak || *nextBreak == text.Length()) {
+ if (lineStart) {
+ // A line start always starts a new word.
+ return lineStart;
+ }
+ if (StaticPrefs::layout_word_select_stop_at_punctuation()) {
+ // If layout.word_select.stop_at_punctuation is true, we have to look
+ // for punctuation class since it may not break state in UAX#29.
+ for (int32_t i = previousPos + 1;
+ i < static_cast<int32_t>(text.Length()); i++) {
+ if (IsAcceptableWordStart(mAcc, text, i)) {
+ return TextLeafPoint(mAcc, i);
+ }
+ }
+ }
+ return TextLeafPoint();
+ }
+ wordStart = AssertedCast<int32_t>(*nextBreak);
+ if (lineStart && wordStart > lineStart.mOffset) {
+ // A line start always starts a new word.
+ return lineStart;
+ }
+ if (IsAcceptableWordStart(mAcc, text, wordStart)) {
+ break;
+ }
+
+ if (StaticPrefs::layout_word_select_stop_at_punctuation()) {
+ // If layout.word_select.stop_at_punctuation is true, we have to look
+ // for punctuation class since it may not break state in UAX#29.
+ for (int32_t i = previousPos + 1; i < wordStart; i++) {
+ if (IsAcceptableWordStart(mAcc, text, i)) {
+ return TextLeafPoint(mAcc, i);
+ }
+ }
+ }
+ previousPos = wordStart;
+ nextBreak = wordBreakIter.Next();
+ }
+ return TextLeafPoint(mAcc, wordStart);
+}
+
+bool TextLeafPoint::IsCaretAtEndOfLine() const {
+ MOZ_ASSERT(IsCaret());
+ if (LocalAccessible* acc = mAcc->AsLocal()) {
+ HyperTextAccessible* ht = HyperTextFor(acc);
+ if (!ht) {
+ return false;
+ }
+ // Use HyperTextAccessible::IsCaretAtEndOfLine. Eventually, we'll want to
+ // move that code into TextLeafPoint, but existing code depends on it living
+ // in HyperTextAccessible (including caret events).
+ return ht->IsCaretAtEndOfLine();
+ }
+ return mAcc->AsRemote()->Document()->IsCaretAtEndOfLine();
+}
+
+TextLeafPoint TextLeafPoint::ActualizeCaret(bool aAdjustAtEndOfLine) const {
+ MOZ_ASSERT(IsCaret());
+ HyperTextAccessibleBase* ht;
+ int32_t htOffset;
+ if (LocalAccessible* acc = mAcc->AsLocal()) {
+ // Use HyperTextAccessible::CaretOffset. Eventually, we'll want to move
+ // that code into TextLeafPoint, but existing code depends on it living in
+ // HyperTextAccessible (including caret events).
+ ht = HyperTextFor(acc);
+ if (!ht) {
+ return TextLeafPoint();
+ }
+ htOffset = ht->CaretOffset();
+ if (htOffset == -1) {
+ return TextLeafPoint();
+ }
+ } else {
+ // Ideally, we'd cache the caret as a leaf, but our events are based on
+ // HyperText for now.
+ std::tie(ht, htOffset) = mAcc->AsRemote()->Document()->GetCaret();
+ if (!ht) {
+ return TextLeafPoint();
+ }
+ }
+ if (aAdjustAtEndOfLine && htOffset > 0 && IsCaretAtEndOfLine()) {
+ // It is the same character offset when the caret is visually at the very
+ // end of a line or the start of a new line (soft line break). Getting text
+ // at the line should provide the line with the visual caret. Otherwise,
+ // screen readers will announce the wrong line as the user presses up or
+ // down arrow and land at the end of a line.
+ --htOffset;
+ }
+ return ht->ToTextLeafPoint(htOffset);
+}
+
+TextLeafPoint TextLeafPoint::FindBoundary(AccessibleTextBoundary aBoundaryType,
+ nsDirection aDirection,
+ BoundaryFlags aFlags) const {
+ if (IsCaret()) {
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
+ if (IsCaretAtEndOfLine()) {
+ // The caret is at the end of the line. Return no character.
+ return ActualizeCaret(/* aAdjustAtEndOfLine */ false);
+ }
+ }
+ return ActualizeCaret().FindBoundary(
+ aBoundaryType, aDirection, aFlags & BoundaryFlags::eIncludeOrigin);
+ }
+
+ bool inEditableAndStopInIt = (aFlags & BoundaryFlags::eStopInEditable) &&
+ mAcc->Parent() &&
+ (mAcc->Parent()->State() & states::EDITABLE);
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) {
+ return FindLineEnd(aDirection,
+ inEditableAndStopInIt
+ ? aFlags
+ : (aFlags & ~BoundaryFlags::eStopInEditable));
+ }
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_WORD_END) {
+ return FindWordEnd(aDirection,
+ inEditableAndStopInIt
+ ? aFlags
+ : (aFlags & ~BoundaryFlags::eStopInEditable));
+ }
+ if ((aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_START ||
+ aBoundaryType == nsIAccessibleText::BOUNDARY_PARAGRAPH) &&
+ (aFlags & BoundaryFlags::eIncludeOrigin) && aDirection == eDirPrevious &&
+ IsEmptyLastLine()) {
+ // If we're at an empty line at the end of an Accessible, we don't want to
+ // walk into the previous line. For example, this can happen if the caret
+ // is positioned on an empty line at the end of a textarea.
+ return *this;
+ }
+ bool includeOrigin = !!(aFlags & BoundaryFlags::eIncludeOrigin);
+ bool ignoreListItemMarker = !!(aFlags & BoundaryFlags::eIgnoreListItemMarker);
+ Accessible* lastAcc = nullptr;
+ for (TextLeafPoint searchFrom = *this; searchFrom;
+ searchFrom = searchFrom.NeighborLeafPoint(
+ aDirection, inEditableAndStopInIt, ignoreListItemMarker)) {
+ lastAcc = searchFrom.mAcc;
+ if (ignoreListItemMarker && searchFrom == *this &&
+ searchFrom.mAcc->Role() == roles::LISTITEM_MARKER) {
+ continue;
+ }
+ TextLeafPoint boundary;
+ // Search for the boundary within the current Accessible.
+ switch (aBoundaryType) {
+ case nsIAccessibleText::BOUNDARY_CHAR:
+ if (includeOrigin) {
+ boundary = searchFrom;
+ } else if (aDirection == eDirPrevious && searchFrom.mOffset > 0) {
+ boundary.mAcc = searchFrom.mAcc;
+ boundary.mOffset = searchFrom.mOffset - 1;
+ } else if (aDirection == eDirNext &&
+ searchFrom.mOffset + 1 <
+ static_cast<int32_t>(
+ nsAccUtils::TextLength(searchFrom.mAcc))) {
+ boundary.mAcc = searchFrom.mAcc;
+ boundary.mOffset = searchFrom.mOffset + 1;
+ }
+ break;
+ case nsIAccessibleText::BOUNDARY_WORD_START:
+ if (aDirection == eDirPrevious) {
+ boundary = searchFrom.FindPrevWordStartSameAcc(includeOrigin);
+ } else {
+ boundary = searchFrom.FindNextWordStartSameAcc(includeOrigin);
+ }
+ break;
+ case nsIAccessibleText::BOUNDARY_LINE_START:
+ boundary = searchFrom.FindLineStartSameAcc(aDirection, includeOrigin,
+ ignoreListItemMarker);
+ break;
+ case nsIAccessibleText::BOUNDARY_PARAGRAPH:
+ boundary = searchFrom.FindParagraphSameAcc(aDirection, includeOrigin,
+ ignoreListItemMarker);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE();
+ break;
+ }
+ if (boundary) {
+ return boundary;
+ }
+
+ // The start/end of the Accessible might be a boundary. If so, we must stop
+ // on it.
+ includeOrigin = true;
+ }
+
+ MOZ_ASSERT(lastAcc);
+ // No further leaf was found. Use the start/end of the first/last leaf.
+ return TextLeafPoint(
+ lastAcc, aDirection == eDirPrevious
+ ? 0
+ : static_cast<int32_t>(nsAccUtils::TextLength(lastAcc)));
+}
+
+TextLeafPoint TextLeafPoint::FindLineEnd(nsDirection aDirection,
+ BoundaryFlags aFlags) const {
+ if (aDirection == eDirPrevious && IsEmptyLastLine()) {
+ // If we're at an empty line at the end of an Accessible, we don't want to
+ // walk into the previous line. For example, this can happen if the caret
+ // is positioned on an empty line at the end of a textarea.
+ // Because we want the line end, we must walk back to the line feed
+ // character.
+ return FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
+ aFlags & ~BoundaryFlags::eIncludeOrigin);
+ }
+ if ((aFlags & BoundaryFlags::eIncludeOrigin) && IsLineFeedChar()) {
+ return *this;
+ }
+ if (aDirection == eDirPrevious && !(aFlags & BoundaryFlags::eIncludeOrigin)) {
+ // If there is a line feed immediately before us, return that.
+ TextLeafPoint prevChar =
+ FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
+ aFlags & ~BoundaryFlags::eIncludeOrigin);
+ if (prevChar.IsLineFeedChar()) {
+ return prevChar;
+ }
+ }
+ TextLeafPoint searchFrom = *this;
+ if (aDirection == eDirNext && IsLineFeedChar()) {
+ // If we search for the next line start from a line feed, we'll get the
+ // character immediately following the line feed. We actually want the
+ // next line start after that. Skip the line feed.
+ searchFrom = FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext,
+ aFlags & ~BoundaryFlags::eIncludeOrigin);
+ }
+ TextLeafPoint lineStart = searchFrom.FindBoundary(
+ nsIAccessibleText::BOUNDARY_LINE_START, aDirection, aFlags);
+ if (aDirection == eDirNext && IsEmptyLastLine()) {
+ // There is a line feed immediately before us, but that's actually the end
+ // of the previous line, not the end of our empty line. Don't walk back.
+ return lineStart;
+ }
+ // If there is a line feed before this line start (at the end of the previous
+ // line), we must return that.
+ TextLeafPoint prevChar =
+ lineStart.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
+ aFlags & ~BoundaryFlags::eIncludeOrigin);
+ if (prevChar && prevChar.IsLineFeedChar()) {
+ return prevChar;
+ }
+ return lineStart;
+}
+
+bool TextLeafPoint::IsSpace() const {
+ return GetWordBreakClass(GetChar()) == eWbcSpace;
+}
+
+TextLeafPoint TextLeafPoint::FindWordEnd(nsDirection aDirection,
+ BoundaryFlags aFlags) const {
+ char16_t origChar = GetChar();
+ const bool origIsSpace = GetWordBreakClass(origChar) == eWbcSpace;
+ bool prevIsSpace = false;
+ if (aDirection == eDirPrevious ||
+ ((aFlags & BoundaryFlags::eIncludeOrigin) && origIsSpace) || !origChar) {
+ TextLeafPoint prev =
+ FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
+ aFlags & ~BoundaryFlags::eIncludeOrigin);
+ if (aDirection == eDirPrevious && prev == *this) {
+ return *this; // Can't go any further.
+ }
+ prevIsSpace = prev.IsSpace();
+ if ((aFlags & BoundaryFlags::eIncludeOrigin) &&
+ (origIsSpace || IsDocEdge(eDirNext)) && !prevIsSpace) {
+ // The origin is space or end of document, but the previous
+ // character is not. This means we're at the end of a word.
+ return *this;
+ }
+ }
+ TextLeafPoint boundary = *this;
+ if (aDirection == eDirPrevious && !prevIsSpace) {
+ // If there isn't space immediately before us, first find the start of the
+ // previous word.
+ boundary = FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START,
+ eDirPrevious, aFlags);
+ } else if (aDirection == eDirNext &&
+ (origIsSpace || (!origChar && prevIsSpace))) {
+ // We're within the space at the end of the word. Skip over the space. We
+ // can do that by searching for the next word start.
+ boundary = FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, eDirNext,
+ aFlags & ~BoundaryFlags::eIncludeOrigin);
+ if (boundary.IsSpace()) {
+ // The next word starts with a space. This can happen if there is a space
+ // after or at the start of a block element.
+ return boundary;
+ }
+ }
+ if (aDirection == eDirNext) {
+ BoundaryFlags flags = aFlags;
+ if (IsDocEdge(eDirPrevious)) {
+ // If this is the start of the doc don't be inclusive in the word-start
+ // search because there is no preceding block where this could be a
+ // word-end for.
+ flags &= ~BoundaryFlags::eIncludeOrigin;
+ }
+ boundary = boundary.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START,
+ eDirNext, flags);
+ }
+ // At this point, boundary is either the start of a word or at a space. A
+ // word ends at the beginning of consecutive space. Therefore, skip back to
+ // the start of any space before us.
+ TextLeafPoint prev = boundary;
+ for (;;) {
+ prev = prev.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
+ aFlags & ~BoundaryFlags::eIncludeOrigin);
+ if (prev == boundary) {
+ break; // Can't go any further.
+ }
+ if (!prev.IsSpace()) {
+ break;
+ }
+ boundary = prev;
+ }
+ return boundary;
+}
+
+TextLeafPoint TextLeafPoint::FindParagraphSameAcc(
+ nsDirection aDirection, bool aIncludeOrigin,
+ bool aIgnoreListItemMarker) const {
+ if (aIncludeOrigin && IsDocEdge(eDirPrevious)) {
+ // The top of the document is a paragraph boundary.
+ return *this;
+ }
+
+ if (aIgnoreListItemMarker && aIncludeOrigin && mOffset == 0 &&
+ IsLeafAfterListItemMarker()) {
+ // If we are in a list item and the previous sibling is
+ // a bullet, the 0 offset in this leaf is a line start.
+ return *this;
+ }
+
+ if (mAcc->IsTextLeaf() &&
+ // We don't want to copy strings unnecessarily. See below for the context
+ // of these individual conditions.
+ ((aIncludeOrigin && mOffset > 0) || aDirection == eDirNext ||
+ mOffset >= 2)) {
+ // If there is a line feed, a new paragraph begins after it.
+ nsAutoString text;
+ mAcc->AppendTextTo(text);
+ if (aIncludeOrigin && mOffset > 0 && text.CharAt(mOffset - 1) == '\n') {
+ return TextLeafPoint(mAcc, mOffset);
+ }
+ int32_t lfOffset = -1;
+ if (aDirection == eDirNext) {
+ lfOffset = text.FindChar('\n', mOffset);
+ } else if (mOffset >= 2) {
+ // A line feed at mOffset - 1 means the origin begins a new paragraph,
+ // but we already handled aIncludeOrigin above. Therefore, we search from
+ // mOffset - 2.
+ lfOffset = text.RFindChar('\n', mOffset - 2);
+ }
+ if (lfOffset != -1 && lfOffset + 1 < static_cast<int32_t>(text.Length())) {
+ return TextLeafPoint(mAcc, lfOffset + 1);
+ }
+ }
+
+ if (aIgnoreListItemMarker && mOffset > 0 && aDirection == eDirPrevious &&
+ IsLeafAfterListItemMarker()) {
+ // No line breaks were found in the preceding text to this offset.
+ // If we are in a list item and the previous sibling is
+ // a bullet, the 0 offset in this leaf is a line start.
+ return TextLeafPoint(mAcc, 0);
+ }
+
+ // Check whether this Accessible begins a paragraph.
+ if ((!aIncludeOrigin && mOffset == 0) ||
+ (aDirection == eDirNext && mOffset > 0)) {
+ // The caller isn't interested in whether this Accessible begins a
+ // paragraph.
+ return TextLeafPoint();
+ }
+ Accessible* prevLeaf = PrevLeaf(mAcc);
+ BlockRule blockRule;
+ Pivot pivot(nsAccUtils::DocumentFor(mAcc));
+ Accessible* prevBlock = pivot.Prev(mAcc, blockRule);
+ // Check if we're the first leaf after a block element.
+ if (prevBlock) {
+ if (
+ // If there's no previous leaf, we must be the first leaf after the
+ // block.
+ !prevLeaf ||
+ // A block can be a leaf; e.g. an empty div or paragraph.
+ prevBlock == prevLeaf) {
+ return TextLeafPoint(mAcc, 0);
+ }
+ if (prevBlock->IsAncestorOf(mAcc)) {
+ // We're inside the block.
+ if (!prevBlock->IsAncestorOf(prevLeaf)) {
+ // The previous leaf isn't inside the block. That means we're the first
+ // leaf in the block.
+ return TextLeafPoint(mAcc, 0);
+ }
+ } else {
+ // We aren't inside the block, so the block ends before us.
+ if (prevBlock->IsAncestorOf(prevLeaf)) {
+ // The previous leaf is inside the block. That means we're the first
+ // leaf after the block. This case is necessary because a block causes a
+ // paragraph break both before and after it.
+ return TextLeafPoint(mAcc, 0);
+ }
+ }
+ }
+ if (!prevLeaf || prevLeaf->IsHTMLBr()) {
+ // We're the first leaf after a line break or the start of the document.
+ return TextLeafPoint(mAcc, 0);
+ }
+ if (prevLeaf->IsTextLeaf()) {
+ // There's a text leaf before us. Check if it ends with a line feed.
+ nsAutoString text;
+ prevLeaf->AppendTextTo(text, nsAccUtils::TextLength(prevLeaf) - 1, 1);
+ if (text.CharAt(0) == '\n') {
+ return TextLeafPoint(mAcc, 0);
+ }
+ }
+ return TextLeafPoint();
+}
+
+bool TextLeafPoint::IsInSpellingError() const {
+ if (LocalAccessible* acc = mAcc->AsLocal()) {
+ auto domRanges = FindDOMSpellingErrors(acc, mOffset, mOffset + 1);
+ // If there is a spelling error overlapping this character, we're in a
+ // spelling error.
+ return !domRanges.IsEmpty();
+ }
+
+ RemoteAccessible* acc = mAcc->AsRemote();
+ MOZ_ASSERT(acc);
+ if (!acc->mCachedFields) {
+ return false;
+ }
+ auto spellingErrors = acc->mCachedFields->GetAttribute<nsTArray<int32_t>>(
+ CacheKey::SpellingErrors);
+ if (!spellingErrors) {
+ return false;
+ }
+ size_t index;
+ const bool foundOrigin = BinarySearch(
+ *spellingErrors, 0, spellingErrors->Length(), mOffset, &index);
+ // In spellingErrors, even indices are start offsets, odd indices are end
+ // offsets.
+ const bool foundStart = index % 2 == 0;
+ if (foundOrigin) {
+ // mOffset is a spelling error boundary. If it's a start offset, we're in a
+ // spelling error.
+ return foundStart;
+ }
+ // index points at the next spelling error boundary after mOffset.
+ if (index == 0) {
+ return false; // No spelling errors before mOffset.
+ }
+ if (foundStart) {
+ // We're not in a spelling error because it starts after mOffset.
+ return false;
+ }
+ // A spelling error ends after mOffset.
+ return true;
+}
+
+TextLeafPoint TextLeafPoint::FindSpellingErrorSameAcc(
+ nsDirection aDirection, bool aIncludeOrigin) const {
+ if (!aIncludeOrigin && mOffset == 0 && aDirection == eDirPrevious) {
+ return TextLeafPoint();
+ }
+ if (LocalAccessible* acc = mAcc->AsLocal()) {
+ // We want to find both start and end points, so we pass true for
+ // aAllowAdjacent.
+ auto domRanges =
+ aDirection == eDirNext
+ ? FindDOMSpellingErrors(acc, mOffset,
+ nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT,
+ /* aAllowAdjacent */ true)
+ : FindDOMSpellingErrors(acc, 0, mOffset,
+ /* aAllowAdjacent */ true);
+ nsINode* node = acc->GetNode();
+ if (aDirection == eDirNext) {
+ for (nsRange* domRange : domRanges) {
+ if (domRange->GetStartContainer() == node) {
+ int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset(
+ acc, static_cast<int32_t>(domRange->StartOffset())));
+ if ((aIncludeOrigin && matchOffset == mOffset) ||
+ matchOffset > mOffset) {
+ return TextLeafPoint(mAcc, matchOffset);
+ }
+ }
+ if (domRange->GetEndContainer() == node) {
+ int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset(
+ acc, static_cast<int32_t>(domRange->EndOffset())));
+ if ((aIncludeOrigin && matchOffset == mOffset) ||
+ matchOffset > mOffset) {
+ return TextLeafPoint(mAcc, matchOffset);
+ }
+ }
+ }
+ } else {
+ for (nsRange* domRange : Reversed(domRanges)) {
+ if (domRange->GetEndContainer() == node) {
+ int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset(
+ acc, static_cast<int32_t>(domRange->EndOffset())));
+ if ((aIncludeOrigin && matchOffset == mOffset) ||
+ matchOffset < mOffset) {
+ return TextLeafPoint(mAcc, matchOffset);
+ }
+ }
+ if (domRange->GetStartContainer() == node) {
+ int32_t matchOffset = static_cast<int32_t>(ContentToRenderedOffset(
+ acc, static_cast<int32_t>(domRange->StartOffset())));
+ if ((aIncludeOrigin && matchOffset == mOffset) ||
+ matchOffset < mOffset) {
+ return TextLeafPoint(mAcc, matchOffset);
+ }
+ }
+ }
+ }
+ return TextLeafPoint();
+ }
+
+ RemoteAccessible* acc = mAcc->AsRemote();
+ MOZ_ASSERT(acc);
+ if (!acc->mCachedFields) {
+ return TextLeafPoint();
+ }
+ auto spellingErrors = acc->mCachedFields->GetAttribute<nsTArray<int32_t>>(
+ CacheKey::SpellingErrors);
+ if (!spellingErrors) {
+ return TextLeafPoint();
+ }
+ size_t index;
+ if (BinarySearch(*spellingErrors, 0, spellingErrors->Length(), mOffset,
+ &index)) {
+ // mOffset is in spellingErrors.
+ if (aIncludeOrigin) {
+ return *this;
+ }
+ if (aDirection == eDirNext) {
+ // We don't want the origin, so move to the next spelling error boundary
+ // after mOffset.
+ ++index;
+ }
+ }
+ // index points at the next spelling error boundary after mOffset.
+ if (aDirection == eDirNext) {
+ if (spellingErrors->Length() == index) {
+ return TextLeafPoint(); // No spelling error boundary after us.
+ }
+ return TextLeafPoint(mAcc, (*spellingErrors)[index]);
+ }
+ if (index == 0) {
+ return TextLeafPoint(); // No spelling error boundary before us.
+ }
+ // Decrement index so it points at a spelling error boundary before mOffset.
+ --index;
+ if ((*spellingErrors)[index] == -1) {
+ MOZ_ASSERT(index == 0);
+ // A spelling error starts before mAcc.
+ return TextLeafPoint();
+ }
+ return TextLeafPoint(mAcc, (*spellingErrors)[index]);
+}
+
+TextLeafPoint TextLeafPoint::NeighborLeafPoint(
+ nsDirection aDirection, bool aIsEditable,
+ bool aIgnoreListItemMarker) const {
+ Accessible* acc = aDirection == eDirPrevious
+ ? PrevLeaf(mAcc, aIsEditable, aIgnoreListItemMarker)
+ : NextLeaf(mAcc, aIsEditable, aIgnoreListItemMarker);
+ if (!acc) {
+ return TextLeafPoint();
+ }
+
+ return TextLeafPoint(
+ acc, aDirection == eDirPrevious
+ ? static_cast<int32_t>(nsAccUtils::TextLength(acc)) - 1
+ : 0);
+}
+
+LayoutDeviceIntRect TextLeafPoint::ComputeBoundsFromFrame() const {
+ LocalAccessible* local = mAcc->AsLocal();
+ MOZ_ASSERT(local, "Can't compute bounds in frame from non-local acc");
+ nsIFrame* frame = local->GetFrame();
+ MOZ_ASSERT(frame, "No frame found for acc!");
+
+ if (!frame || !frame->IsTextFrame()) {
+ return local->Bounds();
+ }
+
+ // Substring must be entirely within the same text node.
+ MOZ_ASSERT(frame->IsPrimaryFrame(),
+ "Cannot compute content offset on non-primary frame");
+ nsIFrame::RenderedText text = frame->GetRenderedText(
+ mOffset, mOffset + 1, nsIFrame::TextOffsetType::OffsetsInRenderedText,
+ nsIFrame::TrailingWhitespace::DontTrim);
+ int32_t contentOffset = text.mOffsetWithinNodeText;
+ int32_t contentOffsetInFrame;
+ // Get the right frame continuation -- not really a child, but a sibling of
+ // the primary frame passed in
+ nsresult rv = frame->GetChildFrameContainingOffset(
+ contentOffset, true, &contentOffsetInFrame, &frame);
+ NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
+
+ // Start with this frame's screen rect, which we will shrink based on
+ // the char we care about within it.
+ nsRect frameScreenRect = frame->GetScreenRectInAppUnits();
+
+ // Add the point where the char starts to the frameScreenRect
+ nsPoint frameTextStartPoint;
+ rv = frame->GetPointFromOffset(contentOffset, &frameTextStartPoint);
+ NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
+
+ // Use the next offset to calculate the width
+ // XXX(morgan) does this work for vertical text?
+ nsPoint frameTextEndPoint;
+ rv = frame->GetPointFromOffset(contentOffset + 1, &frameTextEndPoint);
+ NS_ENSURE_SUCCESS(rv, LayoutDeviceIntRect());
+
+ frameScreenRect.SetRectX(
+ frameScreenRect.X() +
+ std::min(frameTextStartPoint.x, frameTextEndPoint.x),
+ mozilla::Abs(frameTextStartPoint.x - frameTextEndPoint.x));
+
+ nsPresContext* presContext = local->Document()->PresContext();
+ return LayoutDeviceIntRect::FromAppUnitsToNearest(
+ frameScreenRect, presContext->AppUnitsPerDevPixel());
+}
+
+/* static */
+nsTArray<int32_t> TextLeafPoint::GetSpellingErrorOffsets(
+ LocalAccessible* aAcc) {
+ nsINode* node = aAcc->GetNode();
+ auto domRanges = FindDOMSpellingErrors(
+ aAcc, 0, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
+ // Our offsets array will contain two offsets for each range: one for the
+ // start, one for the end. That is, the array is of the form:
+ // [r1start, r1end, r2start, r2end, ...]
+ nsTArray<int32_t> offsets(domRanges.Length() * 2);
+ for (nsRange* domRange : domRanges) {
+ if (domRange->GetStartContainer() == node) {
+ offsets.AppendElement(static_cast<int32_t>(ContentToRenderedOffset(
+ aAcc, static_cast<int32_t>(domRange->StartOffset()))));
+ } else {
+ // This range overlaps aAcc, but starts before it.
+ // This can only happen for the first range.
+ MOZ_ASSERT(domRange == *domRanges.begin() && offsets.IsEmpty());
+ // Using -1 here means this won't be treated as the start of a spelling
+ // error range, while still indicating that we're within a spelling error.
+ offsets.AppendElement(-1);
+ }
+ if (domRange->GetEndContainer() == node) {
+ offsets.AppendElement(static_cast<int32_t>(ContentToRenderedOffset(
+ aAcc, static_cast<int32_t>(domRange->EndOffset()))));
+ } else {
+ // This range overlaps aAcc, but ends after it.
+ // This can only happen for the last range.
+ MOZ_ASSERT(domRange == *domRanges.rbegin());
+ // We don't append -1 here because this would just make things harder for
+ // a binary search.
+ }
+ }
+ return offsets;
+}
+
+/* static */
+void TextLeafPoint::UpdateCachedSpellingError(dom::Document* aDocument,
+ const nsRange& aRange) {
+ DocAccessible* docAcc = GetExistingDocAccessible(aDocument);
+ if (!docAcc) {
+ return;
+ }
+ LocalAccessible* startAcc = docAcc->GetAccessible(aRange.GetStartContainer());
+ LocalAccessible* endAcc = docAcc->GetAccessible(aRange.GetEndContainer());
+ if (!startAcc || !endAcc) {
+ return;
+ }
+ for (Accessible* acc = startAcc; acc; acc = NextLeaf(acc)) {
+ if (acc->IsTextLeaf()) {
+ docAcc->QueueCacheUpdate(acc->AsLocal(), CacheDomain::Spelling);
+ }
+ if (acc == endAcc) {
+ // Subtle: We check this here rather than in the loop condition because
+ // we want to include endAcc but stop once we reach it. Putting it in the
+ // loop condition would mean we stop at endAcc, but we would also exclude
+ // it; i.e. we wouldn't push the cache for it.
+ break;
+ }
+ }
+}
+
+already_AddRefed<AccAttributes> TextLeafPoint::GetTextAttributesLocalAcc(
+ bool aIncludeDefaults) const {
+ LocalAccessible* acc = mAcc->AsLocal();
+ MOZ_ASSERT(acc);
+ MOZ_ASSERT(acc->IsText());
+ // TextAttrsMgr wants a HyperTextAccessible.
+ LocalAccessible* parent = acc->LocalParent();
+ HyperTextAccessible* hyperAcc = parent->AsHyperText();
+ MOZ_ASSERT(hyperAcc);
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+ if (hyperAcc) {
+ TextAttrsMgr mgr(hyperAcc, aIncludeDefaults, acc,
+ acc ? acc->IndexInParent() : -1);
+ mgr.GetAttributes(attributes, nullptr, nullptr);
+ }
+ return attributes.forget();
+}
+
+already_AddRefed<AccAttributes> TextLeafPoint::GetTextAttributes(
+ bool aIncludeDefaults) const {
+ if (!mAcc->IsText()) {
+ return nullptr;
+ }
+ RefPtr<AccAttributes> attrs;
+ if (mAcc->IsLocal()) {
+ attrs = GetTextAttributesLocalAcc(aIncludeDefaults);
+ } else {
+ attrs = new AccAttributes();
+ if (aIncludeDefaults) {
+ Accessible* parent = mAcc->Parent();
+ if (parent && parent->IsRemote() && parent->IsHyperText()) {
+ if (auto defAttrs = parent->AsRemote()->GetCachedTextAttributes()) {
+ defAttrs->CopyTo(attrs);
+ }
+ }
+ }
+ if (auto thisAttrs = mAcc->AsRemote()->GetCachedTextAttributes()) {
+ thisAttrs->CopyTo(attrs);
+ }
+ }
+ if (IsInSpellingError()) {
+ attrs->SetAttribute(nsGkAtoms::invalid, nsGkAtoms::spelling);
+ }
+ return attrs.forget();
+}
+
+TextLeafPoint TextLeafPoint::FindTextAttrsStart(nsDirection aDirection,
+ bool aIncludeOrigin) const {
+ if (IsCaret()) {
+ return ActualizeCaret().FindTextAttrsStart(aDirection, aIncludeOrigin);
+ }
+ const bool isRemote = mAcc->IsRemote();
+ RefPtr<const AccAttributes> lastAttrs =
+ isRemote ? mAcc->AsRemote()->GetCachedTextAttributes()
+ : GetTextAttributesLocalAcc();
+ if (aIncludeOrigin && aDirection == eDirNext && mOffset == 0) {
+ // Even when searching forward, the only way to know whether the origin is
+ // the start of a text attrs run is to compare with the previous sibling.
+ // Anything other than text breaks an attrs run.
+ TextLeafPoint point;
+ point.mAcc = mAcc->PrevSibling();
+ if (!point.mAcc || !point.mAcc->IsText()) {
+ return *this;
+ }
+ // For RemoteAccessible, we can get attributes from the cache without any
+ // calculation or copying.
+ RefPtr<const AccAttributes> attrs =
+ isRemote ? point.mAcc->AsRemote()->GetCachedTextAttributes()
+ : point.GetTextAttributesLocalAcc();
+ if (attrs && lastAttrs && !attrs->Equal(lastAttrs)) {
+ return *this;
+ }
+ }
+ TextLeafPoint lastPoint = *this;
+ for (;;) {
+ if (TextLeafPoint spelling = lastPoint.FindSpellingErrorSameAcc(
+ aDirection, aIncludeOrigin && lastPoint.mAcc == mAcc)) {
+ // A spelling error starts or ends somewhere in the Accessible we're
+ // considering. This causes an attribute change, so return that point.
+ return spelling;
+ }
+ TextLeafPoint point;
+ point.mAcc = aDirection == eDirNext ? lastPoint.mAcc->NextSibling()
+ : lastPoint.mAcc->PrevSibling();
+ if (!point.mAcc || !point.mAcc->IsText()) {
+ break;
+ }
+ RefPtr<const AccAttributes> attrs =
+ isRemote ? point.mAcc->AsRemote()->GetCachedTextAttributes()
+ : point.GetTextAttributesLocalAcc();
+ if (attrs && lastAttrs && !attrs->Equal(lastAttrs)) {
+ // The attributes change here. If we're moving forward, we want to
+ // return this point. If we're moving backward, we've now moved before
+ // the start of the attrs run containing the origin, so return that start
+ // point; i.e. the start of the last Accessible we hit.
+ if (aDirection == eDirPrevious) {
+ point = lastPoint;
+ point.mOffset = 0;
+ }
+ if (!aIncludeOrigin && point == *this) {
+ MOZ_ASSERT(aDirection == eDirPrevious);
+ // The origin is the start of an attrs run, but the caller doesn't want
+ // the origin included.
+ continue;
+ }
+ return point;
+ }
+ lastPoint = point;
+ if (aDirection == eDirPrevious) {
+ // On the next iteration, we want to search for spelling errors from the
+ // end of this Accessible.
+ lastPoint.mOffset =
+ static_cast<int32_t>(nsAccUtils::TextLength(point.mAcc));
+ }
+ lastAttrs = attrs;
+ }
+ // We couldn't move any further. Use the start/end.
+ return TextLeafPoint(
+ lastPoint.mAcc,
+ aDirection == eDirPrevious
+ ? 0
+ : static_cast<int32_t>(nsAccUtils::TextLength(lastPoint.mAcc)));
+}
+
+LayoutDeviceIntRect TextLeafPoint::CharBounds() {
+ if (mAcc && !mAcc->IsText()) {
+ // If we're dealing with an empty container, return the
+ // accessible's non-text bounds.
+ return mAcc->Bounds();
+ }
+
+ if (!mAcc || (mAcc->IsRemote() && !mAcc->AsRemote()->mCachedFields)) {
+ return LayoutDeviceIntRect();
+ }
+
+ if (LocalAccessible* local = mAcc->AsLocal()) {
+ if (!local->IsTextLeaf() || nsAccUtils::TextLength(local) == 0) {
+ // Empty content, use our own bounds to at least get x,y coordinates
+ return local->Bounds();
+ }
+
+ if (mOffset >= 0 &&
+ static_cast<uint32_t>(mOffset) >= nsAccUtils::TextLength(local)) {
+ // It's valid for a caller to query the length because the caret might be
+ // at the end of editable text. In that case, we should just silently
+ // return. However, we assert that the offset isn't greater than the
+ // length.
+ NS_ASSERTION(
+ static_cast<uint32_t>(mOffset) <= nsAccUtils::TextLength(local),
+ "Wrong in offset");
+ return LayoutDeviceIntRect();
+ }
+
+ LayoutDeviceIntRect bounds = ComputeBoundsFromFrame();
+
+ // This document may have a resolution set, we will need to multiply
+ // the document-relative coordinates by that value and re-apply the doc's
+ // screen coordinates.
+ nsPresContext* presContext = local->Document()->PresContext();
+ nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame();
+ LayoutDeviceIntRect orgRectPixels =
+ LayoutDeviceIntRect::FromAppUnitsToNearest(
+ rootFrame->GetScreenRectInAppUnits(),
+ presContext->AppUnitsPerDevPixel());
+ bounds.MoveBy(-orgRectPixels.X(), -orgRectPixels.Y());
+ bounds.ScaleRoundOut(presContext->PresShell()->GetResolution());
+ bounds.MoveBy(orgRectPixels.X(), orgRectPixels.Y());
+ return bounds;
+ }
+
+ RemoteAccessible* remote = mAcc->AsRemote();
+ nsRect charBounds = remote->GetCachedCharRect(mOffset);
+ if (!charBounds.IsEmpty()) {
+ return remote->BoundsWithOffset(Some(charBounds));
+ }
+
+ return LayoutDeviceIntRect();
+}
+
+bool TextLeafPoint::ContainsPoint(int32_t aX, int32_t aY) {
+ if (mAcc && !mAcc->IsText()) {
+ // If we're dealing with an empty embedded object, use the
+ // accessible's non-text bounds.
+ return mAcc->Bounds().Contains(aX, aY);
+ }
+
+ return CharBounds().Contains(aX, aY);
+}
+
+bool TextLeafRange::Crop(Accessible* aContainer) {
+ TextLeafPoint containerStart(aContainer, 0);
+ TextLeafPoint containerEnd(aContainer,
+ nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
+
+ if (mEnd < containerStart || containerEnd < mStart) {
+ // The range ends before the container, or starts after it.
+ return false;
+ }
+
+ if (mStart < containerStart) {
+ // If range start is before container start, adjust range start to
+ // start of container.
+ mStart = containerStart;
+ }
+
+ if (containerEnd < mEnd) {
+ // If range end is after container end, adjust range end to end of
+ // container.
+ mEnd = containerEnd;
+ }
+
+ return true;
+}
+
+LayoutDeviceIntRect TextLeafRange::Bounds() const {
+ if (mEnd == mStart || mEnd < mStart) {
+ return LayoutDeviceIntRect();
+ }
+
+ bool locatedFinalLine = false;
+ TextLeafPoint currPoint = mStart;
+ LayoutDeviceIntRect result = currPoint.CharBounds();
+
+ // Union the first and last chars of each line to create a line rect. Then,
+ // union the lines together.
+ while (!locatedFinalLine) {
+ // Fetch the last point in the current line by getting the
+ // start of the next line and going back one char. We don't
+ // use BOUNDARY_LINE_END here because it is equivalent to LINE_START when
+ // the line doesn't end with a line feed character.
+ TextLeafPoint lineStartPoint = currPoint.FindBoundary(
+ nsIAccessibleText::BOUNDARY_LINE_START, eDirNext);
+ TextLeafPoint lastPointInLine = lineStartPoint.FindBoundary(
+ nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
+ // If currPoint is the end of the document, lineStartPoint will be equal
+ // to currPoint and we would be in an endless loop.
+ if (lineStartPoint == currPoint || mEnd <= lastPointInLine) {
+ lastPointInLine = mEnd;
+ locatedFinalLine = true;
+ }
+
+ LayoutDeviceIntRect currLine = currPoint.CharBounds();
+ currLine.UnionRect(currLine, lastPointInLine.CharBounds());
+ result.UnionRect(result, currLine);
+
+ currPoint = lineStartPoint;
+ }
+
+ return result;
+}
+
+bool TextLeafRange::SetSelection(int32_t aSelectionNum) const {
+ if (!mStart || !mEnd || mStart.mAcc->IsLocal() != mEnd.mAcc->IsLocal()) {
+ return false;
+ }
+
+ if (mStart.mAcc->IsRemote()) {
+ DocAccessibleParent* doc = mStart.mAcc->AsRemote()->Document();
+ if (doc != mEnd.mAcc->AsRemote()->Document()) {
+ return false;
+ }
+
+ Unused << doc->SendSetTextSelection(mStart.mAcc->ID(), mStart.mOffset,
+ mEnd.mAcc->ID(), mEnd.mOffset,
+ aSelectionNum);
+ return true;
+ }
+
+ bool reversed = mEnd < mStart;
+ auto [startContent, startContentOffset] =
+ !reversed ? mStart.ToDOMPoint(false) : mEnd.ToDOMPoint(false);
+ auto [endContent, endContentOffset] =
+ !reversed ? mEnd.ToDOMPoint(false) : mStart.ToDOMPoint(false);
+
+ if (!startContent || !endContent) {
+ return false;
+ }
+
+ RefPtr<dom::Selection> domSel = GetDOMSelection(startContent, endContent);
+ if (!domSel) {
+ return false;
+ }
+
+ uint32_t rangeCount = domSel->RangeCount();
+ RefPtr<nsRange> domRange = nullptr;
+ if (aSelectionNum == static_cast<int32_t>(rangeCount) || aSelectionNum < 0) {
+ domRange = nsRange::Create(startContent);
+ } else {
+ domRange = domSel->GetRangeAt(AssertedCast<uint32_t>(aSelectionNum));
+ }
+ if (!domRange) {
+ return false;
+ }
+
+ domRange->SetStart(startContent, startContentOffset);
+ domRange->SetEnd(endContent, endContentOffset);
+
+ // If this is not a new range, notify selection listeners that the existing
+ // selection range has changed. Otherwise, just add the new range.
+ if (aSelectionNum != static_cast<int32_t>(rangeCount)) {
+ domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*domRange,
+ IgnoreErrors());
+ }
+
+ IgnoredErrorResult err;
+ domSel->AddRangeAndSelectFramesAndNotifyListeners(*domRange, err);
+ if (!err.Failed()) {
+ // Changing the direction of the selection assures that the caret
+ // will be at the logical end of the selection.
+ domSel->SetDirection(reversed ? eDirPrevious : eDirNext);
+ return true;
+ }
+
+ return false;
+}
+
+void TextLeafRange::ScrollIntoView(uint32_t aScrollType) const {
+ if (!mStart || !mEnd || mStart.mAcc->IsLocal() != mEnd.mAcc->IsLocal()) {
+ return;
+ }
+
+ if (mStart.mAcc->IsRemote()) {
+ DocAccessibleParent* doc = mStart.mAcc->AsRemote()->Document();
+ if (doc != mEnd.mAcc->AsRemote()->Document()) {
+ // Can't scroll range that spans docs.
+ return;
+ }
+
+ Unused << doc->SendScrollTextLeafRangeIntoView(
+ mStart.mAcc->ID(), mStart.mOffset, mEnd.mAcc->ID(), mEnd.mOffset,
+ aScrollType);
+ return;
+ }
+
+ auto [startContent, startContentOffset] = mStart.ToDOMPoint();
+ auto [endContent, endContentOffset] = mEnd.ToDOMPoint();
+
+ if (!startContent || !endContent) {
+ return;
+ }
+
+ ErrorResult er;
+ RefPtr<nsRange> domRange = nsRange::Create(startContent, startContentOffset,
+ endContent, endContentOffset, er);
+ if (er.Failed()) {
+ return;
+ }
+
+ nsCoreUtils::ScrollSubstringTo(mStart.mAcc->AsLocal()->GetFrame(), domRange,
+ aScrollType);
+}
+
+TextLeafRange::Iterator TextLeafRange::Iterator::BeginIterator(
+ const TextLeafRange& aRange) {
+ Iterator result(aRange);
+
+ result.mSegmentStart = aRange.mStart;
+ if (aRange.mStart.mAcc == aRange.mEnd.mAcc) {
+ result.mSegmentEnd = aRange.mEnd;
+ } else {
+ result.mSegmentEnd = TextLeafPoint(
+ aRange.mStart.mAcc, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
+ }
+
+ return result;
+}
+
+TextLeafRange::Iterator TextLeafRange::Iterator::EndIterator(
+ const TextLeafRange& aRange) {
+ Iterator result(aRange);
+
+ result.mSegmentEnd = TextLeafPoint();
+ result.mSegmentStart = TextLeafPoint();
+
+ return result;
+}
+
+TextLeafRange::Iterator& TextLeafRange::Iterator::operator++() {
+ if (mSegmentEnd.mAcc == mRange.mEnd.mAcc) {
+ mSegmentEnd = TextLeafPoint();
+ mSegmentStart = TextLeafPoint();
+ return *this;
+ }
+
+ if (Accessible* nextLeaf = NextLeaf(mSegmentEnd.mAcc)) {
+ mSegmentStart = TextLeafPoint(nextLeaf, 0);
+ if (nextLeaf == mRange.mEnd.mAcc) {
+ mSegmentEnd = TextLeafPoint(nextLeaf, mRange.mEnd.mOffset);
+ } else {
+ mSegmentEnd =
+ TextLeafPoint(nextLeaf, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
+ }
+ } else {
+ mSegmentEnd = TextLeafPoint();
+ mSegmentStart = TextLeafPoint();
+ }
+
+ return *this;
+}
+
+} // namespace mozilla::a11y
diff --git a/accessible/base/TextLeafRange.h b/accessible/base/TextLeafRange.h
new file mode 100644
index 0000000000..23fea2ecfb
--- /dev/null
+++ b/accessible/base/TextLeafRange.h
@@ -0,0 +1,360 @@
+/* -*- 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_TextLeafRange_h__
+#define mozilla_a11y_TextLeafRange_h__
+
+#include <stdint.h>
+
+#include "AccAttributes.h"
+#include "nsDirection.h"
+#include "nsIAccessibleText.h"
+
+class nsRange;
+
+namespace mozilla {
+namespace dom {
+class Document;
+}
+
+namespace a11y {
+class Accessible;
+class LocalAccessible;
+
+/**
+ * Represents a point within accessible text.
+ * This is stored as a leaf Accessible and an offset into that Accessible.
+ * For an empty Accessible, the offset will always be 0.
+ * This will eventually replace TextPoint. Unlike TextPoint, this does not
+ * use HyperTextAccessible offsets.
+ */
+class TextLeafPoint final {
+ public:
+ TextLeafPoint(Accessible* aAcc, int32_t aOffset);
+
+ /**
+ * Constructs an invalid TextPoint (mAcc is null).
+ * A TextLeafPoint in this state will evaluate to false.
+ * mAcc can be set later. Alternatively, this can be used to indicate an error
+ * (e.g. if a requested point couldn't be found).
+ */
+ TextLeafPoint() : mAcc(nullptr), mOffset(0) {}
+
+ /**
+ * Construct a TextLeafPoint representing the caret.
+ * The actual offset used for the caret differs depending on whether the
+ * caret is at the end of a line and the query being made. Thus, mOffset on
+ * the returned TextLeafPoint is not a valid offset.
+ */
+ static TextLeafPoint GetCaret(Accessible* aAcc) {
+ return TextLeafPoint(aAcc, nsIAccessibleText::TEXT_OFFSET_CARET);
+ }
+
+ Accessible* mAcc;
+ int32_t mOffset;
+
+ bool operator==(const TextLeafPoint& aPoint) const {
+ return mAcc == aPoint.mAcc && mOffset == aPoint.mOffset;
+ }
+
+ bool operator!=(const TextLeafPoint& aPoint) const {
+ return !(*this == aPoint);
+ }
+
+ bool operator<(const TextLeafPoint& aPoint) const;
+
+ bool operator<=(const TextLeafPoint& aPoint) const;
+
+ /**
+ * A valid TextLeafPoint evaluates to true. An invalid TextLeafPoint
+ * evaluates to false.
+ */
+ explicit operator bool() const { return !!mAcc; }
+
+ bool IsCaret() const {
+ return mOffset == nsIAccessibleText::TEXT_OFFSET_CARET;
+ }
+
+ bool IsCaretAtEndOfLine() const;
+
+ /**
+ * Get a TextLeafPoint at the actual caret offset.
+ * This should only be called on a TextLeafPoint created with GetCaret.
+ * If aAdjustAtEndOfLine is true, the point will be adjusted if the caret is
+ * at the end of a line so that word and line boundaries can be calculated
+ * correctly.
+ */
+ TextLeafPoint ActualizeCaret(bool aAdjustAtEndOfLine = true) const;
+
+ enum class BoundaryFlags : uint32_t {
+ eDefaultBoundaryFlags = 0,
+ // Return point unchanged if it is at the given boundary type.
+ eIncludeOrigin = 1 << 0,
+ // If current point is in editable, return point within samme editable.
+ eStopInEditable = 1 << 1,
+ // Skip over list items in searches and don't consider them line or
+ // paragraph starts.
+ eIgnoreListItemMarker = 1 << 2,
+ };
+
+ /**
+ * Find a boundary (word start, line start, etc.) in a specific direction.
+ * If no boundary is found, the start/end of the document is returned
+ * (depending on the direction).
+ */
+ TextLeafPoint FindBoundary(
+ AccessibleTextBoundary aBoundaryType, nsDirection aDirection,
+ BoundaryFlags aFlags = BoundaryFlags::eDefaultBoundaryFlags) const;
+
+ /**
+ * These two functions find a line start boundary within the same
+ * LocalAccessible as this. That is, they do not cross Accessibles. If no
+ * boundary is found, an invalid TextLeafPoint is returned.
+ * These are used by FindBoundary. Most callers will want FindBoundary
+ * instead.
+ */
+ TextLeafPoint FindPrevLineStartSameLocalAcc(bool aIncludeOrigin) const;
+ TextLeafPoint FindNextLineStartSameLocalAcc(bool aIncludeOrigin) const;
+
+ /**
+ * These two functions find a word start boundary within the same
+ * Accessible as this. That is, they do not cross Accessibles. If no
+ * boundary is found, an invalid TextLeafPoint is returned.
+ * These are used by FindBoundary. Most callers will want FindBoundary
+ * instead.
+ */
+ TextLeafPoint FindPrevWordStartSameAcc(bool aIncludeOrigin) const;
+ TextLeafPoint FindNextWordStartSameAcc(bool aIncludeOrigin) const;
+
+ /**
+ * Get the text attributes at this point.
+ * If aIncludeDefaults is true, default attributes on the HyperTextAccessible
+ * will be included.
+ */
+ already_AddRefed<AccAttributes> GetTextAttributes(
+ bool aIncludeDefaults = true) const;
+
+ /**
+ * Get Get the text attributes at this point in a LocalAccessible.
+ * This is used by GetTextAttributes. Most callers will want GetTextAttributes
+ * instead.
+ */
+ already_AddRefed<AccAttributes> GetTextAttributesLocalAcc(
+ bool aIncludeDefaults = true) const;
+
+ /**
+ * Get the offsets of all spelling errors in a given LocalAccessible. This
+ * should only be used when pushing the cache. Most callers will want
+ * FindTextAttrsStart instead.
+ */
+ static nsTArray<int32_t> GetSpellingErrorOffsets(LocalAccessible* aAcc);
+
+ /**
+ * Queue a cache update for a spelling error in a given DOM range.
+ */
+ static void UpdateCachedSpellingError(dom::Document* aDocument,
+ const nsRange& aRange);
+
+ /**
+ * Find the start of a run of text attributes in a specific direction.
+ * A text attributes run is a span of text where the attributes are the same.
+ * If no boundary is found, the start/end of the container is returned
+ * (depending on the direction).
+ * If aIncludeorigin is true and this is at a boundary, this will be
+ * returned unchanged.
+ */
+ TextLeafPoint FindTextAttrsStart(nsDirection aDirection,
+ bool aIncludeOrigin = false) const;
+
+ /**
+ * Returns a rect (in dev pixels) describing position and size of
+ * the character at mOffset in mAcc. This rect is screen-relative.
+ * This function only works on remote accessibles, and assumes caching
+ * is enabled.
+ */
+ LayoutDeviceIntRect CharBounds();
+
+ /**
+ * Returns true if the given point (in screen coords) is contained
+ * in the char bounds of the current TextLeafPoint. Returns false otherwise.
+ * If the current point is an empty container, we use the acc's bounds instead
+ * of char bounds. Because this depends on CharBounds, this function only
+ * works on remote accessibles, and assumes caching is enabled.
+ */
+ bool ContainsPoint(int32_t aX, int32_t aY);
+
+ bool IsLineFeedChar() const { return GetChar() == '\n'; }
+
+ bool IsSpace() const;
+
+ bool IsParagraphStart(bool aIgnoreListItemMarker = false) const {
+ return mOffset == 0 &&
+ FindParagraphSameAcc(eDirPrevious, true, aIgnoreListItemMarker);
+ }
+
+ /**
+ * Translate given TextLeafPoint into a DOM point.
+ */
+ MOZ_CAN_RUN_SCRIPT std::pair<nsIContent*, int32_t> ToDOMPoint(
+ bool aIncludeGenerated = true) const;
+
+ private:
+ bool IsEmptyLastLine() const;
+
+ bool IsDocEdge(nsDirection aDirection) const;
+
+ bool IsLeafAfterListItemMarker() const;
+
+ char16_t GetChar() const;
+
+ TextLeafPoint FindLineStartSameRemoteAcc(nsDirection aDirection,
+ bool aIncludeOrigin) const;
+
+ /**
+ * Helper which just calls the appropriate function based on whether mAcc
+ *is local or remote.
+ */
+ TextLeafPoint FindLineStartSameAcc(nsDirection aDirection,
+ bool aIncludeOrigin,
+ bool aIgnoreListItemMarker = false) const;
+
+ TextLeafPoint FindLineEnd(nsDirection aDirection, BoundaryFlags aFlags) const;
+ TextLeafPoint FindWordEnd(nsDirection aDirection, BoundaryFlags aFlags) const;
+
+ TextLeafPoint FindParagraphSameAcc(nsDirection aDirection,
+ bool aIncludeOrigin,
+ bool aIgnoreListItemMarker = false) const;
+
+ bool IsInSpellingError() const;
+
+ /**
+ * Find a spelling error boundary in the same Accessible. This function
+ * searches for either start or end points, since either means a change in
+ * text attributes.
+ */
+ TextLeafPoint FindSpellingErrorSameAcc(nsDirection aDirection,
+ bool aIncludeOrigin) const;
+
+ // Return the point immediately succeeding or preceding this leaf depending
+ // on given direction.
+ TextLeafPoint NeighborLeafPoint(nsDirection aDirection, bool aIsEditable,
+ bool aIgnoreListItemMarker) const;
+
+ /**
+ * This function assumes mAcc is a LocalAccessible.
+ * It iterates the continuations of mAcc's primary frame until it locates
+ * the continuation containing mOffset (a rendered offset). It then uses
+ * GetScreenRectInAppUnits to compute screen coords for the frame, resizing
+ * such that the resulting rect contains only one character.
+ */
+ LayoutDeviceIntRect ComputeBoundsFromFrame() const;
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(TextLeafPoint::BoundaryFlags)
+
+/**
+ * Represents a range of accessible text.
+ * This will eventually replace TextRange.
+ */
+class TextLeafRange final {
+ public:
+ TextLeafRange(const TextLeafPoint& aStart, const TextLeafPoint& aEnd)
+ : mStart(aStart), mEnd(aEnd) {}
+ explicit TextLeafRange(const TextLeafPoint& aStart)
+ : mStart(aStart), mEnd(aStart) {}
+ explicit TextLeafRange() {}
+
+ /**
+ * A valid TextLeafRange evaluates to true. An invalid TextLeafRange
+ * evaluates to false.
+ */
+ explicit operator bool() const { return !!mStart && !!mEnd; }
+
+ bool operator!=(const TextLeafRange& aOther) const {
+ return mEnd != aOther.mEnd || mStart != aOther.mStart;
+ }
+
+ bool operator==(const TextLeafRange& aOther) const {
+ return mEnd == aOther.mEnd && mStart == aOther.mStart;
+ }
+
+ TextLeafPoint Start() const { return mStart; }
+ void SetStart(const TextLeafPoint& aStart) { mStart = aStart; }
+ TextLeafPoint End() const { return mEnd; }
+ void SetEnd(const TextLeafPoint& aEnd) { mEnd = aEnd; }
+
+ bool Crop(Accessible* aContainer);
+
+ /**
+ * Returns a union rect (in dev pixels) of all character bounds in this range.
+ * This rect is screen-relative and inclusive of mEnd. This function only
+ * works on remote accessibles, and assumes caching is enabled.
+ */
+ LayoutDeviceIntRect Bounds() const;
+
+ /**
+ * Set range as DOM selection.
+ * aSelectionNum is the selection index to use. If aSelectionNum is
+ * out of bounds for current selection ranges, or is -1, a new selection
+ * range is created.
+ */
+ MOZ_CAN_RUN_SCRIPT bool SetSelection(int32_t aSelectionNum) const;
+
+ MOZ_CAN_RUN_SCRIPT void ScrollIntoView(uint32_t aScrollType) const;
+
+ private:
+ TextLeafPoint mStart;
+ TextLeafPoint mEnd;
+
+ public:
+ /**
+ * A TextLeafRange iterator will iterate through single leaf segments of the
+ * given range.
+ */
+
+ class Iterator {
+ public:
+ Iterator(Iterator&& aOther)
+ : mRange(aOther.mRange),
+ mSegmentStart(aOther.mSegmentStart),
+ mSegmentEnd(aOther.mSegmentEnd) {}
+
+ static Iterator BeginIterator(const TextLeafRange& aRange);
+
+ static Iterator EndIterator(const TextLeafRange& aRange);
+
+ Iterator& operator++();
+
+ bool operator!=(const Iterator& aOther) const {
+ return mRange != aOther.mRange || mSegmentStart != aOther.mSegmentStart ||
+ mSegmentEnd != aOther.mSegmentEnd;
+ }
+
+ TextLeafRange operator*() {
+ return TextLeafRange(mSegmentStart, mSegmentEnd);
+ }
+
+ private:
+ explicit Iterator(const TextLeafRange& aRange) : mRange(aRange) {}
+
+ Iterator() = delete;
+ Iterator(const Iterator&) = delete;
+ Iterator& operator=(const Iterator&) = delete;
+ Iterator& operator=(const Iterator&&) = delete;
+
+ const TextLeafRange& mRange;
+ TextLeafPoint mSegmentStart;
+ TextLeafPoint mSegmentEnd;
+ };
+
+ Iterator begin() const { return Iterator::BeginIterator(*this); }
+ Iterator end() const { return Iterator::EndIterator(*this); }
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/TextRange-inl.h b/accessible/base/TextRange-inl.h
new file mode 100644
index 0000000000..3c53bd5038
--- /dev/null
+++ b/accessible/base/TextRange-inl.h
@@ -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/. */
+
+#ifndef mozilla_a11y_TextRange_inl_h__
+#define mozilla_a11y_TextRange_inl_h__
+
+#include "TextRange.h"
+
+namespace mozilla {
+namespace a11y {
+
+inline Accessible* TextRange::Container() const {
+ uint32_t pos1 = 0, pos2 = 0;
+ AutoTArray<Accessible*, 30> parents1, parents2;
+ return CommonParent(mStartContainer, mEndContainer, &parents1, &pos1,
+ &parents2, &pos2);
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/TextRange.cpp b/accessible/base/TextRange.cpp
new file mode 100644
index 0000000000..15ff8bd05a
--- /dev/null
+++ b/accessible/base/TextRange.cpp
@@ -0,0 +1,376 @@
+/* -*- 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 "TextRange-inl.h"
+
+#include "LocalAccessible-inl.h"
+#include "HyperTextAccessible-inl.h"
+#include "mozilla/IntegerRange.h"
+#include "mozilla/dom/Selection.h"
+#include "nsAccUtils.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Returns a text point for aAcc within aContainer.
+ */
+static void ToTextPoint(Accessible* aAcc, Accessible** aContainer,
+ int32_t* aOffset, bool aIsBefore = true) {
+ if (aAcc->IsHyperText()) {
+ *aContainer = aAcc;
+ *aOffset =
+ aIsBefore
+ ? 0
+ : static_cast<int32_t>(aAcc->AsHyperTextBase()->CharacterCount());
+ return;
+ }
+
+ Accessible* child = nullptr;
+ Accessible* parent = aAcc;
+ do {
+ child = parent;
+ parent = parent->Parent();
+ } while (parent && !parent->IsHyperText());
+
+ if (parent) {
+ *aContainer = parent;
+ *aOffset = parent->AsHyperTextBase()->GetChildOffset(
+ child->IndexInParent() + static_cast<int32_t>(!aIsBefore));
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// TextPoint
+
+bool TextPoint::operator<(const TextPoint& aPoint) const {
+ if (mContainer == aPoint.mContainer) return mOffset < aPoint.mOffset;
+
+ // Build the chain of parents
+ Accessible* p1 = mContainer;
+ Accessible* p2 = aPoint.mContainer;
+ AutoTArray<Accessible*, 30> parents1, parents2;
+ do {
+ parents1.AppendElement(p1);
+ p1 = p1->Parent();
+ } while (p1);
+ do {
+ parents2.AppendElement(p2);
+ p2 = p2->Parent();
+ } while (p2);
+
+ // Find where the parent chain differs
+ uint32_t pos1 = parents1.Length(), pos2 = parents2.Length();
+ for (uint32_t len = std::min(pos1, pos2); len > 0; --len) {
+ Accessible* child1 = parents1.ElementAt(--pos1);
+ Accessible* child2 = parents2.ElementAt(--pos2);
+ if (child1 != child2) {
+ return child1->IndexInParent() < child2->IndexInParent();
+ }
+ }
+
+ if (pos1 != 0) {
+ // If parents1 is a superset of parents2 then mContainer is a
+ // descendant of aPoint.mContainer. The next element down in parents1
+ // is mContainer's ancestor that is the child of aPoint.mContainer.
+ // We compare its end offset in aPoint.mContainer with aPoint.mOffset.
+ Accessible* child = parents1.ElementAt(pos1 - 1);
+ MOZ_ASSERT(child->Parent() == aPoint.mContainer);
+ return child->EndOffset() < static_cast<uint32_t>(aPoint.mOffset);
+ }
+
+ if (pos2 != 0) {
+ // If parents2 is a superset of parents1 then aPoint.mContainer is a
+ // descendant of mContainer. The next element down in parents2
+ // is aPoint.mContainer's ancestor that is the child of mContainer.
+ // We compare its start offset in mContainer with mOffset.
+ Accessible* child = parents2.ElementAt(pos2 - 1);
+ MOZ_ASSERT(child->Parent() == mContainer);
+ return static_cast<uint32_t>(mOffset) < child->StartOffset();
+ }
+
+ NS_ERROR("Broken tree?!");
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// TextRange
+
+TextRange::TextRange(Accessible* aRoot, Accessible* aStartContainer,
+ int32_t aStartOffset, Accessible* aEndContainer,
+ int32_t aEndOffset)
+ : mRoot(aRoot),
+ mStartContainer(aStartContainer),
+ mEndContainer(aEndContainer),
+ mStartOffset(aStartOffset),
+ mEndOffset(aEndOffset) {}
+
+bool TextRange::Crop(Accessible* aContainer) {
+ uint32_t boundaryPos = 0, containerPos = 0;
+ AutoTArray<Accessible*, 30> boundaryParents, containerParents;
+
+ // Crop the start boundary.
+ Accessible* container = nullptr;
+ HyperTextAccessibleBase* startHyper = mStartContainer->AsHyperTextBase();
+ Accessible* boundary = startHyper->GetChildAtOffset(mStartOffset);
+ if (boundary != aContainer) {
+ CommonParent(boundary, aContainer, &boundaryParents, &boundaryPos,
+ &containerParents, &containerPos);
+
+ if (boundaryPos == 0) {
+ if (containerPos != 0) {
+ // The container is contained by the start boundary, reduce the range to
+ // the point starting at the container.
+ ToTextPoint(aContainer, &mStartContainer, &mStartOffset);
+ } else {
+ // The start boundary and the container are siblings.
+ container = aContainer;
+ }
+ } else {
+ // The container does not contain the start boundary.
+ boundary = boundaryParents[boundaryPos];
+ container = containerParents[containerPos];
+ }
+
+ if (container) {
+ // If the range start is after the container, then make the range invalid.
+ if (boundary->IndexInParent() > container->IndexInParent()) {
+ return !!(mRoot = nullptr);
+ }
+
+ // If the range starts before the container, then reduce the range to
+ // the point starting at the container.
+ if (boundary->IndexInParent() < container->IndexInParent()) {
+ ToTextPoint(container, &mStartContainer, &mStartOffset);
+ }
+ }
+
+ boundaryParents.SetLengthAndRetainStorage(0);
+ containerParents.SetLengthAndRetainStorage(0);
+ }
+
+ HyperTextAccessibleBase* endHyper = mEndContainer->AsHyperTextBase();
+ boundary = endHyper->GetChildAtOffset(mEndOffset);
+ if (boundary == aContainer) {
+ return true;
+ }
+
+ // Crop the end boundary.
+ container = nullptr;
+ CommonParent(boundary, aContainer, &boundaryParents, &boundaryPos,
+ &containerParents, &containerPos);
+
+ if (boundaryPos == 0) {
+ if (containerPos != 0) {
+ ToTextPoint(aContainer, &mEndContainer, &mEndOffset, false);
+ } else {
+ container = aContainer;
+ }
+ } else {
+ boundary = boundaryParents[boundaryPos];
+ container = containerParents[containerPos];
+ }
+
+ if (!container) {
+ return true;
+ }
+
+ if (boundary->IndexInParent() < container->IndexInParent()) {
+ return !!(mRoot = nullptr);
+ }
+
+ if (boundary->IndexInParent() > container->IndexInParent()) {
+ ToTextPoint(container, &mEndContainer, &mEndOffset, false);
+ }
+
+ return true;
+}
+
+/**
+ * Convert the given DOM point to a DOM point in non-generated contents.
+ *
+ * If aDOMPoint is in ::before, the result is immediately after it.
+ * If aDOMPoint is in ::after, the result is immediately before it.
+ */
+static DOMPoint ClosestNotGeneratedDOMPoint(const DOMPoint& aDOMPoint,
+ nsIContent* aElementContent) {
+ MOZ_ASSERT(aDOMPoint.node, "The node must not be null");
+
+ // ::before pseudo element
+ if (aElementContent &&
+ aElementContent->IsGeneratedContentContainerForBefore()) {
+ MOZ_ASSERT(aElementContent->GetParent(),
+ "::before must have parent element");
+ // The first child of its parent (i.e., immediately after the ::before) is
+ // good point for a DOM range.
+ return DOMPoint(aElementContent->GetParent(), 0);
+ }
+
+ // ::after pseudo element
+ if (aElementContent &&
+ aElementContent->IsGeneratedContentContainerForAfter()) {
+ MOZ_ASSERT(aElementContent->GetParent(),
+ "::after must have parent element");
+ // The end of its parent (i.e., immediately before the ::after) is good
+ // point for a DOM range.
+ return DOMPoint(aElementContent->GetParent(),
+ aElementContent->GetParent()->GetChildCount());
+ }
+
+ return aDOMPoint;
+}
+
+/**
+ * GetElementAsContentOf() returns a content representing an element which is
+ * or includes aNode.
+ *
+ * XXX This method is enough to retrieve ::before or ::after pseudo element.
+ * So, if you want to use this for other purpose, you might need to check
+ * ancestors too.
+ */
+static nsIContent* GetElementAsContentOf(nsINode* aNode) {
+ if (auto* element = dom::Element::FromNode(aNode)) {
+ return element;
+ }
+ return aNode->GetParentElement();
+}
+
+bool TextRange::AssignDOMRange(nsRange* aRange, bool* aReversed) const {
+ MOZ_ASSERT(mRoot->IsLocal(), "Not supported for RemoteAccessible");
+ bool reversed = EndPoint() < StartPoint();
+ if (aReversed) {
+ *aReversed = reversed;
+ }
+
+ HyperTextAccessible* startHyper = mStartContainer->AsLocal()->AsHyperText();
+ HyperTextAccessible* endHyper = mEndContainer->AsLocal()->AsHyperText();
+ DOMPoint startPoint = reversed ? endHyper->OffsetToDOMPoint(mEndOffset)
+ : startHyper->OffsetToDOMPoint(mStartOffset);
+ if (!startPoint.node) {
+ return false;
+ }
+
+ // HyperTextAccessible manages pseudo elements generated by ::before or
+ // ::after. However, contents of them are not in the DOM tree normally.
+ // Therefore, they are not selectable and editable. So, when this creates
+ // a DOM range, it should not start from nor end in any pseudo contents.
+
+ nsIContent* container = GetElementAsContentOf(startPoint.node);
+ DOMPoint startPointForDOMRange =
+ ClosestNotGeneratedDOMPoint(startPoint, container);
+ aRange->SetStart(startPointForDOMRange.node, startPointForDOMRange.idx);
+
+ // If the caller wants collapsed range, let's collapse the range to its start.
+ if (mEndContainer == mStartContainer && mEndOffset == mStartOffset) {
+ aRange->Collapse(true);
+ return true;
+ }
+
+ DOMPoint endPoint = reversed ? startHyper->OffsetToDOMPoint(mStartOffset)
+ : endHyper->OffsetToDOMPoint(mEndOffset);
+ if (!endPoint.node) {
+ return false;
+ }
+
+ if (startPoint.node != endPoint.node) {
+ container = GetElementAsContentOf(endPoint.node);
+ }
+
+ DOMPoint endPointForDOMRange =
+ ClosestNotGeneratedDOMPoint(endPoint, container);
+ aRange->SetEnd(endPointForDOMRange.node, endPointForDOMRange.idx);
+ return true;
+}
+
+void TextRange::TextRangesFromSelection(dom::Selection* aSelection,
+ nsTArray<TextRange>* aRanges) {
+ MOZ_ASSERT(aRanges->Length() == 0, "TextRange array supposed to be empty");
+
+ aRanges->SetCapacity(aSelection->RangeCount());
+
+ const uint32_t rangeCount = aSelection->RangeCount();
+ for (const uint32_t idx : IntegerRange(rangeCount)) {
+ MOZ_ASSERT(aSelection->RangeCount() == rangeCount);
+ const nsRange* DOMRange = aSelection->GetRangeAt(idx);
+ MOZ_ASSERT(DOMRange);
+ HyperTextAccessible* startContainer =
+ nsAccUtils::GetTextContainer(DOMRange->GetStartContainer());
+ HyperTextAccessible* endContainer =
+ nsAccUtils::GetTextContainer(DOMRange->GetEndContainer());
+ HyperTextAccessible* commonAncestor = nsAccUtils::GetTextContainer(
+ DOMRange->GetClosestCommonInclusiveAncestor());
+ if (!startContainer || !endContainer) {
+ continue;
+ }
+
+ int32_t startOffset = startContainer->DOMPointToOffset(
+ DOMRange->GetStartContainer(), DOMRange->StartOffset(), false);
+ int32_t endOffset = endContainer->DOMPointToOffset(
+ DOMRange->GetEndContainer(), DOMRange->EndOffset(), true);
+
+ TextRange tr(commonAncestor && commonAncestor->IsTextField()
+ ? commonAncestor
+ : startContainer->Document(),
+ startContainer, startOffset, endContainer, endOffset);
+ *(aRanges->AppendElement()) = std::move(tr);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// pivate
+
+void TextRange::Set(Accessible* aRoot, Accessible* aStartContainer,
+ int32_t aStartOffset, Accessible* aEndContainer,
+ int32_t aEndOffset) {
+ mRoot = aRoot;
+ mStartContainer = aStartContainer;
+ mEndContainer = aEndContainer;
+ mStartOffset = aStartOffset;
+ mEndOffset = aEndOffset;
+}
+
+Accessible* TextRange::CommonParent(Accessible* aAcc1, Accessible* aAcc2,
+ nsTArray<Accessible*>* aParents1,
+ uint32_t* aPos1,
+ nsTArray<Accessible*>* aParents2,
+ uint32_t* aPos2) const {
+ if (aAcc1 == aAcc2) {
+ return aAcc1;
+ }
+
+ MOZ_ASSERT(aParents1->Length() == 0 || aParents2->Length() == 0,
+ "Wrong arguments");
+
+ // Build the chain of parents.
+ Accessible* p1 = aAcc1;
+ Accessible* p2 = aAcc2;
+ do {
+ aParents1->AppendElement(p1);
+ p1 = p1->Parent();
+ } while (p1);
+ do {
+ aParents2->AppendElement(p2);
+ p2 = p2->Parent();
+ } while (p2);
+
+ // Find where the parent chain differs
+ *aPos1 = aParents1->Length();
+ *aPos2 = aParents2->Length();
+ Accessible* parent = nullptr;
+ uint32_t len = 0;
+ for (len = std::min(*aPos1, *aPos2); len > 0; --len) {
+ Accessible* child1 = aParents1->ElementAt(--(*aPos1));
+ Accessible* child2 = aParents2->ElementAt(--(*aPos2));
+ if (child1 != child2) break;
+
+ parent = child1;
+ }
+
+ return parent;
+}
+
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/base/TextRange.h b/accessible/base/TextRange.h
new file mode 100644
index 0000000000..121dbe8399
--- /dev/null
+++ b/accessible/base/TextRange.h
@@ -0,0 +1,164 @@
+/* -*- 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_TextRange_h__
+#define mozilla_a11y_TextRange_h__
+
+#include <utility>
+
+#include "nsTArray.h"
+
+class nsRange;
+
+namespace mozilla {
+namespace dom {
+class Selection;
+} // namespace dom
+namespace a11y {
+
+class Accessible;
+class LocalAccessible;
+
+/**
+ * A text point (HyperText + offset), represents a boundary of text range.
+ * In new code, This should only be used when you explicitly need to deal with
+ * HyperText containers and offsets, including embedded objects; e.g. for
+ * IAccessible2 and ATK. Otherwise, use TextLeafPoint instead.
+ */
+struct TextPoint final {
+ TextPoint(Accessible* aContainer, int32_t aOffset)
+ : mContainer(aContainer), mOffset(aOffset) {}
+ TextPoint(const TextPoint& aPoint)
+ : mContainer(aPoint.mContainer), mOffset(aPoint.mOffset) {}
+
+ Accessible* mContainer;
+ int32_t mOffset;
+
+ bool operator==(const TextPoint& aPoint) const {
+ return mContainer == aPoint.mContainer && mOffset == aPoint.mOffset;
+ }
+ bool operator<(const TextPoint& aPoint) const;
+};
+
+/**
+ * Represents a HyperText range within the text control or document.
+ * In new code, This should only be used when you explicitly need to deal with
+ * HyperText containers and offsets, including embedded objects; e.g. for
+ * IAccessible2 and ATK. Otherwise, use TextLeafRange instead.
+ */
+class TextRange final {
+ public:
+ TextRange(Accessible* aRoot, Accessible* aStartContainer,
+ int32_t aStartOffset, Accessible* aEndContainer,
+ int32_t aEndOffset);
+ TextRange() : mStartOffset{0}, mEndOffset{0} {}
+ TextRange(TextRange&& aRange)
+ : mRoot(std::move(aRange.mRoot)),
+ mStartContainer(std::move(aRange.mStartContainer)),
+ mEndContainer(std::move(aRange.mEndContainer)),
+ mStartOffset(aRange.mStartOffset),
+ mEndOffset(aRange.mEndOffset) {}
+
+ TextRange& operator=(TextRange&& aRange) {
+ mRoot = std::move(aRange.mRoot);
+ mStartContainer = std::move(aRange.mStartContainer);
+ mEndContainer = std::move(aRange.mEndContainer);
+ mStartOffset = aRange.mStartOffset;
+ mEndOffset = aRange.mEndOffset;
+ return *this;
+ }
+
+ Accessible* Root() { return mRoot; }
+ Accessible* StartContainer() const { return mStartContainer; }
+ int32_t StartOffset() const { return mStartOffset; }
+ Accessible* EndContainer() const { return mEndContainer; }
+ int32_t EndOffset() const { return mEndOffset; }
+
+ bool operator==(const TextRange& aRange) const {
+ return mStartContainer == aRange.mStartContainer &&
+ mStartOffset == aRange.mStartOffset &&
+ mEndContainer == aRange.mEndContainer &&
+ mEndOffset == aRange.mEndOffset;
+ }
+
+ TextPoint StartPoint() const {
+ return TextPoint(mStartContainer, mStartOffset);
+ }
+ TextPoint EndPoint() const { return TextPoint(mEndContainer, mEndOffset); }
+
+ /**
+ * Return a container containing both start and end points.
+ */
+ Accessible* Container() const;
+
+ /**
+ * Crops the range if it overlaps the given accessible element boundaries,
+ * returns true if the range was cropped successfully.
+ */
+ bool Crop(Accessible* aContainer);
+
+ /**
+ * Convert stored hypertext offsets into DOM offsets and assign it to DOM
+ * range.
+ *
+ * Note that if start and/or end accessible offsets are in generated content
+ * such as ::before or
+ * ::after, the result range excludes the generated content. See also
+ * ClosestNotGeneratedDOMPoint() for more information.
+ *
+ * @param aRange [in, out] the range whose bounds to set
+ * @param aReversed [out] whether the start/end offsets were reversed.
+ * @return true if conversion was successful
+ */
+ bool AssignDOMRange(nsRange* aRange, bool* aReversed = nullptr) const;
+
+ /**
+ * Return true if this TextRange object represents an actual range of text.
+ */
+ bool IsValid() const { return mRoot; }
+
+ void SetStartPoint(Accessible* aContainer, int32_t aOffset) {
+ mStartContainer = aContainer;
+ mStartOffset = aOffset;
+ }
+ void SetEndPoint(Accessible* aContainer, int32_t aOffset) {
+ mStartContainer = aContainer;
+ mStartOffset = aOffset;
+ }
+
+ static void TextRangesFromSelection(dom::Selection* aSelection,
+ nsTArray<TextRange>* aRanges);
+
+ private:
+ TextRange(const TextRange& aRange) = delete;
+ TextRange& operator=(const TextRange& aRange) = delete;
+
+ friend class HyperTextAccessible;
+ friend class xpcAccessibleTextRange;
+
+ void Set(Accessible* aRoot, Accessible* aStartContainer, int32_t aStartOffset,
+ Accessible* aEndContainer, int32_t aEndOffset);
+
+ /**
+ * A helper method returning a common parent for two given accessible
+ * elements.
+ */
+ Accessible* CommonParent(Accessible* aAcc1, Accessible* aAcc2,
+ nsTArray<Accessible*>* aParents1, uint32_t* aPos1,
+ nsTArray<Accessible*>* aParents2,
+ uint32_t* aPos2) const;
+
+ Accessible* mRoot;
+ Accessible* mStartContainer;
+ Accessible* mEndContainer;
+ int32_t mStartOffset;
+ int32_t mEndOffset;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/TextUpdater.cpp b/accessible/base/TextUpdater.cpp
new file mode 100644
index 0000000000..29f8ac11f3
--- /dev/null
+++ b/accessible/base/TextUpdater.cpp
@@ -0,0 +1,215 @@
+/* -*- 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 "TextUpdater.h"
+
+#include "CacheConstants.h"
+#include "DocAccessible-inl.h"
+#include "TextLeafAccessible.h"
+#include <algorithm>
+
+using namespace mozilla::a11y;
+
+void TextUpdater::Run(DocAccessible* aDocument, TextLeafAccessible* aTextLeaf,
+ const nsAString& aNewText) {
+ NS_ASSERTION(aTextLeaf, "No text leaf accessible?");
+
+ const nsString& oldText = aTextLeaf->Text();
+ uint32_t oldLen = oldText.Length(), newLen = aNewText.Length();
+ uint32_t minLen = std::min(oldLen, newLen);
+
+ // Skip coinciding begin substrings.
+ uint32_t skipStart = 0;
+ for (; skipStart < minLen; skipStart++) {
+ if (aNewText[skipStart] != oldText[skipStart]) break;
+ }
+
+ // The text was changed. Do update.
+ if (skipStart != minLen || oldLen != newLen) {
+ TextUpdater updater(aDocument, aTextLeaf);
+ updater.DoUpdate(aNewText, oldText, skipStart);
+ aDocument->QueueCacheUpdate(aTextLeaf, CacheDomain::Text);
+ }
+}
+
+void TextUpdater::DoUpdate(const nsAString& aNewText, const nsAString& aOldText,
+ uint32_t aSkipStart) {
+ LocalAccessible* parent = mTextLeaf->LocalParent();
+ if (!parent) return;
+
+ mHyperText = parent->AsHyperText();
+ if (!mHyperText) {
+ MOZ_ASSERT_UNREACHABLE("Text leaf parent is not hypertext!");
+ return;
+ }
+
+ // Get the text leaf accessible offset and invalidate cached offsets after it.
+ mTextOffset = mHyperText->GetChildOffset(mTextLeaf, true);
+ NS_ASSERTION(mTextOffset != -1, "Text leaf hasn't offset within hyper text!");
+
+ // Don't bother diffing if the hypertext isn't editable. Diffing non-editable
+ // text can lead to weird screen reader results with live regions, e.g.,
+ // changing "text" to "testing" might read the diff "s ing" when we'd really
+ // just like to hear "testing."
+ if (!mHyperText->IsEditable()) {
+ // Fire text change event for removal.
+ RefPtr<AccEvent> textRemoveEvent =
+ new AccTextChangeEvent(mHyperText, mTextOffset, aOldText, false);
+ mDocument->FireDelayedEvent(textRemoveEvent);
+
+ // Fire text change event for insertion if there's text to insert.
+ if (!aNewText.IsEmpty()) {
+ RefPtr<AccEvent> textInsertEvent =
+ new AccTextChangeEvent(mHyperText, mTextOffset, aNewText, true);
+ mDocument->FireDelayedEvent(textInsertEvent);
+ }
+
+ mDocument->MaybeNotifyOfValueChange(mHyperText);
+
+ // Update the text.
+ mTextLeaf->SetText(aNewText);
+ return;
+ }
+
+ uint32_t oldLen = aOldText.Length(), newLen = aNewText.Length();
+ uint32_t minLen = std::min(oldLen, newLen);
+
+ // Trim coinciding substrings from the end.
+ uint32_t skipEnd = 0;
+ while (minLen - skipEnd > aSkipStart &&
+ aNewText[newLen - skipEnd - 1] == aOldText[oldLen - skipEnd - 1]) {
+ skipEnd++;
+ }
+
+ uint32_t strLen1 = oldLen - aSkipStart - skipEnd;
+ uint32_t strLen2 = newLen - aSkipStart - skipEnd;
+
+ const nsAString& str1 = Substring(aOldText, aSkipStart, strLen1);
+ const nsAString& str2 = Substring(aNewText, aSkipStart, strLen2);
+
+ // Increase offset of the text leaf on skipped characters amount.
+ mTextOffset += aSkipStart;
+
+ // It could be single insertion or removal or the case of long strings. Do not
+ // calculate the difference between long strings and prefer to fire pair of
+ // insert/remove events as the old string was replaced on the new one.
+ if (strLen1 == 0 || strLen2 == 0 || strLen1 > kMaxStrLen ||
+ strLen2 > kMaxStrLen) {
+ if (strLen1 > 0) {
+ // Fire text change event for removal.
+ RefPtr<AccEvent> textRemoveEvent =
+ new AccTextChangeEvent(mHyperText, mTextOffset, str1, false);
+ mDocument->FireDelayedEvent(textRemoveEvent);
+ }
+
+ if (strLen2 > 0) {
+ // Fire text change event for insertion.
+ RefPtr<AccEvent> textInsertEvent =
+ new AccTextChangeEvent(mHyperText, mTextOffset, str2, true);
+ mDocument->FireDelayedEvent(textInsertEvent);
+ }
+
+ mDocument->MaybeNotifyOfValueChange(mHyperText);
+
+ // Update the text.
+ mTextLeaf->SetText(aNewText);
+ return;
+ }
+
+ // Otherwise find the difference between strings and fire events.
+ // Note: we can skip initial and final coinciding characters since they don't
+ // affect the Levenshtein distance.
+
+ // Compute the flat structured matrix need to compute the difference.
+ uint32_t len1 = strLen1 + 1, len2 = strLen2 + 1;
+ uint32_t* entries = new uint32_t[len1 * len2];
+
+ for (uint32_t colIdx = 0; colIdx < len1; colIdx++) entries[colIdx] = colIdx;
+
+ uint32_t* row = entries;
+ for (uint32_t rowIdx = 1; rowIdx < len2; rowIdx++) {
+ uint32_t* prevRow = row;
+ row += len1;
+ row[0] = rowIdx;
+ for (uint32_t colIdx = 1; colIdx < len1; colIdx++) {
+ if (str1[colIdx - 1] != str2[rowIdx - 1]) {
+ uint32_t left = row[colIdx - 1];
+ uint32_t up = prevRow[colIdx];
+ uint32_t upleft = prevRow[colIdx - 1];
+ row[colIdx] = std::min(upleft, std::min(left, up)) + 1;
+ } else {
+ row[colIdx] = prevRow[colIdx - 1];
+ }
+ }
+ }
+
+ // Compute events based on the difference.
+ nsTArray<RefPtr<AccEvent> > events;
+ ComputeTextChangeEvents(str1, str2, entries, events);
+
+ delete[] entries;
+
+ // Fire events.
+ for (int32_t idx = events.Length() - 1; idx >= 0; idx--) {
+ mDocument->FireDelayedEvent(events[idx]);
+ }
+
+ mDocument->MaybeNotifyOfValueChange(mHyperText);
+
+ // Update the text.
+ mTextLeaf->SetText(aNewText);
+}
+
+void TextUpdater::ComputeTextChangeEvents(
+ const nsAString& aStr1, const nsAString& aStr2, uint32_t* aEntries,
+ nsTArray<RefPtr<AccEvent> >& aEvents) {
+ int32_t colIdx = aStr1.Length(), rowIdx = aStr2.Length();
+
+ // Point at which strings last matched.
+ int32_t colEnd = colIdx;
+ int32_t rowEnd = rowIdx;
+
+ int32_t colLen = colEnd + 1;
+ uint32_t* row = aEntries + rowIdx * colLen;
+ uint32_t dist = row[colIdx]; // current Levenshtein distance
+ while (rowIdx && colIdx) { // stop when we can't move diagonally
+ if (aStr1[colIdx - 1] == aStr2[rowIdx - 1]) { // match
+ if (rowIdx < rowEnd) { // deal with any pending insertion
+ FireInsertEvent(Substring(aStr2, rowIdx, rowEnd - rowIdx), rowIdx,
+ aEvents);
+ }
+ if (colIdx < colEnd) { // deal with any pending deletion
+ FireDeleteEvent(Substring(aStr1, colIdx, colEnd - colIdx), rowIdx,
+ aEvents);
+ }
+
+ colEnd = --colIdx; // reset the match point
+ rowEnd = --rowIdx;
+ row -= colLen;
+ continue;
+ }
+ --dist;
+ if (dist == row[colIdx - 1 - colLen]) { // substitution
+ --colIdx;
+ --rowIdx;
+ row -= colLen;
+ continue;
+ }
+ if (dist == row[colIdx - colLen]) { // insertion
+ --rowIdx;
+ row -= colLen;
+ continue;
+ }
+ if (dist == row[colIdx - 1]) { // deletion
+ --colIdx;
+ continue;
+ }
+ MOZ_ASSERT_UNREACHABLE("huh?");
+ return;
+ }
+
+ if (rowEnd) FireInsertEvent(Substring(aStr2, 0, rowEnd), 0, aEvents);
+ if (colEnd) FireDeleteEvent(Substring(aStr1, 0, colEnd), 0, aEvents);
+}
diff --git a/accessible/base/TextUpdater.h b/accessible/base/TextUpdater.h
new file mode 100644
index 0000000000..87905cf082
--- /dev/null
+++ b/accessible/base/TextUpdater.h
@@ -0,0 +1,95 @@
+/* -*- 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_TextUpdater_h__
+#define mozilla_a11y_TextUpdater_h__
+
+#include "AccEvent.h"
+#include "HyperTextAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Used to find a difference between old and new text and fire text change
+ * events.
+ */
+class TextUpdater {
+ public:
+ /**
+ * Start text of the text leaf update.
+ */
+ static void Run(DocAccessible* aDocument, TextLeafAccessible* aTextLeaf,
+ const nsAString& aNewText);
+
+ private:
+ TextUpdater(DocAccessible* aDocument, TextLeafAccessible* aTextLeaf)
+ : mDocument(aDocument),
+ mTextLeaf(aTextLeaf),
+ mHyperText(nullptr),
+ mTextOffset(-1) {}
+
+ ~TextUpdater() {
+ mDocument = nullptr;
+ mTextLeaf = nullptr;
+ mHyperText = nullptr;
+ }
+
+ /**
+ * Update text of the text leaf accessible, fire text change and value change
+ * (if applicable) events for its container hypertext accessible.
+ */
+ void DoUpdate(const nsAString& aNewText, const nsAString& aOldText,
+ uint32_t aSkipStart);
+
+ private:
+ TextUpdater();
+ TextUpdater(const TextUpdater&);
+ TextUpdater& operator=(const TextUpdater&);
+
+ /**
+ * Fire text change events based on difference between strings.
+ */
+ void ComputeTextChangeEvents(const nsAString& aStr1, const nsAString& aStr2,
+ uint32_t* aEntries,
+ nsTArray<RefPtr<AccEvent> >& aEvents);
+
+ /**
+ * Helper to create text change events for inserted text.
+ */
+ inline void FireInsertEvent(const nsAString& aText, uint32_t aAddlOffset,
+ nsTArray<RefPtr<AccEvent> >& aEvents) {
+ RefPtr<AccEvent> event = new AccTextChangeEvent(
+ mHyperText, mTextOffset + aAddlOffset, aText, true);
+ aEvents.AppendElement(event);
+ }
+
+ /**
+ * Helper to create text change events for removed text.
+ */
+ inline void FireDeleteEvent(const nsAString& aText, uint32_t aAddlOffset,
+ nsTArray<RefPtr<AccEvent> >& aEvents) {
+ RefPtr<AccEvent> event = new AccTextChangeEvent(
+ mHyperText, mTextOffset + aAddlOffset, aText, false);
+ aEvents.AppendElement(event);
+ }
+
+ /**
+ * The constant used to skip string difference calculation in case of long
+ * strings.
+ */
+ const static uint32_t kMaxStrLen = 1 << 6;
+
+ private:
+ DocAccessible* mDocument;
+ TextLeafAccessible* mTextLeaf;
+ HyperTextAccessible* mHyperText;
+ int32_t mTextOffset;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/TreeWalker.cpp b/accessible/base/TreeWalker.cpp
new file mode 100644
index 0000000000..ff314bd83c
--- /dev/null
+++ b/accessible/base/TreeWalker.cpp
@@ -0,0 +1,348 @@
+/* -*- 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 "TreeWalker.h"
+
+#include "nsAccessibilityService.h"
+#include "DocAccessible.h"
+
+#include "mozilla/dom/ChildIterator.h"
+#include "mozilla/dom/Element.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// TreeWalker
+////////////////////////////////////////////////////////////////////////////////
+
+TreeWalker::TreeWalker(LocalAccessible* aContext)
+ : mDoc(aContext->Document()),
+ mContext(aContext),
+ mAnchorNode(nullptr),
+ mARIAOwnsIdx(0),
+ mChildFilter(nsIContent::eSkipPlaceholderContent),
+ mFlags(0),
+ mPhase(eAtStart) {
+ mChildFilter |= nsIContent::eAllChildren;
+
+ mAnchorNode = mContext->IsDoc() ? mDoc->DocumentNode()->GetRootElement()
+ : mContext->GetContent();
+
+ MOZ_COUNT_CTOR(TreeWalker);
+}
+
+TreeWalker::TreeWalker(LocalAccessible* aContext, nsIContent* aAnchorNode,
+ uint32_t aFlags)
+ : mDoc(aContext->Document()),
+ mContext(aContext),
+ mAnchorNode(aAnchorNode),
+ mARIAOwnsIdx(0),
+ mChildFilter(nsIContent::eSkipPlaceholderContent),
+ mFlags(aFlags),
+ mPhase(eAtStart) {
+ MOZ_ASSERT(mFlags & eWalkCache,
+ "This constructor cannot be used for tree creation");
+ MOZ_ASSERT(aAnchorNode, "No anchor node for the accessible tree walker");
+
+ mChildFilter |= nsIContent::eAllChildren;
+
+ MOZ_COUNT_CTOR(TreeWalker);
+}
+
+TreeWalker::TreeWalker(DocAccessible* aDocument, nsIContent* aAnchorNode)
+ : mDoc(aDocument),
+ mContext(nullptr),
+ mAnchorNode(aAnchorNode),
+ mARIAOwnsIdx(0),
+ mChildFilter(nsIContent::eSkipPlaceholderContent |
+ nsIContent::eAllChildren),
+ mFlags(eWalkCache),
+ mPhase(eAtStart) {
+ MOZ_ASSERT(aAnchorNode, "No anchor node for the accessible tree walker");
+ MOZ_COUNT_CTOR(TreeWalker);
+}
+
+TreeWalker::~TreeWalker() { MOZ_COUNT_DTOR(TreeWalker); }
+
+LocalAccessible* TreeWalker::Scope(nsIContent* aAnchorNode) {
+ Reset();
+
+ mAnchorNode = aAnchorNode;
+
+ mFlags |= eScoped;
+
+ bool skipSubtree = false;
+ LocalAccessible* acc = AccessibleFor(aAnchorNode, 0, &skipSubtree);
+ if (acc) {
+ mPhase = eAtEnd;
+ return acc;
+ }
+
+ return skipSubtree ? nullptr : Next();
+}
+
+bool TreeWalker::Seek(nsIContent* aChildNode) {
+ MOZ_ASSERT(aChildNode, "Child cannot be null");
+
+ Reset();
+
+ if (mAnchorNode == aChildNode) {
+ return true;
+ }
+
+ nsIContent* childNode = nullptr;
+ nsINode* parentNode = aChildNode;
+ do {
+ childNode = parentNode->AsContent();
+ parentNode = childNode->GetFlattenedTreeParent();
+
+ // Handle the special case of XBL binding child under a shadow root.
+ if (parentNode && parentNode->IsShadowRoot()) {
+ parentNode = childNode->GetFlattenedTreeParent();
+ if (parentNode == mAnchorNode) {
+ return true;
+ }
+ continue;
+ }
+
+ if (!parentNode || !parentNode->IsElement()) {
+ return false;
+ }
+
+ // If ARIA owned child.
+ LocalAccessible* child = mDoc->GetAccessible(childNode);
+ if (child && child->IsRelocated()) {
+ MOZ_ASSERT(
+ !(mFlags & eScoped),
+ "Walker should not be scoped when seeking into relocated children");
+ if (child->LocalParent() != mContext) {
+ return false;
+ }
+
+ LocalAccessible* ownedChild = nullptr;
+ while ((ownedChild = mDoc->ARIAOwnedAt(mContext, mARIAOwnsIdx++)) &&
+ ownedChild != child) {
+ ;
+ }
+
+ MOZ_ASSERT(ownedChild, "A child has to be in ARIA owned elements");
+ mPhase = eAtARIAOwns;
+ return true;
+ }
+
+ // Look in DOM.
+ dom::AllChildrenIterator* iter =
+ PrependState(parentNode->AsElement(), true);
+ if (!iter->Seek(childNode)) {
+ return false;
+ }
+
+ if (parentNode == mAnchorNode) {
+ mPhase = eAtDOM;
+ return true;
+ }
+ } while (true);
+
+ MOZ_ASSERT_UNREACHABLE("because the do-while loop never breaks");
+}
+
+LocalAccessible* TreeWalker::Next() {
+ if (mStateStack.IsEmpty()) {
+ if (mPhase == eAtEnd) {
+ return nullptr;
+ }
+
+ if (mPhase == eAtDOM || mPhase == eAtARIAOwns) {
+ if (!(mFlags & eScoped)) {
+ mPhase = eAtARIAOwns;
+ LocalAccessible* child = mDoc->ARIAOwnedAt(mContext, mARIAOwnsIdx);
+ if (child) {
+ mARIAOwnsIdx++;
+ return child;
+ }
+ }
+ MOZ_ASSERT(!(mFlags & eScoped) || mPhase != eAtARIAOwns,
+ "Don't walk relocated children in scoped mode");
+ mPhase = eAtEnd;
+ return nullptr;
+ }
+
+ if (!mAnchorNode) {
+ mPhase = eAtEnd;
+ return nullptr;
+ }
+
+ mPhase = eAtDOM;
+ PushState(mAnchorNode, true);
+ }
+
+ dom::AllChildrenIterator* top = &mStateStack[mStateStack.Length() - 1];
+ while (top) {
+ while (nsIContent* childNode = top->GetNextChild()) {
+ bool skipSubtree = false;
+ LocalAccessible* child = AccessibleFor(childNode, mFlags, &skipSubtree);
+ if (child) {
+ return child;
+ }
+
+ // Walk down the subtree if allowed.
+ if (!skipSubtree && childNode->IsElement()) {
+ top = PushState(childNode, true);
+ }
+ }
+ top = PopState();
+ }
+
+ // If we traversed the whole subtree of the anchor node. Move to next node
+ // relative anchor node within the context subtree if asked.
+ if (mFlags != eWalkContextTree) {
+ // eWalkCache flag presence indicates that the search is scoped to the
+ // anchor (no ARIA owns stuff).
+ if (mFlags & eWalkCache) {
+ mPhase = eAtEnd;
+ return nullptr;
+ }
+ return Next();
+ }
+
+ nsINode* contextNode = mContext->GetNode();
+ while (mAnchorNode != contextNode) {
+ nsINode* parentNode = mAnchorNode->GetFlattenedTreeParent();
+ if (!parentNode || !parentNode->IsElement()) return nullptr;
+
+ nsIContent* parent = parentNode->AsElement();
+ top = PushState(parent, true);
+ if (top->Seek(mAnchorNode)) {
+ mAnchorNode = parent;
+ return Next();
+ }
+
+ // XXX We really should never get here, it means we're trying to find an
+ // accessible for a dom node where iterating over its parent's children
+ // doesn't return it. However this sometimes happens when we're asked for
+ // the nearest accessible to place holder content which we ignore.
+ mAnchorNode = parent;
+ }
+
+ return Next();
+}
+
+LocalAccessible* TreeWalker::Prev() {
+ if (mStateStack.IsEmpty()) {
+ if (mPhase == eAtStart || mPhase == eAtDOM) {
+ mPhase = eAtStart;
+ return nullptr;
+ }
+
+ if (mPhase == eAtEnd) {
+ if (mFlags & eScoped) {
+ mPhase = eAtDOM;
+ } else {
+ mPhase = eAtARIAOwns;
+ mARIAOwnsIdx = mDoc->ARIAOwnedCount(mContext);
+ }
+ }
+
+ if (mPhase == eAtARIAOwns) {
+ MOZ_ASSERT(!(mFlags & eScoped),
+ "Should not walk relocated children in scoped mode");
+ if (mARIAOwnsIdx > 0) {
+ return mDoc->ARIAOwnedAt(mContext, --mARIAOwnsIdx);
+ }
+
+ if (!mAnchorNode) {
+ mPhase = eAtStart;
+ return nullptr;
+ }
+
+ mPhase = eAtDOM;
+ PushState(mAnchorNode, false);
+ }
+ }
+
+ dom::AllChildrenIterator* top = &mStateStack[mStateStack.Length() - 1];
+ while (top) {
+ while (nsIContent* childNode = top->GetPreviousChild()) {
+ // No accessible creation on the way back.
+ bool skipSubtree = false;
+ LocalAccessible* child =
+ AccessibleFor(childNode, eWalkCache, &skipSubtree);
+ if (child) {
+ return child;
+ }
+
+ // Walk down into subtree to find accessibles.
+ if (!skipSubtree && childNode->IsElement()) {
+ top = PushState(childNode, false);
+ }
+ }
+ top = PopState();
+ }
+
+ // Move to a previous node relative the anchor node within the context
+ // subtree if asked.
+ if (mFlags != eWalkContextTree) {
+ mPhase = eAtStart;
+ return nullptr;
+ }
+
+ nsINode* contextNode = mContext->GetNode();
+ while (mAnchorNode != contextNode) {
+ nsINode* parentNode = mAnchorNode->GetFlattenedTreeParent();
+ if (!parentNode || !parentNode->IsElement()) {
+ return nullptr;
+ }
+
+ nsIContent* parent = parentNode->AsElement();
+ top = PushState(parent, true);
+ if (top->Seek(mAnchorNode)) {
+ mAnchorNode = parent;
+ return Prev();
+ }
+
+ mAnchorNode = parent;
+ }
+
+ mPhase = eAtStart;
+ return nullptr;
+}
+
+LocalAccessible* TreeWalker::AccessibleFor(nsIContent* aNode, uint32_t aFlags,
+ bool* aSkipSubtree) {
+ // Ignore the accessible and its subtree if it was repositioned by means
+ // of aria-owns.
+ LocalAccessible* child = mDoc->GetAccessible(aNode);
+ if (child) {
+ if (child->IsRelocated()) {
+ *aSkipSubtree = true;
+ return nullptr;
+ }
+ return child;
+ }
+
+ // Create an accessible if allowed.
+ if (!(aFlags & eWalkCache) && mContext->IsAcceptableChild(aNode)) {
+ // We may have ARIA owned element in the dependent attributes map, but the
+ // element may be not allowed for this ARIA owns relation, if the relation
+ // crosses out XBL anonymous content boundaries. In this case we won't
+ // create an accessible object for it, when aria-owns is processed, which
+ // may make the element subtree inaccessible. To avoid that let's create
+ // an accessible object now, and later, if allowed, move it in the tree,
+ // when aria-owns relation is processed.
+ if (mDoc->RelocateARIAOwnedIfNeeded(aNode) && !aNode->IsXULElement()) {
+ *aSkipSubtree = true;
+ return nullptr;
+ }
+ return GetAccService()->CreateAccessible(aNode, mContext, aSkipSubtree);
+ }
+
+ return nullptr;
+}
+
+dom::AllChildrenIterator* TreeWalker::PopState() {
+ mStateStack.RemoveLastElement();
+ return mStateStack.IsEmpty() ? nullptr : &mStateStack.LastElement();
+}
diff --git a/accessible/base/TreeWalker.h b/accessible/base/TreeWalker.h
new file mode 100644
index 0000000000..9093d8187f
--- /dev/null
+++ b/accessible/base/TreeWalker.h
@@ -0,0 +1,142 @@
+/* -*- 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_TreeWalker_h_
+#define mozilla_a11y_TreeWalker_h_
+
+#include "mozilla/Attributes.h"
+#include <stdint.h>
+#include "mozilla/dom/ChildIterator.h"
+#include "nsCOMPtr.h"
+
+class nsIContent;
+
+namespace mozilla {
+namespace a11y {
+
+class LocalAccessible;
+class DocAccessible;
+
+/**
+ * This class is used to walk the DOM tree to create accessible tree.
+ */
+class TreeWalker final {
+ public:
+ enum {
+ // used to walk the existing tree of the given node
+ eWalkCache = 1,
+ // used to walk the context tree starting from given node
+ eWalkContextTree = 2 | eWalkCache,
+ eScoped = 4
+ };
+
+ /**
+ * Used to navigate and create if needed the accessible children.
+ */
+ explicit TreeWalker(LocalAccessible* aContext);
+
+ /**
+ * Used to navigate the accessible children relative to the anchor.
+ *
+ * @param aContext [in] container accessible for the given node, used to
+ * define accessible context
+ * @param aAnchorNode [in] the node the search will be prepared relative to
+ * @param aFlags [in] flags (see enum above)
+ */
+ TreeWalker(LocalAccessible* aContext, nsIContent* aAnchorNode,
+ uint32_t aFlags = eWalkCache);
+
+ /**
+ * Navigates the accessible children within the anchor node subtree.
+ */
+ TreeWalker(DocAccessible* aDocument, nsIContent* aAnchorNode);
+
+ ~TreeWalker();
+
+ /**
+ * Resets the walker state, and sets the given node as an anchor. Returns a
+ * first accessible element within the node including the node itself.
+ */
+ LocalAccessible* Scope(nsIContent* aAnchorNode);
+
+ /**
+ * Resets the walker state.
+ */
+ void Reset() {
+ mPhase = eAtStart;
+ mStateStack.Clear();
+ mARIAOwnsIdx = 0;
+ }
+
+ /**
+ * Sets the walker state to the given child node if it's within the anchor.
+ */
+ bool Seek(nsIContent* aChildNode);
+
+ /**
+ * Return the next/prev accessible.
+ *
+ * @note Returned accessible is bound to the document, if the accessible is
+ * rejected during tree creation then the caller should be unbind it
+ * from the document.
+ */
+ LocalAccessible* Next();
+ LocalAccessible* Prev();
+
+ LocalAccessible* Context() const { return mContext; }
+ DocAccessible* Document() const { return mDoc; }
+
+ private:
+ TreeWalker();
+ TreeWalker(const TreeWalker&);
+ TreeWalker& operator=(const TreeWalker&);
+
+ /**
+ * Return an accessible for the given node if any.
+ */
+ LocalAccessible* AccessibleFor(nsIContent* aNode, uint32_t aFlags,
+ bool* aSkipSubtree);
+
+ /**
+ * Create new state for the given node and push it on top of stack / at bottom
+ * of stack.
+ *
+ * @note State stack is used to navigate up/down the DOM subtree during
+ * accessible children search.
+ */
+ dom::AllChildrenIterator* PushState(nsIContent* aContent,
+ bool aStartAtBeginning) {
+ return mStateStack.AppendElement(
+ dom::AllChildrenIterator(aContent, mChildFilter, aStartAtBeginning));
+ }
+ dom::AllChildrenIterator* PrependState(nsIContent* aContent,
+ bool aStartAtBeginning) {
+ return mStateStack.InsertElementAt(
+ 0, dom::AllChildrenIterator(aContent, mChildFilter, aStartAtBeginning));
+ }
+
+ /**
+ * Pop state from stack.
+ */
+ dom::AllChildrenIterator* PopState();
+
+ DocAccessible* mDoc;
+ LocalAccessible* mContext;
+ nsIContent* mAnchorNode;
+
+ AutoTArray<dom::AllChildrenIterator, 20> mStateStack;
+ uint32_t mARIAOwnsIdx;
+
+ int32_t mChildFilter;
+ uint32_t mFlags;
+
+ enum Phase { eAtStart, eAtDOM, eAtARIAOwns, eAtEnd };
+ Phase mPhase;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_TreeWalker_h_
diff --git a/accessible/base/XULMap.h b/accessible/base/XULMap.h
new file mode 100644
index 0000000000..4687e85814
--- /dev/null
+++ b/accessible/base/XULMap.h
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+XULMAP_TYPE(browser, OuterDocAccessible)
+XULMAP_TYPE(button, XULButtonAccessible)
+XULMAP_TYPE(checkbox, CheckboxAccessible)
+XULMAP_TYPE(dropMarker, XULDropmarkerAccessible)
+XULMAP_TYPE(editor, OuterDocAccessible)
+XULMAP_TYPE(findbar, XULToolbarAccessible)
+XULMAP_TYPE(groupbox, XULGroupboxAccessible)
+XULMAP_TYPE(iframe, OuterDocAccessible)
+XULMAP_TYPE(listheader, XULColumAccessible)
+XULMAP_TYPE(menu, XULMenuitemAccessible)
+XULMAP_TYPE(menubar, XULMenubarAccessible)
+XULMAP_TYPE(menucaption, XULMenuitemAccessible)
+XULMAP_TYPE(menuitem, XULMenuitemAccessible)
+XULMAP_TYPE(menulist, XULComboboxAccessible)
+XULMAP_TYPE(menuseparator, XULMenuSeparatorAccessible)
+XULMAP_TYPE(notification, XULAlertAccessible)
+XULMAP_TYPE(radio, XULRadioButtonAccessible)
+XULMAP_TYPE(radiogroup, XULRadioGroupAccessible)
+XULMAP_TYPE(richlistbox, XULListboxAccessible)
+XULMAP_TYPE(richlistitem, XULListitemAccessible)
+XULMAP_TYPE(statusbar, XULStatusBarAccessible)
+XULMAP_TYPE(tab, XULTabAccessible)
+XULMAP_TYPE(tabpanels, XULTabpanelsAccessible)
+XULMAP_TYPE(tabs, XULTabsAccessible)
+XULMAP_TYPE(toolbarseparator, XULToolbarSeparatorAccessible)
+XULMAP_TYPE(toolbarspacer, XULToolbarSeparatorAccessible)
+XULMAP_TYPE(toolbarspring, XULToolbarSeparatorAccessible)
+XULMAP_TYPE(treecol, XULColumnItemAccessible)
+XULMAP_TYPE(treecols, XULTreeColumAccessible)
+XULMAP_TYPE(toolbar, XULToolbarAccessible)
+XULMAP_TYPE(toolbarbutton, XULToolbarButtonAccessible)
+
+XULMAP(description,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ if (aElement->ClassList()->Contains(u"tooltip-label"_ns)) {
+ // FIXME(emilio): Why this special case?
+ return nullptr;
+ }
+
+ return new XULLabelAccessible(aElement, aContext->Document());
+ })
+
+XULMAP(tooltip,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ return new XULTooltipAccessible(aElement, aContext->Document());
+ })
+
+XULMAP(label,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ if (aElement->ClassList()->Contains(u"text-link"_ns)) {
+ return new XULLinkAccessible(aElement, aContext->Document());
+ }
+ return new XULLabelAccessible(aElement, aContext->Document());
+ })
+
+XULMAP(image,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ // Don't include nameless images in accessible tree.
+ if (!aElement->HasAttr(nsGkAtoms::tooltiptext)) {
+ return nullptr;
+ }
+
+ return new ImageAccessible(aElement, aContext->Document());
+ })
+
+XULMAP(menupopup, [](Element* aElement, LocalAccessible* aContext) {
+ return CreateMenupopupAccessible(aElement, aContext);
+})
+
+XULMAP(panel,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ static const Element::AttrValuesArray sIgnoreTypeVals[] = {
+ nsGkAtoms::autocomplete_richlistbox, nsGkAtoms::autocomplete,
+ nullptr};
+
+ if (aElement->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type,
+ sIgnoreTypeVals, eIgnoreCase) >= 0) {
+ return nullptr;
+ }
+
+ if (aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::noautofocus,
+ nsGkAtoms::_true, eCaseMatters)) {
+ return new XULAlertAccessible(aElement, aContext->Document());
+ }
+
+ return new EnumRoleAccessible<roles::PANE>(aElement,
+ aContext->Document());
+ })
+
+XULMAP(tree,
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
+ nsIContent* child =
+ nsTreeUtils::GetDescendantChild(aElement, nsGkAtoms::treechildren);
+ if (!child) return nullptr;
+
+ nsTreeBodyFrame* treeFrame = do_QueryFrame(child->GetPrimaryFrame());
+ if (!treeFrame) return nullptr;
+
+ RefPtr<nsTreeColumns> treeCols = treeFrame->Columns();
+ uint32_t count = treeCols->Count();
+
+ // Outline of list accessible.
+ if (count == 1) {
+ return new XULTreeAccessible(aElement, aContext->Document(),
+ treeFrame);
+ }
+
+ // Table or tree table accessible.
+ return new XULTreeGridAccessible(aElement, aContext->Document(),
+ treeFrame);
+ })
diff --git a/accessible/base/moz.build b/accessible/base/moz.build
new file mode 100644
index 0000000000..b65c90ceba
--- /dev/null
+++ b/accessible/base/moz.build
@@ -0,0 +1,122 @@
+# -*- 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/.
+
+GeneratedFile(
+ "RelationType.h",
+ script="/accessible/base/RelationTypeGen.py",
+ entry_point="generate",
+ inputs=["/accessible/interfaces/nsIAccessibleRelation.idl"],
+)
+GeneratedFile(
+ "Role.h",
+ script="/accessible/base/RoleHGen.py",
+ entry_point="generate",
+ inputs=["/accessible/interfaces/nsIAccessibleRole.idl"],
+)
+
+EXPORTS += ["AccEvent.h", "nsAccessibilityService.h"]
+
+EXPORTS.mozilla.a11y += [
+ "!RelationType.h",
+ "!Role.h",
+ "AccAttributes.h",
+ "AccGroupInfo.h",
+ "AccTypes.h",
+ "CacheConstants.h",
+ "DocManager.h",
+ "FocusManager.h",
+ "IDSet.h",
+ "Platform.h",
+ "SelectionManager.h",
+ "States.h",
+]
+
+if CONFIG["MOZ_DEBUG"]:
+ EXPORTS.mozilla.a11y += [
+ "Logging.h",
+ ]
+
+UNIFIED_SOURCES += [
+ "AccAttributes.cpp",
+ "AccEvent.cpp",
+ "AccGroupInfo.cpp",
+ "AccIterator.cpp",
+ "ARIAMap.cpp",
+ "ARIAStateMap.cpp",
+ "Asserts.cpp",
+ "CachedTableAccessible.cpp",
+ "DocManager.cpp",
+ "EmbeddedObjCollector.cpp",
+ "EventQueue.cpp",
+ "EventTree.cpp",
+ "Filters.cpp",
+ "FocusManager.cpp",
+ "NotificationController.cpp",
+ "nsAccessibilityService.cpp",
+ "nsAccUtils.cpp",
+ "nsCoreUtils.cpp",
+ "nsEventShell.cpp",
+ "nsTextEquivUtils.cpp",
+ "Pivot.cpp",
+ "SelectionManager.cpp",
+ "StyleInfo.cpp",
+ "TextAttrs.cpp",
+ "TextLeafRange.cpp",
+ "TextRange.cpp",
+ "TextUpdater.cpp",
+ "TreeWalker.cpp",
+]
+
+if CONFIG["A11Y_LOG"]:
+ UNIFIED_SOURCES += [
+ "Logging.cpp",
+ ]
+
+LOCAL_INCLUDES += [
+ "/accessible/generic",
+ "/accessible/html",
+ "/accessible/ipc",
+ "/dom/base",
+ "/dom/xul",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/xpcom",
+ "/accessible/xul",
+ "/dom/base",
+ "/ipc/chromium/src",
+ "/layout/generic",
+ "/layout/style",
+ "/layout/xul",
+ "/layout/xul/tree/",
+]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ LOCAL_INCLUDES += [
+ "/accessible/atk",
+ "/gfx/cairo/cairo/src",
+ ]
+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",
+ ]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/accessible/base/nsAccCache.h b/accessible/base/nsAccCache.h
new file mode 100644
index 0000000000..db0c877035
--- /dev/null
+++ b/accessible/base/nsAccCache.h
@@ -0,0 +1,24 @@
+/* -*- 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 _nsAccCache_H_
+#define _nsAccCache_H_
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible cache utils
+////////////////////////////////////////////////////////////////////////////////
+
+template <class T>
+void UnbindCacheEntriesFromDocument(
+ nsRefPtrHashtable<nsPtrHashKey<const void>, T>& aCache) {
+ for (auto iter = aCache.Iter(); !iter.Done(); iter.Next()) {
+ T* accessible = iter.Data();
+ MOZ_ASSERT(accessible && !accessible->IsDefunct());
+ accessible->Document()->UnbindFromDocument(accessible);
+ iter.Remove();
+ }
+}
+
+#endif
diff --git a/accessible/base/nsAccUtils.cpp b/accessible/base/nsAccUtils.cpp
new file mode 100644
index 0000000000..82af56348f
--- /dev/null
+++ b/accessible/base/nsAccUtils.cpp
@@ -0,0 +1,626 @@
+/* -*- 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 "nsAccUtils.h"
+
+#include "AccAttributes.h"
+#include "ARIAMap.h"
+#include "nsCoreUtils.h"
+#include "nsGenericHTMLElement.h"
+#include "DocAccessible.h"
+#include "DocAccessibleParent.h"
+#include "HyperTextAccessible.h"
+#include "nsIAccessibleTypes.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+#include "TextLeafAccessible.h"
+
+#include "nsIBaseWindow.h"
+#include "nsIDocShellTreeOwner.h"
+#include "nsIDOMXULContainerElement.h"
+#include "mozilla/a11y/RemoteAccessible.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/ElementInternals.h"
+#include "nsAccessibilityService.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+void nsAccUtils::SetAccGroupAttrs(AccAttributes* aAttributes, int32_t aLevel,
+ int32_t aSetSize, int32_t aPosInSet) {
+ nsAutoString value;
+
+ if (aLevel) {
+ aAttributes->SetAttribute(nsGkAtoms::level, aLevel);
+ }
+
+ if (aSetSize && aPosInSet) {
+ aAttributes->SetAttribute(nsGkAtoms::posinset, aPosInSet);
+ aAttributes->SetAttribute(nsGkAtoms::setsize, aSetSize);
+ }
+}
+
+int32_t nsAccUtils::GetLevelForXULContainerItem(nsIContent* aContent) {
+ nsCOMPtr<nsIDOMXULContainerItemElement> item =
+ aContent->AsElement()->AsXULContainerItem();
+ if (!item) return 0;
+
+ nsCOMPtr<dom::Element> containerElement;
+ item->GetParentContainer(getter_AddRefs(containerElement));
+ nsCOMPtr<nsIDOMXULContainerElement> container =
+ containerElement ? containerElement->AsXULContainer() : nullptr;
+ if (!container) return 0;
+
+ // Get level of the item.
+ int32_t level = -1;
+ while (container) {
+ level++;
+
+ container->GetParentContainer(getter_AddRefs(containerElement));
+ container = containerElement ? containerElement->AsXULContainer() : nullptr;
+ }
+
+ return level;
+}
+
+void nsAccUtils::SetLiveContainerAttributes(AccAttributes* aAttributes,
+ Accessible* aStartAcc) {
+ nsAutoString live, relevant, busy;
+ nsStaticAtom* role = nullptr;
+ Maybe<bool> atomic;
+ for (Accessible* acc = aStartAcc; acc; acc = acc->Parent()) {
+ // We only want the nearest value for each attribute. If we already got a
+ // value, don't bother fetching it from further ancestors.
+ const bool wasLiveEmpty = live.IsEmpty();
+ acc->LiveRegionAttributes(wasLiveEmpty ? &live : nullptr,
+ relevant.IsEmpty() ? &relevant : nullptr,
+ atomic ? nullptr : &atomic,
+ busy.IsEmpty() ? &busy : nullptr);
+ if (wasLiveEmpty) {
+ const nsRoleMapEntry* roleMap = acc->ARIARoleMap();
+ if (live.IsEmpty()) {
+ // aria-live wasn't explicitly set. See if an aria-live value is implied
+ // by an ARIA role or markup element.
+ if (roleMap) {
+ GetLiveAttrValue(roleMap->liveAttRule, live);
+ } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
+ acc, nsGkAtoms::aria_live)) {
+ value->ToString(live);
+ }
+ }
+ if (!live.IsEmpty() && roleMap &&
+ roleMap->roleAtom != nsGkAtoms::_empty) {
+ role = roleMap->roleAtom;
+ }
+ }
+ if (acc->IsDoc()) {
+ break;
+ }
+ }
+ if (!live.IsEmpty()) {
+ aAttributes->SetAttribute(nsGkAtoms::containerLive, std::move(live));
+ }
+ if (role) {
+ aAttributes->SetAttribute(nsGkAtoms::containerLiveRole, std::move(role));
+ }
+ if (!relevant.IsEmpty()) {
+ aAttributes->SetAttribute(nsGkAtoms::containerRelevant,
+ std::move(relevant));
+ }
+ if (atomic) {
+ aAttributes->SetAttribute(nsGkAtoms::containerAtomic, *atomic);
+ }
+ if (!busy.IsEmpty()) {
+ aAttributes->SetAttribute(nsGkAtoms::containerBusy, std::move(busy));
+ }
+}
+
+bool nsAccUtils::HasDefinedARIAToken(nsIContent* aContent, nsAtom* aAtom) {
+ NS_ASSERTION(aContent, "aContent is null in call to HasDefinedARIAToken!");
+
+ if (!aContent->IsElement()) return false;
+
+ dom::Element* element = aContent->AsElement();
+ if (auto* htmlElement = nsGenericHTMLElement::FromNode(element);
+ htmlElement && !element->HasAttr(aAtom)) {
+ const auto* defaults = GetARIADefaults(htmlElement);
+ if (!defaults) {
+ return false;
+ }
+ return HasDefinedARIAToken(defaults, aAtom);
+ }
+ return HasDefinedARIAToken(&element->GetAttrs(), aAtom);
+}
+
+bool nsAccUtils::HasDefinedARIAToken(const AttrArray* aAttrs, nsAtom* aAtom) {
+ return aAttrs->HasAttr(aAtom) &&
+ !aAttrs->AttrValueIs(kNameSpaceID_None, aAtom, nsGkAtoms::_empty,
+ eCaseMatters) &&
+ !aAttrs->AttrValueIs(kNameSpaceID_None, aAtom, nsGkAtoms::_undefined,
+ eCaseMatters);
+}
+
+nsStaticAtom* nsAccUtils::NormalizeARIAToken(const AttrArray* aAttrs,
+ nsAtom* aAttr) {
+ if (!HasDefinedARIAToken(aAttrs, aAttr)) {
+ return nsGkAtoms::_empty;
+ }
+
+ if (aAttr == nsGkAtoms::aria_current) {
+ static AttrArray::AttrValuesArray tokens[] = {
+ nsGkAtoms::page, nsGkAtoms::step, nsGkAtoms::location_,
+ nsGkAtoms::date, nsGkAtoms::time, nsGkAtoms::_true,
+ nullptr};
+ int32_t idx =
+ aAttrs->FindAttrValueIn(kNameSpaceID_None, aAttr, tokens, eCaseMatters);
+ // If the token is present, return it, otherwise TRUE as per spec.
+ return (idx >= 0) ? tokens[idx] : nsGkAtoms::_true;
+ }
+
+ static AttrArray::AttrValuesArray tokens[] = {
+ nsGkAtoms::_false, nsGkAtoms::_true, nsGkAtoms::mixed, nullptr};
+ int32_t idx =
+ aAttrs->FindAttrValueIn(kNameSpaceID_None, aAttr, tokens, eCaseMatters);
+ if (idx >= 0) {
+ return tokens[idx];
+ }
+
+ return nullptr;
+}
+
+nsStaticAtom* nsAccUtils::NormalizeARIAToken(dom::Element* aElement,
+ nsAtom* aAttr) {
+ if (auto* htmlElement = nsGenericHTMLElement::FromNode(aElement);
+ htmlElement && !aElement->HasAttr(aAttr)) {
+ const auto* defaults = GetARIADefaults(htmlElement);
+ if (!defaults) {
+ return nsGkAtoms::_empty;
+ }
+ return NormalizeARIAToken(defaults, aAttr);
+ }
+ return NormalizeARIAToken(&aElement->GetAttrs(), aAttr);
+}
+
+Accessible* nsAccUtils::GetSelectableContainer(const Accessible* aAccessible,
+ uint64_t aState) {
+ if (!aAccessible) return nullptr;
+
+ if (!(aState & states::SELECTABLE)) return nullptr;
+ MOZ_ASSERT(!aAccessible->IsDoc());
+
+ const Accessible* parent = aAccessible;
+ while ((parent = parent->Parent()) && !parent->IsSelect()) {
+ if (parent->IsDoc() || parent->Role() == roles::PANE) {
+ return nullptr;
+ }
+ }
+ return const_cast<Accessible*>(parent);
+}
+
+LocalAccessible* nsAccUtils::GetSelectableContainer(
+ LocalAccessible* aAccessible, uint64_t aState) {
+ Accessible* selectable =
+ GetSelectableContainer(static_cast<Accessible*>(aAccessible), aState);
+ return selectable ? selectable->AsLocal() : nullptr;
+}
+
+bool nsAccUtils::IsDOMAttrTrue(const LocalAccessible* aAccessible,
+ nsAtom* aAttr) {
+ dom::Element* el = aAccessible->Elm();
+ return el && ARIAAttrValueIs(el, aAttr, nsGkAtoms::_true, eCaseMatters);
+}
+
+Accessible* nsAccUtils::TableFor(Accessible* aAcc) {
+ if (!aAcc ||
+ (!aAcc->IsTable() && !aAcc->IsTableRow() && !aAcc->IsTableCell())) {
+ return nullptr;
+ }
+ Accessible* table = aAcc;
+ for (; table && !table->IsTable(); table = table->Parent()) {
+ }
+ // We don't assert (table && table->IsTable()) here because
+ // it's possible for this tree walk to yield no table at all
+ // ex. because a table part has been moved in the tree
+ // using aria-owns.
+ return table;
+}
+
+LocalAccessible* nsAccUtils::TableFor(LocalAccessible* aRow) {
+ Accessible* table = TableFor(static_cast<Accessible*>(aRow));
+ return table ? table->AsLocal() : nullptr;
+}
+
+HyperTextAccessible* nsAccUtils::GetTextContainer(nsINode* aNode) {
+ // Get text accessible containing the result node.
+ DocAccessible* doc = GetAccService()->GetDocAccessible(aNode->OwnerDoc());
+ LocalAccessible* accessible =
+ doc ? doc->GetAccessibleOrContainer(aNode) : nullptr;
+ if (!accessible) return nullptr;
+
+ do {
+ HyperTextAccessible* textAcc = accessible->AsHyperText();
+ if (textAcc) return textAcc;
+
+ accessible = accessible->LocalParent();
+ } while (accessible);
+
+ return nullptr;
+}
+
+LayoutDeviceIntPoint nsAccUtils::ConvertToScreenCoords(
+ int32_t aX, int32_t aY, uint32_t aCoordinateType, Accessible* aAccessible) {
+ LayoutDeviceIntPoint coords(aX, aY);
+
+ switch (aCoordinateType) {
+ // Regardless of coordinate type, the coords returned
+ // are in dev pixels.
+ case nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE:
+ break;
+
+ case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE: {
+ coords += GetScreenCoordsForWindow(aAccessible);
+ break;
+ }
+
+ case nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE: {
+ coords += GetScreenCoordsForParent(aAccessible);
+ break;
+ }
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("invalid coord type!");
+ }
+
+ return coords;
+}
+
+void nsAccUtils::ConvertScreenCoordsTo(int32_t* aX, int32_t* aY,
+ uint32_t aCoordinateType,
+ Accessible* aAccessible) {
+ switch (aCoordinateType) {
+ // Regardless of coordinate type, the values returned for
+ // aX and aY are in dev pixels.
+ case nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE:
+ break;
+
+ case nsIAccessibleCoordinateType::COORDTYPE_WINDOW_RELATIVE: {
+ LayoutDeviceIntPoint coords = GetScreenCoordsForWindow(aAccessible);
+ *aX -= coords.x;
+ *aY -= coords.y;
+ break;
+ }
+
+ case nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE: {
+ LayoutDeviceIntPoint coords = GetScreenCoordsForParent(aAccessible);
+ *aX -= coords.x;
+ *aY -= coords.y;
+ break;
+ }
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("invalid coord type!");
+ }
+}
+
+LayoutDeviceIntPoint nsAccUtils::GetScreenCoordsForParent(
+ Accessible* aAccessible) {
+ if (!aAccessible) return LayoutDeviceIntPoint();
+
+ if (Accessible* parent = aAccessible->Parent()) {
+ LayoutDeviceIntRect parentBounds = parent->Bounds();
+ // The rect returned from Bounds() is already in dev
+ // pixels, so we don't need to do any conversion here.
+ return parentBounds.TopLeft();
+ }
+
+ return LayoutDeviceIntPoint();
+}
+
+LayoutDeviceIntPoint nsAccUtils::GetScreenCoordsForWindow(
+ Accessible* aAccessible) {
+ LayoutDeviceIntPoint coords(0, 0);
+ a11y::LocalAccessible* localAcc = aAccessible->AsLocal();
+ if (!localAcc) {
+ localAcc = aAccessible->AsRemote()->OuterDocOfRemoteBrowser();
+ if (!localAcc) {
+ // This could be null if the tab is closing but the document is still
+ // being shut down.
+ return coords;
+ }
+ }
+
+ nsCOMPtr<nsIDocShellTreeItem> treeItem(
+ nsCoreUtils::GetDocShellFor(localAcc->GetNode()));
+ if (!treeItem) return coords;
+
+ nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
+ treeItem->GetTreeOwner(getter_AddRefs(treeOwner));
+ if (!treeOwner) return coords;
+
+ nsCOMPtr<nsIBaseWindow> baseWindow = do_QueryInterface(treeOwner);
+ if (baseWindow) {
+ baseWindow->GetPosition(&coords.x.value,
+ &coords.y.value); // in device pixels
+ }
+
+ return coords;
+}
+
+bool nsAccUtils::GetLiveAttrValue(uint32_t aRule, nsAString& aValue) {
+ switch (aRule) {
+ case eOffLiveAttr:
+ aValue = u"off"_ns;
+ return true;
+ case ePoliteLiveAttr:
+ aValue = u"polite"_ns;
+ return true;
+ case eAssertiveLiveAttr:
+ aValue = u"assertive"_ns;
+ return true;
+ }
+
+ return false;
+}
+
+#ifdef DEBUG
+
+bool nsAccUtils::IsTextInterfaceSupportCorrect(LocalAccessible* aAccessible) {
+ // Don't test for accessible docs, it makes us create accessibles too
+ // early and fire mutation events before we need to
+ if (aAccessible->IsDoc()) return true;
+
+ bool foundText = false;
+ uint32_t childCount = aAccessible->ChildCount();
+ for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
+ LocalAccessible* child = aAccessible->LocalChildAt(childIdx);
+ if (child && child->IsText()) {
+ foundText = true;
+ break;
+ }
+ }
+
+ return !foundText || aAccessible->IsHyperText();
+}
+#endif
+
+uint32_t nsAccUtils::TextLength(Accessible* aAccessible) {
+ if (!aAccessible->IsText()) {
+ return 1;
+ }
+
+ if (LocalAccessible* localAcc = aAccessible->AsLocal()) {
+ TextLeafAccessible* textLeaf = localAcc->AsTextLeaf();
+ if (textLeaf) {
+ return textLeaf->Text().Length();
+ }
+ } else if (aAccessible->IsText()) {
+ RemoteAccessible* remoteAcc = aAccessible->AsRemote();
+ MOZ_ASSERT(remoteAcc);
+ return remoteAcc->GetCachedTextLength();
+ }
+
+ // For list bullets (or anything other accessible which would compute its own
+ // text. They don't have their own frame.
+ // XXX In the future, list bullets may have frame and anon content, so
+ // we should be able to remove this at that point
+ nsAutoString text;
+ aAccessible->AppendTextTo(text); // Get all the text
+ return text.Length();
+}
+
+bool nsAccUtils::MustPrune(Accessible* aAccessible) {
+ MOZ_ASSERT(aAccessible);
+ roles::Role role = aAccessible->Role();
+
+ if (role == roles::SLIDER || role == roles::PROGRESSBAR) {
+ // Always prune the tree for sliders and progressbars, as it doesn't make
+ // sense for either to have descendants. Per the ARIA spec, children of
+ // these elements are presentational. They also confuse NVDA.
+ return true;
+ }
+
+ if (role != roles::MENUITEM && role != roles::COMBOBOX_OPTION &&
+ role != roles::OPTION && role != roles::ENTRY &&
+ role != roles::FLAT_EQUATION && role != roles::PASSWORD_TEXT &&
+ role != roles::PUSHBUTTON && role != roles::TOGGLE_BUTTON &&
+ role != roles::GRAPHIC && role != roles::SEPARATOR) {
+ // If it doesn't match any of these roles, don't prune its children.
+ return false;
+ }
+
+ if (aAccessible->ChildCount() != 1) {
+ // If the accessible has more than one child, don't prune it.
+ return false;
+ }
+
+ roles::Role childRole = aAccessible->FirstChild()->Role();
+ // If the accessible's child is a text leaf, prune the accessible.
+ return childRole == roles::TEXT_LEAF || childRole == roles::STATICTEXT;
+}
+
+bool nsAccUtils::IsARIALive(const LocalAccessible* aAccessible) {
+ // Get computed aria-live property based on the closest container with the
+ // attribute. Inner nodes override outer nodes within the same
+ // document.
+ // This should be the same as the container-live attribute, but we don't need
+ // the other container-* attributes, so we can't use the same function.
+ nsIContent* ancestor = aAccessible->GetContent();
+ if (!ancestor) {
+ return false;
+ }
+ dom::Document* doc = ancestor->GetComposedDoc();
+ if (!doc) {
+ return false;
+ }
+ dom::Element* topEl = doc->GetRootElement();
+ while (ancestor) {
+ const nsRoleMapEntry* role = nullptr;
+ if (ancestor->IsElement()) {
+ role = aria::GetRoleMap(ancestor->AsElement());
+ }
+ nsAutoString live;
+ if (HasDefinedARIAToken(ancestor, nsGkAtoms::aria_live)) {
+ GetARIAAttr(ancestor->AsElement(), nsGkAtoms::aria_live, live);
+ } else if (role) {
+ GetLiveAttrValue(role->liveAttRule, live);
+ } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
+ ancestor, nsGkAtoms::aria_live)) {
+ value->ToString(live);
+ }
+ if (!live.IsEmpty() && !live.EqualsLiteral("off")) {
+ return true;
+ }
+
+ if (ancestor == topEl) {
+ break;
+ }
+
+ ancestor = ancestor->GetParent();
+ if (!ancestor) {
+ ancestor = topEl; // Use <body>/<frameset>
+ }
+ }
+
+ return false;
+}
+
+Accessible* nsAccUtils::DocumentFor(Accessible* aAcc) {
+ if (!aAcc) {
+ return nullptr;
+ }
+ if (LocalAccessible* localAcc = aAcc->AsLocal()) {
+ return localAcc->Document();
+ }
+ return aAcc->AsRemote()->Document();
+}
+
+Accessible* nsAccUtils::GetAccessibleByID(Accessible* aDoc, uint64_t aID) {
+ if (!aDoc) {
+ return nullptr;
+ }
+ if (LocalAccessible* localAcc = aDoc->AsLocal()) {
+ if (DocAccessible* doc = localAcc->AsDoc()) {
+ if (!aID) {
+ // GetAccessibleByUniqueID doesn't treat 0 as the document.
+ return aDoc;
+ }
+ return doc->GetAccessibleByUniqueID(
+ reinterpret_cast<void*>(static_cast<uintptr_t>(aID)));
+ }
+ } else if (DocAccessibleParent* doc = aDoc->AsRemote()->AsDoc()) {
+ return doc->GetAccessible(aID);
+ }
+ return nullptr;
+}
+
+void nsAccUtils::DocumentURL(Accessible* aDoc, nsAString& aURL) {
+ MOZ_ASSERT(aDoc && aDoc->IsDoc());
+ if (LocalAccessible* localAcc = aDoc->AsLocal()) {
+ return localAcc->AsDoc()->URL(aURL);
+ }
+ return aDoc->AsRemote()->AsDoc()->URL(aURL);
+}
+
+void nsAccUtils::DocumentMimeType(Accessible* aDoc, nsAString& aMimeType) {
+ MOZ_ASSERT(aDoc && aDoc->IsDoc());
+ if (LocalAccessible* localAcc = aDoc->AsLocal()) {
+ return localAcc->AsDoc()->MimeType(aMimeType);
+ }
+ return aDoc->AsRemote()->AsDoc()->MimeType(aMimeType);
+}
+
+// ARIA Accessibility Default Accessors
+const AttrArray* nsAccUtils::GetARIADefaults(dom::Element* aElement) {
+ auto* element = nsGenericHTMLElement::FromNode(aElement);
+ if (!element) {
+ return nullptr;
+ }
+ auto* internals = element->GetInternals();
+ if (!internals) {
+ return nullptr;
+ }
+ return &internals->GetAttrs();
+}
+
+bool nsAccUtils::HasARIAAttr(dom::Element* aElement, const nsAtom* aName) {
+ if (aElement->HasAttr(aName)) {
+ return true;
+ }
+ const auto* defaults = GetARIADefaults(aElement);
+ if (!defaults) {
+ return false;
+ }
+ return defaults->HasAttr(aName);
+}
+
+bool nsAccUtils::GetARIAAttr(dom::Element* aElement, const nsAtom* aName,
+ nsAString& aResult) {
+ if (aElement->GetAttr(aName, aResult)) {
+ return true;
+ }
+ const auto* defaults = GetARIADefaults(aElement);
+ if (!defaults) {
+ return false;
+ }
+ return defaults->GetAttr(aName, aResult);
+}
+
+const nsAttrValue* nsAccUtils::GetARIAAttr(dom::Element* aElement,
+ const nsAtom* aName) {
+ if (const auto* val = aElement->GetParsedAttr(aName, kNameSpaceID_None)) {
+ return val;
+ }
+ const auto* defaults = GetARIADefaults(aElement);
+ if (!defaults) {
+ return nullptr;
+ }
+ return defaults->GetAttr(aName, kNameSpaceID_None);
+}
+
+bool nsAccUtils::ARIAAttrValueIs(dom::Element* aElement, const nsAtom* aName,
+ const nsAString& aValue,
+ nsCaseTreatment aCaseSensitive) {
+ if (aElement->AttrValueIs(kNameSpaceID_None, aName, aValue, aCaseSensitive)) {
+ return true;
+ }
+ const auto* defaults = GetARIADefaults(aElement);
+ if (!defaults) {
+ return false;
+ }
+ return defaults->AttrValueIs(kNameSpaceID_None, aName, aValue,
+ aCaseSensitive);
+}
+
+bool nsAccUtils::ARIAAttrValueIs(dom::Element* aElement, const nsAtom* aName,
+ const nsAtom* aValue,
+ nsCaseTreatment aCaseSensitive) {
+ if (aElement->AttrValueIs(kNameSpaceID_None, aName, aValue, aCaseSensitive)) {
+ return true;
+ }
+ const auto* defaults = GetARIADefaults(aElement);
+ if (!defaults) {
+ return false;
+ }
+ return defaults->AttrValueIs(kNameSpaceID_None, aName, aValue,
+ aCaseSensitive);
+}
+
+int32_t nsAccUtils::FindARIAAttrValueIn(dom::Element* aElement,
+ const nsAtom* aName,
+ AttrArray::AttrValuesArray* aValues,
+ nsCaseTreatment aCaseSensitive) {
+ int32_t index = aElement->FindAttrValueIn(kNameSpaceID_None, aName, aValues,
+ aCaseSensitive);
+ if (index == AttrArray::ATTR_MISSING) {
+ const auto* defaults = GetARIADefaults(aElement);
+ if (!defaults) {
+ return index;
+ }
+ index = defaults->FindAttrValueIn(kNameSpaceID_None, aName, aValues,
+ aCaseSensitive);
+ }
+ return index;
+}
diff --git a/accessible/base/nsAccUtils.h b/accessible/base/nsAccUtils.h
new file mode 100644
index 0000000000..ed51eef709
--- /dev/null
+++ b/accessible/base/nsAccUtils.h
@@ -0,0 +1,299 @@
+/* -*- 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 nsAccUtils_h_
+#define nsAccUtils_h_
+
+#include "mozilla/a11y/LocalAccessible.h"
+#include "mozilla/a11y/DocManager.h"
+
+#include "nsAccessibilityService.h"
+#include "nsCoreUtils.h"
+
+#include "nsIDocShell.h"
+#include "nsPoint.h"
+
+namespace mozilla {
+
+class PresShell;
+
+namespace dom {
+class Element;
+}
+
+namespace a11y {
+
+class HyperTextAccessible;
+class DocAccessible;
+class Attribute;
+
+class nsAccUtils {
+ public:
+ /**
+ * Set group attributes ('level', 'setsize', 'posinset').
+ */
+ static void SetAccGroupAttrs(AccAttributes* aAttributes, int32_t aLevel,
+ int32_t aSetSize, int32_t aPosInSet);
+
+ /**
+ * Compute group level for nsIDOMXULContainerItemElement node.
+ */
+ static int32_t GetLevelForXULContainerItem(nsIContent* aContent);
+
+ /**
+ * Set container-foo live region attributes for the given node.
+ *
+ * @param aAttributes where to store the attributes
+ * @param aStartAcc Accessible to start from
+ */
+ static void SetLiveContainerAttributes(AccAttributes* aAttributes,
+ Accessible* aStartAcc);
+
+ /**
+ * Any ARIA property of type boolean or NMTOKEN is undefined if the ARIA
+ * property is not present, or is "" or "undefined". Do not call
+ * this method for properties of type string, decimal, IDREF or IDREFS.
+ *
+ * Return true if the ARIA property is defined, otherwise false
+ */
+ static bool HasDefinedARIAToken(nsIContent* aContent, nsAtom* aAtom);
+ static bool HasDefinedARIAToken(const AttrArray* aAttrs, nsAtom* aAtom);
+
+ /**
+ * If the given ARIA attribute has a specific known token value, return it.
+ * If the specification demands for a fallback value for unknown attribute
+ * values, return that. For all others, return a nullptr.
+ */
+ static nsStaticAtom* NormalizeARIAToken(const AttrArray* aAttrs,
+ nsAtom* aAttr);
+ static nsStaticAtom* NormalizeARIAToken(mozilla::dom::Element* aElement,
+ nsAtom* aAttr);
+
+ /**
+ * Return document accessible for the given DOM node.
+ */
+ static DocAccessible* GetDocAccessibleFor(nsINode* aNode) {
+ return GetAccService()->GetDocAccessible(
+ nsCoreUtils::GetPresShellFor(aNode));
+ }
+
+ /**
+ * Return document accessible for the given docshell.
+ */
+ static DocAccessible* GetDocAccessibleFor(nsIDocShellTreeItem* aContainer) {
+ nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aContainer));
+ return GetAccService()->GetDocAccessible(docShell->GetPresShell());
+ }
+
+ /**
+ * Return single or multi selectable container for the given item.
+ *
+ * @param aAccessible [in] the item accessible
+ * @param aState [in] the state of the item accessible
+ */
+ static Accessible* GetSelectableContainer(const Accessible* aAccessible,
+ uint64_t aState);
+ static LocalAccessible* GetSelectableContainer(LocalAccessible* aAccessible,
+ uint64_t aState);
+
+ /**
+ * Return a text container accessible for the given node.
+ */
+ static HyperTextAccessible* GetTextContainer(nsINode* aNode);
+
+ static Accessible* TableFor(Accessible* aRow);
+ static LocalAccessible* TableFor(LocalAccessible* aRow);
+
+ static const LocalAccessible* TableFor(const LocalAccessible* aAcc) {
+ return TableFor(const_cast<LocalAccessible*>(aAcc));
+ }
+
+ /**
+ * Return true if the DOM node of a given accessible has a given attribute
+ * with a value of "true".
+ */
+ static bool IsDOMAttrTrue(const LocalAccessible* aAccessible, nsAtom* aAttr);
+
+ /**
+ * Return true if the DOM node of given accessible has aria-selected="true"
+ * attribute.
+ */
+ static inline bool IsARIASelected(const LocalAccessible* aAccessible) {
+ return IsDOMAttrTrue(aAccessible, nsGkAtoms::aria_selected);
+ }
+
+ /**
+ * Return true if the DOM node of given accessible has
+ * aria-multiselectable="true" attribute.
+ */
+ static inline bool IsARIAMultiSelectable(const LocalAccessible* aAccessible) {
+ return IsDOMAttrTrue(aAccessible, nsGkAtoms::aria_multiselectable);
+ }
+
+ /**
+ * Converts the given coordinates to coordinates relative screen.
+ *
+ * @param aX [in] the given x coord in dev pixels
+ * @param aY [in] the given y coord in dev pixels
+ * @param aCoordinateType [in] specifies coordinates origin (refer to
+ * nsIAccessibleCoordinateType)
+ * @param aAccessible [in] the accessible if coordinates are given
+ * relative it.
+ * @return converted coordinates
+ */
+ static LayoutDeviceIntPoint ConvertToScreenCoords(int32_t aX, int32_t aY,
+ uint32_t aCoordinateType,
+ Accessible* aAccessible);
+
+ /**
+ * Converts the given coordinates relative screen to another coordinate
+ * system.
+ *
+ * @param aX [in, out] the given x coord in dev pixels
+ * @param aY [in, out] the given y coord in dev pixels
+ * @param aCoordinateType [in] specifies coordinates origin (refer to
+ * nsIAccessibleCoordinateType)
+ * @param aAccessible [in] the accessible if coordinates are given
+ * relative it
+ */
+ static void ConvertScreenCoordsTo(int32_t* aX, int32_t* aY,
+ uint32_t aCoordinateType,
+ Accessible* aAccessible);
+
+ /**
+ * Returns screen-relative coordinates (in dev pixels) for the parent of the
+ * given accessible.
+ *
+ * @param [in] aAccessible the accessible
+ */
+ static LayoutDeviceIntPoint GetScreenCoordsForParent(Accessible* aAccessible);
+
+ /**
+ * Returns coordinates in device pixels relative screen for the top level
+ * window.
+ *
+ * @param aAccessible the acc hosted in the window.
+ */
+ static mozilla::LayoutDeviceIntPoint GetScreenCoordsForWindow(
+ mozilla::a11y::Accessible* aAccessible);
+
+ /**
+ * Get the 'live' or 'container-live' object attribute value from the given
+ * ELiveAttrRule constant.
+ *
+ * @param aRule [in] rule constant (see ELiveAttrRule in nsAccMap.h)
+ * @param aValue [out] object attribute value
+ *
+ * @return true if object attribute should be exposed
+ */
+ static bool GetLiveAttrValue(uint32_t aRule, nsAString& aValue);
+
+#ifdef DEBUG
+ /**
+ * Detect whether the given accessible object implements nsIAccessibleText,
+ * when it is text or has text child node.
+ */
+ static bool IsTextInterfaceSupportCorrect(LocalAccessible* aAccessible);
+#endif
+
+ /**
+ * Return text length of the given accessible, return 0 on failure.
+ */
+ static uint32_t TextLength(Accessible* aAccessible);
+
+ /**
+ * Transform nsIAccessibleStates constants to internal state constant.
+ */
+ static inline uint64_t To64State(uint32_t aState1, uint32_t aState2) {
+ return static_cast<uint64_t>(aState1) +
+ (static_cast<uint64_t>(aState2) << 31);
+ }
+
+ /**
+ * Transform internal state constant to nsIAccessibleStates constants.
+ */
+ static inline void To32States(uint64_t aState64, uint32_t* aState1,
+ uint32_t* aState2) {
+ *aState1 = aState64 & 0x7fffffff;
+ if (aState2) *aState2 = static_cast<uint32_t>(aState64 >> 31);
+ }
+
+ static uint32_t To32States(uint64_t aState, bool* aIsExtra) {
+ uint32_t extraState = aState >> 31;
+ *aIsExtra = !!extraState;
+ return extraState ? extraState : aState;
+ }
+
+ /**
+ * Return true if the given accessible can't have children. Used when exposing
+ * to platform accessibility APIs, should the children be pruned off?
+ */
+ static bool MustPrune(Accessible* aAccessible);
+
+ /**
+ * Return true if the given accessible is within an ARIA live region; i.e.
+ * the container-live attribute would be something other than "off" or empty.
+ */
+ static bool IsARIALive(const LocalAccessible* aAccessible);
+
+ /**
+ * Get the document Accessible which owns a given Accessible.
+ * This function is needed because there is no unified base class for local
+ * and remote documents.
+ * If aAcc is null, null will be returned.
+ */
+ static Accessible* DocumentFor(Accessible* aAcc);
+
+ /**
+ * Get an Accessible in a given document by its unique id.
+ * An Accessible's id can be obtained using Accessible::ID.
+ * This function is needed because there is no unified base class for local
+ * and remote documents.
+ * If aDoc is nul, null will be returned.
+ */
+ static Accessible* GetAccessibleByID(Accessible* aDoc, uint64_t aID);
+
+ /**
+ * Get the URL for a given document.
+ * This function is needed because there is no unified base class for local
+ * and remote documents.
+ */
+ static void DocumentURL(Accessible* aDoc, nsAString& aURL);
+
+ /**
+ * Get the mime type for a given document.
+ * This function is needed because there is no unified base class for local
+ * and remote documents.
+ */
+ static void DocumentMimeType(Accessible* aDoc, nsAString& aMimeType);
+
+ /**
+ * Accessors for element attributes that are aware of CustomElement ARIA
+ * accessibility defaults. If the element does not have the provided
+ * attribute defined directly on it, we will then attempt to fetch the
+ * default instead.
+ */
+ static const AttrArray* GetARIADefaults(dom::Element* aElement);
+ static bool HasARIAAttr(dom::Element* aElement, const nsAtom* aName);
+ static bool GetARIAAttr(dom::Element* aElement, const nsAtom* aName,
+ nsAString& aResult);
+ static const nsAttrValue* GetARIAAttr(dom::Element* aElement,
+ const nsAtom* aName);
+ static bool ARIAAttrValueIs(dom::Element* aElement, const nsAtom* aName,
+ const nsAString& aValue,
+ nsCaseTreatment aCaseSensitive);
+ static bool ARIAAttrValueIs(dom::Element* aElement, const nsAtom* aName,
+ const nsAtom* aValue,
+ nsCaseTreatment aCaseSensitive);
+ static int32_t FindARIAAttrValueIn(dom::Element* aElement,
+ const nsAtom* aName,
+ AttrArray::AttrValuesArray* aValues,
+ nsCaseTreatment aCaseSensitive);
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/base/nsAccessibilityService.cpp b/accessible/base/nsAccessibilityService.cpp
new file mode 100644
index 0000000000..c31dd666ce
--- /dev/null
+++ b/accessible/base/nsAccessibilityService.cpp
@@ -0,0 +1,1933 @@
+/* -*- 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 "nsAccessibilityService.h"
+
+// NOTE: alphabetically ordered
+#include "ApplicationAccessibleWrap.h"
+#include "ARIAGridAccessible.h"
+#include "ARIAMap.h"
+#include "DocAccessible-inl.h"
+#include "DocAccessibleChild.h"
+#include "FocusManager.h"
+#include "HTMLCanvasAccessible.h"
+#include "HTMLElementAccessibles.h"
+#include "HTMLImageMapAccessible.h"
+#include "HTMLLinkAccessible.h"
+#include "HTMLListAccessible.h"
+#include "HTMLSelectAccessible.h"
+#include "HTMLTableAccessible.h"
+#include "HyperTextAccessible.h"
+#include "RootAccessible.h"
+#include "nsAccUtils.h"
+#include "nsArrayUtils.h"
+#include "nsAttrName.h"
+#include "nsDOMTokenList.h"
+#include "nsCRT.h"
+#include "nsEventShell.h"
+#include "nsGkAtoms.h"
+#include "nsIFrameInlines.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTextFormatter.h"
+#include "OuterDocAccessible.h"
+#include "mozilla/a11y/Role.h"
+#ifdef MOZ_ACCESSIBILITY_ATK
+# include "RootAccessibleWrap.h"
+#endif
+#include "States.h"
+#include "Statistics.h"
+#include "TextLeafAccessible.h"
+#include "xpcAccessibleApplication.h"
+
+#ifdef XP_WIN
+# include "mozilla/a11y/Compatibility.h"
+# include "mozilla/StaticPtr.h"
+#endif
+
+#ifdef A11Y_LOG
+# include "Logging.h"
+#endif
+
+#include "nsExceptionHandler.h"
+#include "nsImageFrame.h"
+#include "nsIObserverService.h"
+#include "nsMenuPopupFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsTreeBodyFrame.h"
+#include "nsTreeUtils.h"
+#include "mozilla/a11y/AccTypes.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/dom/DOMStringList.h"
+#include "mozilla/dom/EventTarget.h"
+#include "mozilla/dom/HTMLTableElement.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Services.h"
+
+#include "XULAlertAccessible.h"
+#include "XULComboboxAccessible.h"
+#include "XULElementAccessibles.h"
+#include "XULFormControlAccessible.h"
+#include "XULListboxAccessible.h"
+#include "XULMenuAccessible.h"
+#include "XULTabAccessible.h"
+#include "XULTreeGridAccessible.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+using namespace mozilla::dom;
+
+/**
+ * Accessibility service force enable/disable preference.
+ * Supported values:
+ * Accessibility is force enabled (accessibility should always be enabled): -1
+ * Accessibility is enabled (will be started upon a request, default value): 0
+ * Accessibility is force disabled (never enable accessibility): 1
+ */
+#define PREF_ACCESSIBILITY_FORCE_DISABLED "accessibility.force_disabled"
+
+////////////////////////////////////////////////////////////////////////////////
+// Statics
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * If the element has an ARIA attribute that requires a specific Accessible
+ * class, create and return it. Otherwise, return null.
+ */
+static LocalAccessible* MaybeCreateSpecificARIAAccessible(
+ const nsRoleMapEntry* aRoleMapEntry, const LocalAccessible* aContext,
+ nsIContent* aContent, DocAccessible* aDocument) {
+ if (aRoleMapEntry && aRoleMapEntry->accTypes & eTableCell) {
+ if (aContent->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th) &&
+ aContext->IsHTMLTableRow()) {
+ // Don't use ARIAGridCellAccessible for a valid td/th because
+ // HTMLTableCellAccessible can provide additional info; e.g. row/col span
+ // from the layout engine.
+ return nullptr;
+ }
+ // A cell must be in a row.
+ const Accessible* parent = aContext;
+ if (parent->IsGeneric()) {
+ parent = parent->GetNonGenericParent();
+ }
+ if (!parent || parent->Role() != roles::ROW) {
+ return nullptr;
+ }
+ // That row must be in a table, though there may be an intervening rowgroup.
+ parent = parent->GetNonGenericParent();
+ if (!parent) {
+ return nullptr;
+ }
+ if (!parent->IsTable() && parent->Role() == roles::GROUPING) {
+ parent = parent->GetNonGenericParent();
+ if (!parent) {
+ return nullptr;
+ }
+ }
+ if (parent->IsTable()) {
+ return new ARIAGridCellAccessible(aContent, aDocument);
+ }
+ }
+ return nullptr;
+}
+
+/**
+ * Return true if the element has an attribute (ARIA, title, or relation) that
+ * requires the creation of an Accessible for the element.
+ */
+static bool AttributesMustBeAccessible(nsIContent* aContent,
+ DocAccessible* aDocument) {
+ if (aContent->IsElement()) {
+ uint32_t attrCount = aContent->AsElement()->GetAttrCount();
+ for (uint32_t attrIdx = 0; attrIdx < attrCount; attrIdx++) {
+ const nsAttrName* attr = aContent->AsElement()->GetAttrNameAt(attrIdx);
+ if (attr->NamespaceEquals(kNameSpaceID_None)) {
+ nsAtom* attrAtom = attr->Atom();
+ if (attrAtom == nsGkAtoms::title && aContent->IsHTMLElement()) {
+ // If the author provided a title on an element that would not
+ // be accessible normally, assume an intent and make it accessible.
+ return true;
+ }
+
+ nsDependentAtomString attrStr(attrAtom);
+ if (!StringBeginsWith(attrStr, u"aria-"_ns)) continue; // not ARIA
+
+ // A global state or a property and in case of token defined.
+ uint8_t attrFlags = aria::AttrCharacteristicsFor(attrAtom);
+ if ((attrFlags & ATTR_GLOBAL) &&
+ (!(attrFlags & ATTR_VALTOKEN) ||
+ nsAccUtils::HasDefinedARIAToken(aContent, attrAtom))) {
+ return true;
+ }
+ }
+ }
+
+ // If the given ID is referred by relation attribute then create an
+ // Accessible for it.
+ nsAutoString id;
+ if (nsCoreUtils::GetID(aContent, id) && !id.IsEmpty()) {
+ return aDocument->IsDependentID(aContent->AsElement(), id);
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Return true if the element must be a generic Accessible, even if it has been
+ * marked presentational with role="presentation", etc. MustBeAccessible causes
+ * an Accessible to be created as if it weren't marked presentational at all;
+ * e.g. <table role="presentation" tabindex="0"> will expose roles::TABLE and
+ * support TableAccessible. In contrast, this function causes a generic
+ * Accessible to be created; e.g. <table role="presentation" style="position:
+ * fixed;"> will expose roles::TEXT_CONTAINER and will not support
+ * TableAccessible. This is necessary in certain cases for the
+ * RemoteAccessible cache.
+ */
+static bool MustBeGenericAccessible(nsIContent* aContent,
+ DocAccessible* aDocument) {
+ if (aContent->IsInNativeAnonymousSubtree() || aContent->IsSVGElement()) {
+ // We should not force create accs for anonymous content.
+ // This is an issue for inputs, which have an intermediate
+ // container with relevant overflow styling between the input
+ // and its internal input content.
+ // We should also avoid this for SVG elements (ie. `<foreignobject>`s
+ // which have default overflow:hidden styling).
+ return false;
+ }
+ nsIFrame* frame = aContent->GetPrimaryFrame();
+ MOZ_ASSERT(frame);
+ nsAutoCString overflow;
+ frame->Style()->GetComputedPropertyValue(eCSSProperty_overflow, overflow);
+ // If the frame has been transformed, and the content has any children, we
+ // should create an Accessible so that we can account for the transform when
+ // calculating the Accessible's bounds using the parent process cache.
+ // Ditto for content which is position: fixed or sticky or has overflow
+ // styling (auto, scroll, hidden).
+ // However, don't do this for XUL widgets, as this breaks XUL a11y code
+ // expectations in some cases. XUL widgets are only used in the parent
+ // process and can't be cached anyway.
+ return !aContent->IsXULElement() &&
+ ((aContent->HasChildren() && frame->IsTransformed()) ||
+ frame->IsStickyPositioned() ||
+ (frame->StyleDisplay()->mPosition == StylePositionProperty::Fixed &&
+ nsLayoutUtils::IsReallyFixedPos(frame)) ||
+ overflow.Equals("auto"_ns) || overflow.Equals("scroll"_ns) ||
+ overflow.Equals("hidden"_ns));
+}
+
+/**
+ * Return true if the element must be accessible.
+ */
+static bool MustBeAccessible(nsIContent* aContent, DocAccessible* aDocument) {
+ nsIFrame* frame = aContent->GetPrimaryFrame();
+ MOZ_ASSERT(frame);
+ // This document might be invisible when it first loads. Therefore, we must
+ // check focusability irrespective of visibility here. Otherwise, we might not
+ // create Accessibles for some focusable elements; e.g. a span with only a
+ // tabindex. Elements that are invisible within this document are excluded
+ // earlier in CreateAccessible.
+ if (frame->IsFocusable(/* aWithMouse */ false,
+ /* aCheckVisibility */ false)) {
+ return true;
+ }
+
+ return AttributesMustBeAccessible(aContent, aDocument);
+}
+
+bool nsAccessibilityService::ShouldCreateImgAccessible(
+ mozilla::dom::Element* aElement, DocAccessible* aDocument) {
+ // The element must have a layout frame for us to proceed. If there is no
+ // frame, the image is likely hidden.
+ nsIFrame* frame = aElement->GetPrimaryFrame();
+ if (!frame) {
+ return false;
+ }
+
+ // If the element is not an img, and also not an embedded image via embed or
+ // object, then we should not create an accessible.
+ if (!aElement->IsHTMLElement(nsGkAtoms::img) &&
+ ((!aElement->IsHTMLElement(nsGkAtoms::embed) &&
+ !aElement->IsHTMLElement(nsGkAtoms::object)) ||
+ frame->AccessibleType() != AccType::eImageType)) {
+ return false;
+ }
+
+ nsAutoString newAltText;
+ const bool hasAlt = aElement->GetAttr(nsGkAtoms::alt, newAltText);
+ if (!hasAlt || !newAltText.IsEmpty()) {
+ // If there is no alt attribute, we should create an accessible. The
+ // author may have missed the attribute, and the AT may want to provide a
+ // name. If there is alt text, we should create an accessible.
+ return true;
+ }
+
+ if (newAltText.IsEmpty() && (nsCoreUtils::HasClickListener(aElement) ||
+ MustBeAccessible(aElement, aDocument))) {
+ // If there is empty alt text, but there is a click listener for this img,
+ // or if it otherwise must be an accessible (e.g., if it has an aria-label
+ // attribute), we should create an accessible.
+ return true;
+ }
+
+ // Otherwise, no alt text means we should not create an accessible.
+ return false;
+}
+
+/**
+ * Return true if the SVG element should be accessible
+ */
+static bool MustSVGElementBeAccessible(nsIContent* aContent,
+ DocAccessible* aDocument) {
+ // https://w3c.github.io/svg-aam/#include_elements
+ for (nsIContent* childElm = aContent->GetFirstChild(); childElm;
+ childElm = childElm->GetNextSibling()) {
+ if (childElm->IsAnyOfSVGElements(nsGkAtoms::title, nsGkAtoms::desc)) {
+ return true;
+ }
+ }
+ return MustBeAccessible(aContent, aDocument);
+}
+
+/**
+ * Used by XULMap.h to map both menupopup and popup elements
+ */
+LocalAccessible* CreateMenupopupAccessible(Element* aElement,
+ LocalAccessible* aContext) {
+#ifdef MOZ_ACCESSIBILITY_ATK
+ // ATK considers this node to be redundant when within menubars, and it makes
+ // menu navigation with assistive technologies more difficult
+ // XXX In the future we will should this for consistency across the
+ // nsIAccessible implementations on each platform for a consistent scripting
+ // environment, but then strip out redundant accessibles in the AccessibleWrap
+ // class for each platform.
+ nsIContent* parent = aElement->GetParent();
+ if (parent && parent->IsXULElement(nsGkAtoms::menu)) return nullptr;
+#endif
+
+ return new XULMenupopupAccessible(aElement, aContext->Document());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible constructors
+
+static LocalAccessible* New_HyperText(Element* aElement,
+ LocalAccessible* aContext) {
+ return new HyperTextAccessible(aElement, aContext->Document());
+}
+
+template <typename AccClass>
+static LocalAccessible* New_HTMLDtOrDd(Element* aElement,
+ LocalAccessible* aContext) {
+ nsIContent* parent = aContext->GetContent();
+ if (parent->IsHTMLElement(nsGkAtoms::div)) {
+ // It is conforming in HTML to use a div to group dt/dd elements.
+ parent = parent->GetParent();
+ }
+
+ if (parent && parent->IsHTMLElement(nsGkAtoms::dl)) {
+ return new AccClass(aElement, aContext->Document());
+ }
+
+ return nullptr;
+}
+
+/**
+ * Cached value of the PREF_ACCESSIBILITY_FORCE_DISABLED preference.
+ */
+static int32_t sPlatformDisabledState = 0;
+
+////////////////////////////////////////////////////////////////////////////////
+// Markup maps array.
+
+#define Attr(name, value) \
+ { nsGkAtoms::name, nsGkAtoms::value }
+
+#define AttrFromDOM(name, DOMAttrName) \
+ { nsGkAtoms::name, nullptr, nsGkAtoms::DOMAttrName }
+
+#define AttrFromDOMIf(name, DOMAttrName, DOMAttrValue) \
+ { nsGkAtoms::name, nullptr, nsGkAtoms::DOMAttrName, nsGkAtoms::DOMAttrValue }
+
+#define MARKUPMAP(atom, new_func, r, ...) \
+ {nsGkAtoms::atom, new_func, static_cast<a11y::role>(r), {__VA_ARGS__}},
+
+static const MarkupMapInfo sHTMLMarkupMapList[] = {
+#include "HTMLMarkupMap.h"
+};
+
+static const MarkupMapInfo sMathMLMarkupMapList[] = {
+#include "MathMLMarkupMap.h"
+};
+
+#undef MARKUPMAP
+
+#define XULMAP(atom, ...) {nsGkAtoms::atom, __VA_ARGS__},
+
+#define XULMAP_TYPE(atom, new_type) \
+ XULMAP( \
+ atom, \
+ [](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* { \
+ return new new_type(aElement, aContext->Document()); \
+ })
+
+static const XULMarkupMapInfo sXULMarkupMapList[] = {
+#include "XULMap.h"
+};
+
+#undef XULMAP_TYPE
+#undef XULMAP
+
+#undef Attr
+#undef AttrFromDOM
+#undef AttrFromDOMIf
+
+////////////////////////////////////////////////////////////////////////////////
+// nsAccessibilityService
+////////////////////////////////////////////////////////////////////////////////
+
+nsAccessibilityService* nsAccessibilityService::gAccessibilityService = nullptr;
+ApplicationAccessible* nsAccessibilityService::gApplicationAccessible = nullptr;
+xpcAccessibleApplication* nsAccessibilityService::gXPCApplicationAccessible =
+ nullptr;
+uint32_t nsAccessibilityService::gConsumers = 0;
+
+nsAccessibilityService::nsAccessibilityService()
+ : mHTMLMarkupMap(ArrayLength(sHTMLMarkupMapList)),
+ mMathMLMarkupMap(ArrayLength(sMathMLMarkupMapList)),
+ mXULMarkupMap(ArrayLength(sXULMarkupMapList)) {}
+
+nsAccessibilityService::~nsAccessibilityService() {
+ NS_ASSERTION(IsShutdown(), "Accessibility wasn't shutdown!");
+ gAccessibilityService = nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIListenerChangeListener
+
+NS_IMETHODIMP
+nsAccessibilityService::ListenersChanged(nsIArray* aEventChanges) {
+ uint32_t targetCount;
+ nsresult rv = aEventChanges->GetLength(&targetCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t i = 0; i < targetCount; i++) {
+ nsCOMPtr<nsIEventListenerChange> change =
+ do_QueryElementAt(aEventChanges, i);
+
+ RefPtr<EventTarget> target;
+ change->GetTarget(getter_AddRefs(target));
+ nsIContent* content(nsIContent::FromEventTargetOrNull(target));
+ if (!content || !content->IsHTMLElement()) {
+ continue;
+ }
+
+ uint32_t changeCount;
+ change->GetCountOfEventListenerChangesAffectingAccessibility(&changeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (changeCount) {
+ Document* ownerDoc = content->OwnerDoc();
+ DocAccessible* document = GetExistingDocAccessible(ownerDoc);
+
+ if (document) {
+ LocalAccessible* acc = document->GetAccessible(content);
+ if (!acc && (content == document->GetContent() ||
+ content == document->DocumentNode()->GetRootElement())) {
+ acc = document;
+ }
+ if (!acc && content->IsElement() &&
+ content->AsElement()->IsHTMLElement(nsGkAtoms::area)) {
+ // For area accessibles, we have to recreate the entire image map,
+ // since the image map accessible manages the tree itself. The click
+ // listener change may require us to update the role for the
+ // accessible associated with the area element.
+ LocalAccessible* areaAcc =
+ document->GetAccessibleEvenIfNotInMap(content);
+ if (areaAcc && areaAcc->LocalParent()) {
+ document->RecreateAccessible(areaAcc->LocalParent()->GetContent());
+ }
+ }
+ if (!acc && nsCoreUtils::HasClickListener(content)) {
+ // Create an accessible for a inaccessible element having click event
+ // handler.
+ document->ContentInserted(content, content->GetNextSibling());
+ } else if (acc) {
+ if ((acc->IsHTMLLink() && !acc->AsHTMLLink()->IsLinked()) ||
+ (content->IsElement() &&
+ content->AsElement()->IsHTMLElement(nsGkAtoms::a) &&
+ !acc->IsHTMLLink())) {
+ // An HTML link without an href attribute should have a generic
+ // role, unless it has a click listener. Since we might have gained
+ // or lost a click listener here, recreate the accessible so that we
+ // can create the correct type of accessible. If it was a link, it
+ // may no longer be one. If it wasn't, it may become one.
+ document->RecreateAccessible(content);
+ }
+
+ // A click listener change might mean losing or gaining an action.
+ document->QueueCacheUpdate(acc, CacheDomain::Actions);
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_IMPL_ISUPPORTS_INHERITED(nsAccessibilityService, DocManager, nsIObserver,
+ nsIListenerChangeListener,
+ nsISelectionListener) // from SelectionManager
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIObserver
+
+NS_IMETHODIMP
+nsAccessibilityService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!nsCRT::strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ Shutdown();
+ }
+
+ return NS_OK;
+}
+
+void nsAccessibilityService::NotifyOfAnchorJumpTo(nsIContent* aTargetNode) {
+ Document* documentNode = aTargetNode->GetUncomposedDoc();
+ if (!documentNode) {
+ return;
+ }
+ DocAccessible* document = GetDocAccessible(documentNode);
+ if (!document) {
+ return;
+ }
+ // If the document has focus when we get this notification, ensure that
+ // we fire a start scrolling event.
+ const Accessible* focusedAcc = FocusedAccessible();
+ if (focusedAcc &&
+ (focusedAcc == document || focusedAcc->IsNonInteractive())) {
+ LocalAccessible* targetAcc = document->GetAccessible(aTargetNode);
+ if (targetAcc) {
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SCROLLING_START,
+ targetAcc);
+ document->SetAnchorJump(nullptr);
+ } else {
+ // We can't find the target accessible in the document yet. Set the
+ // anchor jump so that we can fire the scrolling start event later.
+ document->SetAnchorJump(aTargetNode);
+ }
+ } else {
+ document->SetAnchorJump(aTargetNode);
+ }
+}
+
+void nsAccessibilityService::FireAccessibleEvent(uint32_t aEvent,
+ LocalAccessible* aTarget) {
+ nsEventShell::FireEvent(aEvent, aTarget);
+}
+
+void nsAccessibilityService::NotifyOfPossibleBoundsChange(
+ mozilla::PresShell* aPresShell, nsIContent* aContent) {
+ if (IPCAccessibilityActive()) {
+ DocAccessible* document = aPresShell->GetDocAccessible();
+ if (document) {
+ // DocAccessible::GetAccessible() won't return the document if a root
+ // element like body is passed.
+ LocalAccessible* accessible = aContent == document->GetContent()
+ ? document
+ : document->GetAccessible(aContent);
+ if (accessible) {
+ document->QueueCacheUpdate(accessible, CacheDomain::Bounds);
+ }
+ }
+ }
+}
+
+void nsAccessibilityService::NotifyOfComputedStyleChange(
+ mozilla::PresShell* aPresShell, nsIContent* aContent) {
+ DocAccessible* document = aPresShell->GetDocAccessible();
+ if (!document) {
+ return;
+ }
+
+ // DocAccessible::GetAccessible() won't return the document if a root
+ // element like body is passed.
+ LocalAccessible* accessible = aContent == document->GetContent()
+ ? document
+ : document->GetAccessible(aContent);
+ if (!accessible && aContent && aContent->HasChildren() &&
+ !aContent->IsInNativeAnonymousSubtree()) {
+ // If the content has children and its frame has a transform, create an
+ // Accessible so that we can account for the transform when calculating
+ // the Accessible's bounds using the parent process cache. Ditto for
+ // position: fixed/sticky and content with overflow styling (hidden, auto,
+ // scroll)
+ if (const nsIFrame* frame = aContent->GetPrimaryFrame()) {
+ const auto& disp = *frame->StyleDisplay();
+ if (disp.HasTransform(frame) ||
+ disp.mPosition == StylePositionProperty::Fixed ||
+ disp.mPosition == StylePositionProperty::Sticky ||
+ disp.IsScrollableOverflow()) {
+ document->ContentInserted(aContent, aContent->GetNextSibling());
+ }
+ }
+ } else if (accessible && IPCAccessibilityActive()) {
+ accessible->MaybeQueueCacheUpdateForStyleChanges();
+ }
+}
+
+void nsAccessibilityService::NotifyOfResolutionChange(
+ mozilla::PresShell* aPresShell, float aResolution) {
+ DocAccessible* document = aPresShell->GetDocAccessible();
+ if (document && document->IPCDoc()) {
+ AutoTArray<mozilla::a11y::CacheData, 1> data;
+ RefPtr<AccAttributes> fields = new AccAttributes();
+ fields->SetAttribute(CacheKey::Resolution, aResolution);
+ data.AppendElement(mozilla::a11y::CacheData(0, fields));
+ document->IPCDoc()->SendCache(CacheUpdateType::Update, data);
+ }
+}
+
+void nsAccessibilityService::NotifyOfDevPixelRatioChange(
+ mozilla::PresShell* aPresShell, int32_t aAppUnitsPerDevPixel) {
+ DocAccessible* document = aPresShell->GetDocAccessible();
+ if (document && document->IPCDoc()) {
+ AutoTArray<mozilla::a11y::CacheData, 1> data;
+ RefPtr<AccAttributes> fields = new AccAttributes();
+ fields->SetAttribute(CacheKey::AppUnitsPerDevPixel, aAppUnitsPerDevPixel);
+ data.AppendElement(mozilla::a11y::CacheData(0, fields));
+ document->IPCDoc()->SendCache(CacheUpdateType::Update, data);
+ }
+}
+
+LocalAccessible* nsAccessibilityService::GetRootDocumentAccessible(
+ PresShell* aPresShell, bool aCanCreate) {
+ PresShell* presShell = aPresShell;
+ Document* documentNode = aPresShell->GetDocument();
+ if (documentNode) {
+ nsCOMPtr<nsIDocShellTreeItem> treeItem(documentNode->GetDocShell());
+ if (treeItem) {
+ nsCOMPtr<nsIDocShellTreeItem> rootTreeItem;
+ treeItem->GetInProcessRootTreeItem(getter_AddRefs(rootTreeItem));
+ if (treeItem != rootTreeItem) {
+ nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(rootTreeItem));
+ presShell = docShell->GetPresShell();
+ }
+
+ return aCanCreate ? GetDocAccessible(presShell)
+ : presShell->GetDocAccessible();
+ }
+ }
+ return nullptr;
+}
+
+void nsAccessibilityService::NotifyOfTabPanelVisibilityChange(
+ PresShell* aPresShell, Element* aPanel, bool aNowVisible) {
+ MOZ_ASSERT(aPanel->GetParent()->IsXULElement(nsGkAtoms::tabpanels));
+
+ DocAccessible* document = GetDocAccessible(aPresShell);
+ if (!document) {
+ return;
+ }
+
+ if (LocalAccessible* acc = document->GetAccessible(aPanel)) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(acc, states::OFFSCREEN, aNowVisible);
+ document->FireDelayedEvent(event);
+ }
+}
+
+void nsAccessibilityService::ContentRangeInserted(PresShell* aPresShell,
+ nsIContent* aStartChild,
+ nsIContent* aEndChild) {
+ DocAccessible* document = GetDocAccessible(aPresShell);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eTree)) {
+ logging::MsgBegin("TREE", "content inserted; doc: %p", document);
+ logging::Node("container", aStartChild->GetParentNode());
+ for (nsIContent* child = aStartChild; child != aEndChild;
+ child = child->GetNextSibling()) {
+ logging::Node("content", child);
+ }
+ logging::MsgEnd();
+ logging::Stack();
+ }
+#endif
+
+ if (document) {
+ document->ContentInserted(aStartChild, aEndChild);
+ }
+}
+
+void nsAccessibilityService::ScheduleAccessibilitySubtreeUpdate(
+ PresShell* aPresShell, nsIContent* aContent) {
+ DocAccessible* document = GetDocAccessible(aPresShell);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eTree)) {
+ logging::MsgBegin("TREE", "schedule update; doc: %p", document);
+ logging::Node("content node", aContent);
+ logging::MsgEnd();
+ }
+#endif
+
+ if (document) {
+ document->ScheduleTreeUpdate(aContent);
+ }
+}
+
+void nsAccessibilityService::ContentRemoved(PresShell* aPresShell,
+ nsIContent* aChildNode) {
+ DocAccessible* document = GetDocAccessible(aPresShell);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eTree)) {
+ logging::MsgBegin("TREE", "content removed; doc: %p", document);
+ logging::Node("container node", aChildNode->GetFlattenedTreeParent());
+ logging::Node("content node", aChildNode);
+ logging::MsgEnd();
+ }
+#endif
+
+ if (document) {
+ document->ContentRemoved(aChildNode);
+ }
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eTree)) {
+ logging::MsgEnd();
+ logging::Stack();
+ }
+#endif
+}
+
+void nsAccessibilityService::TableLayoutGuessMaybeChanged(
+ PresShell* aPresShell, nsIContent* aContent) {
+ if (DocAccessible* document = GetDocAccessible(aPresShell)) {
+ if (LocalAccessible* acc = document->GetAccessible(aContent)) {
+ if (LocalAccessible* table = nsAccUtils::TableFor(acc)) {
+ document->QueueCacheUpdate(table, CacheDomain::Table);
+ }
+ }
+ }
+}
+
+void nsAccessibilityService::ComboboxOptionMaybeChanged(
+ PresShell* aPresShell, nsIContent* aMutatingNode) {
+ DocAccessible* document = GetDocAccessible(aPresShell);
+ if (!document) {
+ return;
+ }
+
+ for (nsIContent* cur = aMutatingNode; cur; cur = cur->GetParent()) {
+ if (cur->IsHTMLElement(nsGkAtoms::option)) {
+ if (LocalAccessible* accessible = document->GetAccessible(cur)) {
+ document->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE,
+ accessible);
+ break;
+ }
+ if (cur->IsHTMLElement(nsGkAtoms::select)) {
+ break;
+ }
+ }
+ }
+}
+
+void nsAccessibilityService::UpdateText(PresShell* aPresShell,
+ nsIContent* aContent) {
+ DocAccessible* document = GetDocAccessible(aPresShell);
+ if (document) document->UpdateText(aContent);
+}
+
+void nsAccessibilityService::TreeViewChanged(PresShell* aPresShell,
+ nsIContent* aContent,
+ nsITreeView* aView) {
+ DocAccessible* document = GetDocAccessible(aPresShell);
+ if (document) {
+ LocalAccessible* accessible = document->GetAccessible(aContent);
+ if (accessible) {
+ XULTreeAccessible* treeAcc = accessible->AsXULTree();
+ if (treeAcc) treeAcc->TreeViewChanged(aView);
+ }
+ }
+}
+
+void nsAccessibilityService::RangeValueChanged(PresShell* aPresShell,
+ nsIContent* aContent) {
+ DocAccessible* document = GetDocAccessible(aPresShell);
+ if (document) {
+ LocalAccessible* accessible = document->GetAccessible(aContent);
+ if (accessible) {
+ document->FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE,
+ accessible);
+ }
+ }
+}
+
+void nsAccessibilityService::UpdateImageMap(nsImageFrame* aImageFrame) {
+ PresShell* presShell = aImageFrame->PresShell();
+ DocAccessible* document = GetDocAccessible(presShell);
+ if (document) {
+ LocalAccessible* accessible =
+ document->GetAccessible(aImageFrame->GetContent());
+ if (accessible) {
+ HTMLImageMapAccessible* imageMap = accessible->AsImageMap();
+ if (imageMap) {
+ imageMap->UpdateChildAreas();
+ return;
+ }
+
+ // If image map was initialized after we created an accessible (that'll
+ // be an image accessible) then recreate it.
+ RecreateAccessible(presShell, aImageFrame->GetContent());
+ }
+ }
+}
+
+void nsAccessibilityService::UpdateLabelValue(PresShell* aPresShell,
+ nsIContent* aLabelElm,
+ const nsString& aNewValue) {
+ DocAccessible* document = GetDocAccessible(aPresShell);
+ if (document) {
+ LocalAccessible* accessible = document->GetAccessible(aLabelElm);
+ if (accessible) {
+ XULLabelAccessible* xulLabel = accessible->AsXULLabel();
+ NS_ASSERTION(xulLabel,
+ "UpdateLabelValue was called for wrong accessible!");
+ if (xulLabel) xulLabel->UpdateLabelValue(aNewValue);
+ }
+ }
+}
+
+void nsAccessibilityService::PresShellActivated(PresShell* aPresShell) {
+ DocAccessible* document = aPresShell->GetDocAccessible();
+ if (document) {
+ RootAccessible* rootDocument = document->RootAccessible();
+ NS_ASSERTION(rootDocument, "Entirely broken tree: no root document!");
+ if (rootDocument) rootDocument->DocumentActivated(document);
+ }
+}
+
+void nsAccessibilityService::RecreateAccessible(PresShell* aPresShell,
+ nsIContent* aContent) {
+ DocAccessible* document = GetDocAccessible(aPresShell);
+ if (document) document->RecreateAccessible(aContent);
+}
+
+void nsAccessibilityService::GetStringRole(uint32_t aRole, nsAString& aString) {
+#define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
+ msaaRole, ia2Role, androidClass, nameRule) \
+ case roles::geckoRole: \
+ aString.AssignLiteral(stringRole); \
+ return;
+
+ switch (aRole) {
+#include "RoleMap.h"
+ default:
+ aString.AssignLiteral("unknown");
+ return;
+ }
+
+#undef ROLE
+}
+
+void nsAccessibilityService::GetStringStates(uint32_t aState,
+ uint32_t aExtraState,
+ nsISupports** aStringStates) {
+ RefPtr<DOMStringList> stringStates =
+ GetStringStates(nsAccUtils::To64State(aState, aExtraState));
+
+ // unknown state
+ if (!stringStates->Length()) {
+ stringStates->Add(u"unknown"_ns);
+ }
+
+ stringStates.forget(aStringStates);
+}
+
+already_AddRefed<DOMStringList> nsAccessibilityService::GetStringStates(
+ uint64_t aStates) const {
+ RefPtr<DOMStringList> stringStates = new DOMStringList();
+
+ if (aStates & states::UNAVAILABLE) {
+ stringStates->Add(u"unavailable"_ns);
+ }
+ if (aStates & states::SELECTED) {
+ stringStates->Add(u"selected"_ns);
+ }
+ if (aStates & states::FOCUSED) {
+ stringStates->Add(u"focused"_ns);
+ }
+ if (aStates & states::PRESSED) {
+ stringStates->Add(u"pressed"_ns);
+ }
+ if (aStates & states::CHECKED) {
+ stringStates->Add(u"checked"_ns);
+ }
+ if (aStates & states::MIXED) {
+ stringStates->Add(u"mixed"_ns);
+ }
+ if (aStates & states::READONLY) {
+ stringStates->Add(u"readonly"_ns);
+ }
+ if (aStates & states::HOTTRACKED) {
+ stringStates->Add(u"hottracked"_ns);
+ }
+ if (aStates & states::DEFAULT) {
+ stringStates->Add(u"default"_ns);
+ }
+ if (aStates & states::EXPANDED) {
+ stringStates->Add(u"expanded"_ns);
+ }
+ if (aStates & states::COLLAPSED) {
+ stringStates->Add(u"collapsed"_ns);
+ }
+ if (aStates & states::BUSY) {
+ stringStates->Add(u"busy"_ns);
+ }
+ if (aStates & states::FLOATING) {
+ stringStates->Add(u"floating"_ns);
+ }
+ if (aStates & states::ANIMATED) {
+ stringStates->Add(u"animated"_ns);
+ }
+ if (aStates & states::INVISIBLE) {
+ stringStates->Add(u"invisible"_ns);
+ }
+ if (aStates & states::OFFSCREEN) {
+ stringStates->Add(u"offscreen"_ns);
+ }
+ if (aStates & states::SIZEABLE) {
+ stringStates->Add(u"sizeable"_ns);
+ }
+ if (aStates & states::MOVEABLE) {
+ stringStates->Add(u"moveable"_ns);
+ }
+ if (aStates & states::SELFVOICING) {
+ stringStates->Add(u"selfvoicing"_ns);
+ }
+ if (aStates & states::FOCUSABLE) {
+ stringStates->Add(u"focusable"_ns);
+ }
+ if (aStates & states::SELECTABLE) {
+ stringStates->Add(u"selectable"_ns);
+ }
+ if (aStates & states::LINKED) {
+ stringStates->Add(u"linked"_ns);
+ }
+ if (aStates & states::TRAVERSED) {
+ stringStates->Add(u"traversed"_ns);
+ }
+ if (aStates & states::MULTISELECTABLE) {
+ stringStates->Add(u"multiselectable"_ns);
+ }
+ if (aStates & states::EXTSELECTABLE) {
+ stringStates->Add(u"extselectable"_ns);
+ }
+ if (aStates & states::PROTECTED) {
+ stringStates->Add(u"protected"_ns);
+ }
+ if (aStates & states::HASPOPUP) {
+ stringStates->Add(u"haspopup"_ns);
+ }
+ if (aStates & states::REQUIRED) {
+ stringStates->Add(u"required"_ns);
+ }
+ if (aStates & states::ALERT) {
+ stringStates->Add(u"alert"_ns);
+ }
+ if (aStates & states::INVALID) {
+ stringStates->Add(u"invalid"_ns);
+ }
+ if (aStates & states::CHECKABLE) {
+ stringStates->Add(u"checkable"_ns);
+ }
+ if (aStates & states::SUPPORTS_AUTOCOMPLETION) {
+ stringStates->Add(u"autocompletion"_ns);
+ }
+ if (aStates & states::DEFUNCT) {
+ stringStates->Add(u"defunct"_ns);
+ }
+ if (aStates & states::SELECTABLE_TEXT) {
+ stringStates->Add(u"selectable text"_ns);
+ }
+ if (aStates & states::EDITABLE) {
+ stringStates->Add(u"editable"_ns);
+ }
+ if (aStates & states::ACTIVE) {
+ stringStates->Add(u"active"_ns);
+ }
+ if (aStates & states::MODAL) {
+ stringStates->Add(u"modal"_ns);
+ }
+ if (aStates & states::MULTI_LINE) {
+ stringStates->Add(u"multi line"_ns);
+ }
+ if (aStates & states::HORIZONTAL) {
+ stringStates->Add(u"horizontal"_ns);
+ }
+ if (aStates & states::OPAQUE1) {
+ stringStates->Add(u"opaque"_ns);
+ }
+ if (aStates & states::SINGLE_LINE) {
+ stringStates->Add(u"single line"_ns);
+ }
+ if (aStates & states::TRANSIENT) {
+ stringStates->Add(u"transient"_ns);
+ }
+ if (aStates & states::VERTICAL) {
+ stringStates->Add(u"vertical"_ns);
+ }
+ if (aStates & states::STALE) {
+ stringStates->Add(u"stale"_ns);
+ }
+ if (aStates & states::ENABLED) {
+ stringStates->Add(u"enabled"_ns);
+ }
+ if (aStates & states::SENSITIVE) {
+ stringStates->Add(u"sensitive"_ns);
+ }
+ if (aStates & states::EXPANDABLE) {
+ stringStates->Add(u"expandable"_ns);
+ }
+ if (aStates & states::PINNED) {
+ stringStates->Add(u"pinned"_ns);
+ }
+ if (aStates & states::CURRENT) {
+ stringStates->Add(u"current"_ns);
+ }
+
+ return stringStates.forget();
+}
+
+void nsAccessibilityService::GetStringEventType(uint32_t aEventType,
+ nsAString& aString) {
+ static_assert(
+ nsIAccessibleEvent::EVENT_LAST_ENTRY == ArrayLength(kEventTypeNames),
+ "nsIAccessibleEvent constants are out of sync to kEventTypeNames");
+
+ if (aEventType >= ArrayLength(kEventTypeNames)) {
+ aString.AssignLiteral("unknown");
+ return;
+ }
+
+ aString.AssignASCII(kEventTypeNames[aEventType]);
+}
+
+void nsAccessibilityService::GetStringEventType(uint32_t aEventType,
+ nsACString& aString) {
+ MOZ_ASSERT(
+ nsIAccessibleEvent::EVENT_LAST_ENTRY == ArrayLength(kEventTypeNames),
+ "nsIAccessibleEvent constants are out of sync to kEventTypeNames");
+
+ if (aEventType >= ArrayLength(kEventTypeNames)) {
+ aString.AssignLiteral("unknown");
+ return;
+ }
+
+ aString = nsDependentCString(kEventTypeNames[aEventType]);
+}
+
+void nsAccessibilityService::GetStringRelationType(uint32_t aRelationType,
+ nsAString& aString) {
+ NS_ENSURE_TRUE_VOID(aRelationType <=
+ static_cast<uint32_t>(RelationType::LAST));
+
+#define RELATIONTYPE(geckoType, geckoTypeName, atkType, msaaType, ia2Type) \
+ case RelationType::geckoType: \
+ aString.AssignLiteral(geckoTypeName); \
+ return;
+
+ RelationType relationType = static_cast<RelationType>(aRelationType);
+ switch (relationType) {
+#include "RelationTypeMap.h"
+ default:
+ aString.AssignLiteral("unknown");
+ return;
+ }
+
+#undef RELATIONTYPE
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsAccessibilityService public
+
+LocalAccessible* nsAccessibilityService::CreateAccessible(
+ nsINode* aNode, LocalAccessible* aContext, bool* aIsSubtreeHidden) {
+ MOZ_ASSERT(aContext, "No context provided");
+ MOZ_ASSERT(aNode, "No node to create an accessible for");
+ MOZ_ASSERT(gConsumers, "No creation after shutdown");
+
+ if (aIsSubtreeHidden) *aIsSubtreeHidden = false;
+
+ DocAccessible* document = aContext->Document();
+ MOZ_ASSERT(!document->GetAccessible(aNode),
+ "We already have an accessible for this node.");
+
+ if (aNode->IsDocument()) {
+ // If it's document node then ask accessible document loader for
+ // document accessible, otherwise return null.
+ return GetDocAccessible(aNode->AsDocument());
+ }
+
+ // We have a content node.
+ if (!aNode->GetComposedDoc()) {
+ NS_WARNING("Creating accessible for node with no document");
+ return nullptr;
+ }
+
+ if (aNode->OwnerDoc() != document->DocumentNode()) {
+ NS_ERROR("Creating accessible for wrong document");
+ return nullptr;
+ }
+
+ if (!aNode->IsContent()) return nullptr;
+
+ nsIContent* content = aNode->AsContent();
+ if (aria::HasDefinedARIAHidden(content)) {
+ if (aIsSubtreeHidden) {
+ *aIsSubtreeHidden = true;
+ }
+ return nullptr;
+ }
+
+ // Check frame and its visibility.
+ nsIFrame* frame = content->GetPrimaryFrame();
+ if (frame) {
+ // If invisible or inert, we don't create an accessible, but we don't mark
+ // it with aIsSubtreeHidden = true, since visibility: hidden frame allows
+ // visible elements in subtree, and inert elements allow non-inert
+ // elements.
+ if (!frame->StyleVisibility()->IsVisible() || frame->StyleUI()->IsInert()) {
+ return nullptr;
+ }
+ } else if (nsCoreUtils::CanCreateAccessibleWithoutFrame(content)) {
+ // display:contents element doesn't have a frame, but retains the
+ // semantics. All its children are unaffected.
+ const nsRoleMapEntry* roleMapEntry = aria::GetRoleMap(content->AsElement());
+ RefPtr<LocalAccessible> newAcc = MaybeCreateSpecificARIAAccessible(
+ roleMapEntry, aContext, content, document);
+ const MarkupMapInfo* markupMap = nullptr;
+ if (!newAcc) {
+ markupMap = GetMarkupMapInfoFor(content);
+ if (markupMap && markupMap->new_func) {
+ newAcc = markupMap->new_func(content->AsElement(), aContext);
+ }
+ }
+
+ // Check whether this element has an ARIA role or attribute that requires
+ // us to create an Accessible.
+ const bool hasNonPresentationalARIARole =
+ roleMapEntry && !roleMapEntry->Is(nsGkAtoms::presentation) &&
+ !roleMapEntry->Is(nsGkAtoms::none);
+ if (!newAcc && (hasNonPresentationalARIARole ||
+ AttributesMustBeAccessible(content, document))) {
+ newAcc = new HyperTextAccessible(content, document);
+ }
+
+ // If there's still no Accessible but we do have an entry in the markup
+ // map for this non-presentational element, create a generic
+ // HyperTextAccessible.
+ if (!newAcc && markupMap &&
+ (!roleMapEntry || hasNonPresentationalARIARole)) {
+ newAcc = new HyperTextAccessible(content, document);
+ }
+
+ if (newAcc) {
+ document->BindToDocument(newAcc, roleMapEntry);
+ }
+ return newAcc;
+ } else {
+ if (aIsSubtreeHidden) {
+ *aIsSubtreeHidden = true;
+ }
+ return nullptr;
+ }
+
+ if (frame->IsHiddenByContentVisibilityOnAnyAncestor(
+ nsIFrame::IncludeContentVisibility::Hidden)) {
+ if (aIsSubtreeHidden) {
+ *aIsSubtreeHidden = true;
+ }
+ return nullptr;
+ }
+
+ if (nsMenuPopupFrame* popupFrame = do_QueryFrame(frame)) {
+ // Hidden tooltips and panels don't create accessibles in the whole subtree.
+ // Showing them gets handled by RootAccessible::ProcessDOMEvent.
+ if (content->IsAnyOfXULElements(nsGkAtoms::tooltip, nsGkAtoms::panel)) {
+ nsPopupState popupState = popupFrame->PopupState();
+ if (popupState == ePopupHiding || popupState == ePopupInvisible ||
+ popupState == ePopupClosed) {
+ if (aIsSubtreeHidden) {
+ *aIsSubtreeHidden = true;
+ }
+ return nullptr;
+ }
+ }
+ }
+
+ if (frame->GetContent() != content) {
+ // Not the main content for this frame. This happens because <area>
+ // elements return the image frame as their primary frame. The main content
+ // for the image frame is the image content. If the frame is not an image
+ // frame or the node is not an area element then null is returned.
+ // This setup will change when bug 135040 is fixed. Make sure we don't
+ // create area accessible here. Hopefully assertion below will handle that.
+
+#ifdef DEBUG
+ nsImageFrame* imageFrame = do_QueryFrame(frame);
+ NS_ASSERTION(imageFrame && content->IsHTMLElement(nsGkAtoms::area),
+ "Unknown case of not main content for the frame!");
+#endif
+ return nullptr;
+ }
+
+#ifdef DEBUG
+ nsImageFrame* imageFrame = do_QueryFrame(frame);
+ NS_ASSERTION(!imageFrame || !content->IsHTMLElement(nsGkAtoms::area),
+ "Image map manages the area accessible creation!");
+#endif
+
+ // Attempt to create an accessible based on what we know.
+ RefPtr<LocalAccessible> newAcc;
+
+ // Create accessible for visible text frames.
+ if (content->IsText()) {
+ nsIFrame::RenderedText text = frame->GetRenderedText(
+ 0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText,
+ nsIFrame::TrailingWhitespace::DontTrim);
+ // Ignore not rendered text nodes and whitespace text nodes between table
+ // cells.
+ if (text.mString.IsEmpty() ||
+ (aContext->IsTableRow() &&
+ nsCoreUtils::IsWhitespaceString(text.mString))) {
+ if (aIsSubtreeHidden) *aIsSubtreeHidden = true;
+
+ return nullptr;
+ }
+
+ newAcc = CreateAccessibleByFrameType(frame, content, aContext);
+ MOZ_ASSERT(newAcc, "Accessible not created for text node!");
+ document->BindToDocument(newAcc, nullptr);
+ newAcc->AsTextLeaf()->SetText(text.mString);
+ return newAcc;
+ }
+
+ if (content->IsHTMLElement(nsGkAtoms::map)) {
+ // Create hyper text accessible for HTML map if it is used to group links
+ // (see http://www.w3.org/TR/WCAG10-HTML-TECHS/#group-bypass). If the HTML
+ // map rect is empty then it is used for links grouping. Otherwise it should
+ // be used in conjunction with HTML image element and in this case we don't
+ // create any accessible for it and don't walk into it. The accessibles for
+ // HTML area (HTMLAreaAccessible) the map contains are attached as
+ // children of the appropriate accessible for HTML image
+ // (ImageAccessible).
+ if (nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame->GetParent())
+ .IsEmpty()) {
+ if (aIsSubtreeHidden) *aIsSubtreeHidden = true;
+
+ return nullptr;
+ }
+
+ newAcc = new HyperTextAccessible(content, document);
+ document->BindToDocument(newAcc, aria::GetRoleMap(content->AsElement()));
+ return newAcc;
+ }
+
+ const nsRoleMapEntry* roleMapEntry = aria::GetRoleMap(content->AsElement());
+
+ if (roleMapEntry && (roleMapEntry->Is(nsGkAtoms::presentation) ||
+ roleMapEntry->Is(nsGkAtoms::none))) {
+ if (MustBeAccessible(content, document)) {
+ // If the element is focusable, a global ARIA attribute is applied to it
+ // or it is referenced by an ARIA relationship, then treat
+ // role="presentation" on the element as if the role is not there.
+ roleMapEntry = nullptr;
+ } else if (MustBeGenericAccessible(content, document)) {
+ // Clear roleMapEntry so that we use the generic role specified below.
+ // Otherwise, we'd expose roles::NOTHING as specified for presentation in
+ // ARIAMap.
+ roleMapEntry = nullptr;
+ newAcc = new EnumRoleHyperTextAccessible<roles::TEXT_CONTAINER>(content,
+ document);
+ } else {
+ return nullptr;
+ }
+ }
+
+ // We should always use OuterDocAccessible for OuterDocs, even if there's a
+ // specific ARIA class we would otherwise use.
+ if (!newAcc && frame->AccessibleType() != eOuterDocType) {
+ newAcc = MaybeCreateSpecificARIAAccessible(roleMapEntry, aContext, content,
+ document);
+ }
+
+ if (!newAcc && content->IsHTMLElement()) { // HTML accessibles
+ // Prefer to use markup to decide if and what kind of accessible to
+ // create,
+ const MarkupMapInfo* markupMap =
+ mHTMLMarkupMap.Get(content->NodeInfo()->NameAtom());
+ if (markupMap && markupMap->new_func) {
+ newAcc = markupMap->new_func(content->AsElement(), aContext);
+ }
+
+ if (!newAcc) { // try by frame accessible type.
+ newAcc = CreateAccessibleByFrameType(frame, content, aContext);
+ }
+
+ // If table has strong ARIA role then all table descendants shouldn't
+ // expose their native roles.
+ if (!roleMapEntry && newAcc && aContext->HasStrongARIARole()) {
+ if (frame->AccessibleType() == eHTMLTableRowType) {
+ const nsRoleMapEntry* contextRoleMap = aContext->ARIARoleMap();
+ if (!contextRoleMap->IsOfType(eTable)) {
+ roleMapEntry = &aria::gEmptyRoleMap;
+ }
+
+ } else if (frame->AccessibleType() == eHTMLTableCellType &&
+ aContext->ARIARoleMap() == &aria::gEmptyRoleMap) {
+ roleMapEntry = &aria::gEmptyRoleMap;
+
+ } else if (content->IsAnyOfHTMLElements(nsGkAtoms::dt, nsGkAtoms::li,
+ nsGkAtoms::dd) ||
+ frame->AccessibleType() == eHTMLLiType) {
+ const nsRoleMapEntry* contextRoleMap = aContext->ARIARoleMap();
+ if (!contextRoleMap->IsOfType(eList)) {
+ roleMapEntry = &aria::gEmptyRoleMap;
+ }
+ }
+ }
+ }
+
+ // XUL accessibles.
+ if (!newAcc && content->IsXULElement()) {
+ if (content->IsXULElement(nsGkAtoms::panel)) {
+ // We filter here instead of in the XUL map because
+ // if we filter there and return null, we still end up
+ // creating a generic accessible at the end of this function.
+ // Doing the filtering here ensures we never create accessibles
+ // for panels whose popups aren't visible.
+ nsMenuPopupFrame* popupFrame = do_QueryFrame(frame);
+ if (!popupFrame) {
+ return nullptr;
+ }
+
+ nsPopupState popupState = popupFrame->PopupState();
+ if (popupState == ePopupHiding || popupState == ePopupInvisible ||
+ popupState == ePopupClosed) {
+ return nullptr;
+ }
+ }
+
+ // Prefer to use XUL to decide if and what kind of accessible to create.
+ const XULMarkupMapInfo* xulMap =
+ mXULMarkupMap.Get(content->NodeInfo()->NameAtom());
+ if (xulMap && xulMap->new_func) {
+ newAcc = xulMap->new_func(content->AsElement(), aContext);
+ }
+
+ // Any XUL/flex box can be used as tabpanel, make sure we create a proper
+ // accessible for it.
+ if (!newAcc && aContext->IsXULTabpanels() &&
+ content->GetParent() == aContext->GetContent()) {
+ LayoutFrameType frameType = frame->Type();
+ // FIXME(emilio): Why only these frame types?
+ if (frameType == LayoutFrameType::FlexContainer ||
+ frameType == LayoutFrameType::Scroll) {
+ newAcc = new XULTabpanelAccessible(content, document);
+ }
+ }
+ }
+
+ if (!newAcc) {
+ if (content->IsSVGElement()) {
+ if (content->IsSVGGeometryElement() ||
+ content->IsSVGElement(nsGkAtoms::image)) {
+ // Shape elements: rect, circle, ellipse, line, path, polygon,
+ // and polyline. 'use' and 'text' graphic elements require
+ // special support.
+ if (MustSVGElementBeAccessible(content, document)) {
+ newAcc = new EnumRoleAccessible<roles::GRAPHIC>(content, document);
+ }
+ } else if (content->IsSVGElement(nsGkAtoms::text)) {
+ newAcc = new HyperTextAccessible(content->AsElement(), document);
+ } else if (content->IsSVGElement(nsGkAtoms::svg)) {
+ // An <svg> element could contain <foreignObject>, which contains HTML
+ // but does not normally create its own Accessible. This means that the
+ // <svg> Accessible could have TextLeafAccessible children, so it must
+ // be a HyperTextAccessible.
+ newAcc =
+ new EnumRoleHyperTextAccessible<roles::DIAGRAM>(content, document);
+ } else if (content->IsSVGElement(nsGkAtoms::g) &&
+ MustSVGElementBeAccessible(content, document)) {
+ // <g> can also contain <foreignObject>.
+ newAcc =
+ new EnumRoleHyperTextAccessible<roles::GROUPING>(content, document);
+ } else if (content->IsSVGElement(nsGkAtoms::a)) {
+ newAcc = new HTMLLinkAccessible(content, document);
+ }
+
+ } else if (content->IsMathMLElement()) {
+ const MarkupMapInfo* markupMap =
+ mMathMLMarkupMap.Get(content->NodeInfo()->NameAtom());
+ if (markupMap && markupMap->new_func) {
+ newAcc = markupMap->new_func(content->AsElement(), aContext);
+ }
+
+ // Fall back to text when encountering Content MathML.
+ if (!newAcc && !content->IsAnyOfMathMLElements(
+ nsGkAtoms::annotation_, nsGkAtoms::annotation_xml_,
+ nsGkAtoms::mpadded_, nsGkAtoms::mphantom_,
+ nsGkAtoms::maligngroup_, nsGkAtoms::malignmark_,
+ nsGkAtoms::mspace_, nsGkAtoms::semantics_)) {
+ newAcc = new HyperTextAccessible(content, document);
+ }
+ } else if (content->IsGeneratedContentContainerForMarker()) {
+ if (aContext->IsHTMLListItem()) {
+ newAcc = new HTMLListBulletAccessible(content, document);
+ }
+ if (aIsSubtreeHidden) {
+ *aIsSubtreeHidden = true;
+ }
+ }
+ }
+
+ // If no accessible, see if we need to create a generic accessible because
+ // of some property that makes this object interesting
+ // We don't do this for <body>, <html>, <window>, <dialog> etc. which
+ // correspond to the doc accessible and will be created in any case
+ if (!newAcc && !content->IsHTMLElement(nsGkAtoms::body) &&
+ content->GetParent() &&
+ (roleMapEntry || MustBeAccessible(content, document) ||
+ (content->IsHTMLElement() && nsCoreUtils::HasClickListener(content)))) {
+ // This content is focusable or has an interesting dynamic content
+ // accessibility property. If it's interesting we need it in the
+ // accessibility hierarchy so that events or other accessibles can point to
+ // it, or so that it can hold a state, etc.
+ if (content->IsHTMLElement() || content->IsMathMLElement() ||
+ content->IsSVGElement(nsGkAtoms::foreignObject)) {
+ // Interesting container which may have selectable text and/or embedded
+ // objects.
+ newAcc = new HyperTextAccessible(content, document);
+ } else { // XUL, other SVG, etc.
+ // Interesting generic non-HTML container
+ newAcc = new AccessibleWrap(content, document);
+ }
+ } else if (!newAcc && MustBeGenericAccessible(content, document)) {
+ newAcc = new EnumRoleHyperTextAccessible<roles::TEXT_CONTAINER>(content,
+ document);
+ }
+
+ if (newAcc) {
+ document->BindToDocument(newAcc, roleMapEntry);
+ }
+ return newAcc;
+}
+
+#if defined(ANDROID)
+# include "mozilla/Monitor.h"
+# include "mozilla/Maybe.h"
+
+static Maybe<Monitor> sAndroidMonitor;
+
+mozilla::Monitor& nsAccessibilityService::GetAndroidMonitor() {
+ if (!sAndroidMonitor.isSome()) {
+ sAndroidMonitor.emplace("nsAccessibility::sAndroidMonitor");
+ }
+
+ return *sAndroidMonitor;
+}
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+// nsAccessibilityService private
+
+bool nsAccessibilityService::Init() {
+ AUTO_PROFILER_MARKER_TEXT("nsAccessibilityService::Init", A11Y, {}, ""_ns);
+ // DO NOT ADD CODE ABOVE HERE: THIS CODE IS MEASURING TIMINGS.
+
+ // Initialize accessible document manager.
+ if (!DocManager::Init()) return false;
+
+ // Add observers.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) return false;
+
+ observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+
+#if defined(XP_WIN)
+ // This information needs to be initialized before the observer fires.
+ if (XRE_IsParentProcess()) {
+ Compatibility::Init();
+ }
+#endif // defined(XP_WIN)
+
+ // Subscribe to EventListenerService.
+ nsCOMPtr<nsIEventListenerService> eventListenerService =
+ do_GetService("@mozilla.org/eventlistenerservice;1");
+ if (!eventListenerService) return false;
+
+ eventListenerService->AddListenerChangeListener(this);
+
+ for (uint32_t i = 0; i < ArrayLength(sHTMLMarkupMapList); i++) {
+ mHTMLMarkupMap.InsertOrUpdate(sHTMLMarkupMapList[i].tag,
+ &sHTMLMarkupMapList[i]);
+ }
+ for (const auto& info : sMathMLMarkupMapList) {
+ mMathMLMarkupMap.InsertOrUpdate(info.tag, &info);
+ }
+
+ for (uint32_t i = 0; i < ArrayLength(sXULMarkupMapList); i++) {
+ mXULMarkupMap.InsertOrUpdate(sXULMarkupMapList[i].tag,
+ &sXULMarkupMapList[i]);
+ }
+
+#ifdef A11Y_LOG
+ logging::CheckEnv();
+#endif
+
+ gAccessibilityService = this;
+ NS_ADDREF(gAccessibilityService); // will release in Shutdown()
+
+ if (XRE_IsParentProcess()) {
+ gApplicationAccessible = new ApplicationAccessibleWrap();
+ } else {
+ gApplicationAccessible = new ApplicationAccessible();
+ }
+
+ NS_ADDREF(gApplicationAccessible); // will release in Shutdown()
+ gApplicationAccessible->Init();
+
+ CrashReporter::AnnotateCrashReport(CrashReporter::Annotation::Accessibility,
+ "Active"_ns);
+
+ // Now its safe to start platform accessibility.
+ if (XRE_IsParentProcess()) PlatformInit();
+
+ statistics::A11yInitialized();
+
+ static const char16_t kInitIndicator[] = {'1', 0};
+ observerService->NotifyObservers(nullptr, "a11y-init-or-shutdown",
+ kInitIndicator);
+
+ return true;
+}
+
+void nsAccessibilityService::Shutdown() {
+ // Application is going to be closed, shutdown accessibility and mark
+ // accessibility service as shutdown to prevent calls of its methods.
+ // Don't null accessibility service static member at this point to be safe
+ // if someone will try to operate with it.
+
+ MOZ_ASSERT(gConsumers, "Accessibility was shutdown already");
+ UnsetConsumers(eXPCOM | eMainProcess | ePlatformAPI);
+
+ // Remove observers.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ }
+
+ // Stop accessible document loader.
+ DocManager::Shutdown();
+
+ SelectionManager::Shutdown();
+
+ if (XRE_IsParentProcess()) PlatformShutdown();
+
+ gApplicationAccessible->Shutdown();
+ NS_RELEASE(gApplicationAccessible);
+ gApplicationAccessible = nullptr;
+
+ NS_IF_RELEASE(gXPCApplicationAccessible);
+ gXPCApplicationAccessible = nullptr;
+
+#if defined(ANDROID)
+ // Don't allow the service to shut down while an a11y request is being handled
+ // in the UI thread, as the request may depend on state from the service.
+ MonitorAutoLock mal(GetAndroidMonitor());
+#endif
+ NS_RELEASE(gAccessibilityService);
+ gAccessibilityService = nullptr;
+
+ if (observerService) {
+ static const char16_t kShutdownIndicator[] = {'0', 0};
+ observerService->NotifyObservers(nullptr, "a11y-init-or-shutdown",
+ kShutdownIndicator);
+ }
+}
+
+already_AddRefed<LocalAccessible>
+nsAccessibilityService::CreateAccessibleByFrameType(nsIFrame* aFrame,
+ nsIContent* aContent,
+ LocalAccessible* aContext) {
+ DocAccessible* document = aContext->Document();
+
+ RefPtr<LocalAccessible> newAcc;
+ switch (aFrame->AccessibleType()) {
+ case eNoType:
+ return nullptr;
+ case eHTMLBRType:
+ newAcc = new HTMLBRAccessible(aContent, document);
+ break;
+ case eHTMLButtonType:
+ newAcc = new HTMLButtonAccessible(aContent, document);
+ break;
+ case eHTMLCanvasType:
+ newAcc = new HTMLCanvasAccessible(aContent, document);
+ break;
+ case eHTMLCaptionType:
+ if (aContext->IsTable() &&
+ aContext->GetContent() == aContent->GetParent()) {
+ newAcc = new HTMLCaptionAccessible(aContent, document);
+ }
+ break;
+ case eHTMLCheckboxType:
+ newAcc = new CheckboxAccessible(aContent, document);
+ break;
+ case eHTMLComboboxType:
+ newAcc = new HTMLComboboxAccessible(aContent, document);
+ break;
+ case eHTMLFileInputType:
+ newAcc = new HTMLFileInputAccessible(aContent, document);
+ break;
+ case eHTMLGroupboxType:
+ newAcc = new HTMLGroupboxAccessible(aContent, document);
+ break;
+ case eHTMLHRType:
+ newAcc = new HTMLHRAccessible(aContent, document);
+ break;
+ case eHTMLImageMapType:
+ newAcc = new HTMLImageMapAccessible(aContent, document);
+ break;
+ case eHTMLLiType:
+ if (aContext->IsList() &&
+ aContext->GetContent() == aContent->GetParent()) {
+ newAcc = new HTMLLIAccessible(aContent, document);
+ } else {
+ // Otherwise create a generic text accessible to avoid text jamming.
+ newAcc = new HyperTextAccessible(aContent, document);
+ }
+ break;
+ case eHTMLSelectListType:
+ newAcc = new HTMLSelectListAccessible(aContent, document);
+ break;
+ case eHTMLMediaType:
+ newAcc = new EnumRoleAccessible<roles::GROUPING>(aContent, document);
+ break;
+ case eHTMLRadioButtonType:
+ newAcc = new HTMLRadioButtonAccessible(aContent, document);
+ break;
+ case eHTMLRangeType:
+ newAcc = new HTMLRangeAccessible(aContent, document);
+ break;
+ case eHTMLSpinnerType:
+ newAcc = new HTMLSpinnerAccessible(aContent, document);
+ break;
+ case eHTMLTableType:
+ case eHTMLTableCellType:
+ // We handle markup and ARIA tables elsewhere. If we reach here, this is
+ // a CSS table part. Just create a generic text container.
+ newAcc = new HyperTextAccessible(aContent, document);
+ break;
+ case eHTMLTableRowType:
+ // This is a CSS table row. Don't expose it at all.
+ break;
+ case eHTMLTextFieldType:
+ newAcc = new HTMLTextFieldAccessible(aContent, document);
+ break;
+ case eHyperTextType: {
+ if (aContext->IsTable() || aContext->IsTableRow()) {
+ // This is some generic hyperText, for example a block frame element
+ // inserted between a table and table row. Treat it as presentational.
+ return nullptr;
+ }
+
+ if (!aContent->IsAnyOfHTMLElements(nsGkAtoms::dt, nsGkAtoms::dd,
+ nsGkAtoms::div, nsGkAtoms::thead,
+ nsGkAtoms::tfoot, nsGkAtoms::tbody)) {
+ newAcc = new HyperTextAccessible(aContent, document);
+ }
+ break;
+ }
+ case eImageType:
+ if (aContent->IsElement() &&
+ ShouldCreateImgAccessible(aContent->AsElement(), document)) {
+ newAcc = new ImageAccessible(aContent, document);
+ }
+ break;
+ case eOuterDocType:
+ newAcc = new OuterDocAccessible(aContent, document);
+ break;
+ case eTextLeafType:
+ newAcc = new TextLeafAccessible(aContent, document);
+ break;
+ default:
+ MOZ_ASSERT(false);
+ break;
+ }
+
+ return newAcc.forget();
+}
+
+void nsAccessibilityService::MarkupAttributes(
+ Accessible* aAcc, AccAttributes* aAttributes) const {
+ const mozilla::a11y::MarkupMapInfo* markupMap = GetMarkupMapInfoFor(aAcc);
+ if (!markupMap) return;
+
+ dom::Element* el = aAcc->IsLocal() ? aAcc->AsLocal()->Elm() : nullptr;
+ for (uint32_t i = 0; i < ArrayLength(markupMap->attrs); i++) {
+ const MarkupAttrInfo* info = markupMap->attrs + i;
+ if (!info->name) break;
+
+ if (info->DOMAttrName) {
+ if (!el) {
+ // XXX Expose DOM attributes for cached RemoteAccessibles.
+ continue;
+ }
+ if (info->DOMAttrValue) {
+ if (el->AttrValueIs(kNameSpaceID_None, info->DOMAttrName,
+ info->DOMAttrValue, eCaseMatters)) {
+ aAttributes->SetAttribute(info->name, info->DOMAttrValue);
+ }
+ continue;
+ }
+
+ nsString value;
+ el->GetAttr(info->DOMAttrName, value);
+
+ if (!value.IsEmpty()) {
+ aAttributes->SetAttribute(info->name, std::move(value));
+ }
+
+ continue;
+ }
+
+ aAttributes->SetAttribute(info->name, info->value);
+ }
+}
+
+LocalAccessible* nsAccessibilityService::AddNativeRootAccessible(
+ void* aAtkAccessible) {
+#ifdef MOZ_ACCESSIBILITY_ATK
+ ApplicationAccessible* applicationAcc = ApplicationAcc();
+ if (!applicationAcc) return nullptr;
+
+ GtkWindowAccessible* nativeWnd =
+ new GtkWindowAccessible(static_cast<AtkObject*>(aAtkAccessible));
+
+ if (applicationAcc->AppendChild(nativeWnd)) return nativeWnd;
+#endif
+
+ return nullptr;
+}
+
+void nsAccessibilityService::RemoveNativeRootAccessible(
+ LocalAccessible* aAccessible) {
+#ifdef MOZ_ACCESSIBILITY_ATK
+ ApplicationAccessible* applicationAcc = ApplicationAcc();
+
+ if (applicationAcc) applicationAcc->RemoveChild(aAccessible);
+#endif
+}
+
+bool nsAccessibilityService::HasAccessible(nsINode* aDOMNode) {
+ if (!aDOMNode) return false;
+
+ Document* document = aDOMNode->OwnerDoc();
+ if (!document) return false;
+
+ DocAccessible* docAcc = GetExistingDocAccessible(aDOMNode->OwnerDoc());
+ if (!docAcc) return false;
+
+ return docAcc->HasAccessible(aDOMNode);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsAccessibilityService private (DON'T put methods here)
+
+void nsAccessibilityService::SetConsumers(uint32_t aConsumers, bool aNotify) {
+ if (gConsumers & aConsumers) {
+ return;
+ }
+
+ gConsumers |= aConsumers;
+ if (aNotify) {
+ NotifyOfConsumersChange();
+ }
+}
+
+void nsAccessibilityService::UnsetConsumers(uint32_t aConsumers) {
+ if (!(gConsumers & aConsumers)) {
+ return;
+ }
+
+ gConsumers &= ~aConsumers;
+ NotifyOfConsumersChange();
+}
+
+void nsAccessibilityService::GetConsumers(nsAString& aString) {
+ const char16_t* kJSONFmt =
+ u"{ \"XPCOM\": %s, \"MainProcess\": %s, \"PlatformAPI\": %s }";
+ nsString json;
+ nsTextFormatter::ssprintf(json, kJSONFmt,
+ gConsumers & eXPCOM ? "true" : "false",
+ gConsumers & eMainProcess ? "true" : "false",
+ gConsumers & ePlatformAPI ? "true" : "false");
+ aString.Assign(json);
+}
+
+void nsAccessibilityService::NotifyOfConsumersChange() {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+
+ if (!observerService) {
+ return;
+ }
+
+ nsAutoString consumers;
+ GetConsumers(consumers);
+ observerService->NotifyObservers(nullptr, "a11y-consumers-changed",
+ consumers.get());
+}
+
+const mozilla::a11y::MarkupMapInfo* nsAccessibilityService::GetMarkupMapInfoFor(
+ Accessible* aAcc) const {
+ if (LocalAccessible* localAcc = aAcc->AsLocal()) {
+ return localAcc->HasOwnContent()
+ ? GetMarkupMapInfoFor(localAcc->GetContent())
+ : nullptr;
+ }
+ // XXX For now, we assume all RemoteAccessibles are HTML elements. This
+ // isn't strictly correct, but as far as current callers are concerned,
+ // this doesn't matter. If that changes in future, we could expose the
+ // element type via AccGenericType.
+ return mHTMLMarkupMap.Get(aAcc->TagName());
+}
+
+nsAccessibilityService* GetOrCreateAccService(uint32_t aNewConsumer) {
+ // Do not initialize accessibility if it is force disabled.
+ if (PlatformDisabledState() == ePlatformIsDisabled) {
+ return nullptr;
+ }
+
+ if (!nsAccessibilityService::gAccessibilityService) {
+ RefPtr<nsAccessibilityService> service = new nsAccessibilityService();
+ if (!service->Init()) {
+ service->Shutdown();
+ return nullptr;
+ }
+ }
+
+ MOZ_ASSERT(nsAccessibilityService::gAccessibilityService,
+ "LocalAccessible service is not initialized.");
+ nsAccessibilityService::gAccessibilityService->SetConsumers(aNewConsumer);
+ return nsAccessibilityService::gAccessibilityService;
+}
+
+void MaybeShutdownAccService(uint32_t aFormerConsumer) {
+ nsAccessibilityService* accService =
+ nsAccessibilityService::gAccessibilityService;
+
+ if (!accService || nsAccessibilityService::IsShutdown()) {
+ return;
+ }
+
+ // Still used by XPCOM
+ if (nsCoreUtils::AccEventObserversExist() ||
+ xpcAccessibilityService::IsInUse() || accService->HasXPCDocuments()) {
+ // In case the XPCOM flag was unset (possibly because of the shutdown
+ // timer in the xpcAccessibilityService) ensure it is still present. Note:
+ // this should be fixed when all the consumer logic is taken out as a
+ // separate class.
+ accService->SetConsumers(nsAccessibilityService::eXPCOM, false);
+
+ if (aFormerConsumer != nsAccessibilityService::eXPCOM) {
+ // Only unset non-XPCOM consumers.
+ accService->UnsetConsumers(aFormerConsumer);
+ }
+ return;
+ }
+
+ if (nsAccessibilityService::gConsumers & ~aFormerConsumer) {
+ accService->UnsetConsumers(aFormerConsumer);
+ } else {
+ accService
+ ->Shutdown(); // Will unset all nsAccessibilityService::gConsumers
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Services
+////////////////////////////////////////////////////////////////////////////////
+
+namespace mozilla {
+namespace a11y {
+
+FocusManager* FocusMgr() {
+ return nsAccessibilityService::gAccessibilityService;
+}
+
+SelectionManager* SelectionMgr() {
+ return nsAccessibilityService::gAccessibilityService;
+}
+
+ApplicationAccessible* ApplicationAcc() {
+ return nsAccessibilityService::gApplicationAccessible;
+}
+
+xpcAccessibleApplication* XPCApplicationAcc() {
+ if (!nsAccessibilityService::gXPCApplicationAccessible &&
+ nsAccessibilityService::gApplicationAccessible) {
+ nsAccessibilityService::gXPCApplicationAccessible =
+ new xpcAccessibleApplication(
+ nsAccessibilityService::gApplicationAccessible);
+ NS_ADDREF(nsAccessibilityService::gXPCApplicationAccessible);
+ }
+
+ return nsAccessibilityService::gXPCApplicationAccessible;
+}
+
+EPlatformDisabledState PlatformDisabledState() {
+ static bool platformDisabledStateCached = false;
+ if (platformDisabledStateCached) {
+ return static_cast<EPlatformDisabledState>(sPlatformDisabledState);
+ }
+
+ platformDisabledStateCached = true;
+ Preferences::RegisterCallback(PrefChanged, PREF_ACCESSIBILITY_FORCE_DISABLED);
+ return ReadPlatformDisabledState();
+}
+
+EPlatformDisabledState ReadPlatformDisabledState() {
+ sPlatformDisabledState =
+ Preferences::GetInt(PREF_ACCESSIBILITY_FORCE_DISABLED, 0);
+ if (sPlatformDisabledState < ePlatformIsForceEnabled) {
+ sPlatformDisabledState = ePlatformIsForceEnabled;
+ } else if (sPlatformDisabledState > ePlatformIsDisabled) {
+ sPlatformDisabledState = ePlatformIsDisabled;
+ }
+
+ return static_cast<EPlatformDisabledState>(sPlatformDisabledState);
+}
+
+void PrefChanged(const char* aPref, void* aClosure) {
+ if (ReadPlatformDisabledState() == ePlatformIsDisabled) {
+ // Force shut down accessibility.
+ nsAccessibilityService* accService =
+ nsAccessibilityService::gAccessibilityService;
+ if (accService && !nsAccessibilityService::IsShutdown()) {
+ accService->Shutdown();
+ }
+ }
+}
+
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/base/nsAccessibilityService.h b/accessible/base/nsAccessibilityService.h
new file mode 100644
index 0000000000..0b3f172f89
--- /dev/null
+++ b/accessible/base/nsAccessibilityService.h
@@ -0,0 +1,492 @@
+/* -*- 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 __nsAccessibilityService_h__
+#define __nsAccessibilityService_h__
+
+#include "mozilla/a11y/DocManager.h"
+#include "mozilla/a11y/FocusManager.h"
+#include "mozilla/a11y/Platform.h"
+#include "mozilla/a11y/Role.h"
+#include "mozilla/a11y/SelectionManager.h"
+#include "mozilla/Preferences.h"
+
+#include "nsAtomHashKeys.h"
+#include "nsIContent.h"
+#include "nsIObserver.h"
+#include "nsIAccessibleEvent.h"
+#include "nsIEventListenerService.h"
+#include "nsXULAppAPI.h"
+#include "xpcAccessibilityService.h"
+
+class nsImageFrame;
+class nsIArray;
+class nsITreeView;
+
+namespace mozilla {
+
+class PresShell;
+class Monitor;
+namespace dom {
+class DOMStringList;
+class Element;
+} // namespace dom
+
+namespace a11y {
+
+class AccAttributes;
+class Accessible;
+class ApplicationAccessible;
+class xpcAccessibleApplication;
+
+/**
+ * Return focus manager.
+ */
+FocusManager* FocusMgr();
+
+/**
+ * Return selection manager.
+ */
+SelectionManager* SelectionMgr();
+
+/**
+ * Returns the application accessible.
+ */
+ApplicationAccessible* ApplicationAcc();
+xpcAccessibleApplication* XPCApplicationAcc();
+
+typedef LocalAccessible*(New_Accessible)(mozilla::dom::Element* aElement,
+ LocalAccessible* aContext);
+
+// These fields are not `nsStaticAtom* const` because MSVC doesn't like it.
+struct MarkupAttrInfo {
+ nsStaticAtom* name;
+ nsStaticAtom* value;
+
+ nsStaticAtom* DOMAttrName;
+ nsStaticAtom* DOMAttrValue;
+};
+
+struct MarkupMapInfo {
+ nsStaticAtom* const tag;
+ New_Accessible* new_func;
+ a11y::role role;
+ MarkupAttrInfo attrs[4];
+};
+
+struct XULMarkupMapInfo {
+ nsStaticAtom* const tag;
+ New_Accessible* new_func;
+};
+
+/**
+ * PREF_ACCESSIBILITY_FORCE_DISABLED preference change callback.
+ */
+void PrefChanged(const char* aPref, void* aClosure);
+
+/**
+ * Read and normalize PREF_ACCESSIBILITY_FORCE_DISABLED preference.
+ */
+EPlatformDisabledState ReadPlatformDisabledState();
+
+} // namespace a11y
+} // namespace mozilla
+
+class nsAccessibilityService final : public mozilla::a11y::DocManager,
+ public mozilla::a11y::FocusManager,
+ public mozilla::a11y::SelectionManager,
+ public nsIListenerChangeListener,
+ public nsIObserver {
+ public:
+ typedef mozilla::a11y::LocalAccessible LocalAccessible;
+ typedef mozilla::a11y::DocAccessible DocAccessible;
+
+ // nsIListenerChangeListener
+ NS_IMETHOD ListenersChanged(nsIArray* aEventChanges) override;
+
+ protected:
+ ~nsAccessibilityService();
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIOBSERVER
+
+ LocalAccessible* GetRootDocumentAccessible(mozilla::PresShell* aPresShell,
+ bool aCanCreate);
+
+ /**
+ * Adds/remove ATK root accessible for gtk+ native window to/from children
+ * of the application accessible.
+ */
+ LocalAccessible* AddNativeRootAccessible(void* aAtkAccessible);
+ void RemoveNativeRootAccessible(LocalAccessible* aRootAccessible);
+
+ bool HasAccessible(nsINode* aDOMNode);
+
+ /**
+ * Get a string equivalent for an accessible role value.
+ */
+ void GetStringRole(uint32_t aRole, nsAString& aString);
+
+ /**
+ * Get a string equivalent for an accessible state/extra state.
+ */
+ already_AddRefed<mozilla::dom::DOMStringList> GetStringStates(
+ uint64_t aStates) const;
+ void GetStringStates(uint32_t aState, uint32_t aExtraState,
+ nsISupports** aStringStates);
+
+ /**
+ * Get a string equivalent for an accessible event value.
+ */
+ void GetStringEventType(uint32_t aEventType, nsAString& aString);
+
+ /**
+ * Get a string equivalent for an accessible event value.
+ */
+ void GetStringEventType(uint32_t aEventType, nsACString& aString);
+
+ /**
+ * Get a string equivalent for an accessible relation type.
+ */
+ void GetStringRelationType(uint32_t aRelationType, nsAString& aString);
+
+ // nsAccesibilityService
+ /**
+ * Notification used to update the accessible tree when new content is
+ * inserted.
+ */
+ void ContentRangeInserted(mozilla::PresShell* aPresShell,
+ nsIContent* aStartChild, nsIContent* aEndChild);
+
+ /**
+ * Triggers a re-evaluation of the a11y tree of aContent after the next
+ * refresh. This is important because whether we create accessibles may
+ * depend on the frame tree / style.
+ */
+ void ScheduleAccessibilitySubtreeUpdate(mozilla::PresShell* aPresShell,
+ nsIContent* aStartChild);
+
+ /**
+ * Notification used to update the accessible tree when content is removed.
+ */
+ void ContentRemoved(mozilla::PresShell* aPresShell, nsIContent* aChild);
+
+ /**
+ * Notification used to invalidate the isLayoutTable cache.
+ */
+ void TableLayoutGuessMaybeChanged(mozilla::PresShell* aPresShell,
+ nsIContent* aContent);
+
+ /**
+ * Notifies when a combobox <option> text or label changes.
+ */
+ void ComboboxOptionMaybeChanged(mozilla::PresShell*,
+ nsIContent* aMutatingNode);
+
+ void UpdateText(mozilla::PresShell* aPresShell, nsIContent* aContent);
+
+ /**
+ * Update XUL:tree accessible tree when treeview is changed.
+ */
+ void TreeViewChanged(mozilla::PresShell* aPresShell, nsIContent* aContent,
+ nsITreeView* aView);
+
+ /**
+ * Notify of input@type="element" value change.
+ */
+ void RangeValueChanged(mozilla::PresShell* aPresShell, nsIContent* aContent);
+
+ /**
+ * Update the image map.
+ */
+ void UpdateImageMap(nsImageFrame* aImageFrame);
+
+ /**
+ * Update the label accessible tree when rendered @value is changed.
+ */
+ void UpdateLabelValue(mozilla::PresShell* aPresShell, nsIContent* aLabelElm,
+ const nsString& aNewValue);
+
+ /**
+ * Notify accessibility that anchor jump has been accomplished to the given
+ * target. Used by layout.
+ */
+ void NotifyOfAnchorJumpTo(nsIContent* aTarget);
+
+ /**
+ * Notify that presshell is activated.
+ */
+ void PresShellActivated(mozilla::PresShell* aPresShell);
+
+ /**
+ * Recreate an accessible for the given content node in the presshell.
+ */
+ void RecreateAccessible(mozilla::PresShell* aPresShell, nsIContent* aContent);
+
+ void FireAccessibleEvent(uint32_t aEvent, LocalAccessible* aTarget);
+
+ void NotifyOfPossibleBoundsChange(mozilla::PresShell* aPresShell,
+ nsIContent* aContent);
+
+ void NotifyOfComputedStyleChange(mozilla::PresShell* aPresShell,
+ nsIContent* aContent);
+
+ void NotifyOfTabPanelVisibilityChange(mozilla::PresShell* aPresShell,
+ mozilla::dom::Element* aPanel,
+ bool aVisible);
+
+ void NotifyOfResolutionChange(mozilla::PresShell* aPresShell,
+ float aResolution);
+
+ void NotifyOfDevPixelRatioChange(mozilla::PresShell* aPresShell,
+ int32_t aAppUnitsPerDevPixel);
+
+ // nsAccessibiltiyService
+
+ /**
+ * Return true if accessibility service has been shutdown.
+ */
+ static bool IsShutdown() { return gConsumers == 0; };
+
+ /**
+ * Return true if there should be an image accessible for the given element.
+ */
+ static bool ShouldCreateImgAccessible(mozilla::dom::Element* aElement,
+ DocAccessible* aDocument);
+
+ /**
+ * Creates an accessible for the given DOM node.
+ *
+ * @param aNode [in] the given node
+ * @param aContext [in] context the accessible is created in
+ * @param aIsSubtreeHidden [out, optional] indicates whether the node's
+ * frame and its subtree is hidden
+ */
+ LocalAccessible* CreateAccessible(nsINode* aNode, LocalAccessible* aContext,
+ bool* aIsSubtreeHidden = nullptr);
+
+ mozilla::a11y::role MarkupRole(const nsIContent* aContent) const {
+ const mozilla::a11y::MarkupMapInfo* markupMap =
+ GetMarkupMapInfoFor(aContent);
+ return markupMap ? markupMap->role : mozilla::a11y::roles::NOTHING;
+ }
+
+ /**
+ * Return the associated value for a given attribute if
+ * it appears in the MarkupMap. Otherwise, it returns null. This can be
+ * called with either an nsIContent or an Accessible.
+ */
+ template <typename T>
+ nsStaticAtom* MarkupAttribute(T aSource, nsStaticAtom* aAtom) const {
+ const mozilla::a11y::MarkupMapInfo* markupMap =
+ GetMarkupMapInfoFor(aSource);
+ if (markupMap) {
+ for (size_t i = 0; i < mozilla::ArrayLength(markupMap->attrs); i++) {
+ const mozilla::a11y::MarkupAttrInfo* info = markupMap->attrs + i;
+ if (info->name == aAtom) {
+ return info->value;
+ }
+ }
+ }
+ return nullptr;
+ }
+
+ /**
+ * Set the object attribute defined by markup for the given element.
+ */
+ void MarkupAttributes(mozilla::a11y::Accessible* aAcc,
+ mozilla::a11y::AccAttributes* aAttributes) const;
+
+ /**
+ * A list of possible accessibility service consumers. Accessibility service
+ * can only be shut down when there are no remaining consumers.
+ *
+ * eXPCOM - accessibility service is used by XPCOM.
+ *
+ * eMainProcess - accessibility service was started by main process in the
+ * content process.
+ *
+ * ePlatformAPI - accessibility service is used by the platform api in the
+ * main process.
+ */
+ enum ServiceConsumer {
+ eXPCOM = 1 << 0,
+ eMainProcess = 1 << 1,
+ ePlatformAPI = 1 << 2,
+ };
+
+#if defined(ANDROID)
+ static mozilla::Monitor& GetAndroidMonitor();
+#endif
+
+ private:
+ // nsAccessibilityService creation is controlled by friend
+ // GetOrCreateAccService, keep constructors private.
+ nsAccessibilityService();
+ nsAccessibilityService(const nsAccessibilityService&);
+ nsAccessibilityService& operator=(const nsAccessibilityService&);
+
+ private:
+ /**
+ * Initialize accessibility service.
+ */
+ bool Init();
+
+ /**
+ * Shutdowns accessibility service.
+ */
+ void Shutdown();
+
+ /**
+ * Create an accessible whose type depends on the given frame.
+ */
+ already_AddRefed<LocalAccessible> CreateAccessibleByFrameType(
+ nsIFrame* aFrame, nsIContent* aContent, LocalAccessible* aContext);
+
+ /**
+ * Notify observers about change of the accessibility service's consumers.
+ */
+ void NotifyOfConsumersChange();
+
+ /**
+ * Get a JSON string representing the accessibility service consumers.
+ */
+ void GetConsumers(nsAString& aString);
+
+ /**
+ * Set accessibility service consumers.
+ */
+ void SetConsumers(uint32_t aConsumers, bool aNotify = true);
+
+ /**
+ * Unset accessibility service consumers.
+ */
+ void UnsetConsumers(uint32_t aConsumers);
+
+ /**
+ * Reference for accessibility service instance.
+ */
+ static nsAccessibilityService* gAccessibilityService;
+
+ /**
+ * Reference for application accessible instance.
+ */
+ static mozilla::a11y::ApplicationAccessible* gApplicationAccessible;
+ static mozilla::a11y::xpcAccessibleApplication* gXPCApplicationAccessible;
+
+ /**
+ * Contains a set of accessibility service consumers.
+ */
+ static uint32_t gConsumers;
+
+ // Can be weak because all atoms are known static
+ using MarkupMap = nsTHashMap<nsAtom*, const mozilla::a11y::MarkupMapInfo*>;
+ MarkupMap mHTMLMarkupMap;
+ MarkupMap mMathMLMarkupMap;
+
+ const mozilla::a11y::MarkupMapInfo* GetMarkupMapInfoFor(
+ const nsIContent* aContent) const {
+ if (aContent->IsHTMLElement()) {
+ return mHTMLMarkupMap.Get(aContent->NodeInfo()->NameAtom());
+ }
+ if (aContent->IsMathMLElement()) {
+ return mMathMLMarkupMap.Get(aContent->NodeInfo()->NameAtom());
+ }
+ // This function can be called by MarkupAttribute, etc. which might in turn
+ // be called on a XUL, SVG, etc. element. For example, this can happen
+ // with nsAccUtils::SetLiveContainerAttributes.
+ return nullptr;
+ }
+
+ const mozilla::a11y::MarkupMapInfo* GetMarkupMapInfoFor(
+ mozilla::a11y::Accessible* aAcc) const;
+
+ nsTHashMap<nsAtom*, const mozilla::a11y::XULMarkupMapInfo*> mXULMarkupMap;
+
+ friend nsAccessibilityService* GetAccService();
+ friend nsAccessibilityService* GetOrCreateAccService(uint32_t);
+ friend void MaybeShutdownAccService(uint32_t);
+ friend void mozilla::a11y::PrefChanged(const char*, void*);
+ friend mozilla::a11y::FocusManager* mozilla::a11y::FocusMgr();
+ friend mozilla::a11y::SelectionManager* mozilla::a11y::SelectionMgr();
+ friend mozilla::a11y::ApplicationAccessible* mozilla::a11y::ApplicationAcc();
+ friend mozilla::a11y::xpcAccessibleApplication*
+ mozilla::a11y::XPCApplicationAcc();
+ friend class xpcAccessibilityService;
+};
+
+/**
+ * Return the accessibility service instance. (Handy global function)
+ */
+inline nsAccessibilityService* GetAccService() {
+ return nsAccessibilityService::gAccessibilityService;
+}
+
+/**
+ * Return accessibility service instance; creating one if necessary.
+ */
+nsAccessibilityService* GetOrCreateAccService(
+ uint32_t aNewConsumer = nsAccessibilityService::ePlatformAPI);
+
+/**
+ * Shutdown accessibility service if needed.
+ */
+void MaybeShutdownAccService(uint32_t aFormerConsumer);
+
+/**
+ * Return true if we're in a content process and not B2G.
+ */
+inline bool IPCAccessibilityActive() { return XRE_IsContentProcess(); }
+
+/**
+ * Map nsIAccessibleEvents constants to strings. Used by
+ * nsAccessibilityService::GetStringEventType() method.
+ */
+static const char kEventTypeNames[][40] = {
+ "unknown", //
+ "show", // EVENT_SHOW
+ "hide", // EVENT_HIDE
+ "reorder", // EVENT_REORDER
+ "focus", // EVENT_FOCUS
+ "state change", // EVENT_STATE_CHANGE
+ "name changed", // EVENT_NAME_CHANGE
+ "description change", // EVENT_DESCRIPTION_CHANGE
+ "value change", // EVENT_VALUE_CHANGE
+ "selection", // EVENT_SELECTION
+ "selection add", // EVENT_SELECTION_ADD
+ "selection remove", // EVENT_SELECTION_REMOVE
+ "selection within", // EVENT_SELECTION_WITHIN
+ "alert", // EVENT_ALERT
+ "menu start", // EVENT_MENU_START
+ "menu end", // EVENT_MENU_END
+ "menupopup start", // EVENT_MENUPOPUP_START
+ "menupopup end", // EVENT_MENUPOPUP_END
+ "dragdrop start", // EVENT_DRAGDROP_START
+ "scrolling start", // EVENT_SCROLLING_START
+ "scrolling end", // EVENT_SCROLLING_END
+ "document load complete", // EVENT_DOCUMENT_LOAD_COMPLETE
+ "document reload", // EVENT_DOCUMENT_RELOAD
+ "document load stopped", // EVENT_DOCUMENT_LOAD_STOPPED
+ "text attribute changed", // EVENT_TEXT_ATTRIBUTE_CHANGED
+ "text caret moved", // EVENT_TEXT_CARET_MOVED
+ "text inserted", // EVENT_TEXT_INSERTED
+ "text removed", // EVENT_TEXT_REMOVED
+ "text selection changed", // EVENT_TEXT_SELECTION_CHANGED
+ "window activate", // EVENT_WINDOW_ACTIVATE
+ "window deactivate", // EVENT_WINDOW_DEACTIVATE
+ "window maximize", // EVENT_WINDOW_MAXIMIZE
+ "window minimize", // EVENT_WINDOW_MINIMIZE
+ "window restore", // EVENT_WINDOW_RESTORE
+ "object attribute changed", // EVENT_OBJECT_ATTRIBUTE_CHANGED
+ "text value change", // EVENT_TEXT_VALUE_CHANGE
+ "scrolling", // EVENT_SCROLLING
+ "announcement", // EVENT_ANNOUNCEMENT
+ "live region added", // EVENT_LIVE_REGION_ADDED
+ "live region removed", // EVENT_LIVE_REGION_REMOVED
+ "inner reorder", // EVENT_INNER_REORDER
+};
+
+#endif
diff --git a/accessible/base/nsCoreUtils.cpp b/accessible/base/nsCoreUtils.cpp
new file mode 100644
index 0000000000..80739bb401
--- /dev/null
+++ b/accessible/base/nsCoreUtils.cpp
@@ -0,0 +1,622 @@
+/* -*- 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 "nsCoreUtils.h"
+
+#include "nsAttrValue.h"
+#include "nsIAccessibleTypes.h"
+
+#include "mozilla/dom/Document.h"
+#include "nsAccUtils.h"
+#include "nsRange.h"
+#include "nsXULElement.h"
+#include "nsIDocShell.h"
+#include "nsIObserverService.h"
+#include "nsPresContext.h"
+#include "nsIScrollableFrame.h"
+#include "nsISelectionController.h"
+#include "nsISimpleEnumerator.h"
+#include "mozilla/dom/TouchEvent.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/EventListenerManager.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/TouchEvents.h"
+#include "nsView.h"
+#include "nsGkAtoms.h"
+
+#include "nsComponentManagerUtils.h"
+
+#include "XULTreeElement.h"
+#include "nsIContentInlines.h"
+#include "nsTreeColumns.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLLabelElement.h"
+#include "mozilla/dom/MouseEventBinding.h"
+#include "mozilla/dom/Selection.h"
+
+using namespace mozilla;
+
+using mozilla::dom::DOMRect;
+using mozilla::dom::Element;
+using mozilla::dom::Selection;
+using mozilla::dom::XULTreeElement;
+
+using mozilla::a11y::nsAccUtils;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsCoreUtils
+////////////////////////////////////////////////////////////////////////////////
+
+bool nsCoreUtils::IsLabelWithControl(nsIContent* aContent) {
+ dom::HTMLLabelElement* label = dom::HTMLLabelElement::FromNode(aContent);
+ if (label && label->GetControl()) return true;
+
+ return false;
+}
+
+bool nsCoreUtils::HasClickListener(nsIContent* aContent) {
+ NS_ENSURE_TRUE(aContent, false);
+ EventListenerManager* listenerManager =
+ aContent->GetExistingListenerManager();
+
+ return listenerManager &&
+ (listenerManager->HasListenersFor(nsGkAtoms::onclick) ||
+ listenerManager->HasListenersFor(nsGkAtoms::onmousedown) ||
+ listenerManager->HasListenersFor(nsGkAtoms::onmouseup));
+}
+
+void nsCoreUtils::DispatchClickEvent(XULTreeElement* aTree, int32_t aRowIndex,
+ nsTreeColumn* aColumn,
+ const nsAString& aPseudoElt) {
+ RefPtr<dom::Element> tcElm = aTree->GetTreeBody();
+ if (!tcElm) return;
+
+ Document* document = tcElm->GetUncomposedDoc();
+ if (!document) return;
+
+ RefPtr<PresShell> presShell = document->GetPresShell();
+ if (!presShell) {
+ return;
+ }
+
+ // Ensure row is visible.
+ aTree->EnsureRowIsVisible(aRowIndex);
+
+ // Calculate x and y coordinates.
+ nsresult rv;
+ nsIntRect rect =
+ aTree->GetCoordsForCellItem(aRowIndex, aColumn, aPseudoElt, rv);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ RefPtr<DOMRect> treeBodyRect = tcElm->GetBoundingClientRect();
+ int32_t tcX = (int32_t)treeBodyRect->X();
+ int32_t tcY = (int32_t)treeBodyRect->Y();
+
+ // Dispatch mouse events.
+ AutoWeakFrame tcFrame = tcElm->GetPrimaryFrame();
+ nsIFrame* rootFrame = presShell->GetRootFrame();
+
+ nsPoint offset;
+ nsCOMPtr<nsIWidget> rootWidget =
+ rootFrame->GetView()->GetNearestWidget(&offset);
+
+ RefPtr<nsPresContext> presContext = presShell->GetPresContext();
+
+ int32_t cnvdX = presContext->CSSPixelsToDevPixels(tcX + int32_t(rect.x) + 1) +
+ presContext->AppUnitsToDevPixels(offset.x);
+ int32_t cnvdY = presContext->CSSPixelsToDevPixels(tcY + int32_t(rect.y) + 1) +
+ presContext->AppUnitsToDevPixels(offset.y);
+
+ // XUL is just desktop, so there is no real reason for senfing touch events.
+ DispatchMouseEvent(eMouseDown, cnvdX, cnvdY, tcElm, tcFrame, presShell,
+ rootWidget);
+
+ DispatchMouseEvent(eMouseUp, cnvdX, cnvdY, tcElm, tcFrame, presShell,
+ rootWidget);
+}
+
+void nsCoreUtils::DispatchMouseEvent(EventMessage aMessage, int32_t aX,
+ int32_t aY, nsIContent* aContent,
+ nsIFrame* aFrame, PresShell* aPresShell,
+ nsIWidget* aRootWidget) {
+ WidgetMouseEvent event(true, aMessage, aRootWidget, WidgetMouseEvent::eReal,
+ WidgetMouseEvent::eNormal);
+
+ event.mRefPoint = LayoutDeviceIntPoint(aX, aY);
+
+ event.mClickCount = 1;
+ event.mButton = MouseButton::ePrimary;
+ event.mInputSource = dom::MouseEvent_Binding::MOZ_SOURCE_UNKNOWN;
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ aPresShell->HandleEventWithTarget(&event, aFrame, aContent, &status);
+}
+
+void nsCoreUtils::DispatchTouchEvent(EventMessage aMessage, int32_t aX,
+ int32_t aY, nsIContent* aContent,
+ nsIFrame* aFrame, PresShell* aPresShell,
+ nsIWidget* aRootWidget) {
+ nsIDocShell* docShell = nullptr;
+ if (aPresShell->GetDocument()) {
+ docShell = aPresShell->GetDocument()->GetDocShell();
+ }
+ if (!dom::TouchEvent::PrefEnabled(docShell)) {
+ return;
+ }
+
+ WidgetTouchEvent event(true, aMessage, aRootWidget);
+
+ // XXX: Touch has an identifier of -1 to hint that it is synthesized.
+ RefPtr<dom::Touch> t = new dom::Touch(-1, LayoutDeviceIntPoint(aX, aY),
+ LayoutDeviceIntPoint(1, 1), 0.0f, 1.0f);
+ t->SetTouchTarget(aContent);
+ event.mTouches.AppendElement(t);
+ nsEventStatus status = nsEventStatus_eIgnore;
+ aPresShell->HandleEventWithTarget(&event, aFrame, aContent, &status);
+}
+
+uint32_t nsCoreUtils::GetAccessKeyFor(nsIContent* aContent) {
+ // Accesskeys are registered by @accesskey attribute only. At first check
+ // whether it is presented on the given element to avoid the slow
+ // EventStateManager::GetRegisteredAccessKey() method.
+ if (!aContent->IsElement() || !aContent->AsElement()->HasAttr(
+ kNameSpaceID_None, nsGkAtoms::accesskey)) {
+ return 0;
+ }
+
+ nsPresContext* presContext = aContent->OwnerDoc()->GetPresContext();
+ if (!presContext) return 0;
+
+ EventStateManager* esm = presContext->EventStateManager();
+ if (!esm) return 0;
+
+ return esm->GetRegisteredAccessKey(aContent->AsElement());
+}
+
+nsIContent* nsCoreUtils::GetDOMElementFor(nsIContent* aContent) {
+ if (aContent->IsElement()) return aContent;
+
+ if (aContent->IsText()) return aContent->GetFlattenedTreeParent();
+
+ return nullptr;
+}
+
+nsINode* nsCoreUtils::GetDOMNodeFromDOMPoint(nsINode* aNode, uint32_t aOffset) {
+ if (aNode && aNode->IsElement()) {
+ uint32_t childCount = aNode->GetChildCount();
+ NS_ASSERTION(aOffset <= childCount, "Wrong offset of the DOM point!");
+
+ // The offset can be after last child of container node that means DOM point
+ // is placed immediately after the last child. In this case use the DOM node
+ // from the given DOM point is used as result node.
+ if (aOffset != childCount) return aNode->GetChildAt_Deprecated(aOffset);
+ }
+
+ return aNode;
+}
+
+bool nsCoreUtils::IsAncestorOf(nsINode* aPossibleAncestorNode,
+ nsINode* aPossibleDescendantNode,
+ nsINode* aRootNode) {
+ NS_ENSURE_TRUE(aPossibleAncestorNode && aPossibleDescendantNode, false);
+
+ nsINode* parentNode = aPossibleDescendantNode;
+ while ((parentNode = parentNode->GetParentNode()) &&
+ parentNode != aRootNode) {
+ if (parentNode == aPossibleAncestorNode) return true;
+ }
+
+ return false;
+}
+
+nsresult nsCoreUtils::ScrollSubstringTo(nsIFrame* aFrame, nsRange* aRange,
+ uint32_t aScrollType) {
+ ScrollAxis vertical, horizontal;
+ ConvertScrollTypeToPercents(aScrollType, &vertical, &horizontal);
+
+ return ScrollSubstringTo(aFrame, aRange, vertical, horizontal);
+}
+
+nsresult nsCoreUtils::ScrollSubstringTo(nsIFrame* aFrame, nsRange* aRange,
+ ScrollAxis aVertical,
+ ScrollAxis aHorizontal) {
+ if (!aFrame || !aRange) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsPresContext* presContext = aFrame->PresContext();
+
+ nsCOMPtr<nsISelectionController> selCon;
+ aFrame->GetSelectionController(presContext, getter_AddRefs(selCon));
+ NS_ENSURE_TRUE(selCon, NS_ERROR_FAILURE);
+
+ RefPtr<dom::Selection> selection =
+ selCon->GetSelection(nsISelectionController::SELECTION_ACCESSIBILITY);
+
+ selection->RemoveAllRanges(IgnoreErrors());
+ selection->AddRangeAndSelectFramesAndNotifyListeners(*aRange, IgnoreErrors());
+
+ selection->ScrollIntoView(nsISelectionController::SELECTION_ANCHOR_REGION,
+ aVertical, aHorizontal,
+ Selection::SCROLL_SYNCHRONOUS);
+
+ selection->CollapseToStart(IgnoreErrors());
+
+ return NS_OK;
+}
+
+void nsCoreUtils::ScrollFrameToPoint(nsIFrame* aScrollableFrame,
+ nsIFrame* aFrame,
+ const LayoutDeviceIntPoint& aPoint) {
+ nsIScrollableFrame* scrollableFrame = do_QueryFrame(aScrollableFrame);
+ if (!scrollableFrame) return;
+
+ nsPoint point = LayoutDeviceIntPoint::ToAppUnits(
+ aPoint, aFrame->PresContext()->AppUnitsPerDevPixel());
+ nsRect frameRect = aFrame->GetScreenRectInAppUnits();
+ nsPoint deltaPoint = point - frameRect.TopLeft();
+
+ nsPoint scrollPoint = scrollableFrame->GetScrollPosition();
+ scrollPoint -= deltaPoint;
+
+ scrollableFrame->ScrollTo(scrollPoint, ScrollMode::Instant);
+}
+
+void nsCoreUtils::ConvertScrollTypeToPercents(uint32_t aScrollType,
+ ScrollAxis* aVertical,
+ ScrollAxis* aHorizontal) {
+ WhereToScroll whereY, whereX;
+ WhenToScroll whenY, whenX;
+ switch (aScrollType) {
+ case nsIAccessibleScrollType::SCROLL_TYPE_TOP_LEFT:
+ whereY = WhereToScroll::Start;
+ whenY = WhenToScroll::Always;
+ whereX = WhereToScroll::Start;
+ whenX = WhenToScroll::Always;
+ break;
+ case nsIAccessibleScrollType::SCROLL_TYPE_BOTTOM_RIGHT:
+ whereY = WhereToScroll::End;
+ whenY = WhenToScroll::Always;
+ whereX = WhereToScroll::End;
+ whenX = WhenToScroll::Always;
+ break;
+ case nsIAccessibleScrollType::SCROLL_TYPE_TOP_EDGE:
+ whereY = WhereToScroll::Start;
+ whenY = WhenToScroll::Always;
+ whereX = WhereToScroll::Nearest;
+ whenX = WhenToScroll::IfNotFullyVisible;
+ break;
+ case nsIAccessibleScrollType::SCROLL_TYPE_BOTTOM_EDGE:
+ whereY = WhereToScroll::End;
+ whenY = WhenToScroll::Always;
+ whereX = WhereToScroll::Nearest;
+ whenX = WhenToScroll::IfNotFullyVisible;
+ break;
+ case nsIAccessibleScrollType::SCROLL_TYPE_LEFT_EDGE:
+ whereY = WhereToScroll::Nearest;
+ whenY = WhenToScroll::IfNotFullyVisible;
+ whereX = WhereToScroll::Start;
+ whenX = WhenToScroll::Always;
+ break;
+ case nsIAccessibleScrollType::SCROLL_TYPE_RIGHT_EDGE:
+ whereY = WhereToScroll::Nearest;
+ whenY = WhenToScroll::IfNotFullyVisible;
+ whereX = WhereToScroll::End;
+ whenX = WhenToScroll::Always;
+ break;
+ default:
+ whereY = WhereToScroll::Center;
+ whenY = WhenToScroll::IfNotFullyVisible;
+ whereX = WhereToScroll::Center;
+ whenX = WhenToScroll::IfNotFullyVisible;
+ }
+ *aVertical = ScrollAxis(whereY, whenY);
+ *aHorizontal = ScrollAxis(whereX, whenX);
+}
+
+already_AddRefed<nsIDocShell> nsCoreUtils::GetDocShellFor(nsINode* aNode) {
+ if (!aNode) return nullptr;
+
+ nsCOMPtr<nsIDocShell> docShell = aNode->OwnerDoc()->GetDocShell();
+ return docShell.forget();
+}
+
+bool nsCoreUtils::IsRootDocument(Document* aDocument) {
+ nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem = aDocument->GetDocShell();
+ NS_ASSERTION(docShellTreeItem, "No document shell for document!");
+
+ nsCOMPtr<nsIDocShellTreeItem> parentTreeItem;
+ docShellTreeItem->GetInProcessParent(getter_AddRefs(parentTreeItem));
+
+ return !parentTreeItem;
+}
+
+bool nsCoreUtils::IsTopLevelContentDocInProcess(Document* aDocumentNode) {
+ mozilla::dom::BrowsingContext* bc = aDocumentNode->GetBrowsingContext();
+ return bc->IsContent() && (
+ // Tab document.
+ bc->IsTop() ||
+ // Out-of-process iframe.
+ !bc->GetParent()->IsInProcess());
+}
+
+bool nsCoreUtils::IsErrorPage(Document* aDocument) {
+ nsIURI* uri = aDocument->GetDocumentURI();
+ if (!uri->SchemeIs("about")) {
+ return false;
+ }
+
+ nsAutoCString path;
+ uri->GetPathQueryRef(path);
+
+ constexpr auto neterror = "neterror"_ns;
+ constexpr auto certerror = "certerror"_ns;
+
+ return StringBeginsWith(path, neterror) || StringBeginsWith(path, certerror);
+}
+
+PresShell* nsCoreUtils::GetPresShellFor(nsINode* aNode) {
+ return aNode->OwnerDoc()->GetPresShell();
+}
+
+bool nsCoreUtils::GetID(nsIContent* aContent, nsAString& aID) {
+ return aContent->IsElement() &&
+ aContent->AsElement()->GetAttr(nsGkAtoms::id, aID);
+}
+
+bool nsCoreUtils::GetUIntAttr(nsIContent* aContent, nsAtom* aAttr,
+ int32_t* aUInt) {
+ if (!aContent->IsElement()) {
+ return false;
+ }
+ return GetUIntAttrValue(nsAccUtils::GetARIAAttr(aContent->AsElement(), aAttr),
+ aUInt);
+}
+
+bool nsCoreUtils::GetUIntAttrValue(const nsAttrValue* aVal, int32_t* aUInt) {
+ if (!aVal) {
+ return false;
+ }
+ nsAutoString value;
+ aVal->ToString(value);
+ if (!value.IsEmpty()) {
+ nsresult error = NS_OK;
+ int32_t integer = value.ToInteger(&error);
+ if (NS_SUCCEEDED(error) && integer > 0) {
+ *aUInt = integer;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void nsCoreUtils::GetLanguageFor(nsIContent* aContent, nsIContent* aRootContent,
+ nsAString& aLanguage) {
+ aLanguage.Truncate();
+
+ nsIContent* walkUp = aContent;
+ while (walkUp && walkUp != aRootContent &&
+ (!walkUp->IsElement() ||
+ !walkUp->AsElement()->GetAttr(nsGkAtoms::lang, aLanguage))) {
+ walkUp = walkUp->GetParent();
+ }
+}
+
+XULTreeElement* nsCoreUtils::GetTree(nsIContent* aContent) {
+ // Find DOMNode's parents recursively until reach the <tree> tag
+ nsIContent* currentContent = aContent;
+ while (currentContent) {
+ if (currentContent->NodeInfo()->Equals(nsGkAtoms::tree, kNameSpaceID_XUL)) {
+ return XULTreeElement::FromNode(currentContent);
+ }
+ currentContent = currentContent->GetFlattenedTreeParent();
+ }
+
+ return nullptr;
+}
+
+already_AddRefed<nsTreeColumn> nsCoreUtils::GetFirstSensibleColumn(
+ XULTreeElement* aTree, FlushType aFlushType) {
+ if (!aTree) {
+ return nullptr;
+ }
+
+ RefPtr<nsTreeColumns> cols = aTree->GetColumns(aFlushType);
+ if (!cols) {
+ return nullptr;
+ }
+
+ RefPtr<nsTreeColumn> column = cols->GetFirstColumn();
+ if (column && IsColumnHidden(column)) return GetNextSensibleColumn(column);
+
+ return column.forget();
+}
+
+uint32_t nsCoreUtils::GetSensibleColumnCount(XULTreeElement* aTree) {
+ uint32_t count = 0;
+ if (!aTree) {
+ return count;
+ }
+
+ RefPtr<nsTreeColumns> cols = aTree->GetColumns();
+ if (!cols) {
+ return count;
+ }
+
+ nsTreeColumn* column = cols->GetFirstColumn();
+
+ while (column) {
+ if (!IsColumnHidden(column)) count++;
+
+ column = column->GetNext();
+ }
+
+ return count;
+}
+
+already_AddRefed<nsTreeColumn> nsCoreUtils::GetSensibleColumnAt(
+ XULTreeElement* aTree, uint32_t aIndex) {
+ if (!aTree) {
+ return nullptr;
+ }
+
+ uint32_t idx = aIndex;
+
+ nsCOMPtr<nsTreeColumn> column = GetFirstSensibleColumn(aTree);
+ while (column) {
+ if (idx == 0) return column.forget();
+
+ idx--;
+ column = GetNextSensibleColumn(column);
+ }
+
+ return nullptr;
+}
+
+already_AddRefed<nsTreeColumn> nsCoreUtils::GetNextSensibleColumn(
+ nsTreeColumn* aColumn) {
+ if (!aColumn) {
+ return nullptr;
+ }
+
+ RefPtr<nsTreeColumn> nextColumn = aColumn->GetNext();
+
+ while (nextColumn && IsColumnHidden(nextColumn)) {
+ nextColumn = nextColumn->GetNext();
+ }
+
+ return nextColumn.forget();
+}
+
+already_AddRefed<nsTreeColumn> nsCoreUtils::GetPreviousSensibleColumn(
+ nsTreeColumn* aColumn) {
+ if (!aColumn) {
+ return nullptr;
+ }
+
+ RefPtr<nsTreeColumn> prevColumn = aColumn->GetPrevious();
+
+ while (prevColumn && IsColumnHidden(prevColumn)) {
+ prevColumn = prevColumn->GetPrevious();
+ }
+
+ return prevColumn.forget();
+}
+
+bool nsCoreUtils::IsColumnHidden(nsTreeColumn* aColumn) {
+ if (!aColumn) {
+ return false;
+ }
+
+ Element* element = aColumn->Element();
+ return element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters);
+}
+
+void nsCoreUtils::ScrollTo(PresShell* aPresShell, nsIContent* aContent,
+ uint32_t aScrollType) {
+ ScrollAxis vertical, horizontal;
+ ConvertScrollTypeToPercents(aScrollType, &vertical, &horizontal);
+ aPresShell->ScrollContentIntoView(aContent, vertical, horizontal,
+ ScrollFlags::ScrollOverflowHidden);
+}
+
+bool nsCoreUtils::IsHTMLTableHeader(nsIContent* aContent) {
+ return aContent->NodeInfo()->Equals(nsGkAtoms::th) ||
+ (aContent->IsElement() &&
+ aContent->AsElement()->HasAttr(nsGkAtoms::scope));
+}
+
+bool nsCoreUtils::IsWhitespaceString(const nsAString& aString) {
+ nsAString::const_char_iterator iterBegin, iterEnd;
+
+ aString.BeginReading(iterBegin);
+ aString.EndReading(iterEnd);
+
+ while (iterBegin != iterEnd && IsWhitespace(*iterBegin)) ++iterBegin;
+
+ return iterBegin == iterEnd;
+}
+
+bool nsCoreUtils::AccEventObserversExist() {
+ nsCOMPtr<nsIObserverService> obsService = services::GetObserverService();
+ NS_ENSURE_TRUE(obsService, false);
+
+ nsCOMPtr<nsISimpleEnumerator> observers;
+ obsService->EnumerateObservers(NS_ACCESSIBLE_EVENT_TOPIC,
+ getter_AddRefs(observers));
+ NS_ENSURE_TRUE(observers, false);
+
+ bool hasObservers = false;
+ observers->HasMoreElements(&hasObservers);
+
+ return hasObservers;
+}
+
+void nsCoreUtils::DispatchAccEvent(RefPtr<nsIAccessibleEvent> event) {
+ nsCOMPtr<nsIObserverService> obsService = services::GetObserverService();
+ NS_ENSURE_TRUE_VOID(obsService);
+
+ obsService->NotifyObservers(event, NS_ACCESSIBLE_EVENT_TOPIC, nullptr);
+}
+
+bool nsCoreUtils::IsDisplayContents(nsIContent* aContent) {
+ auto* element = Element::FromNodeOrNull(aContent);
+ return element && element->IsDisplayContents();
+}
+
+bool nsCoreUtils::CanCreateAccessibleWithoutFrame(nsIContent* aContent) {
+ auto* element = Element::FromNodeOrNull(aContent);
+ if (!element) {
+ return false;
+ }
+ if (!element->HasServoData() || Servo_Element_IsDisplayNone(element)) {
+ // Out of the flat tree or in a display: none subtree.
+ return false;
+ }
+
+ // If we aren't display: contents or option/optgroup we can't create an
+ // accessible without frame. Our select combobox code relies on the latter.
+ if (!element->IsDisplayContents() &&
+ !element->IsAnyOfHTMLElements(nsGkAtoms::option, nsGkAtoms::optgroup)) {
+ return false;
+ }
+
+ // Even if we're display: contents or optgroups, we might not be able to
+ // create an accessible if we're in a content-visibility: hidden subtree.
+ //
+ // To check that, find the closest ancestor element with a frame.
+ for (nsINode* ancestor = element->GetFlattenedTreeParentNode();
+ ancestor && ancestor->IsContent();
+ ancestor = ancestor->GetFlattenedTreeParentNode()) {
+ if (nsIFrame* f = ancestor->AsContent()->GetPrimaryFrame()) {
+ if (f->HidesContent(nsIFrame::IncludeContentVisibility::Hidden) ||
+ f->IsHiddenByContentVisibilityOnAnyAncestor(
+ nsIFrame::IncludeContentVisibility::Hidden)) {
+ return false;
+ }
+ break;
+ }
+ }
+
+ return true;
+}
+
+bool nsCoreUtils::IsDocumentVisibleConsideringInProcessAncestors(
+ const Document* aDocument) {
+ const Document* parent = aDocument;
+ do {
+ if (!parent->IsVisible()) {
+ return false;
+ }
+ } while ((parent = parent->GetInProcessParentDocument()));
+ return true;
+}
diff --git a/accessible/base/nsCoreUtils.h b/accessible/base/nsCoreUtils.h
new file mode 100644
index 0000000000..2c3e7330ff
--- /dev/null
+++ b/accessible/base/nsCoreUtils.h
@@ -0,0 +1,329 @@
+/* -*- 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 nsCoreUtils_h_
+#define nsCoreUtils_h_
+
+#include "AttrArray.h"
+#include "mozilla/EventForwards.h"
+#include "nsCaseTreatment.h"
+#include "nsIAccessibleEvent.h"
+#include "nsIContent.h"
+#include "mozilla/FlushType.h"
+#include "mozilla/PresShellForwards.h"
+
+#include "nsPoint.h"
+#include "nsTArray.h"
+#include "Units.h"
+
+class nsAttrValue;
+class nsGenericHTMLElement;
+class nsRange;
+class nsTreeColumn;
+class nsIFrame;
+class nsIDocShell;
+class nsIWidget;
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class Document;
+class XULTreeElement;
+} // namespace dom
+} // namespace mozilla
+
+/**
+ * Core utils.
+ */
+class nsCoreUtils {
+ public:
+ typedef mozilla::PresShell PresShell;
+ typedef mozilla::dom::Document Document;
+
+ /**
+ * Return true if the given node is a label of a control.
+ */
+ static bool IsLabelWithControl(nsIContent* aContent);
+
+ /**
+ * Return true if the given node has registered click, mousedown or mouseup
+ * event listeners.
+ */
+ static bool HasClickListener(nsIContent* aContent);
+
+ /**
+ * Dispatch click event to XUL tree cell.
+ *
+ * @param aTree [in] tree
+ * @param aRowIndex [in] row index
+ * @param aColumn [in] column object
+ * @param aPseudoElm [in] pseudo element inside the cell, see
+ * XULTreeElement for available values
+ */
+ MOZ_CAN_RUN_SCRIPT
+ static void DispatchClickEvent(mozilla::dom::XULTreeElement* aTree,
+ int32_t aRowIndex, nsTreeColumn* aColumn,
+ const nsAString& aPseudoElt = u""_ns);
+
+ /**
+ * Send mouse event to the given element.
+ *
+ * @param aMessage [in] an event message (see EventForwards.h)
+ * @param aX [in] x coordinate in dev pixels
+ * @param aY [in] y coordinate in dev pixels
+ * @param aContent [in] the element
+ * @param aFrame [in] frame of the element
+ * @param aPresShell [in] the presshell for the element
+ * @param aRootWidget [in] the root widget of the element
+ */
+ MOZ_CAN_RUN_SCRIPT
+ static void DispatchMouseEvent(mozilla::EventMessage aMessage, int32_t aX,
+ int32_t aY, nsIContent* aContent,
+ nsIFrame* aFrame, PresShell* aPresShell,
+ nsIWidget* aRootWidget);
+
+ /**
+ * Send a touch event with a single touch point to the given element.
+ *
+ * @param aMessage [in] an event message (see EventForwards.h)
+ * @param aX [in] x coordinate in dev pixels
+ * @param aY [in] y coordinate in dev pixels
+ * @param aContent [in] the element
+ * @param aFrame [in] frame of the element
+ * @param aPresShell [in] the presshell for the element
+ * @param aRootWidget [in] the root widget of the element
+ */
+ MOZ_CAN_RUN_SCRIPT
+ static void DispatchTouchEvent(mozilla::EventMessage aMessage, int32_t aX,
+ int32_t aY, nsIContent* aContent,
+ nsIFrame* aFrame, PresShell* aPresShell,
+ nsIWidget* aRootWidget);
+
+ /**
+ * Return an accesskey registered on the given element by
+ * EventStateManager or 0 if there is no registered accesskey.
+ *
+ * @param aContent - the given element.
+ */
+ static uint32_t GetAccessKeyFor(nsIContent* aContent);
+
+ /**
+ * Return DOM element related with the given node, i.e.
+ * a) itself if it is DOM element
+ * b) parent element if it is text node
+ * c) otherwise nullptr
+ *
+ * @param aNode [in] the given DOM node
+ */
+ static nsIContent* GetDOMElementFor(nsIContent* aContent);
+
+ /**
+ * Return DOM node for the given DOM point.
+ */
+ static nsINode* GetDOMNodeFromDOMPoint(nsINode* aNode, uint32_t aOffset);
+
+ /**
+ * Is the first passed in node an ancestor of the second?
+ * Note: A node is not considered to be the ancestor of itself.
+ *
+ * @param aPossibleAncestorNode [in] node to test for ancestor-ness of
+ * aPossibleDescendantNode
+ * @param aPossibleDescendantNode [in] node to test for descendant-ness of
+ * aPossibleAncestorNode
+ * @param aRootNode [in, optional] the root node that search
+ * search should be performed within
+ * @return true if aPossibleAncestorNode is an ancestor of
+ * aPossibleDescendantNode
+ */
+ static bool IsAncestorOf(nsINode* aPossibleAncestorNode,
+ nsINode* aPossibleDescendantNode,
+ nsINode* aRootNode = nullptr);
+
+ /**
+ * Helper method to scroll range into view, used for implementation of
+ * nsIAccessibleText::scrollSubstringTo().
+ *
+ * @param aFrame the frame for accessible the range belongs to.
+ * @param aRange the range to scroll to
+ * @param aScrollType the place a range should be scrolled to
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY static nsresult ScrollSubstringTo(
+ nsIFrame* aFrame, nsRange* aRange, uint32_t aScrollType);
+
+ /** Helper method to scroll range into view, used for implementation of
+ * nsIAccessibleText::scrollSubstringTo[Point]().
+ *
+ * @param aFrame the frame for accessible the range belongs to.
+ * @param aRange the range to scroll to
+ * @param aVertical how to align vertically, specified in percents, and
+ * when.
+ * @param aHorizontal how to align horizontally, specified in percents,
+ * and when.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY static nsresult ScrollSubstringTo(
+ nsIFrame* aFrame, nsRange* aRange, mozilla::ScrollAxis aVertical,
+ mozilla::ScrollAxis aHorizontal);
+
+ /**
+ * Scrolls the given frame to the point, used for implememntation of
+ * nsIAccessible::scrollToPoint and nsIAccessibleText::scrollSubstringToPoint.
+ *
+ * @param aScrollableFrame the scrollable frame
+ * @param aFrame the frame to scroll
+ * @param aPoint the point scroll to (in dev pixels)
+ */
+ static void ScrollFrameToPoint(nsIFrame* aScrollableFrame, nsIFrame* aFrame,
+ const mozilla::LayoutDeviceIntPoint& aPoint);
+
+ /**
+ * Converts scroll type constant defined in nsIAccessibleScrollType to
+ * vertical and horizontal parameters.
+ */
+ static void ConvertScrollTypeToPercents(uint32_t aScrollType,
+ mozilla::ScrollAxis* aVertical,
+ mozilla::ScrollAxis* aHorizontal);
+
+ /**
+ * Return document shell for the given DOM node.
+ */
+ static already_AddRefed<nsIDocShell> GetDocShellFor(nsINode* aNode);
+
+ /**
+ * Return true if the given document is root document.
+ */
+ static bool IsRootDocument(Document* aDocument);
+
+ /**
+ * Return true if the given document is a top level content document in this
+ * process.
+ * This will be true for tab documents and out-of-process iframe documents.
+ */
+ static bool IsTopLevelContentDocInProcess(Document* aDocumentNode);
+
+ /**
+ * Return true if the given document is an error page.
+ */
+ static bool IsErrorPage(Document* aDocument);
+
+ /**
+ * Return presShell for the document containing the given DOM node.
+ */
+ static PresShell* GetPresShellFor(nsINode* aNode);
+
+ /**
+ * Get the ID for an element, in some types of XML this may not be the ID
+ * attribute
+ * @param aContent Node to get the ID for
+ * @param aID Where to put ID string
+ * @return true if there is an ID set for this node
+ */
+ static bool GetID(nsIContent* aContent, nsAString& aID);
+
+ /**
+ * Convert attribute value of the given node to positive integer. If no
+ * attribute or wrong value then false is returned.
+ */
+ static bool GetUIntAttr(nsIContent* aContent, nsAtom* aAttr, int32_t* aUInt);
+ static bool GetUIntAttrValue(const nsAttrValue* aVal, int32_t* aUInt);
+
+ /**
+ * Returns language for the given node.
+ *
+ * @param aContent [in] the given node
+ * @param aRootContent [in] container of the given node
+ * @param aLanguage [out] language
+ */
+ static void GetLanguageFor(nsIContent* aContent, nsIContent* aRootContent,
+ nsAString& aLanguage);
+
+ /**
+ * Return tree from any levels DOMNode under the XUL tree.
+ */
+ static mozilla::dom::XULTreeElement* GetTree(nsIContent* aContent);
+
+ /**
+ * Return first sensible column for the given tree box object.
+ */
+ static already_AddRefed<nsTreeColumn> GetFirstSensibleColumn(
+ mozilla::dom::XULTreeElement* aTree,
+ mozilla::FlushType = mozilla::FlushType::Frames);
+
+ /**
+ * Return sensible columns count for the given tree box object.
+ */
+ static uint32_t GetSensibleColumnCount(mozilla::dom::XULTreeElement* aTree);
+
+ /**
+ * Return sensible column at the given index for the given tree box object.
+ */
+ static already_AddRefed<nsTreeColumn> GetSensibleColumnAt(
+ mozilla::dom::XULTreeElement* aTree, uint32_t aIndex);
+
+ /**
+ * Return next sensible column for the given column.
+ */
+ static already_AddRefed<nsTreeColumn> GetNextSensibleColumn(
+ nsTreeColumn* aColumn);
+
+ /**
+ * Return previous sensible column for the given column.
+ */
+ static already_AddRefed<nsTreeColumn> GetPreviousSensibleColumn(
+ nsTreeColumn* aColumn);
+
+ /**
+ * Return true if the given column is hidden (i.e. not sensible).
+ */
+ static bool IsColumnHidden(nsTreeColumn* aColumn);
+
+ /**
+ * Scroll content into view.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ static void ScrollTo(PresShell* aPresShell, nsIContent* aContent,
+ uint32_t aScrollType);
+
+ /**
+ * Return true if the given node is table header element.
+ */
+ static bool IsHTMLTableHeader(nsIContent* aContent);
+
+ /**
+ * Returns true if the given string is empty or contains whitespace symbols
+ * only. In contrast to nsWhitespaceTokenizer class it takes into account
+ * non-breaking space (0xa0).
+ */
+ static bool IsWhitespaceString(const nsAString& aString);
+
+ /**
+ * Returns true if the given character is whitespace symbol.
+ */
+ static bool IsWhitespace(char16_t aChar) {
+ return aChar == ' ' || aChar == '\n' || aChar == '\r' || aChar == '\t' ||
+ aChar == 0xa0;
+ }
+
+ /*
+ * Return true if there are any observers of accessible events.
+ */
+ static bool AccEventObserversExist();
+
+ /**
+ * Notify accessible event observers of an event.
+ */
+ static void DispatchAccEvent(RefPtr<nsIAccessibleEvent> aEvent);
+
+ static bool IsDisplayContents(nsIContent* aContent);
+ static bool CanCreateAccessibleWithoutFrame(nsIContent* aContent);
+
+ /**
+ * Return whether the document and all its in-process ancestors are visible in
+ * the sense of pageshow / hide.
+ */
+ static bool IsDocumentVisibleConsideringInProcessAncestors(
+ const Document* aDocument);
+};
+
+#endif
diff --git a/accessible/base/nsEventShell.cpp b/accessible/base/nsEventShell.cpp
new file mode 100644
index 0000000000..fcdc954919
--- /dev/null
+++ b/accessible/base/nsEventShell.cpp
@@ -0,0 +1,81 @@
+/* -*- 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 "nsEventShell.h"
+
+#include "nsAccUtils.h"
+#include "Logging.h"
+#include "AccAttributes.h"
+
+#include "mozilla/StaticPtr.h"
+#include "mozilla/dom/DOMStringList.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsEventShell
+////////////////////////////////////////////////////////////////////////////////
+
+void nsEventShell::FireEvent(AccEvent* aEvent) {
+ if (!aEvent || aEvent->mEventRule == AccEvent::eDoNotEmit) return;
+
+ LocalAccessible* accessible = aEvent->GetAccessible();
+ NS_ENSURE_TRUE_VOID(accessible);
+
+ nsINode* node = accessible->GetNode();
+ if (node) {
+ sEventTargetNode = node;
+ sEventFromUserInput = aEvent->IsFromUserInput();
+ }
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eEvents)) {
+ logging::MsgBegin("EVENTS", "events fired");
+ nsAutoString type;
+ GetAccService()->GetStringEventType(aEvent->GetEventType(), type);
+ logging::MsgEntry("type: %s", NS_ConvertUTF16toUTF8(type).get());
+ if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_STATE_CHANGE) {
+ AccStateChangeEvent* event = downcast_accEvent(aEvent);
+ RefPtr<dom::DOMStringList> stringStates =
+ GetAccService()->GetStringStates(event->GetState());
+ nsAutoString state;
+ stringStates->Item(0, state);
+ logging::MsgEntry("state: %s = %s", NS_ConvertUTF16toUTF8(state).get(),
+ event->IsStateEnabled() ? "true" : "false");
+ }
+ logging::AccessibleInfo("target", aEvent->GetAccessible());
+ logging::MsgEnd();
+ }
+#endif
+
+ accessible->HandleAccEvent(aEvent);
+ aEvent->mEventRule = AccEvent::eDoNotEmit;
+
+ sEventTargetNode = nullptr;
+}
+
+void nsEventShell::FireEvent(uint32_t aEventType, LocalAccessible* aAccessible,
+ EIsFromUserInput aIsFromUserInput) {
+ NS_ENSURE_TRUE_VOID(aAccessible);
+
+ RefPtr<AccEvent> event =
+ new AccEvent(aEventType, aAccessible, aIsFromUserInput);
+
+ FireEvent(event);
+}
+
+void nsEventShell::GetEventAttributes(nsINode* aNode,
+ AccAttributes* aAttributes) {
+ if (aNode != sEventTargetNode) return;
+
+ aAttributes->SetAttribute(nsGkAtoms::eventFromInput, sEventFromUserInput);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsEventShell: private
+
+bool nsEventShell::sEventFromUserInput = false;
+StaticRefPtr<nsINode> nsEventShell::sEventTargetNode;
diff --git a/accessible/base/nsEventShell.h b/accessible/base/nsEventShell.h
new file mode 100644
index 0000000000..ff2e062750
--- /dev/null
+++ b/accessible/base/nsEventShell.h
@@ -0,0 +1,66 @@
+/* -*- 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 _nsEventShell_H_
+#define _nsEventShell_H_
+
+#include "AccEvent.h"
+
+namespace mozilla {
+template <typename T>
+class StaticRefPtr;
+}
+
+/**
+ * Used for everything about events.
+ */
+class nsEventShell {
+ public:
+ /**
+ * Fire the accessible event.
+ */
+ static void FireEvent(mozilla::a11y::AccEvent* aEvent);
+
+ /**
+ * Fire accessible event of the given type for the given accessible.
+ *
+ * @param aEventType [in] the event type
+ * @param aAccessible [in] the event target
+ */
+ static void FireEvent(uint32_t aEventType,
+ mozilla::a11y::LocalAccessible* aAccessible,
+ mozilla::a11y::EIsFromUserInput aIsFromUserInput =
+ mozilla::a11y::eAutoDetect);
+
+ /**
+ * Fire state change event.
+ */
+ static void FireEvent(mozilla::a11y::LocalAccessible* aTarget,
+ uint64_t aState, bool aIsEnabled,
+ bool aIsFromUserInput) {
+ RefPtr<mozilla::a11y::AccStateChangeEvent> stateChangeEvent =
+ new mozilla::a11y::AccStateChangeEvent(
+ aTarget, aState, aIsEnabled,
+ (aIsFromUserInput ? mozilla::a11y::eFromUserInput
+ : mozilla::a11y::eNoUserInput));
+ FireEvent(stateChangeEvent);
+ }
+
+ /**
+ * Append 'event-from-input' object attribute if the accessible event has
+ * been fired just now for the given node.
+ *
+ * @param aNode [in] the DOM node
+ * @param aAttributes [in, out] the attributes
+ */
+ static void GetEventAttributes(nsINode* aNode,
+ mozilla::a11y::AccAttributes* aAttributes);
+
+ private:
+ static mozilla::StaticRefPtr<nsINode> sEventTargetNode;
+ static bool sEventFromUserInput;
+};
+
+#endif
diff --git a/accessible/base/nsTextEquivUtils.cpp b/accessible/base/nsTextEquivUtils.cpp
new file mode 100644
index 0000000000..d95229c1dc
--- /dev/null
+++ b/accessible/base/nsTextEquivUtils.cpp
@@ -0,0 +1,360 @@
+/* -*- 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 "nsTextEquivUtils.h"
+
+#include "LocalAccessible-inl.h"
+#include "AccIterator.h"
+#include "nsCoreUtils.h"
+#include "mozilla/dom/ChildIterator.h"
+#include "mozilla/dom/Text.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+/**
+ * The accessible for which we are computing a text equivalent. It is useful
+ * for bailing out during recursive text computation, or for special cases
+ * like step f. of the ARIA implementation guide.
+ */
+static const Accessible* sInitiatorAcc = nullptr;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsTextEquivUtils. Public.
+
+nsresult nsTextEquivUtils::GetNameFromSubtree(
+ const LocalAccessible* aAccessible, nsAString& aName) {
+ aName.Truncate();
+
+ if (sInitiatorAcc) return NS_OK;
+
+ sInitiatorAcc = aAccessible;
+ if (GetRoleRule(aAccessible->Role()) == eNameFromSubtreeRule) {
+ // XXX: is it necessary to care the accessible is not a document?
+ if (aAccessible->IsContent()) {
+ nsAutoString name;
+ AppendFromAccessibleChildren(aAccessible, &name);
+ name.CompressWhitespace();
+ if (!nsCoreUtils::IsWhitespaceString(name)) aName = name;
+ }
+ }
+
+ sInitiatorAcc = nullptr;
+
+ return NS_OK;
+}
+
+nsresult nsTextEquivUtils::GetTextEquivFromIDRefs(
+ const LocalAccessible* aAccessible, nsAtom* aIDRefsAttr,
+ nsAString& aTextEquiv) {
+ aTextEquiv.Truncate();
+
+ nsIContent* content = aAccessible->GetContent();
+ if (!content) return NS_OK;
+
+ nsIContent* refContent = nullptr;
+ IDRefsIterator iter(aAccessible->Document(), content, aIDRefsAttr);
+ while ((refContent = iter.NextElem())) {
+ if (!aTextEquiv.IsEmpty()) aTextEquiv += ' ';
+
+ if (refContent->IsHTMLElement(nsGkAtoms::slot)) printf("jtd idref slot\n");
+ nsresult rv =
+ AppendTextEquivFromContent(aAccessible, refContent, &aTextEquiv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsTextEquivUtils::AppendTextEquivFromContent(
+ const LocalAccessible* aInitiatorAcc, nsIContent* aContent,
+ nsAString* aString) {
+ // Prevent recursion which can cause infinite loops.
+ if (sInitiatorAcc) return NS_OK;
+
+ sInitiatorAcc = aInitiatorAcc;
+
+ nsresult rv = NS_ERROR_FAILURE;
+ if (LocalAccessible* accessible =
+ aInitiatorAcc->Document()->GetAccessible(aContent)) {
+ rv = AppendFromAccessible(accessible, aString);
+ } else {
+ // The given content is invisible or otherwise inaccessible, so use the DOM
+ // subtree.
+ rv = AppendFromDOMNode(aContent, aString);
+ }
+
+ sInitiatorAcc = nullptr;
+ return rv;
+}
+
+nsresult nsTextEquivUtils::AppendTextEquivFromTextContent(nsIContent* aContent,
+ nsAString* aString) {
+ if (aContent->IsText()) {
+ if (aContent->TextLength() > 0) {
+ nsIFrame* frame = aContent->GetPrimaryFrame();
+ if (frame) {
+ nsIFrame::RenderedText text = frame->GetRenderedText(
+ 0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText,
+ nsIFrame::TrailingWhitespace::DontTrim);
+ aString->Append(text.mString);
+ } else {
+ // If aContent is an object that is display: none, we have no a frame.
+ aContent->GetAsText()->AppendTextTo(*aString);
+ }
+ }
+
+ return NS_OK;
+ }
+
+ if (aContent->IsHTMLElement() &&
+ aContent->NodeInfo()->Equals(nsGkAtoms::br)) {
+ aString->AppendLiteral("\r\n");
+ return NS_OK;
+ }
+
+ return NS_OK_NO_NAME_CLAUSE_HANDLED;
+}
+
+nsresult nsTextEquivUtils::AppendFromDOMChildren(nsIContent* aContent,
+ nsAString* aString) {
+ auto iter =
+ dom::AllChildrenIterator(aContent, nsIContent::eAllChildren, true);
+ while (nsIContent* childContent = iter.GetNextChild()) {
+ nsresult rv = AppendFromDOMNode(childContent, aString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsTextEquivUtils. Private.
+
+nsresult nsTextEquivUtils::AppendFromAccessibleChildren(
+ const Accessible* aAccessible, nsAString* aString) {
+ nsresult rv = NS_OK_NO_NAME_CLAUSE_HANDLED;
+
+ uint32_t childCount = aAccessible->ChildCount();
+ for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
+ Accessible* child = aAccessible->ChildAt(childIdx);
+ rv = AppendFromAccessible(child, aString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return rv;
+}
+
+nsresult nsTextEquivUtils::AppendFromAccessible(Accessible* aAccessible,
+ nsAString* aString) {
+ // XXX: is it necessary to care the accessible is not a document?
+ bool isHTMLBlock = false;
+ if (aAccessible->IsLocal() && aAccessible->AsLocal()->IsContent()) {
+ nsIContent* content = aAccessible->AsLocal()->GetContent();
+ nsresult rv = AppendTextEquivFromTextContent(content, aString);
+ if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) return rv;
+ if (!content->IsText()) {
+ nsIFrame* frame = content->GetPrimaryFrame();
+ if (frame) {
+ // If this is a block level frame (as opposed to span level), we need to
+ // add spaces around that block's text, so we don't get words jammed
+ // together in final name.
+ const nsStyleDisplay* display = frame->StyleDisplay();
+ if (display->IsBlockOutsideStyle() ||
+ display->mDisplay == StyleDisplay::TableCell) {
+ isHTMLBlock = true;
+ if (!aString->IsEmpty()) {
+ aString->Append(char16_t(' '));
+ }
+ }
+ }
+ }
+ }
+
+ bool isEmptyTextEquiv = true;
+
+ // If the name is from tooltip then append it to result string in the end
+ // (see h. step of name computation guide).
+ nsAutoString text;
+ if (aAccessible->Name(text) != eNameFromTooltip) {
+ isEmptyTextEquiv = !AppendString(aString, text);
+ }
+
+ // Implementation of f. step.
+ nsresult rv = AppendFromValue(aAccessible, aString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) isEmptyTextEquiv = false;
+
+ // Implementation of g) step of text equivalent computation guide. Go down
+ // into subtree if accessible allows "text equivalent from subtree rule" or
+ // it's not root and not control.
+ if (isEmptyTextEquiv) {
+ if (ShouldIncludeInSubtreeCalculation(aAccessible)) {
+ rv = AppendFromAccessibleChildren(aAccessible, aString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) isEmptyTextEquiv = false;
+ }
+ }
+
+ // Implementation of h. step
+ if (isEmptyTextEquiv && !text.IsEmpty()) {
+ AppendString(aString, text);
+ if (isHTMLBlock) {
+ aString->Append(char16_t(' '));
+ }
+ return NS_OK;
+ }
+
+ if (!isEmptyTextEquiv && isHTMLBlock) {
+ aString->Append(char16_t(' '));
+ }
+ return rv;
+}
+
+nsresult nsTextEquivUtils::AppendFromValue(Accessible* aAccessible,
+ nsAString* aString) {
+ if (GetRoleRule(aAccessible->Role()) != eNameFromValueRule) {
+ return NS_OK_NO_NAME_CLAUSE_HANDLED;
+ }
+
+ // Implementation of step f. of text equivalent computation. If the given
+ // accessible is not root accessible (the accessible the text equivalent is
+ // computed for in the end) then append accessible value. Otherwise append
+ // value if and only if the given accessible is in the middle of its parent.
+
+ nsAutoString text;
+ if (aAccessible != sInitiatorAcc) {
+ aAccessible->Value(text);
+
+ return AppendString(aString, text) ? NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED;
+ }
+
+ // XXX: is it necessary to care the accessible is not a document?
+ if (aAccessible->IsDoc()) return NS_ERROR_UNEXPECTED;
+
+ for (Accessible* next = aAccessible->NextSibling(); next;
+ next = next->NextSibling()) {
+ if (!IsWhitespaceLeaf(next)) {
+ for (Accessible* prev = aAccessible->PrevSibling(); prev;
+ prev = prev->PrevSibling()) {
+ if (!IsWhitespaceLeaf(prev)) {
+ aAccessible->Value(text);
+
+ return AppendString(aString, text) ? NS_OK
+ : NS_OK_NO_NAME_CLAUSE_HANDLED;
+ }
+ }
+ }
+ }
+
+ return NS_OK_NO_NAME_CLAUSE_HANDLED;
+}
+
+nsresult nsTextEquivUtils::AppendFromDOMNode(nsIContent* aContent,
+ nsAString* aString) {
+ nsresult rv = AppendTextEquivFromTextContent(aContent, aString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) return NS_OK;
+
+ if (aContent->IsAnyOfHTMLElements(nsGkAtoms::script, nsGkAtoms::style)) {
+ // The text within these elements is never meant for users.
+ return NS_OK;
+ }
+
+ if (aContent->IsXULElement()) {
+ nsAutoString textEquivalent;
+ if (aContent->NodeInfo()->Equals(nsGkAtoms::label, kNameSpaceID_XUL)) {
+ aContent->AsElement()->GetAttr(nsGkAtoms::value, textEquivalent);
+ } else {
+ aContent->AsElement()->GetAttr(nsGkAtoms::label, textEquivalent);
+ }
+
+ if (textEquivalent.IsEmpty()) {
+ aContent->AsElement()->GetAttr(nsGkAtoms::tooltiptext, textEquivalent);
+ }
+
+ AppendString(aString, textEquivalent);
+ }
+
+ return AppendFromDOMChildren(aContent, aString);
+}
+
+bool nsTextEquivUtils::AppendString(nsAString* aString,
+ const nsAString& aTextEquivalent) {
+ if (aTextEquivalent.IsEmpty()) return false;
+
+ // Insert spaces to insure that words from controls aren't jammed together.
+ if (!aString->IsEmpty() && !nsCoreUtils::IsWhitespace(aString->Last())) {
+ aString->Append(char16_t(' '));
+ }
+
+ aString->Append(aTextEquivalent);
+
+ if (!nsCoreUtils::IsWhitespace(aString->Last())) {
+ aString->Append(char16_t(' '));
+ }
+
+ return true;
+}
+
+uint32_t nsTextEquivUtils::GetRoleRule(role aRole) {
+#define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
+ msaaRole, ia2Role, androidClass, nameRule) \
+ case roles::geckoRole: \
+ return nameRule;
+
+ switch (aRole) {
+#include "RoleMap.h"
+ default:
+ MOZ_CRASH("Unknown role.");
+ }
+
+#undef ROLE
+}
+
+bool nsTextEquivUtils::ShouldIncludeInSubtreeCalculation(
+ Accessible* aAccessible) {
+ uint32_t nameRule = GetRoleRule(aAccessible->Role());
+ if (nameRule == eNameFromSubtreeRule) {
+ return true;
+ }
+ if (!(nameRule & eNameFromSubtreeIfReqRule)) {
+ return false;
+ }
+
+ if (aAccessible == sInitiatorAcc) {
+ // We're calculating the text equivalent for this accessible, but this
+ // accessible should only be included when calculating the text equivalent
+ // for something else.
+ return false;
+ }
+
+ // sInitiatorAcc can be null when, for example, LocalAccessible::Value calls
+ // GetTextEquivFromSubtree.
+ role initiatorRole = sInitiatorAcc ? sInitiatorAcc->Role() : roles::NOTHING;
+ if (initiatorRole == roles::OUTLINEITEM &&
+ aAccessible->Role() == roles::GROUPING) {
+ // Child treeitems are contained in a group. We don't want to include those
+ // in the parent treeitem's text equivalent.
+ return false;
+ }
+
+ return true;
+}
+
+bool nsTextEquivUtils::IsWhitespaceLeaf(Accessible* aAccessible) {
+ if (!aAccessible || !aAccessible->IsTextLeaf()) {
+ return false;
+ }
+
+ nsAutoString name;
+ aAccessible->Name(name);
+ return nsCoreUtils::IsWhitespaceString(name);
+}
diff --git a/accessible/base/nsTextEquivUtils.h b/accessible/base/nsTextEquivUtils.h
new file mode 100644
index 0000000000..525727b102
--- /dev/null
+++ b/accessible/base/nsTextEquivUtils.h
@@ -0,0 +1,182 @@
+/* -*- 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 _nsTextEquivUtils_H_
+#define _nsTextEquivUtils_H_
+
+#include "mozilla/a11y/Accessible.h"
+#include "mozilla/a11y/Role.h"
+
+class nsIContent;
+
+namespace mozilla {
+namespace a11y {
+class LocalAccessible;
+}
+} // namespace mozilla
+
+/**
+ * Text equivalent computation rules (see nsTextEquivUtils::gRoleToNameRulesMap)
+ */
+enum ETextEquivRule {
+ // No rule.
+ eNoNameRule = 0x00,
+
+ // Walk into subtree only if the currently navigated accessible is not root
+ // accessible (i.e. if the accessible is part of text equivalent computation).
+ eNameFromSubtreeIfReqRule = 0x01,
+
+ // Text equivalent computation from subtree is allowed.
+ eNameFromSubtreeRule = 0x03,
+
+ // The accessible allows to append its value to text equivalent.
+ // XXX: This is temporary solution. Once we move accessible value of links
+ // and linkable accessibles to MSAA part we can remove this.
+ eNameFromValueRule = 0x04
+};
+
+/**
+ * The class provides utils methods to compute the accessible name and
+ * description.
+ */
+class nsTextEquivUtils {
+ public:
+ typedef mozilla::a11y::LocalAccessible LocalAccessible;
+ typedef mozilla::a11y::Accessible Accessible;
+
+ /**
+ * Determines if the accessible has a given name rule.
+ *
+ * @param aAccessible [in] the given accessible
+ * @param aRule [in] a given name rule
+ * @return true if the accessible has the rule
+ */
+ static inline bool HasNameRule(Accessible* aAccessible,
+ ETextEquivRule aRule) {
+ return (GetRoleRule(aAccessible->Role()) & aRule) == aRule;
+ }
+
+ /**
+ * Calculates the name from accessible subtree if allowed.
+ *
+ * @param aAccessible [in] the given accessible
+ * @param aName [out] accessible name
+ */
+ static nsresult GetNameFromSubtree(const LocalAccessible* aAccessible,
+ nsAString& aName);
+
+ /**
+ * Calculates text equivalent from the subtree. Similar to GetNameFromSubtree.
+ * However it returns not empty result for things like HTML p.
+ */
+ static void GetTextEquivFromSubtree(const Accessible* aAccessible,
+ nsString& aTextEquiv) {
+ aTextEquiv.Truncate();
+
+ AppendFromAccessibleChildren(aAccessible, &aTextEquiv);
+ aTextEquiv.CompressWhitespace();
+ }
+
+ /**
+ * Calculates text equivalent for the given accessible from its IDRefs
+ * attribute (like aria-labelledby or aria-describedby).
+ *
+ * @param aAccessible [in] the accessible text equivalent is computed for
+ * @param aIDRefsAttr [in] IDRefs attribute on DOM node of the accessible
+ * @param aTextEquiv [out] result text equivalent
+ */
+ static nsresult GetTextEquivFromIDRefs(const LocalAccessible* aAccessible,
+ nsAtom* aIDRefsAttr,
+ nsAString& aTextEquiv);
+
+ /**
+ * Calculates the text equivalent from the given content and its subtree if
+ * allowed and appends it to the given string.
+ *
+ * @param aInitiatorAcc [in] the accessible text equivalent is computed for
+ * in the end (root accessible of text equivalent
+ * calculation recursion)
+ * @param aContent [in] the given content the text equivalent is
+ * computed from
+ * @param aString [in, out] the string
+ */
+ static nsresult AppendTextEquivFromContent(
+ const LocalAccessible* aInitiatorAcc, nsIContent* aContent,
+ nsAString* aString);
+
+ /**
+ * Calculates the text equivalent from the given text content (may be text
+ * node or html:br) and appends it to the given string.
+ *
+ * @param aContent [in] the text content
+ * @param aString [in, out] the string
+ */
+ static nsresult AppendTextEquivFromTextContent(nsIContent* aContent,
+ nsAString* aString);
+
+ /**
+ * Iterates DOM children and calculates text equivalent from each child node.
+ * Then, appends found text to the given string.
+ *
+ * @param aContent [in] the node to fetch DOM children from
+ * @param aString [in, out] the string
+ */
+ static nsresult AppendFromDOMChildren(nsIContent* aContent,
+ nsAString* aString);
+
+ private:
+ /**
+ * Iterates accessible children and calculates text equivalent from each
+ * child.
+ */
+ static nsresult AppendFromAccessibleChildren(const Accessible* aAccessible,
+ nsAString* aString);
+
+ /**
+ * Calculates text equivalent from the given accessible and its subtree if
+ * allowed.
+ */
+ static nsresult AppendFromAccessible(Accessible* aAccessible,
+ nsAString* aString);
+
+ /**
+ * Calculates text equivalent from the value of given accessible.
+ */
+ static nsresult AppendFromValue(Accessible* aAccessible, nsAString* aString);
+
+ /**
+ * Calculates text equivalent from the given DOM node and its subtree if
+ * allowed.
+ */
+ static nsresult AppendFromDOMNode(nsIContent* aContent, nsAString* aString);
+
+ /**
+ * Concatenates strings and appends space between them. Returns true if
+ * text equivalent string was appended.
+ */
+ static bool AppendString(nsAString* aString,
+ const nsAString& aTextEquivalent);
+
+ /**
+ * Returns the rule (constant of ETextEquivRule) for a given role.
+ */
+ static uint32_t GetRoleRule(mozilla::a11y::roles::Role aRole);
+
+ /**
+ * Returns true if a given accessible should be included when calculating
+ * the text equivalent for the initiator's subtree.
+ */
+ static bool ShouldIncludeInSubtreeCalculation(Accessible* aAccessible);
+
+ /**
+ * Returns true if a given accessible is a text leaf containing only
+ * whitespace.
+ */
+ static bool IsWhitespaceLeaf(Accessible* aAccessible);
+};
+
+#endif
diff --git a/accessible/basetypes/Accessible.cpp b/accessible/basetypes/Accessible.cpp
new file mode 100644
index 0000000000..05c8270483
--- /dev/null
+++ b/accessible/basetypes/Accessible.cpp
@@ -0,0 +1,730 @@
+/* -*- 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 "Accessible.h"
+#include "ARIAMap.h"
+#include "nsAccUtils.h"
+#include "nsIURI.h"
+#include "Relation.h"
+#include "States.h"
+#include "mozilla/a11y/FocusManager.h"
+#include "mozilla/a11y/HyperTextAccessibleBase.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/Components.h"
+#include "nsIStringBundle.h"
+
+#ifdef A11Y_LOG
+# include "nsAccessibilityService.h"
+#endif
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+Accessible::Accessible()
+ : mType(static_cast<uint32_t>(0)),
+ mGenericTypes(static_cast<uint32_t>(0)),
+ mRoleMapEntryIndex(aria::NO_ROLE_MAP_ENTRY_INDEX) {}
+
+Accessible::Accessible(AccType aType, AccGenericType aGenericTypes,
+ uint8_t aRoleMapEntryIndex)
+ : mType(static_cast<uint32_t>(aType)),
+ mGenericTypes(static_cast<uint32_t>(aGenericTypes)),
+ mRoleMapEntryIndex(aRoleMapEntryIndex) {}
+
+void Accessible::StaticAsserts() const {
+ static_assert(eLastAccType <= (1 << kTypeBits) - 1,
+ "Accessible::mType was oversized by eLastAccType!");
+ static_assert(
+ eLastAccGenericType <= (1 << kGenericTypesBits) - 1,
+ "Accessible::mGenericType was oversized by eLastAccGenericType!");
+}
+
+bool Accessible::IsBefore(const Accessible* aAcc) const {
+ // Build the chain of parents.
+ const Accessible* thisP = this;
+ const Accessible* otherP = aAcc;
+ AutoTArray<const Accessible*, 30> thisParents, otherParents;
+ do {
+ thisParents.AppendElement(thisP);
+ thisP = thisP->Parent();
+ } while (thisP);
+ do {
+ otherParents.AppendElement(otherP);
+ otherP = otherP->Parent();
+ } while (otherP);
+
+ // Find where the parent chain differs.
+ uint32_t thisPos = thisParents.Length(), otherPos = otherParents.Length();
+ for (uint32_t len = std::min(thisPos, otherPos); len > 0; --len) {
+ const Accessible* thisChild = thisParents.ElementAt(--thisPos);
+ const Accessible* otherChild = otherParents.ElementAt(--otherPos);
+ if (thisChild != otherChild) {
+ return thisChild->IndexInParent() < otherChild->IndexInParent();
+ }
+ }
+
+ // If the ancestries are the same length (both thisPos and otherPos are 0),
+ // we should have returned by now.
+ MOZ_ASSERT(thisPos != 0 || otherPos != 0);
+ // At this point, one of the ancestries is a superset of the other, so one of
+ // thisPos or otherPos should be 0.
+ MOZ_ASSERT(thisPos != otherPos);
+ // If the other Accessible is deeper than this one (otherPos > 0), this
+ // Accessible comes before the other.
+ return otherPos > 0;
+}
+
+Accessible* Accessible::FocusedChild() {
+ Accessible* doc = nsAccUtils::DocumentFor(this);
+ Accessible* child = doc->FocusedChild();
+ if (child && (child == this || child->Parent() == this)) {
+ return child;
+ }
+
+ return nullptr;
+}
+
+const nsRoleMapEntry* Accessible::ARIARoleMap() const {
+ return aria::GetRoleMapFromIndex(mRoleMapEntryIndex);
+}
+
+bool Accessible::HasARIARole() const {
+ return mRoleMapEntryIndex != aria::NO_ROLE_MAP_ENTRY_INDEX;
+}
+
+bool Accessible::IsARIARole(nsAtom* aARIARole) const {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ return roleMapEntry && roleMapEntry->Is(aARIARole);
+}
+
+bool Accessible::HasStrongARIARole() const {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ return roleMapEntry && roleMapEntry->roleRule == kUseMapRole;
+}
+
+bool Accessible::HasGenericType(AccGenericType aType) const {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ return (mGenericTypes & aType) ||
+ (roleMapEntry && roleMapEntry->IsOfType(aType));
+}
+
+nsIntRect Accessible::BoundsInCSSPixels() const {
+ return BoundsInAppUnits().ToNearestPixels(AppUnitsPerCSSPixel());
+}
+
+LayoutDeviceIntSize Accessible::Size() const { return Bounds().Size(); }
+
+LayoutDeviceIntPoint Accessible::Position(uint32_t aCoordType) {
+ LayoutDeviceIntPoint point = Bounds().TopLeft();
+ nsAccUtils::ConvertScreenCoordsTo(&point.x.value, &point.y.value, aCoordType,
+ this);
+ return point;
+}
+
+bool Accessible::IsTextRole() {
+ if (!IsHyperText()) {
+ return false;
+ }
+
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (roleMapEntry && (roleMapEntry->role == roles::GRAPHIC ||
+ roleMapEntry->role == roles::IMAGE_MAP ||
+ roleMapEntry->role == roles::SLIDER ||
+ roleMapEntry->role == roles::PROGRESSBAR ||
+ roleMapEntry->role == roles::SEPARATOR)) {
+ return false;
+ }
+
+ return true;
+}
+
+uint32_t Accessible::StartOffset() {
+ MOZ_ASSERT(IsLink(), "StartOffset is called not on hyper link!");
+ Accessible* parent = Parent();
+ HyperTextAccessibleBase* hyperText =
+ parent ? parent->AsHyperTextBase() : nullptr;
+ return hyperText ? hyperText->GetChildOffset(this) : 0;
+}
+
+uint32_t Accessible::EndOffset() {
+ MOZ_ASSERT(IsLink(), "EndOffset is called on not hyper link!");
+ Accessible* parent = Parent();
+ HyperTextAccessibleBase* hyperText =
+ parent ? parent->AsHyperTextBase() : nullptr;
+ return hyperText ? (hyperText->GetChildOffset(this) + 1) : 0;
+}
+
+GroupPos Accessible::GroupPosition() {
+ GroupPos groupPos;
+
+ // Try aria-row/colcount/index.
+ if (IsTableRow()) {
+ Accessible* table = nsAccUtils::TableFor(this);
+ if (table) {
+ if (auto count = table->GetIntARIAAttr(nsGkAtoms::aria_rowcount)) {
+ if (*count >= 0) {
+ groupPos.setSize = *count;
+ }
+ }
+ }
+ if (auto index = GetIntARIAAttr(nsGkAtoms::aria_rowindex)) {
+ groupPos.posInSet = *index;
+ }
+ if (groupPos.setSize && groupPos.posInSet) {
+ return groupPos;
+ }
+ }
+ if (IsTableCell()) {
+ Accessible* table;
+ for (table = Parent(); table; table = table->Parent()) {
+ if (table->IsTable()) {
+ break;
+ }
+ }
+ if (table) {
+ if (auto count = table->GetIntARIAAttr(nsGkAtoms::aria_colcount)) {
+ if (*count >= 0) {
+ groupPos.setSize = *count;
+ }
+ }
+ }
+ if (auto index = GetIntARIAAttr(nsGkAtoms::aria_colindex)) {
+ groupPos.posInSet = *index;
+ }
+ if (groupPos.setSize && groupPos.posInSet) {
+ return groupPos;
+ }
+ }
+
+ // Get group position from ARIA attributes.
+ ARIAGroupPosition(&groupPos.level, &groupPos.setSize, &groupPos.posInSet);
+
+ // If ARIA is missed and the accessible is visible then calculate group
+ // position from hierarchy.
+ if (State() & states::INVISIBLE) return groupPos;
+
+ // Calculate group level if ARIA is missed.
+ if (groupPos.level == 0) {
+ groupPos.level = GetLevel(false);
+ }
+
+ // Calculate position in group and group size if ARIA is missed.
+ if (groupPos.posInSet == 0 || groupPos.setSize == 0) {
+ int32_t posInSet = 0, setSize = 0;
+ GetPositionAndSetSize(&posInSet, &setSize);
+ if (posInSet != 0 && setSize != 0) {
+ if (groupPos.posInSet == 0) groupPos.posInSet = posInSet;
+
+ if (groupPos.setSize == 0) groupPos.setSize = setSize;
+ }
+ }
+
+ return groupPos;
+}
+
+int32_t Accessible::GetLevel(bool aFast) const {
+ int32_t level = 0;
+ if (!Parent()) return level;
+
+ roles::Role role = Role();
+ if (role == roles::OUTLINEITEM) {
+ // Always expose 'level' attribute for 'outlineitem' accessible. The number
+ // of nested 'grouping' accessibles containing 'outlineitem' accessible is
+ // its level.
+ level = 1;
+
+ if (!aFast) {
+ const Accessible* parent = this;
+ while ((parent = parent->Parent()) && !parent->IsDoc()) {
+ roles::Role parentRole = parent->Role();
+
+ if (parentRole == roles::OUTLINE) break;
+ if (parentRole == roles::GROUPING) ++level;
+ }
+ }
+ } else if (role == roles::LISTITEM && !aFast) {
+ // Expose 'level' attribute on nested lists. We support two hierarchies:
+ // a) list -> listitem -> list -> listitem (nested list is a last child
+ // of listitem of the parent list);
+ // b) list -> listitem -> group -> listitem (nested listitems are contained
+ // by group that is a last child of the parent listitem).
+
+ // Calculate 'level' attribute based on number of parent listitems.
+ level = 0;
+ const Accessible* parent = this;
+ while ((parent = parent->Parent()) && !parent->IsDoc()) {
+ roles::Role parentRole = parent->Role();
+
+ if (parentRole == roles::LISTITEM) {
+ ++level;
+ } else if (parentRole != roles::LIST && parentRole != roles::GROUPING) {
+ break;
+ }
+ }
+
+ if (level == 0) {
+ // If this listitem is on top of nested lists then expose 'level'
+ // attribute.
+ parent = Parent();
+ uint32_t siblingCount = parent->ChildCount();
+ for (uint32_t siblingIdx = 0; siblingIdx < siblingCount; siblingIdx++) {
+ Accessible* sibling = parent->ChildAt(siblingIdx);
+
+ Accessible* siblingChild = sibling->LastChild();
+ if (siblingChild) {
+ roles::Role lastChildRole = siblingChild->Role();
+ if (lastChildRole == roles::LIST ||
+ lastChildRole == roles::GROUPING) {
+ return 1;
+ }
+ }
+ }
+ } else {
+ ++level; // level is 1-index based
+ }
+ } else if (role == roles::OPTION || role == roles::COMBOBOX_OPTION) {
+ if (const Accessible* parent = Parent()) {
+ if (parent->IsHTMLOptGroup()) {
+ return 2;
+ }
+
+ if (parent->IsListControl() && !parent->ARIARoleMap()) {
+ // This is for HTML selects only.
+ if (aFast) {
+ return 1;
+ }
+
+ for (uint32_t i = 0, count = parent->ChildCount(); i < count; ++i) {
+ if (parent->ChildAt(i)->IsHTMLOptGroup()) {
+ return 1;
+ }
+ }
+ }
+ }
+ } else if (role == roles::HEADING) {
+ nsAtom* tagName = TagName();
+ if (tagName == nsGkAtoms::h1) {
+ return 1;
+ }
+ if (tagName == nsGkAtoms::h2) {
+ return 2;
+ }
+ if (tagName == nsGkAtoms::h3) {
+ return 3;
+ }
+ if (tagName == nsGkAtoms::h4) {
+ return 4;
+ }
+ if (tagName == nsGkAtoms::h5) {
+ return 5;
+ }
+ if (tagName == nsGkAtoms::h6) {
+ return 6;
+ }
+
+ const nsRoleMapEntry* ariaRole = this->ARIARoleMap();
+ if (ariaRole && ariaRole->Is(nsGkAtoms::heading)) {
+ // An aria heading with no aria level has a default level of 2.
+ return 2;
+ }
+ } else if (role == roles::COMMENT) {
+ // For comments, count the ancestor elements with the same role to get the
+ // level.
+ level = 1;
+
+ if (!aFast) {
+ const Accessible* parent = this;
+ while ((parent = parent->Parent()) && !parent->IsDoc()) {
+ roles::Role parentRole = parent->Role();
+ if (parentRole == roles::COMMENT) {
+ ++level;
+ }
+ }
+ }
+ } else if (role == roles::ROW) {
+ // It is a row inside flatten treegrid. Group level is always 1 until it
+ // is overriden by aria-level attribute.
+ const Accessible* parent = Parent();
+ if (parent->Role() == roles::TREE_TABLE) {
+ return 1;
+ }
+ }
+
+ return level;
+}
+
+void Accessible::GetPositionAndSetSize(int32_t* aPosInSet, int32_t* aSetSize) {
+ auto groupInfo = GetOrCreateGroupInfo();
+ if (groupInfo) {
+ *aPosInSet = groupInfo->PosInSet();
+ *aSetSize = groupInfo->SetSize();
+ }
+}
+
+bool Accessible::IsLinkValid() {
+ MOZ_ASSERT(IsLink(), "IsLinkValid is called on not hyper link!");
+
+ // XXX In order to implement this we would need to follow every link
+ // Perhaps we can get information about invalid links from the cache
+ // In the mean time authors can use role="link" aria-invalid="true"
+ // to force it for links they internally know to be invalid
+ return (0 == (State() & mozilla::a11y::states::INVALID));
+}
+
+uint32_t Accessible::AnchorCount() {
+ if (IsImageMap()) {
+ return ChildCount();
+ }
+
+ MOZ_ASSERT(IsLink(), "AnchorCount is called on not hyper link!");
+ return 1;
+}
+
+Accessible* Accessible::AnchorAt(uint32_t aAnchorIndex) const {
+ if (IsImageMap()) {
+ return ChildAt(aAnchorIndex);
+ }
+
+ MOZ_ASSERT(IsLink(), "GetAnchor is called on not hyper link!");
+ return aAnchorIndex == 0 ? const_cast<Accessible*>(this) : nullptr;
+}
+
+already_AddRefed<nsIURI> Accessible::AnchorURIAt(uint32_t aAnchorIndex) const {
+ Accessible* anchor = nullptr;
+
+ if (IsTextLeaf() || IsImage()) {
+ for (Accessible* parent = Parent(); parent && !parent->IsOuterDoc();
+ parent = parent->Parent()) {
+ if (parent->IsLink()) {
+ anchor = parent->AnchorAt(aAnchorIndex);
+ }
+ }
+ } else {
+ anchor = AnchorAt(aAnchorIndex);
+ }
+
+ if (anchor) {
+ RefPtr<nsIURI> uri;
+ nsAutoString spec;
+ anchor->Value(spec);
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), spec);
+ if (NS_SUCCEEDED(rv)) {
+ return uri.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+bool Accessible::IsSearchbox() const {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::searchbox)) {
+ return true;
+ }
+
+ RefPtr<nsAtom> inputType = InputType();
+ return inputType == nsGkAtoms::search;
+}
+
+#ifdef A11Y_LOG
+void Accessible::DebugDescription(nsCString& aDesc) const {
+ aDesc.Truncate();
+ aDesc.AppendPrintf("%s", IsRemote() ? "Remote" : "Local");
+ aDesc.AppendPrintf("[%p] ", this);
+ nsAutoString role;
+ GetAccService()->GetStringRole(Role(), role);
+ aDesc.Append(NS_ConvertUTF16toUTF8(role));
+
+ if (nsAtom* tagAtom = TagName()) {
+ nsAutoCString tag;
+ tagAtom->ToUTF8String(tag);
+ aDesc.AppendPrintf(" %s", tag.get());
+
+ nsAutoString id;
+ DOMNodeID(id);
+ if (!id.IsEmpty()) {
+ aDesc.Append("#");
+ aDesc.Append(NS_ConvertUTF16toUTF8(id));
+ }
+ }
+ nsAutoString id;
+
+ nsAutoString name;
+ Name(name);
+ if (!name.IsEmpty()) {
+ aDesc.Append(" '");
+ aDesc.Append(NS_ConvertUTF16toUTF8(name));
+ aDesc.Append("'");
+ }
+}
+
+void Accessible::DebugPrint(const char* aPrefix,
+ const Accessible* aAccessible) {
+ nsAutoCString desc;
+ if (aAccessible) {
+ aAccessible->DebugDescription(desc);
+ } else {
+ desc.AssignLiteral("[null]");
+ }
+# if defined(ANDROID)
+ printf_stderr("%s %s\n", aPrefix, desc.get());
+# else
+ printf("%s %s\n", aPrefix, desc.get());
+# endif
+}
+
+#endif
+
+void Accessible::TranslateString(const nsString& aKey, nsAString& aStringOut) {
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ components::StringBundle::Service();
+ if (!stringBundleService) return;
+
+ nsCOMPtr<nsIStringBundle> stringBundle;
+ stringBundleService->CreateBundle(
+ "chrome://global-platform/locale/accessible.properties",
+ getter_AddRefs(stringBundle));
+ if (!stringBundle) return;
+
+ nsAutoString xsValue;
+ nsresult rv = stringBundle->GetStringFromName(
+ NS_ConvertUTF16toUTF8(aKey).get(), xsValue);
+ if (NS_SUCCEEDED(rv)) aStringOut.Assign(xsValue);
+}
+
+const Accessible* Accessible::ActionAncestor() const {
+ // We do want to consider a click handler on the document. However, we don't
+ // want to walk outside of this document, so we stop if we see an OuterDoc.
+ for (Accessible* parent = Parent(); parent && !parent->IsOuterDoc();
+ parent = parent->Parent()) {
+ if (parent->HasPrimaryAction()) {
+ return parent;
+ }
+ }
+
+ return nullptr;
+}
+
+nsStaticAtom* Accessible::LandmarkRole() const {
+ nsAtom* tagName = TagName();
+ if (!tagName) {
+ // Either no associated content, or no cache.
+ return nullptr;
+ }
+
+ if (tagName == nsGkAtoms::nav) {
+ return nsGkAtoms::navigation;
+ }
+
+ if (tagName == nsGkAtoms::aside) {
+ return nsGkAtoms::complementary;
+ }
+
+ if (tagName == nsGkAtoms::main) {
+ return nsGkAtoms::main;
+ }
+
+ if (tagName == nsGkAtoms::header) {
+ if (Role() == roles::LANDMARK) {
+ return nsGkAtoms::banner;
+ }
+ }
+
+ if (tagName == nsGkAtoms::footer) {
+ if (Role() == roles::LANDMARK) {
+ return nsGkAtoms::contentinfo;
+ }
+ }
+
+ if (tagName == nsGkAtoms::section) {
+ nsAutoString name;
+ Name(name);
+ if (!name.IsEmpty()) {
+ return nsGkAtoms::region;
+ }
+ }
+
+ if (tagName == nsGkAtoms::form) {
+ nsAutoString name;
+ Name(name);
+ if (!name.IsEmpty()) {
+ return nsGkAtoms::form;
+ }
+ }
+
+ if (tagName == nsGkAtoms::search) {
+ return nsGkAtoms::search;
+ }
+
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ return roleMapEntry && roleMapEntry->IsOfType(eLandmark)
+ ? roleMapEntry->roleAtom
+ : nullptr;
+}
+
+nsStaticAtom* Accessible::ComputedARIARole() const {
+ const nsRoleMapEntry* roleMap = ARIARoleMap();
+ if (roleMap && roleMap->roleAtom != nsGkAtoms::_empty &&
+ // region has its own Gecko role and it needs to be handled specially.
+ roleMap->roleAtom != nsGkAtoms::region &&
+ (roleMap->roleRule == kUseNativeRole || roleMap->IsOfType(eLandmark) ||
+ roleMap->roleAtom == nsGkAtoms::alertdialog ||
+ roleMap->roleAtom == nsGkAtoms::feed ||
+ roleMap->roleAtom == nsGkAtoms::rowgroup)) {
+ // Explicit ARIA role (e.g. specified via the role attribute) which does not
+ // map to a unique Gecko role.
+ return roleMap->roleAtom;
+ }
+ if (IsSearchbox()) {
+ return nsGkAtoms::searchbox;
+ }
+ role geckoRole = Role();
+ if (geckoRole == roles::LANDMARK) {
+ // Landmark role from native markup; e.g. <main>, <nav>.
+ return LandmarkRole();
+ }
+ if (geckoRole == roles::GROUPING) {
+ // Gecko doesn't differentiate between group and rowgroup. It uses
+ // roles::GROUPING for both.
+ nsAtom* tag = TagName();
+ if (tag == nsGkAtoms::tbody || tag == nsGkAtoms::tfoot ||
+ tag == nsGkAtoms::thead) {
+ return nsGkAtoms::rowgroup;
+ }
+ }
+ // Role from native markup or layout.
+#define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
+ msaaRole, ia2Role, androidClass, nameRule) \
+ case roles::_geckoRole: \
+ return ariaRole;
+ switch (geckoRole) {
+#include "RoleMap.h"
+ }
+#undef ROLE
+ MOZ_ASSERT_UNREACHABLE("Unknown role");
+ return nullptr;
+}
+
+void Accessible::ApplyImplicitState(uint64_t& aState) const {
+ // nsAccessibilityService (and thus FocusManager) can be shut down before
+ // RemoteAccessibles.
+ if (const auto* focusMgr = FocusMgr()) {
+ if (focusMgr->IsFocused(this)) {
+ aState |= states::FOCUSED;
+ }
+ }
+
+ // If this is an ARIA item of the selectable widget and if it's focused and
+ // not marked unselected explicitly (i.e. aria-selected="false") then expose
+ // it as selected to make ARIA widget authors life easier.
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (roleMapEntry && !(aState & states::SELECTED) &&
+ ARIASelected().valueOr(true)) {
+ // Special case for tabs: focused tab or focus inside related tab panel
+ // implies selected state.
+ if (roleMapEntry->role == roles::PAGETAB) {
+ if (aState & states::FOCUSED) {
+ aState |= states::SELECTED;
+ } else {
+ // If focus is in a child of the tab panel surely the tab is selected!
+ Relation rel = RelationByType(RelationType::LABEL_FOR);
+ Accessible* relTarget = nullptr;
+ while ((relTarget = rel.Next())) {
+ if (relTarget->Role() == roles::PROPERTYPAGE &&
+ FocusMgr()->IsFocusWithin(relTarget)) {
+ aState |= states::SELECTED;
+ }
+ }
+ }
+ } else if (aState & states::FOCUSED) {
+ Accessible* container = nsAccUtils::GetSelectableContainer(this, aState);
+ if (container && !(container->State() & states::MULTISELECTABLE)) {
+ aState |= states::SELECTED;
+ }
+ }
+ }
+
+ if (Opacity() == 1.0f && !(aState & states::INVISIBLE)) {
+ aState |= states::OPAQUE1;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// KeyBinding class
+
+// static
+uint32_t KeyBinding::AccelModifier() {
+ switch (WidgetInputEvent::AccelModifier()) {
+ case MODIFIER_ALT:
+ return kAlt;
+ case MODIFIER_CONTROL:
+ return kControl;
+ case MODIFIER_META:
+ return kMeta;
+ default:
+ MOZ_CRASH("Handle the new result of WidgetInputEvent::AccelModifier()");
+ return 0;
+ }
+}
+
+void KeyBinding::ToPlatformFormat(nsAString& aValue) const {
+ nsCOMPtr<nsIStringBundle> keyStringBundle;
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ mozilla::components::StringBundle::Service();
+ if (stringBundleService) {
+ stringBundleService->CreateBundle(
+ "chrome://global-platform/locale/platformKeys.properties",
+ getter_AddRefs(keyStringBundle));
+ }
+
+ if (!keyStringBundle) return;
+
+ nsAutoString separator;
+ keyStringBundle->GetStringFromName("MODIFIER_SEPARATOR", separator);
+
+ nsAutoString modifierName;
+ if (mModifierMask & kControl) {
+ keyStringBundle->GetStringFromName("VK_CONTROL", modifierName);
+
+ aValue.Append(modifierName);
+ aValue.Append(separator);
+ }
+
+ if (mModifierMask & kAlt) {
+ keyStringBundle->GetStringFromName("VK_ALT", modifierName);
+
+ aValue.Append(modifierName);
+ aValue.Append(separator);
+ }
+
+ if (mModifierMask & kShift) {
+ keyStringBundle->GetStringFromName("VK_SHIFT", modifierName);
+
+ aValue.Append(modifierName);
+ aValue.Append(separator);
+ }
+
+ if (mModifierMask & kMeta) {
+ keyStringBundle->GetStringFromName("VK_META", modifierName);
+
+ aValue.Append(modifierName);
+ aValue.Append(separator);
+ }
+
+ aValue.Append(mKey);
+}
+
+void KeyBinding::ToAtkFormat(nsAString& aValue) const {
+ nsAutoString modifierName;
+ if (mModifierMask & kControl) aValue.AppendLiteral("<Control>");
+
+ if (mModifierMask & kAlt) aValue.AppendLiteral("<Alt>");
+
+ if (mModifierMask & kShift) aValue.AppendLiteral("<Shift>");
+
+ if (mModifierMask & kMeta) aValue.AppendLiteral("<Meta>");
+
+ aValue.Append(mKey);
+}
diff --git a/accessible/basetypes/Accessible.h b/accessible/basetypes/Accessible.h
new file mode 100644
index 0000000000..9b2e38e94d
--- /dev/null
+++ b/accessible/basetypes/Accessible.h
@@ -0,0 +1,741 @@
+/* -*- 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 _Accessible_H_
+#define _Accessible_H_
+
+#include "mozilla/a11y/Role.h"
+#include "mozilla/a11y/AccTypes.h"
+#include "nsString.h"
+#include "nsRect.h"
+#include "Units.h"
+
+class nsAtom;
+class nsStaticAtom;
+
+struct nsRoleMapEntry;
+
+class nsIURI;
+
+namespace mozilla {
+namespace a11y {
+
+class AccAttributes;
+class AccGroupInfo;
+class HyperTextAccessibleBase;
+class LocalAccessible;
+class Relation;
+enum class RelationType;
+class RemoteAccessible;
+class TableAccessible;
+class TableCellAccessible;
+
+/**
+ * Name type flags.
+ */
+enum ENameValueFlag {
+ /**
+ * Name either
+ * a) present (not empty): !name.IsEmpty()
+ * b) no name (was missed): name.IsVoid()
+ */
+ eNameOK,
+
+ /**
+ * Name was computed from the subtree.
+ */
+ eNameFromSubtree,
+
+ /**
+ * Tooltip was used as a name.
+ */
+ eNameFromTooltip
+};
+
+/**
+ * Group position (level, position in set and set size).
+ */
+struct GroupPos {
+ GroupPos() : level(0), posInSet(0), setSize(0) {}
+ GroupPos(int32_t aLevel, int32_t aPosInSet, int32_t aSetSize)
+ : level(aLevel), posInSet(aPosInSet), setSize(aSetSize) {}
+
+ int32_t level;
+ int32_t posInSet;
+ int32_t setSize;
+};
+
+/**
+ * Represent key binding associated with accessible (such as access key and
+ * global keyboard shortcuts).
+ */
+class KeyBinding {
+ public:
+ /**
+ * Modifier mask values.
+ */
+ static const uint32_t kShift = 1;
+ static const uint32_t kControl = 2;
+ static const uint32_t kAlt = 4;
+ static const uint32_t kMeta = 8;
+
+ static uint32_t AccelModifier();
+
+ KeyBinding() : mKey(0), mModifierMask(0) {}
+ KeyBinding(uint32_t aKey, uint32_t aModifierMask)
+ : mKey(aKey), mModifierMask(aModifierMask) {}
+ explicit KeyBinding(uint64_t aSerialized) : mSerialized(aSerialized) {}
+
+ inline bool IsEmpty() const { return !mKey; }
+ inline uint32_t Key() const { return mKey; }
+ inline uint32_t ModifierMask() const { return mModifierMask; }
+
+ /**
+ * Serialize this KeyBinding to a uint64_t for use in the parent process
+ * cache. This is simpler than custom IPDL serialization for this simple case.
+ */
+ uint64_t Serialize() { return mSerialized; }
+
+ enum Format { ePlatformFormat, eAtkFormat };
+
+ /**
+ * Return formatted string for this key binding depending on the given format.
+ */
+ inline void ToString(nsAString& aValue,
+ Format aFormat = ePlatformFormat) const {
+ aValue.Truncate();
+ AppendToString(aValue, aFormat);
+ }
+ inline void AppendToString(nsAString& aValue,
+ Format aFormat = ePlatformFormat) const {
+ if (mKey) {
+ if (aFormat == ePlatformFormat) {
+ ToPlatformFormat(aValue);
+ } else {
+ ToAtkFormat(aValue);
+ }
+ }
+ }
+
+ private:
+ void ToPlatformFormat(nsAString& aValue) const;
+ void ToAtkFormat(nsAString& aValue) const;
+
+ union {
+ struct {
+ uint32_t mKey;
+ uint32_t mModifierMask;
+ };
+ uint64_t mSerialized;
+ };
+};
+
+/**
+ * The base type for an accessibility tree node. Methods and attributes in this
+ * class are available in both the content process and the parent process.
+ * Overrides for these methods live primarily in LocalAccessible and
+ * RemoteAccessibleBase.
+ */
+class Accessible {
+ protected:
+ Accessible();
+
+ Accessible(AccType aType, AccGenericType aGenericTypes,
+ uint8_t aRoleMapEntryIndex);
+
+ public:
+ /**
+ * Return an id for this Accessible which is unique within the document.
+ * Use nsAccUtils::GetAccessibleByID to retrieve an Accessible given an id
+ * returned from this method.
+ */
+ virtual uint64_t ID() const = 0;
+
+ virtual Accessible* Parent() const = 0;
+
+ virtual role Role() const = 0;
+
+ /**
+ * Return child accessible at the given index.
+ */
+ virtual Accessible* ChildAt(uint32_t aIndex) const = 0;
+
+ virtual Accessible* NextSibling() const = 0;
+ virtual Accessible* PrevSibling() const = 0;
+
+ virtual uint32_t ChildCount() const = 0;
+
+ virtual int32_t IndexInParent() const = 0;
+
+ bool HasChildren() const { return !!FirstChild(); }
+
+ inline Accessible* FirstChild() const {
+ return ChildCount() ? ChildAt(0) : nullptr;
+ }
+
+ inline Accessible* LastChild() const {
+ uint32_t childCount = ChildCount();
+ return childCount ? ChildAt(childCount - 1) : nullptr;
+ }
+
+ /**
+ * Return true if this Accessible is before another Accessible in the tree.
+ */
+ bool IsBefore(const Accessible* aAcc) const;
+
+ bool IsAncestorOf(const Accessible* aAcc) const {
+ for (const Accessible* parent = aAcc->Parent(); parent;
+ parent = parent->Parent()) {
+ if (parent == this) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Used by ChildAtPoint() method to get direct or deepest child at point.
+ */
+ enum class EWhichChildAtPoint { DirectChild, DeepestChild };
+
+ virtual Accessible* ChildAtPoint(int32_t aX, int32_t aY,
+ EWhichChildAtPoint aWhichChild) = 0;
+
+ /**
+ * Return the focused child if any.
+ */
+ virtual Accessible* FocusedChild();
+
+ /**
+ * Return ARIA role map if any.
+ */
+ const nsRoleMapEntry* ARIARoleMap() const;
+
+ /**
+ * Return true if ARIA role is specified on the element.
+ */
+ bool HasARIARole() const;
+ bool IsARIARole(nsAtom* aARIARole) const;
+ bool HasStrongARIARole() const;
+
+ /**
+ * Return true if the accessible belongs to the given accessible type.
+ */
+ bool HasGenericType(AccGenericType aType) const;
+
+ /**
+ * Return group position (level, position in set and set size).
+ */
+ virtual GroupPos GroupPosition();
+
+ /**
+ * Return embedded accessible children count.
+ */
+ virtual uint32_t EmbeddedChildCount() = 0;
+
+ /**
+ * Return embedded accessible child at the given index.
+ */
+ virtual Accessible* EmbeddedChildAt(uint32_t aIndex) = 0;
+
+ /**
+ * Return index of the given embedded accessible child.
+ */
+ virtual int32_t IndexOfEmbeddedChild(Accessible* aChild) = 0;
+
+ // Methods that potentially access a cache.
+
+ /*
+ * Get the name of this accessible.
+ */
+ virtual ENameValueFlag Name(nsString& aName) const = 0;
+
+ /*
+ * Get the description of this accessible.
+ */
+ virtual void Description(nsString& aDescription) const = 0;
+
+ /**
+ * Get the value of this accessible.
+ */
+ virtual void Value(nsString& aValue) const = 0;
+
+ virtual double CurValue() const = 0;
+ virtual double MinValue() const = 0;
+ virtual double MaxValue() const = 0;
+ virtual double Step() const = 0;
+ virtual bool SetCurValue(double aValue) = 0;
+
+ /**
+ * Return boundaries in screen coordinates in device pixels.
+ */
+ virtual LayoutDeviceIntRect Bounds() const = 0;
+
+ /**
+ * Return boundaries in screen coordinates in app units.
+ */
+ virtual nsRect BoundsInAppUnits() const = 0;
+
+ /**
+ * Return boundaries in screen coordinates in CSS pixels.
+ */
+ virtual nsIntRect BoundsInCSSPixels() const;
+
+ /**
+ * Returns text of accessible if accessible has text role otherwise empty
+ * string.
+ *
+ * @param aText [in] returned text of the accessible
+ * @param aStartOffset [in, optional] start offset inside of the accessible,
+ * if missed entire text is appended
+ * @param aLength [in, optional] required length of text, if missed
+ * then text from start offset till the end is appended
+ */
+ virtual void AppendTextTo(nsAString& aText, uint32_t aStartOffset = 0,
+ uint32_t aLength = UINT32_MAX) = 0;
+
+ /**
+ * Return all states of accessible (including ARIA states).
+ */
+ virtual uint64_t State() = 0;
+
+ /**
+ * Return the start offset of the embedded object within the parent
+ * HyperTextAccessibleBase.
+ */
+ virtual uint32_t StartOffset();
+
+ /**
+ * Return the end offset of the link within the parent
+ * HyperTextAccessibleBase.
+ */
+ virtual uint32_t EndOffset();
+
+ /**
+ * Return object attributes for the accessible.
+ */
+ virtual already_AddRefed<AccAttributes> Attributes() = 0;
+
+ virtual already_AddRefed<nsAtom> DisplayStyle() const = 0;
+
+ virtual float Opacity() const = 0;
+
+ /**
+ * Get the live region attributes (if any) for this single Accessible. This
+ * does not propagate attributes from ancestors. If any argument is null, that
+ * attribute is not fetched.
+ */
+ virtual void LiveRegionAttributes(nsAString* aLive, nsAString* aRelevant,
+ Maybe<bool>* aAtomic,
+ nsAString* aBusy) const = 0;
+
+ /**
+ * Get the aria-selected state. aria-selected not being specified is not
+ * always the same as aria-selected="false". If not specified, Nothing() will
+ * be returned.
+ */
+ virtual Maybe<bool> ARIASelected() const = 0;
+
+ LayoutDeviceIntSize Size() const;
+
+ LayoutDeviceIntPoint Position(uint32_t aCoordType);
+
+ virtual Maybe<int32_t> GetIntARIAAttr(nsAtom* aAttrName) const = 0;
+
+ /**
+ * Get the relation of the given type.
+ */
+ virtual Relation RelationByType(RelationType aType) const = 0;
+
+ /**
+ * Get the language associated with the accessible.
+ */
+ virtual void Language(nsAString& aLocale) = 0;
+
+ /**
+ * Get the role of this Accessible as an ARIA role token. This might have been
+ * set explicitly (e.g. role="button") or it might be implicit in native
+ * markup (e.g. <button> returns "button").
+ */
+ nsStaticAtom* ComputedARIARole() const;
+
+ // Methods that interact with content.
+
+ virtual void TakeFocus() const = 0;
+
+ /**
+ * Scroll the accessible into view.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ virtual void ScrollTo(uint32_t aHow) const = 0;
+
+ /**
+ * Scroll the accessible to the given point.
+ */
+ virtual void ScrollToPoint(uint32_t aCoordinateType, int32_t aX,
+ int32_t aY) = 0;
+
+ /**
+ * Return tag name of associated DOM node.
+ */
+ virtual nsAtom* TagName() const = 0;
+
+ /**
+ * Return input `type` attribute
+ */
+ virtual already_AddRefed<nsAtom> InputType() const = 0;
+
+ /**
+ * Return a landmark role if applied.
+ */
+ nsStaticAtom* LandmarkRole() const;
+
+ /**
+ * Return the id of the dom node this accessible represents.
+ */
+ virtual void DOMNodeID(nsString& aID) const = 0;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // ActionAccessible
+
+ /**
+ * Return the number of actions that can be performed on this accessible.
+ */
+ virtual uint8_t ActionCount() const = 0;
+
+ /**
+ * Return action name at given index.
+ */
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) = 0;
+
+ /**
+ * Default to localized action name.
+ */
+ void ActionDescriptionAt(uint8_t aIndex, nsAString& aDescription) {
+ nsAutoString name;
+ ActionNameAt(aIndex, name);
+ TranslateString(name, aDescription);
+ }
+
+ /**
+ * Invoke the accessible action.
+ */
+ virtual bool DoAction(uint8_t aIndex) const = 0;
+
+ /**
+ * Return access key, such as Alt+D.
+ */
+ virtual KeyBinding AccessKey() const = 0;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // SelectAccessible
+
+ /**
+ * Return an array of selected items.
+ */
+ virtual void SelectedItems(nsTArray<Accessible*>* aItems) = 0;
+
+ /**
+ * Return the number of selected items.
+ */
+ virtual uint32_t SelectedItemCount() = 0;
+
+ /**
+ * Return selected item at the given index.
+ */
+ virtual Accessible* GetSelectedItem(uint32_t aIndex) = 0;
+
+ /**
+ * Determine if item at the given index is selected.
+ */
+ virtual bool IsItemSelected(uint32_t aIndex) = 0;
+
+ /**
+ * Add item at the given index the selection. Return true if success.
+ */
+ virtual bool AddItemToSelection(uint32_t aIndex) = 0;
+
+ /**
+ * Remove item at the given index from the selection. Return if success.
+ */
+ virtual bool RemoveItemFromSelection(uint32_t aIndex) = 0;
+
+ /**
+ * Select all items. Return true if success.
+ */
+ virtual bool SelectAll() = 0;
+
+ /**
+ * Unselect all items. Return true if success.
+ */
+ virtual bool UnselectAll() = 0;
+
+ virtual void TakeSelection() = 0;
+
+ virtual void SetSelected(bool aSelect) = 0;
+
+ // Type "is" methods
+
+ bool IsDoc() const { return HasGenericType(eDocument); }
+
+ bool IsTableRow() const { return HasGenericType(eTableRow); }
+
+ bool IsTableCell() const {
+ // The eTableCell type defined in the ARIA map is used in
+ // nsAccessibilityService::CreateAccessible to specify when
+ // ARIAGridCellAccessible should be used for object creation. However, an
+ // invalid table structure might cause this class not to be used after all.
+ // To make sure we're really dealing with a cell, only check the generic
+ // type defined by the class, not the type defined in the ARIA map.
+ return mGenericTypes & eTableCell;
+ }
+
+ bool IsTable() const { return HasGenericType(eTable); }
+
+ bool IsHyperText() const { return HasGenericType(eHyperText); }
+
+ bool IsSelect() const { return HasGenericType(eSelect); }
+
+ bool IsActionable() const { return HasGenericType(eActionable); }
+
+ bool IsText() const { return mGenericTypes & eText; }
+
+ bool IsImage() const { return mType == eImageType; }
+
+ bool IsApplication() const { return mType == eApplicationType; }
+
+ bool IsAlert() const { return HasGenericType(eAlert); }
+
+ bool IsButton() const { return HasGenericType(eButton); }
+
+ bool IsCombobox() const { return HasGenericType(eCombobox); }
+
+ virtual bool IsLink() const = 0;
+
+ /**
+ * Return true if the used ARIA role (if any) allows the hypertext accessible
+ * to expose text interfaces.
+ */
+ bool IsTextRole();
+
+ bool IsGenericHyperText() const { return mType == eHyperTextType; }
+
+ bool IsHTMLBr() const { return mType == eHTMLBRType; }
+ bool IsHTMLCaption() const { return mType == eHTMLCaptionType; }
+ bool IsHTMLCombobox() const { return mType == eHTMLComboboxType; }
+ bool IsHTMLFileInput() const { return mType == eHTMLFileInputType; }
+
+ bool IsHTMLListItem() const { return mType == eHTMLLiType; }
+
+ bool IsHTMLLink() const { return mType == eHTMLLinkType; }
+
+ bool IsHTMLOptGroup() const { return mType == eHTMLOptGroupType; }
+
+ bool IsHTMLRadioButton() const { return mType == eHTMLRadioButtonType; }
+
+ bool IsHTMLTable() const { return mType == eHTMLTableType; }
+ bool IsHTMLTableCell() const { return mType == eHTMLTableCellType; }
+ bool IsHTMLTableRow() const { return mType == eHTMLTableRowType; }
+
+ bool IsImageMap() const { return mType == eImageMapType; }
+
+ bool IsList() const { return HasGenericType(eList); }
+
+ bool IsListControl() const { return HasGenericType(eListControl); }
+
+ bool IsMenuButton() const { return HasGenericType(eMenuButton); }
+
+ bool IsMenuPopup() const { return mType == eMenuPopupType; }
+
+ bool IsOuterDoc() const { return mType == eOuterDocType; }
+
+ bool IsProgress() const { return mType == eProgressType; }
+
+ bool IsRoot() const { return mType == eRootType; }
+
+ bool IsPassword() const { return mType == eHTMLTextPasswordFieldType; }
+
+ bool IsTextLeaf() const { return mType == eTextLeafType; }
+
+ bool IsXULLabel() const { return mType == eXULLabelType; }
+
+ bool IsXULListItem() const { return mType == eXULListItemType; }
+
+ bool IsXULTabpanels() const { return mType == eXULTabpanelsType; }
+
+ bool IsXULTooltip() const { return mType == eXULTooltipType; }
+
+ bool IsXULTree() const { return mType == eXULTreeType; }
+
+ bool IsAutoCompletePopup() const {
+ return HasGenericType(eAutoCompletePopup);
+ }
+
+ bool IsTextField() const {
+ return mType == eHTMLTextFieldType || mType == eHTMLTextPasswordFieldType;
+ }
+
+ bool IsDateTimeField() const { return mType == eHTMLDateTimeFieldType; }
+
+ bool IsSearchbox() const;
+
+ virtual bool HasNumericValue() const = 0;
+
+ /**
+ * Returns true if this is a generic container element that has no meaning on
+ * its own.
+ */
+ bool IsGeneric() const {
+ role accRole = Role();
+ return accRole == roles::TEXT || accRole == roles::TEXT_CONTAINER ||
+ accRole == roles::SECTION;
+ }
+
+ /**
+ * Returns the nearest ancestor which is not a generic element.
+ */
+ Accessible* GetNonGenericParent() const {
+ for (Accessible* parent = Parent(); parent; parent = parent->Parent()) {
+ if (!parent->IsGeneric()) {
+ return parent;
+ }
+ }
+ return nullptr;
+ }
+
+ /**
+ * Returns true if the accessible is non-interactive.
+ */
+ bool IsNonInteractive() const {
+ if (IsGeneric()) {
+ return true;
+ }
+ const role accRole = Role();
+ return accRole == role::LANDMARK || accRole == role::REGION;
+ }
+
+ /**
+ * Return true if the link is valid (e. g. points to a valid URL).
+ */
+ bool IsLinkValid();
+
+ /**
+ * Return the number of anchors within the link.
+ */
+ uint32_t AnchorCount();
+
+ /**
+ * Returns an anchor URI at the given index.
+ */
+ virtual already_AddRefed<nsIURI> AnchorURIAt(uint32_t aAnchorIndex) const;
+
+ /**
+ * Returns an anchor accessible at the given index.
+ */
+ Accessible* AnchorAt(uint32_t aAnchorIndex) const;
+
+ // Remote/Local types
+
+ virtual bool IsRemote() const = 0;
+ RemoteAccessible* AsRemote();
+
+ bool IsLocal() const { return !IsRemote(); }
+ LocalAccessible* AsLocal();
+
+ virtual HyperTextAccessibleBase* AsHyperTextBase() { return nullptr; }
+
+ virtual TableAccessible* AsTable() { return nullptr; }
+ virtual TableCellAccessible* AsTableCell() { return nullptr; }
+
+#ifdef A11Y_LOG
+ /**
+ * Provide a human readable description of the accessible,
+ * including memory address, role, name, DOM tag and DOM ID.
+ */
+ void DebugDescription(nsCString& aDesc) const;
+
+ static void DebugPrint(const char* aPrefix, const Accessible* aAccessible);
+#endif
+
+ /**
+ * Return the localized string for the given key.
+ */
+ static void TranslateString(const nsString& aKey, nsAString& aStringOut);
+
+ protected:
+ // Some abstracted group utility methods.
+
+ /**
+ * Get ARIA group attributes.
+ */
+ virtual void ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize,
+ int32_t* aPosInSet) const = 0;
+
+ /**
+ * Return group info if there is an up-to-date version.
+ */
+ virtual AccGroupInfo* GetGroupInfo() const = 0;
+
+ /**
+ * Return group info or create and update.
+ */
+ virtual AccGroupInfo* GetOrCreateGroupInfo() = 0;
+
+ /*
+ * Return calculated group level based on accessible hierarchy.
+ *
+ * @param aFast [in] Don't climb up tree. Calculate level from aria and
+ * roles.
+ */
+ virtual int32_t GetLevel(bool aFast) const;
+
+ /**
+ * Calculate position in group and group size ('posinset' and 'setsize') based
+ * on accessible hierarchy.
+ *
+ * @param aPosInSet [out] accessible position in the group
+ * @param aSetSize [out] the group size
+ */
+ virtual void GetPositionAndSetSize(int32_t* aPosInSet, int32_t* aSetSize);
+
+ /**
+ * Return the nearest ancestor that has a primary action, or null.
+ */
+ const Accessible* ActionAncestor() const;
+
+ /**
+ * Return true if accessible has a primary action directly related to it, like
+ * "click", "activate", "press", "jump", "open", "close", etc. A non-primary
+ * action would be a complementary one like "showlongdesc".
+ * If an accessible has an action that is associated with an ancestor, it is
+ * not a primary action either.
+ */
+ virtual bool HasPrimaryAction() const = 0;
+
+ /**
+ * Apply states which are implied by other information common to both
+ * LocalAccessible and RemoteAccessible.
+ */
+ void ApplyImplicitState(uint64_t& aState) const;
+
+ private:
+ static const uint8_t kTypeBits = 6;
+ static const uint8_t kGenericTypesBits = 18;
+
+ void StaticAsserts() const;
+
+ protected:
+ uint32_t mType : kTypeBits;
+ uint32_t mGenericTypes : kGenericTypesBits;
+ uint8_t mRoleMapEntryIndex;
+
+ friend class DocAccessibleChild;
+ friend class AccGroupInfo;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/basetypes/HyperTextAccessibleBase.cpp b/accessible/basetypes/HyperTextAccessibleBase.cpp
new file mode 100644
index 0000000000..c66180673b
--- /dev/null
+++ b/accessible/basetypes/HyperTextAccessibleBase.cpp
@@ -0,0 +1,841 @@
+/* -*- 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 "HyperTextAccessibleBase.h"
+
+#include "mozilla/a11y/Accessible.h"
+#include "nsAccUtils.h"
+#include "TextLeafRange.h"
+#include "TextRange.h"
+
+namespace mozilla::a11y {
+
+int32_t HyperTextAccessibleBase::GetChildIndexAtOffset(uint32_t aOffset) const {
+ auto& offsets =
+ const_cast<HyperTextAccessibleBase*>(this)->GetCachedHyperTextOffsets();
+ int32_t lastOffset = 0;
+ const uint32_t offsetCount = offsets.Length();
+
+ if (offsetCount > 0) {
+ lastOffset = offsets[offsetCount - 1];
+ if (static_cast<int32_t>(aOffset) < lastOffset) {
+ // We've cached up to aOffset.
+ size_t index;
+ if (BinarySearch(offsets, 0, offsetCount, static_cast<int32_t>(aOffset),
+ &index)) {
+ // aOffset is the exclusive end of a child, so return the child before
+ // it.
+ return static_cast<int32_t>((index < offsetCount - 1) ? index + 1
+ : index);
+ }
+ if (index == offsetCount) {
+ // aOffset is past the end of the text.
+ return -1;
+ }
+ // index points at the exclusive end after aOffset.
+ return static_cast<int32_t>(index);
+ }
+ }
+
+ // We haven't yet cached up to aOffset. Find it, caching as we go.
+ const Accessible* thisAcc = Acc();
+ uint32_t childCount = thisAcc->ChildCount();
+ // Even though we're only caching up to aOffset, it's likely that we'll
+ // eventually cache offsets for all children. Pre-allocate thus to minimize
+ // re-allocations.
+ offsets.SetCapacity(childCount);
+ while (offsets.Length() < childCount) {
+ Accessible* child = thisAcc->ChildAt(offsets.Length());
+ lastOffset += static_cast<int32_t>(nsAccUtils::TextLength(child));
+ offsets.AppendElement(lastOffset);
+ if (static_cast<int32_t>(aOffset) < lastOffset) {
+ return static_cast<int32_t>(offsets.Length() - 1);
+ }
+ }
+
+ if (static_cast<int32_t>(aOffset) == lastOffset) {
+ return static_cast<int32_t>(offsets.Length() - 1);
+ }
+
+ return -1;
+}
+
+Accessible* HyperTextAccessibleBase::GetChildAtOffset(uint32_t aOffset) const {
+ const Accessible* thisAcc = Acc();
+ return thisAcc->ChildAt(GetChildIndexAtOffset(aOffset));
+}
+
+int32_t HyperTextAccessibleBase::GetChildOffset(const Accessible* aChild,
+ bool aInvalidateAfter) const {
+ const Accessible* thisAcc = Acc();
+ if (aChild->Parent() != thisAcc) {
+ return -1;
+ }
+ int32_t index = aChild->IndexInParent();
+ if (index == -1) {
+ return -1;
+ }
+ return GetChildOffset(index, aInvalidateAfter);
+}
+
+int32_t HyperTextAccessibleBase::GetChildOffset(uint32_t aChildIndex,
+ bool aInvalidateAfter) const {
+ auto& offsets =
+ const_cast<HyperTextAccessibleBase*>(this)->GetCachedHyperTextOffsets();
+ if (aChildIndex == 0) {
+ if (aInvalidateAfter) {
+ offsets.Clear();
+ }
+ return 0;
+ }
+
+ int32_t countCachedAfterChild = static_cast<int32_t>(offsets.Length()) -
+ static_cast<int32_t>(aChildIndex);
+ if (countCachedAfterChild > 0) {
+ // We've cached up to aChildIndex.
+ if (aInvalidateAfter) {
+ offsets.RemoveElementsAt(aChildIndex, countCachedAfterChild);
+ }
+ return offsets[aChildIndex - 1];
+ }
+
+ // We haven't yet cached up to aChildIndex. Find it, caching as we go.
+ const Accessible* thisAcc = Acc();
+ // Even though we're only caching up to aChildIndex, it's likely that we'll
+ // eventually cache offsets for all children. Pre-allocate thus to minimize
+ // re-allocations.
+ offsets.SetCapacity(thisAcc->ChildCount());
+ uint32_t lastOffset = offsets.IsEmpty() ? 0 : offsets[offsets.Length() - 1];
+ while (offsets.Length() < aChildIndex) {
+ Accessible* child = thisAcc->ChildAt(offsets.Length());
+ lastOffset += nsAccUtils::TextLength(child);
+ offsets.AppendElement(lastOffset);
+ }
+
+ return offsets[aChildIndex - 1];
+}
+
+uint32_t HyperTextAccessibleBase::CharacterCount() const {
+ return GetChildOffset(Acc()->ChildCount());
+}
+
+index_t HyperTextAccessibleBase::ConvertMagicOffset(int32_t aOffset) const {
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT) {
+ return CharacterCount();
+ }
+
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ return CaretOffset();
+ }
+
+ return aOffset;
+}
+
+void HyperTextAccessibleBase::TextSubstring(int32_t aStartOffset,
+ int32_t aEndOffset,
+ nsAString& aText) const {
+ aText.Truncate();
+
+ index_t startOffset = ConvertMagicOffset(aStartOffset);
+ index_t endOffset = ConvertMagicOffset(aEndOffset);
+ if (!startOffset.IsValid() || !endOffset.IsValid() ||
+ startOffset > endOffset || endOffset > CharacterCount()) {
+ NS_ERROR("Wrong in offset");
+ return;
+ }
+
+ int32_t startChildIdx = GetChildIndexAtOffset(startOffset);
+ if (startChildIdx == -1) {
+ return;
+ }
+
+ int32_t endChildIdx = GetChildIndexAtOffset(endOffset);
+ if (endChildIdx == -1) {
+ return;
+ }
+
+ const Accessible* thisAcc = Acc();
+ if (startChildIdx == endChildIdx) {
+ int32_t childOffset = GetChildOffset(startChildIdx);
+ if (childOffset == -1) {
+ return;
+ }
+
+ Accessible* child = thisAcc->ChildAt(startChildIdx);
+ child->AppendTextTo(aText, startOffset - childOffset,
+ endOffset - startOffset);
+ return;
+ }
+
+ int32_t startChildOffset = GetChildOffset(startChildIdx);
+ if (startChildOffset == -1) {
+ return;
+ }
+
+ Accessible* startChild = thisAcc->ChildAt(startChildIdx);
+ startChild->AppendTextTo(aText, startOffset - startChildOffset);
+
+ for (int32_t childIdx = startChildIdx + 1; childIdx < endChildIdx;
+ childIdx++) {
+ Accessible* child = thisAcc->ChildAt(childIdx);
+ child->AppendTextTo(aText);
+ }
+
+ int32_t endChildOffset = GetChildOffset(endChildIdx);
+ if (endChildOffset == -1) {
+ return;
+ }
+
+ Accessible* endChild = thisAcc->ChildAt(endChildIdx);
+ endChild->AppendTextTo(aText, 0, endOffset - endChildOffset);
+}
+
+bool HyperTextAccessibleBase::CharAt(int32_t aOffset, nsAString& aChar,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset) {
+ MOZ_ASSERT(!aStartOffset == !aEndOffset,
+ "Offsets should be both defined or both undefined!");
+
+ int32_t childIdx = GetChildIndexAtOffset(aOffset);
+ if (childIdx == -1) {
+ return false;
+ }
+
+ Accessible* child = Acc()->ChildAt(childIdx);
+ child->AppendTextTo(aChar, aOffset - GetChildOffset(childIdx), 1);
+
+ if (aStartOffset && aEndOffset) {
+ *aStartOffset = aOffset;
+ *aEndOffset = aOffset + aChar.Length();
+ }
+ return true;
+}
+
+LayoutDeviceIntRect HyperTextAccessibleBase::CharBounds(int32_t aOffset,
+ uint32_t aCoordType) {
+ index_t offset = ConvertMagicOffset(aOffset);
+ if (!offset.IsValid() || offset > CharacterCount()) {
+ return LayoutDeviceIntRect();
+ }
+ TextLeafPoint point = ToTextLeafPoint(static_cast<int32_t>(offset), false);
+ if (!point.mAcc) {
+ return LayoutDeviceIntRect();
+ }
+
+ LayoutDeviceIntRect bounds = point.CharBounds();
+ if (!bounds.x && !bounds.y && bounds.IsZeroArea()) {
+ return bounds;
+ }
+ nsAccUtils::ConvertScreenCoordsTo(&bounds.x, &bounds.y, aCoordType, Acc());
+ return bounds;
+}
+
+LayoutDeviceIntRect HyperTextAccessibleBase::TextBounds(int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aCoordType) {
+ LayoutDeviceIntRect result;
+ if (CharacterCount() == 0) {
+ result = Acc()->Bounds();
+ nsAccUtils::ConvertScreenCoordsTo(&result.x, &result.y, aCoordType, Acc());
+ return result;
+ }
+
+ index_t startOffset = ConvertMagicOffset(aStartOffset);
+ index_t endOffset = ConvertMagicOffset(aEndOffset);
+ if (!startOffset.IsValid() || startOffset >= endOffset) {
+ return LayoutDeviceIntRect();
+ }
+
+ // Here's where things get complicated. We can't simply query the first
+ // and last character, and union their bounds. They might reside on different
+ // lines, and a simple union may yield an incorrect width. We
+ // should use the length of the longest spanned line for our width.
+
+ TextLeafPoint startPoint =
+ ToTextLeafPoint(static_cast<int32_t>(startOffset), false);
+ TextLeafPoint endPoint =
+ ToTextLeafPoint(static_cast<int32_t>(endOffset), true);
+ if (!endPoint) {
+ // The caller provided an invalid offset.
+ return LayoutDeviceIntRect();
+ }
+
+ // Step backwards from the point returned by ToTextLeafPoint above.
+ // For our purposes, `endPoint` should be inclusive.
+ endPoint =
+ endPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
+ if (endPoint < startPoint) {
+ return result;
+ }
+
+ if (endPoint == startPoint) {
+ result = startPoint.CharBounds();
+ } else {
+ TextLeafRange range(startPoint, endPoint);
+ result = range.Bounds();
+ }
+
+ // Calls to TextLeafRange::Bounds() will construct screen coordinates.
+ // Perform any additional conversions here.
+ nsAccUtils::ConvertScreenCoordsTo(&result.x, &result.y, aCoordType, Acc());
+ return result;
+}
+
+int32_t HyperTextAccessibleBase::OffsetAtPoint(int32_t aX, int32_t aY,
+ uint32_t aCoordType) {
+ Accessible* thisAcc = Acc();
+ LayoutDeviceIntPoint coords =
+ nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc);
+ if (!thisAcc->Bounds().Contains(coords.x, coords.y)) {
+ // The requested point does not exist in this accessible.
+ // Check if we used fuzzy hittesting to get here and, if
+ // so, return 0 to indicate this text leaf is a valid match.
+ LayoutDeviceIntPoint p(aX, aY);
+ if (aCoordType != nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE) {
+ p = nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, thisAcc);
+ }
+ if (Accessible* doc = nsAccUtils::DocumentFor(thisAcc)) {
+ Accessible* hittestMatch = doc->ChildAtPoint(
+ p.x, p.y, Accessible::EWhichChildAtPoint::DeepestChild);
+ if (hittestMatch && thisAcc == hittestMatch->Parent()) {
+ return 0;
+ }
+ }
+ return -1;
+ }
+
+ TextLeafPoint startPoint = ToTextLeafPoint(0, false);
+ // As with TextBounds, we walk to the very end of the text contained in this
+ // hypertext and then step backwards to make our endPoint inclusive.
+ TextLeafPoint endPoint =
+ ToTextLeafPoint(static_cast<int32_t>(CharacterCount()), true);
+ endPoint =
+ endPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
+ TextLeafPoint point = startPoint;
+ // XXX: We should create a TextLeafRange object for this hypertext and move
+ // this search inside the TextLeafRange class.
+ // If there are no characters in this container, we might have moved endPoint
+ // before startPoint. In that case, we shouldn't try to move further forward,
+ // as that might result in an infinite loop.
+ if (startPoint <= endPoint) {
+ for (; !point.ContainsPoint(coords.x, coords.y) && point != endPoint;
+ point =
+ point.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext)) {
+ }
+ }
+ if (!point.ContainsPoint(coords.x, coords.y)) {
+ LayoutDeviceIntRect startRect = startPoint.CharBounds();
+ if (coords.x < startRect.x || coords.y < startRect.y) {
+ // Bug 1816601: The point is within the container but above or to the left
+ // of the rectangle at offset 0. We should really return -1, but we've
+ // returned 0 for many years due to a bug. Some users have unfortunately
+ // come to rely on this, so perpetuate this here.
+ return 0;
+ }
+ return -1;
+ }
+ DebugOnly<bool> ok = false;
+ int32_t htOffset;
+ std::tie(ok, htOffset) =
+ TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false);
+ MOZ_ASSERT(ok, "point should be a descendant of this");
+ return htOffset;
+}
+
+TextLeafPoint HyperTextAccessibleBase::ToTextLeafPoint(int32_t aOffset,
+ bool aDescendToEnd) {
+ Accessible* thisAcc = Acc();
+ if (!thisAcc->HasChildren()) {
+ return TextLeafPoint(thisAcc, 0);
+ }
+ Accessible* child = GetChildAtOffset(aOffset);
+ if (!child) {
+ return TextLeafPoint();
+ }
+ if (HyperTextAccessibleBase* childHt = child->AsHyperTextBase()) {
+ return childHt->ToTextLeafPoint(
+ aDescendToEnd ? static_cast<int32_t>(childHt->CharacterCount()) : 0,
+ aDescendToEnd);
+ }
+ int32_t offset = aOffset - GetChildOffset(child);
+ return TextLeafPoint(child, offset);
+}
+
+std::pair<bool, int32_t> HyperTextAccessibleBase::TransformOffset(
+ Accessible* aDescendant, int32_t aOffset, bool aIsEndOffset) const {
+ const Accessible* thisAcc = Acc();
+ // From the descendant, go up and get the immediate child of this hypertext.
+ int32_t offset = aOffset;
+ Accessible* descendant = aDescendant;
+ while (descendant) {
+ Accessible* parent = descendant->Parent();
+ if (parent == thisAcc) {
+ return {true, GetChildOffset(descendant) + offset};
+ }
+
+ // This offset no longer applies because the passed-in text object is not
+ // a child of the hypertext. This happens when there are nested hypertexts,
+ // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset
+ // to make it relative the hypertext.
+ // If the end offset is not supposed to be inclusive and the original point
+ // is not at 0 offset then the returned offset should be after an embedded
+ // character the original point belongs to.
+ if (aIsEndOffset) {
+ offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0;
+ } else {
+ offset = 0;
+ }
+
+ descendant = parent;
+ }
+
+ // The given a11y point cannot be mapped to an offset relative to this
+ // hypertext accessible. Return the start or the end depending on whether this
+ // is a start ofset or an end offset, thus clipping to the relevant endpoint.
+ return {false, aIsEndOffset ? static_cast<int32_t>(CharacterCount()) : 0};
+}
+
+void HyperTextAccessibleBase::AdjustOriginIfEndBoundary(
+ TextLeafPoint& aOrigin, AccessibleTextBoundary aBoundaryType,
+ bool aAtOffset) const {
+ if (aBoundaryType != nsIAccessibleText::BOUNDARY_LINE_END &&
+ aBoundaryType != nsIAccessibleText::BOUNDARY_WORD_END) {
+ return;
+ }
+ TextLeafPoint actualOrig =
+ aOrigin.IsCaret() ? aOrigin.ActualizeCaret(/* aAdjustAtEndOfLine */ false)
+ : aOrigin;
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_LINE_END) {
+ if (!actualOrig.IsLineFeedChar()) {
+ return;
+ }
+ aOrigin =
+ actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
+ } else { // BOUNDARY_WORD_END
+ if (aAtOffset) {
+ // For TextAtOffset with BOUNDARY_WORD_END, we follow WebKitGtk here and
+ // return the word which ends after the origin if the origin is a word end
+ // boundary. Also, if the caret is at the end of a line, our tests expect
+ // the word after the caret, not the word before. The reason for that
+ // is a mystery lost to history. We can do that by explicitly using the
+ // actualized caret without adjusting for end of line.
+ aOrigin = actualOrig;
+ return;
+ }
+ if (!actualOrig.IsSpace()) {
+ return;
+ }
+ TextLeafPoint prevChar =
+ actualOrig.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
+ if (prevChar != actualOrig && !prevChar.IsSpace()) {
+ // aOrigin is a word end boundary.
+ aOrigin = prevChar;
+ }
+ }
+}
+
+void HyperTextAccessibleBase::TextBeforeOffset(
+ int32_t aOffset, AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) {
+ *aStartOffset = *aEndOffset = 0;
+ aText.Truncate();
+
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START ||
+ aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) {
+ return; // Not implemented.
+ }
+
+ uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
+ if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
+ NS_ERROR("Wrong given offset!");
+ return;
+ }
+
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
+ if (adjustedOffset > 0) {
+ CharAt(static_cast<int32_t>(adjustedOffset) - 1, aText, aStartOffset,
+ aEndOffset);
+ }
+ return;
+ }
+
+ TextLeafPoint orig;
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ orig = TextLeafPoint::GetCaret(Acc());
+ } else {
+ orig = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset));
+ }
+ if (!orig) {
+ // This can happen if aOffset is invalid.
+ return;
+ }
+ AdjustOriginIfEndBoundary(orig, aBoundaryType);
+ TextLeafPoint end =
+ orig.FindBoundary(aBoundaryType, eDirPrevious,
+ TextLeafPoint::BoundaryFlags::eIncludeOrigin);
+ bool ok;
+ std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
+ /* aIsEndOffset */ true);
+ if (!ok) {
+ // There is no previous boundary inside this HyperText.
+ *aStartOffset = *aEndOffset = 0;
+ return;
+ }
+ TextLeafPoint start = end.FindBoundary(aBoundaryType, eDirPrevious);
+ // If TransformOffset fails because start is outside this HyperText,
+ // *aStartOffset will be 0, which is what we want.
+ std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
+ /* aIsEndOffset */ false);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+}
+
+void HyperTextAccessibleBase::TextAtOffset(int32_t aOffset,
+ AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset,
+ nsAString& aText) {
+ *aStartOffset = *aEndOffset = 0;
+ aText.Truncate();
+
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START ||
+ aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) {
+ return; // Not implemented.
+ }
+
+ uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
+ if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
+ NS_ERROR("Wrong given offset!");
+ return;
+ }
+
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ TextLeafPoint caret = TextLeafPoint::GetCaret(Acc());
+ if (caret.IsCaretAtEndOfLine()) {
+ // The caret is at the end of the line. Return no character.
+ *aStartOffset = *aEndOffset = static_cast<int32_t>(adjustedOffset);
+ return;
+ }
+ }
+ CharAt(adjustedOffset, aText, aStartOffset, aEndOffset);
+ return;
+ }
+
+ TextLeafPoint start, end;
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ start = TextLeafPoint::GetCaret(Acc());
+ AdjustOriginIfEndBoundary(start, aBoundaryType, /* aAtOffset */ true);
+ end = start;
+ } else {
+ start = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset));
+ Accessible* childAcc = GetChildAtOffset(adjustedOffset);
+ if (childAcc && childAcc->IsHyperText()) {
+ // We're searching for boundaries enclosing an embedded object.
+ // An embedded object might contain several boundaries itself.
+ // Thus, we must ensure we search for the end boundary from the last
+ // text in the subtree, not just the first.
+ // For example, if the embedded object is a link and it contains two
+ // words, but the second word expands beyond the link, we want to
+ // include the part of the second word which is outside of the link.
+ end = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset),
+ /* aDescendToEnd */ true);
+ } else {
+ AdjustOriginIfEndBoundary(start, aBoundaryType,
+ /* aAtOffset */ true);
+ end = start;
+ }
+ }
+ if (!start) {
+ // This can happen if aOffset is invalid.
+ return;
+ }
+ start = start.FindBoundary(aBoundaryType, eDirPrevious,
+ TextLeafPoint::BoundaryFlags::eIncludeOrigin);
+ bool ok;
+ std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
+ /* aIsEndOffset */ false);
+ end = end.FindBoundary(aBoundaryType, eDirNext);
+ std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
+ /* aIsEndOffset */ true);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+}
+
+void HyperTextAccessibleBase::TextAfterOffset(
+ int32_t aOffset, AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) {
+ *aStartOffset = *aEndOffset = 0;
+ aText.Truncate();
+
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_START ||
+ aBoundaryType == nsIAccessibleText::BOUNDARY_SENTENCE_END) {
+ return; // Not implemented.
+ }
+
+ uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
+ if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
+ NS_ERROR("Wrong given offset!");
+ return;
+ }
+
+ if (aBoundaryType == nsIAccessibleText::BOUNDARY_CHAR) {
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET && adjustedOffset > 0 &&
+ TextLeafPoint::GetCaret(Acc()).IsCaretAtEndOfLine()) {
+ --adjustedOffset;
+ }
+ uint32_t count = CharacterCount();
+ if (adjustedOffset >= count) {
+ *aStartOffset = *aEndOffset = static_cast<int32_t>(count);
+ } else {
+ CharAt(static_cast<int32_t>(adjustedOffset) + 1, aText, aStartOffset,
+ aEndOffset);
+ }
+ return;
+ }
+
+ TextLeafPoint orig;
+ if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) {
+ orig = TextLeafPoint::GetCaret(Acc());
+ } else {
+ orig = ToTextLeafPoint(static_cast<int32_t>(adjustedOffset),
+ /* aDescendToEnd */ true);
+ }
+ if (!orig) {
+ // This can happen if aOffset is invalid.
+ return;
+ }
+ AdjustOriginIfEndBoundary(orig, aBoundaryType);
+ TextLeafPoint start = orig.FindBoundary(aBoundaryType, eDirNext);
+ bool ok;
+ std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
+ /* aIsEndOffset */ false);
+ if (!ok) {
+ // There is no next boundary inside this HyperText.
+ *aStartOffset = *aEndOffset = static_cast<int32_t>(CharacterCount());
+ return;
+ }
+ TextLeafPoint end = start.FindBoundary(aBoundaryType, eDirNext);
+ // If TransformOffset fails because end is outside this HyperText,
+ // *aEndOffset will be CharacterCount(), which is what we want.
+ std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
+ /* aIsEndOffset */ true);
+ TextSubstring(*aStartOffset, *aEndOffset, aText);
+}
+
+int32_t HyperTextAccessibleBase::CaretOffset() const {
+ TextLeafPoint point = TextLeafPoint::GetCaret(const_cast<Accessible*>(Acc()))
+ .ActualizeCaret(/* aAdjustAtEndOfLine */ false);
+ if (point.mOffset == 0 && point.mAcc == Acc()) {
+ // If a text box is empty, there will be no children, so point.mAcc will be
+ // this HyperText.
+ return 0;
+ }
+ auto [ok, htOffset] =
+ TransformOffset(point.mAcc, point.mOffset, /* aIsEndOffset */ false);
+ if (!ok) {
+ // The caret is not within this HyperText.
+ return -1;
+ }
+ return htOffset;
+}
+
+int32_t HyperTextAccessibleBase::CaretLineNumber() {
+ TextLeafPoint point = TextLeafPoint::GetCaret(const_cast<Accessible*>(Acc()))
+ .ActualizeCaret(/* aAdjustAtEndOfLine */ false);
+ if (point.mOffset == 0 && point.mAcc == Acc()) {
+ MOZ_ASSERT(CharacterCount() == 0);
+ // If a text box is empty, there will be no children, so point.mAcc will be
+ // this HyperText.
+ return 1;
+ }
+
+ if (!point.mAcc ||
+ (point.mAcc != Acc() && !Acc()->IsAncestorOf(point.mAcc))) {
+ // The caret is not within this HyperText.
+ return -1;
+ }
+
+ TextLeafPoint firstPointInThis = TextLeafPoint(Acc(), 0);
+ int32_t lineNumber = 1;
+ for (TextLeafPoint line = point; line && firstPointInThis < line;
+ line = line.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START,
+ eDirPrevious)) {
+ lineNumber++;
+ }
+
+ return lineNumber;
+}
+
+bool HyperTextAccessibleBase::IsValidOffset(int32_t aOffset) {
+ index_t offset = ConvertMagicOffset(aOffset);
+ return offset.IsValid() && offset <= CharacterCount();
+}
+
+bool HyperTextAccessibleBase::IsValidRange(int32_t aStartOffset,
+ int32_t aEndOffset) {
+ index_t startOffset = ConvertMagicOffset(aStartOffset);
+ index_t endOffset = ConvertMagicOffset(aEndOffset);
+ return startOffset.IsValid() && endOffset.IsValid() &&
+ startOffset <= endOffset && endOffset <= CharacterCount();
+}
+
+uint32_t HyperTextAccessibleBase::LinkCount() {
+ return Acc()->EmbeddedChildCount();
+}
+
+Accessible* HyperTextAccessibleBase::LinkAt(uint32_t aIndex) {
+ return Acc()->EmbeddedChildAt(aIndex);
+}
+
+int32_t HyperTextAccessibleBase::LinkIndexOf(Accessible* aLink) {
+ return Acc()->IndexOfEmbeddedChild(aLink);
+}
+
+already_AddRefed<AccAttributes> HyperTextAccessibleBase::TextAttributes(
+ bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset,
+ int32_t* aEndOffset) {
+ *aStartOffset = *aEndOffset = 0;
+ index_t offset = ConvertMagicOffset(aOffset);
+ if (!offset.IsValid() || offset > CharacterCount()) {
+ NS_ERROR("Wrong in offset!");
+ return RefPtr{new AccAttributes()}.forget();
+ }
+
+ Accessible* originAcc = GetChildAtOffset(offset);
+ if (!originAcc) {
+ // Offset 0 is correct offset when accessible has empty text. Include
+ // default attributes if they were requested, otherwise return empty set.
+ if (offset == 0) {
+ if (aIncludeDefAttrs) {
+ return DefaultTextAttributes();
+ }
+ }
+ return RefPtr{new AccAttributes()}.forget();
+ }
+
+ if (!originAcc->IsText()) {
+ // This is an embedded object. One or more consecutive embedded objects
+ // form a single attrs run with no attributes.
+ *aStartOffset = aOffset;
+ *aEndOffset = aOffset + 1;
+ Accessible* parent = originAcc->Parent();
+ if (!parent) {
+ return RefPtr{new AccAttributes()}.forget();
+ }
+ int32_t originIdx = originAcc->IndexInParent();
+ if (originIdx > 0) {
+ // Check for embedded objects before the origin.
+ for (uint32_t idx = originIdx - 1;; --idx) {
+ Accessible* sibling = parent->ChildAt(idx);
+ if (sibling->IsText()) {
+ break;
+ }
+ --*aStartOffset;
+ if (idx == 0) {
+ break;
+ }
+ }
+ }
+ // Check for embedded objects after the origin.
+ for (uint32_t idx = originIdx + 1;; ++idx) {
+ Accessible* sibling = parent->ChildAt(idx);
+ if (!sibling || sibling->IsText()) {
+ break;
+ }
+ ++*aEndOffset;
+ }
+ return RefPtr{new AccAttributes()}.forget();
+ }
+
+ TextLeafPoint origin = ToTextLeafPoint(static_cast<int32_t>(offset));
+ TextLeafPoint start =
+ origin.FindTextAttrsStart(eDirPrevious, /* aIncludeOrigin */ true);
+ bool ok;
+ std::tie(ok, *aStartOffset) = TransformOffset(start.mAcc, start.mOffset,
+ /* aIsEndOffset */ false);
+ TextLeafPoint end =
+ origin.FindTextAttrsStart(eDirNext, /* aIncludeOrigin */ false);
+ std::tie(ok, *aEndOffset) = TransformOffset(end.mAcc, end.mOffset,
+ /* aIsEndOffset */ true);
+ return origin.GetTextAttributes(aIncludeDefAttrs);
+}
+
+void HyperTextAccessibleBase::CroppedSelectionRanges(
+ nsTArray<TextRange>& aRanges) const {
+ SelectionRanges(&aRanges);
+ const Accessible* acc = Acc();
+ aRanges.RemoveElementsBy([acc](auto& range) {
+ if (range.StartPoint() == range.EndPoint()) {
+ return true; // Collapsed, so remove this range.
+ }
+ // If this is the document, it contains all ranges, so there's no need to
+ // crop.
+ if (!acc->IsDoc()) {
+ // If we fail to crop, the range is outside acc, so remove it.
+ return !range.Crop(const_cast<Accessible*>(acc));
+ }
+ return false;
+ });
+}
+
+int32_t HyperTextAccessibleBase::SelectionCount() {
+ nsTArray<TextRange> ranges;
+ CroppedSelectionRanges(ranges);
+ return static_cast<int32_t>(ranges.Length());
+}
+
+bool HyperTextAccessibleBase::SelectionBoundsAt(int32_t aSelectionNum,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset) {
+ nsTArray<TextRange> ranges;
+ CroppedSelectionRanges(ranges);
+ if (aSelectionNum >= static_cast<int32_t>(ranges.Length())) {
+ return false;
+ }
+ TextRange& range = ranges[aSelectionNum];
+ Accessible* thisAcc = Acc();
+ if (range.StartContainer() == thisAcc) {
+ *aStartOffset = range.StartOffset();
+ } else {
+ bool ok;
+ // range.StartContainer() isn't a text leaf, so don't use its offset.
+ std::tie(ok, *aStartOffset) =
+ TransformOffset(range.StartContainer(), 0, /* aDescendToEnd */ false);
+ }
+ if (range.EndContainer() == thisAcc) {
+ *aEndOffset = range.EndOffset();
+ } else {
+ bool ok;
+ // range.EndContainer() isn't a text leaf, so don't use its offset. If
+ // range.EndOffset() is > 0, we want to include this container, so pas
+ // offset 1.
+ std::tie(ok, *aEndOffset) =
+ TransformOffset(range.EndContainer(), range.EndOffset() == 0 ? 0 : 1,
+ /* aDescendToEnd */ true);
+ }
+ return true;
+}
+
+bool HyperTextAccessibleBase::SetSelectionBoundsAt(int32_t aSelectionNum,
+ int32_t aStartOffset,
+ int32_t aEndOffset) {
+ TextLeafRange range(ToTextLeafPoint(aStartOffset),
+ ToTextLeafPoint(aEndOffset, true));
+ if (!range) {
+ NS_ERROR("Wrong in offset");
+ return false;
+ }
+
+ return range.SetSelection(aSelectionNum);
+}
+
+void HyperTextAccessibleBase::ScrollSubstringTo(int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aScrollType) {
+ TextLeafRange range(ToTextLeafPoint(aStartOffset),
+ ToTextLeafPoint(aEndOffset, true));
+ range.ScrollIntoView(aScrollType);
+}
+
+} // namespace mozilla::a11y
diff --git a/accessible/basetypes/HyperTextAccessibleBase.h b/accessible/basetypes/HyperTextAccessibleBase.h
new file mode 100644
index 0000000000..e7a7bda016
--- /dev/null
+++ b/accessible/basetypes/HyperTextAccessibleBase.h
@@ -0,0 +1,317 @@
+/* -*- 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 _HyperTextAccessibleBase_H_
+#define _HyperTextAccessibleBase_H_
+
+#include "AccAttributes.h"
+#include "nsIAccessibleText.h"
+#include "nsIAccessibleTypes.h"
+
+namespace mozilla::a11y {
+class Accessible;
+class TextLeafPoint;
+class TextRange;
+
+// This character marks where in the text returned via Text interface,
+// that embedded object characters exist
+const char16_t kEmbeddedObjectChar = 0xfffc;
+const char16_t kImaginaryEmbeddedObjectChar = ' ';
+const char16_t kForcedNewLineChar = '\n';
+
+/**
+ * An index type. Assert if out of range value was attempted to be used.
+ */
+class index_t {
+ public:
+ MOZ_IMPLICIT index_t(int32_t aVal) : mVal(aVal) {}
+
+ operator uint32_t() const {
+ MOZ_ASSERT(mVal >= 0, "Attempt to use wrong index!");
+ return mVal;
+ }
+
+ bool IsValid() const { return mVal >= 0; }
+
+ private:
+ int32_t mVal;
+};
+
+class HyperTextAccessibleBase {
+ public:
+ /**
+ * Return child accessible at the given text offset.
+ *
+ * @param aOffset [in] the given text offset
+ */
+ virtual int32_t GetChildIndexAtOffset(uint32_t aOffset) const;
+
+ /**
+ * Return child accessible at the given text offset.
+ *
+ * @param aOffset [in] the given text offset
+ */
+ virtual Accessible* GetChildAtOffset(uint32_t aOffset) const;
+
+ /**
+ * Return text offset of the given child accessible within hypertext
+ * accessible.
+ *
+ * @param aChild [in] accessible child to get text offset for
+ * @param aInvalidateAfter [in, optional] indicates whether to invalidate
+ * cached offsets for subsequent siblings of the
+ * child.
+ */
+ int32_t GetChildOffset(const Accessible* aChild,
+ bool aInvalidateAfter = false) const;
+
+ /**
+ * Return text offset for the child accessible index.
+ */
+ virtual int32_t GetChildOffset(uint32_t aChildIndex,
+ bool aInvalidateAfter = false) const;
+
+ /**
+ * Return character count within the hypertext accessible.
+ */
+ uint32_t CharacterCount() const;
+
+ /**
+ * Get/set caret offset, if no caret then -1.
+ */
+ virtual int32_t CaretOffset() const;
+ virtual void SetCaretOffset(int32_t aOffset) = 0;
+
+ /**
+ * Provide the line number for the caret.
+ * @return 1-based index for the line number with the caret
+ */
+ virtual int32_t CaretLineNumber();
+
+ /**
+ * Transform magic offset into text offset.
+ */
+ index_t ConvertMagicOffset(int32_t aOffset) const;
+
+ /**
+ * Return text between given offsets.
+ */
+ void TextSubstring(int32_t aStartOffset, int32_t aEndOffset,
+ nsAString& aText) const;
+
+ /**
+ * Get a character at the given offset (don't support magic offsets).
+ */
+ bool CharAt(int32_t aOffset, nsAString& aChar,
+ int32_t* aStartOffset = nullptr, int32_t* aEndOffset = nullptr);
+
+ char16_t CharAt(int32_t aOffset) {
+ nsAutoString charAtOffset;
+ CharAt(aOffset, charAtOffset);
+ return charAtOffset.CharAt(0);
+ }
+
+ /**
+ * Return a rect (in dev pixels) for character at given offset relative
+ * given coordinate system.
+ */
+ LayoutDeviceIntRect CharBounds(int32_t aOffset, uint32_t aCoordType);
+
+ /**
+ * Return a rect (in dev pixels) of the given text range relative given
+ * coordinate system.
+ */
+ LayoutDeviceIntRect TextBounds(
+ int32_t aStartOffset, int32_t aEndOffset,
+ uint32_t aCoordType =
+ nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE);
+
+ /**
+ * Return the offset of the char that contains the given coordinates.
+ */
+ virtual int32_t OffsetAtPoint(int32_t aX, int32_t aY, uint32_t aCoordType);
+
+ /**
+ * Get a TextLeafPoint for a given offset in this HyperTextAccessible.
+ * If the offset points to an embedded object and aDescendToEnd is true,
+ * the point right at the end of this subtree will be returned instead of the
+ * start.
+ */
+ TextLeafPoint ToTextLeafPoint(int32_t aOffset, bool aDescendToEnd = false);
+
+ /**
+ * Return text before/at/after the given offset corresponding to
+ * the boundary type.
+ */
+ void TextBeforeOffset(int32_t aOffset, AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset, int32_t* aEndOffset,
+ nsAString& aText);
+ void TextAtOffset(int32_t aOffset, AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset, int32_t* aEndOffset,
+ nsAString& aText);
+ void TextAfterOffset(int32_t aOffset, AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset, int32_t* aEndOffset,
+ nsAString& aText);
+
+ /**
+ * Return true if the given offset/range is valid.
+ */
+ bool IsValidOffset(int32_t aOffset);
+ bool IsValidRange(int32_t aStartOffset, int32_t aEndOffset);
+
+ /**
+ * Return link count within this hypertext accessible.
+ */
+ uint32_t LinkCount();
+
+ /**
+ * Return link accessible at the given index.
+ */
+ Accessible* LinkAt(uint32_t aIndex);
+
+ /**
+ * Return index for the given link accessible.
+ */
+ int32_t LinkIndexOf(Accessible* aLink);
+
+ /**
+ * Return link accessible at the given text offset.
+ */
+ int32_t LinkIndexAtOffset(uint32_t aOffset) {
+ Accessible* child = GetChildAtOffset(aOffset);
+ return child ? LinkIndexOf(child) : -1;
+ }
+
+ /**
+ * Transform the given a11y point into an offset relative to this hypertext.
+ * Returns {success, offset}, where success is true if successful.
+ * If unsuccessful, the returned offset will be CharacterCount() if
+ * aIsEndOffset is true, 0 otherwise. This means most callers can ignore the
+ * success return value.
+ */
+ std::pair<bool, int32_t> TransformOffset(Accessible* aDescendant,
+ int32_t aOffset,
+ bool aIsEndOffset) const;
+
+ /**
+ * Return text attributes for the given text range.
+ */
+ already_AddRefed<AccAttributes> TextAttributes(bool aIncludeDefAttrs,
+ int32_t aOffset,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset);
+
+ /**
+ * Return text attributes applied to the accessible.
+ */
+ virtual already_AddRefed<AccAttributes> DefaultTextAttributes() = 0;
+
+ /**
+ * Return an array of disjoint ranges for selected text within the text
+ * control or the document this accessible belongs to.
+ */
+ virtual void SelectionRanges(nsTArray<TextRange>* aRanges) const = 0;
+
+ /**
+ * Return text selection ranges cropped to this Accessible (rather than for
+ * the entire text control or document). This also excludes collapsed ranges.
+ */
+ void CroppedSelectionRanges(nsTArray<TextRange>& aRanges) const;
+
+ /**
+ * Return selected regions count within the accessible.
+ */
+ virtual int32_t SelectionCount();
+
+ /**
+ * Return the start and end offset of the specified selection.
+ */
+ virtual bool SelectionBoundsAt(int32_t aSelectionNum, int32_t* aStartOffset,
+ int32_t* aEndOffset);
+
+ /**
+ * Changes the start and end offset of the specified selection.
+ * @return true if succeeded
+ */
+ // TODO: annotate this with `MOZ_CAN_RUN_SCRIPT` instead.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual bool SetSelectionBoundsAt(
+ int32_t aSelectionNum, int32_t aStartOffset, int32_t aEndOffset);
+
+ /**
+ * Adds a selection bounded by the specified offsets.
+ * @return true if succeeded
+ */
+ bool AddToSelection(int32_t aStartOffset, int32_t aEndOffset) {
+ return SetSelectionBoundsAt(-1, aStartOffset, aEndOffset);
+ }
+
+ /**
+ * Removes the specified selection.
+ * @return true if succeeded
+ */
+ // TODO: annotate this with `MOZ_CAN_RUN_SCRIPT` instead.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual bool RemoveFromSelection(
+ int32_t aSelectionNum) = 0;
+
+ /**
+ * Scroll the given text range into view.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void ScrollSubstringTo(int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aScrollType);
+
+ /**
+ * Scroll the given text range to the given point.
+ */
+ virtual void ScrollSubstringToPoint(int32_t aStartOffset, int32_t aEndOffset,
+ uint32_t aCoordinateType, int32_t aX,
+ int32_t aY) = 0;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // EditableTextAccessible
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void ReplaceText(
+ const nsAString& aText) = 0;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void InsertText(const nsAString& aText,
+ int32_t aPosition) = 0;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void CopyText(int32_t aStartPos,
+ int32_t aEndPos) = 0;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void CutText(int32_t aStartPos,
+ int32_t aEndPos) = 0;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void DeleteText(int32_t aStartPos,
+ int32_t aEndPos) = 0;
+ MOZ_CAN_RUN_SCRIPT virtual void PasteText(int32_t aPosition) = 0;
+
+ protected:
+ virtual const Accessible* Acc() const = 0;
+ Accessible* Acc() {
+ const Accessible* acc =
+ const_cast<const HyperTextAccessibleBase*>(this)->Acc();
+ return const_cast<Accessible*>(acc);
+ }
+
+ /**
+ * Get the cached map of child indexes to HyperText offsets.
+ * This is an array which contains the exclusive end offset for each child.
+ * That is, the start offset for child c is array index c - 1.
+ */
+ virtual nsTArray<int32_t>& GetCachedHyperTextOffsets() = 0;
+
+ private:
+ /**
+ * Helper method for TextBefore/At/AfterOffset.
+ * If BOUNDARY_LINE_END was requested and the origin is itself a line end
+ * boundary, we must use the line which ends at the origin. We must do
+ * similarly for BOUNDARY_WORD_END. This method adjusts the origin
+ * accordingly.
+ */
+ void AdjustOriginIfEndBoundary(TextLeafPoint& aOrigin,
+ AccessibleTextBoundary aBoundaryType,
+ bool aAtOffset = false) const;
+};
+
+} // namespace mozilla::a11y
+
+#endif
diff --git a/accessible/basetypes/TableAccessible.h b/accessible/basetypes/TableAccessible.h
new file mode 100644
index 0000000000..fe5b499f75
--- /dev/null
+++ b/accessible/basetypes/TableAccessible.h
@@ -0,0 +1,172 @@
+/* -*- 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 TABLE_ACCESSIBLE_H
+#define TABLE_ACCESSIBLE_H
+
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace a11y {
+
+class Accessible;
+
+/**
+ * Accessible table interface.
+ */
+class TableAccessible {
+ public:
+ /**
+ * Return the caption accessible if any for this table.
+ */
+ virtual Accessible* Caption() const { return nullptr; }
+
+ /**
+ * Get the summary for this table.
+ */
+ virtual void Summary(nsString& aSummary) { aSummary.Truncate(); }
+
+ /**
+ * Return the number of columns in the table.
+ */
+ virtual uint32_t ColCount() const { return 0; }
+
+ /**
+ * Return the number of rows in the table.
+ */
+ virtual uint32_t RowCount() { return 0; }
+
+ /**
+ * Return the accessible for the cell at the given row and column indices.
+ */
+ virtual Accessible* CellAt(uint32_t aRowIdx, uint32_t aColIdx) {
+ return nullptr;
+ }
+
+ /**
+ * Return the index of the cell at the given row and column.
+ */
+ virtual int32_t CellIndexAt(uint32_t aRowIdx, uint32_t aColIdx) { return -1; }
+
+ /**
+ * Return the column index of the cell with the given index.
+ * This returns -1 if the column count is 0 or an invalid index is being
+ * passed in.
+ */
+ virtual int32_t ColIndexAt(uint32_t aCellIdx) { return -1; }
+
+ /**
+ * Return the row index of the cell with the given index.
+ * This returns -1 if the column count is 0 or an invalid index is being
+ * passed in.
+ */
+ virtual int32_t RowIndexAt(uint32_t aCellIdx) { return -1; }
+
+ /**
+ * Get the row and column indices for the cell at the given index.
+ * This returns -1 for both output parameters if the column count is 0 or an
+ * invalid index is being passed in.
+ */
+ virtual void RowAndColIndicesAt(uint32_t aCellIdx, int32_t* aRowIdx,
+ int32_t* aColIdx) {
+ *aRowIdx = -1;
+ *aColIdx = -1;
+ }
+
+ /**
+ * Return the number of columns occupied by the cell at the given row and
+ * column indices.
+ */
+ virtual uint32_t ColExtentAt(uint32_t aRowIdx, uint32_t aColIdx) { return 1; }
+
+ /**
+ * Return the number of rows occupied by the cell at the given row and column
+ * indices.
+ */
+ virtual uint32_t RowExtentAt(uint32_t aRowIdx, uint32_t aColIdx) { return 1; }
+
+ /**
+ * Get the description of the given column.
+ */
+ virtual void ColDescription(uint32_t aColIdx, nsString& aDescription) {
+ aDescription.Truncate();
+ }
+
+ /**
+ * Get the description for the given row.
+ */
+ virtual void RowDescription(uint32_t aRowIdx, nsString& aDescription) {
+ aDescription.Truncate();
+ }
+
+ /**
+ * Return true if the given column is selected.
+ */
+ virtual bool IsColSelected(uint32_t aColIdx) { return false; }
+
+ /**
+ * Return true if the given row is selected.
+ */
+ virtual bool IsRowSelected(uint32_t aRowIdx) { return false; }
+
+ /**
+ * Return true if the given cell is selected.
+ */
+ virtual bool IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) {
+ return false;
+ }
+
+ /**
+ * Return the number of selected cells.
+ */
+ virtual uint32_t SelectedCellCount() { return 0; }
+
+ /**
+ * Return the number of selected columns.
+ */
+ virtual uint32_t SelectedColCount() { return 0; }
+
+ /**
+ * Return the number of selected rows.
+ */
+ virtual uint32_t SelectedRowCount() { return 0; }
+
+ /**
+ * Get the set of selected cells.
+ */
+ virtual void SelectedCells(nsTArray<Accessible*>* aCells) {}
+
+ /**
+ * Get the set of selected cell indices.
+ */
+ virtual void SelectedCellIndices(nsTArray<uint32_t>* aCells) {}
+
+ /**
+ * Get the set of selected column indices.
+ */
+ virtual void SelectedColIndices(nsTArray<uint32_t>* aCols) {}
+
+ /**
+ * Get the set of selected row indices.
+ */
+ virtual void SelectedRowIndices(nsTArray<uint32_t>* aRows) {}
+
+ /**
+ * Return true if the table is probably for layout.
+ */
+ virtual bool IsProbablyLayoutTable() { return false; }
+
+ /**
+ * Convert the table to an Accessible*.
+ */
+ virtual Accessible* AsAccessible() = 0;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/basetypes/TableCellAccessible.h b/accessible/basetypes/TableCellAccessible.h
new file mode 100644
index 0000000000..3e92a7098b
--- /dev/null
+++ b/accessible/basetypes/TableCellAccessible.h
@@ -0,0 +1,68 @@
+/* -*- 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_TableCellAccessible_h__
+#define mozilla_a11y_TableCellAccessible_h__
+
+#include "nsTArray.h"
+#include <stdint.h>
+
+namespace mozilla {
+namespace a11y {
+
+class Accessible;
+class TableAccessible;
+
+/**
+ * Abstract interface implemented by table cell accessibles.
+ */
+class TableCellAccessible {
+ public:
+ /**
+ * Return the table this cell is in.
+ */
+ virtual TableAccessible* Table() const = 0;
+
+ /**
+ * Return the column of the table this cell is in.
+ */
+ virtual uint32_t ColIdx() const = 0;
+
+ /**
+ * Return the row of the table this cell is in.
+ */
+ virtual uint32_t RowIdx() const = 0;
+
+ /**
+ * Return the column extent of this cell.
+ */
+ virtual uint32_t ColExtent() const { return 1; }
+
+ /**
+ * Return the row extent of this cell.
+ */
+ virtual uint32_t RowExtent() const { return 1; }
+
+ /**
+ * Return the column header cells for this cell.
+ */
+ virtual void ColHeaderCells(nsTArray<Accessible*>* aCells) = 0;
+
+ /**
+ * Return the row header cells for this cell.
+ */
+ virtual void RowHeaderCells(nsTArray<Accessible*>* aCells) = 0;
+
+ /**
+ * Returns true if this cell is selected.
+ */
+ virtual bool Selected() = 0;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_TableCellAccessible_h__
diff --git a/accessible/basetypes/moz.build b/accessible/basetypes/moz.build
new file mode 100644
index 0000000000..c0e3fdf9ac
--- /dev/null
+++ b/accessible/basetypes/moz.build
@@ -0,0 +1,25 @@
+# -*- 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 += [
+ "Accessible.h",
+ "HyperTextAccessibleBase.h",
+ "TableAccessible.h",
+ "TableCellAccessible.h",
+]
+
+UNIFIED_SOURCES += [
+ "Accessible.cpp",
+ "HyperTextAccessibleBase.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/accessible/docs/Architecture.md b/accessible/docs/Architecture.md
new file mode 100644
index 0000000000..b74c007787
--- /dev/null
+++ b/accessible/docs/Architecture.md
@@ -0,0 +1,65 @@
+# Architecture
+
+This document provides a high-level overview of how the accessibility code is structured. See [the Document Accessibility Lifecycle page](DocumentAccessibilityLifecycle.md#docaccessible-creation) for a lower level description of the code.
+
+## Process Model
+The accessibility component spans multiple processes. In the parent process, it creates an accessibility tree of the Firefox UI and responds to requests from screen readers. In content processes, the accessibility component creates accessibility trees from web content.
+
+To respond to screen reader requests quickly, the accessibility tree from each content process is cached in the parent process.
+
+## Accessibility Trees
+Accessibility trees can carry different kinds of information: informally, there are "local trees" that represent a document in the current process and "remote trees" that mirror a local tree created in a separate process.
+
+A local tree can only contain nodes in the current process, i.e. you can visit any node in the tab document and its in-process iframes. However, out-of-process iframes appear as a separate local tree in a different process.
+
+A remote tree, on the other hand, unifies these trees: you can visit any node in the tab document and both its in-process and out-of-process iframes.
+
+This means there are multiple accessibility trees for a single tab: one local tree in the content process, one local tree for each out-of-process iframe, and one remote tree in the parent process that mirrors these local trees. The Firefox UI is represented by a single local tree in the parent process.
+
+### Tree Nodes
+An accessibility tree is composed of nodes represented by the `Accessible` class and its subtypes. Below is an example local accessibility tree from [example.com](https://example.com), as printed by `a11y::logging::Tree` (unfortunately, without type information):
+
+<!-- This isn't very accessible, at least in VoiceOver on Safari: VoiceOver only navigates between each word and I'm not sure if it's even possible to skip the whole block. Ideally, we can improve it. -->
+```
+A11Y TREE: Initial subtree; 44:14.388
+ {
+ : 0x107077a00; role: document, name: 'Example Domain', idx: 0, node: 0x105f84800, #document
+ : 0x105fb8b30; role: heading, name: 'Example Domain', idx: 0, node: 0x107b048b0, h1
+ : 0x105fb8c90; role: text leaf, name: 'Example Domain', idx: 0, node: 0x107b05600, #text
+ : 0x105fb8d40; role: paragraph, idx: 1, node: 0x107b04940, p
+ : 0x105fb8df0; role: text leaf, name: 'This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.', idx: 0, node: 0x107b05700, #text
+ : 0x105fb8ea0; role: paragraph, idx: 2, node: 0x107b049d0, p
+ : 0x107922030; role: link, name: 'More information...', idx: 0, node: 0x107b030e0, a [ href="https://www.iana.org/domains/example" ]
+ : 0x1079220e0; role: text leaf, name: 'More information...', idx: 0, node: 0x107b05800, #text
+ }
+```
+
+<!-- Accessible is not in a code block because VoiceOver (on Safari) will not read full paragraphs if they start with <code> for an unknown reason. -->
+Accessible has a direct subtype for different kinds of accessibility trees: `LocalAccessible` for nodes of local trees and `RemoteAccessibleBase` for nodes of remote trees. For example, `LocalAccessible` can be used in content processes for web content and in the parent process for the Firefox UI. The descendants of these two types diverge.
+
+<!-- LocalAccessible is intentionally not in a code block: see above. -->
+LocalAccessible's direct descendant is `AccessibleWrap`. By convention, a class that ends in `Wrap` is a platform-specific implementation so `AccessibleWrap` contains the platform-specific implementations of `Accessible` and `LocalAccessible`. `AccessibleWrap's` direct and indirect subtypes are representations of HTML and XUL nodes such as `HTMLButtonAccessible` and `HTMLListBulletAccessible`. The Document and the root node of the accessibility tree are also represented by the `Accessible` class: `DocAccessible` and `DocAccessibleWrap` as well as `RootAccessible` and `RootAccessibleWrap` extend `AccessibleWrap`.
+
+<!-- RemoteAccessibleBase is intentionally not in a code block: see above. -->
+RemoteAccessibleBase doesn’t have such an extensive type hierarchy. Its primary descendant is `DocAccessibleParent` which is the Document node of a remote tree located in the parent process: its local tree counterpart in a content process is `DocAccessible`.
+
+Below is a graph that displays the same relationships described above. In the graph, solid lines represent direct descendants while dotted lines represent indirect descendants:
+```{mermaid}
+flowchart TD
+accTitle: Graph of the class hierarchy described above
+Accessible --> LocalAccessible[LocalAccessible: local tree] & RemoteAccessibleBase[RemoteAccessibleBase: remote tree]
+LocalAccessible --> AccessibleWrap[AccessibleWrap: platform-specific implementation]
+AccessibleWrap -.-> DocAccessible & HTMLButtonAccessible & HTMLListBulletAccessible
+DocAccessible --> DocAccessibleWrap --> RootAccessible --> RootAccessibleWrap
+RemoteAccessibleBase -.-> DocAccessibleParent
+```
+
+### Platform-Specific Behavior
+Accessibility trees differ by platform. The platform-independent tree, composed of types like `LocalAccessible` and `RemoteAccessibleBase`, is marshalled into a platform-specific tree that makes it easier to implement the platform's accessibility API. The platform tree is composed of the following node types:
+- Windows: [MsaaAccessible] and [ia2Accessible]
+- macOS: [mozAccessible]
+- Linux: [ATKObjects and MaiAtkObjects](https://searchfox.org/mozilla-central/rev/d7d2cc647772de15c4c5aa47f74d25d0e379e404/accessible/atk/nsMai.h#87)
+
+[MsaaAccessible]: https://searchfox.org/mozilla-central/rev/d7d2cc647772de15c4c5aa47f74d25d0e379e404/accessible/windows/msaa/MsaaAccessible.h
+[ia2Accessible]: https://searchfox.org/mozilla-central/rev/d7d2cc647772de15c4c5aa47f74d25d0e379e404/accessible/windows/ia2/ia2Accessible.h#21
+[mozAccessible]: https://searchfox.org/mozilla-central/rev/d7d2cc647772de15c4c5aa47f74d25d0e379e404/accessible/mac/mozAccessible.mm
diff --git a/accessible/docs/ColorsAndHighContrastMode.md b/accessible/docs/ColorsAndHighContrastMode.md
new file mode 100644
index 0000000000..30fd51d17c
--- /dev/null
+++ b/accessible/docs/ColorsAndHighContrastMode.md
@@ -0,0 +1,50 @@
+# Colors and High Contrast Mode
+Firefox offers several customisations to improve the accessibility of colors used to render web content and Firefox chrome. This document describes the customisation options available and their behaviour across platforms. It also describes how these options interact with one another. It is intended for developer reference :)
+
+## The Colors Dialog
+In `about:preferences > Language and Appearance`, you'll find a button labelled "Colors". This button launches the colors dialog, which contains all of our user-facing color customisation options, as well as stylistic customisations like link underlining. This dialog also contains the select that controls what we'll refer to as "Firefox High Contrast Mode", or FF HCM. FF HCM can be enabled "Always", "Never", or "Only with High Contrast Themes".
+> Note: FF HCM only affects web content, so changing the option in this select will only alter color usage for web pages. It will not change FF chrome. Current behaviour on chrome pages (ie. `about:` pages) is undefined.
+
+### User-customisable Colors
+Users can choose to override background color, foreground color, visited link color, and/or unvisited link color by selecting a new color from the color inputs in the dialog. Modifications to these colors are stored in their corresponding user preference:
+- `browser.background_color`
+- `browser.foreground_color`
+- `browser.visited_color`
+- `browser.anchor_color`
+
+### Color Usage and System Colors
+Before we render any Firefox/web content, we need to select a color palette to render that content _with_. We don't always use the colors a user has selected in the colors dialog. In fact, there are three different sets of colors we can use to style Firefox and/or web content:
+- Stand-in colors
+- System colors
+- Colors-dialog colors
+
+> Note: Web pages may supply their own style sheets, which override a user's chosen color palette. When FF HCM is set to "Always", or set to "With High Contrast Themes" and OS HCM is enabled, the chosen color palette is _forced_, meaning it cannot be overridden by web pages. FF HCM and OS HCM do not directly change the way a color palette is chosen, but they _do_ change how the color palette is used.
+
+We decide which set of colors to use in `PreferenceSheet::Load`. If `resistFingerprinting` is enabled, we use stand-in colors. These colors are pre-defined constants and are not dynamically fetched from the operating system. Check out `nsXPLookAndFeel::GetStandinForNativeColor` for more information, as well as the constants themselves.
+
+If we aren't using stand-in colors, we'll check `browser.display.use_system_colors`, which is set from the "Use system colors" checkbox in the colors dialog. If that pref is true, we'll use system colors to style web content and Firefox chrome.
+
+System colors are colors queried from the operating system. They help Firefox adapt to OS-level changes that aren't strictly HCM (ie. light/dark themeing). Because these colors are OS-dependent, a user operating Firefox on a Windows machine with system colors enabled will see Firefox differently than a user with system colors enabled on MacOS.
+
+ So, how do we _get_ system colors? Our style system has a set of pre-defined `ColorID`'s in `ServoStyleConsts.h`, which are mapped to platform-specific colors in `widget/[cocoa | android | windows | gtk]/LookAndFeel.cpp`. Depending on the `ColorID` queried, we may do a dynamic fetch or simply return a constant. On MacOS, for example, `ColorID::TextForeground` and `ColorID::TextBackground` are hard-coded to return black and white respectively. `ColorID::Highlight`, on the other hand, queries the OS for `NSColor.selectedTextBackgroundColor`, which is set based on the accent color a user has selected in System Preferences.
+ > Note: The colors we fetch here are theme-relative. If a user has set their OS to a dark theme, we'll fetch colors from that palette, and likewise for a light theme. Windows HCM, though not strictly a "theme", overrides the colors stored for Windows' light theme, leading to [some confusing code, like this](https://searchfox.org/mozilla-central/rev/b462b11e71b500e084f51e61fbd9e19ea0122c78/layout/style/PreferenceSheet.cpp#202-210).
+
+ Lastly, if we are _not_ using system colors AND we are _not_ styling Firefox chrome AND we are _not_ `resistFingerprinting`, we'll use colors-dialog colors to style web content.
+
+ By default, `browser.display.use_system_colors` is true on Windows and false elsewhere. This means users on Windows will _not_ see their selections in the colors dialog reflected automatically in Firefox. They'll need to uncheck "Use system colors" first.
+ > Note: This is intentional. When Windows HCM is enabled, the system colors Windows exposes are pulled from the chosen HCM theme. With "Use system colors" checked, a Windows HCM user will see their HCM theme choices reflected in Firefox content automatically. Windows HCM is the most robust HCM offered among the operating systems we support, and so we cater to it here :)
+
+Users on non-Windows platforms will see their selections in the colors dialog reflected automatically, but they will _not_ see OS changes until they check "Use system colors".
+
+For a simplified flow chart of this decision tree, check out our [HCM Settings page](/https://firefox-source-docs.mozilla.org/accessible/HCMSettings.html)
+
+## High Contrast Mode
+
+### Operating System High Contrast Mode (OS HCM)
+
+Operating System HCM (or OS HCM) describes a high contrast customisation that is enabled outside of Firefox, in the settings of a user's operating system. Each of our major desktop operating systems has an OS HCM variant:
+- Windows: Settings > Accessibility > Increase Contrast > (select theme) > Apply
+- MacOS: System Preferences > Accessibility > Display > Increase Contrast
+- Linux: Settings > Themes > High Contrast
+
+The presence of an OS HCM is stored in `IntID::UseAccessibilityTheme`.
diff --git a/accessible/docs/DocumentAccessibilityLifecycle.md b/accessible/docs/DocumentAccessibilityLifecycle.md
new file mode 100644
index 0000000000..6c576ec257
--- /dev/null
+++ b/accessible/docs/DocumentAccessibilityLifecycle.md
@@ -0,0 +1,104 @@
+# Document Accessibility Lifecycle
+
+## 1. DocAccessible creation
+When a DocAccessible is created, it is initially empty.
+A DocAccessible can be created in one of several ways:
+
+### Scenario 1: Accessibility service is already started, layout fires an a11y notification for a new document
+1. The layout [PresShell gets initialized (PresShell::Initialize)](https://searchfox.org/mozilla-central/rev/4e87b5392eafe1f1d49017e76f7317b06ec0b1d8/layout/base/PresShell.cpp#1820).
+2. As part of that, layout [inserts content from the root content object down](https://searchfox.org/mozilla-central/rev/4e87b5392eafe1f1d49017e76f7317b06ec0b1d8/layout/base/PresShell.cpp#1885).
+3. This [fires an accessibility insertion notification (nsAccessibilityService::ContentRangeInserted)](https://searchfox.org/mozilla-central/rev/4e87b5392eafe1f1d49017e76f7317b06ec0b1d8/layout/base/nsCSSFrameConstructor.cpp#6863).
+4. That [gets the DocAccessible (DocManager::GetDocAccessible)](https://searchfox.org/mozilla-central/rev/4e87b5392eafe1f1d49017e76f7317b06ec0b1d8/accessible/base/nsAccessibilityService.cpp#463).
+5. Because it doesn't exist yet, [the DocAccessible gets created (DocManager::CreateDocOrRootAccessible)](https://searchfox.org/mozilla-central/rev/4e87b5392eafe1f1d49017e76f7317b06ec0b1d8/accessible/base/DocManager.cpp#62).
+
+### Scenario 2: Accessibility service is already started, DOM loading completes for a new document
+For top level content documents, if the accessibility service is started, layout should fire an a11y notification, resulting in scenario 1 above.
+For child documents (e.g. in-process iframes), this might not happen because the container Accessible for the child document might not be created yet.
+In that case, we can't create a DocAccessible when the layout notification is fired.
+If the container Accessible is created when DOM loading completes for the child document, this scenario can occur.
+
+1. [a11y::DocManager gets notified that the document has stopped loading (DocManager::OnStateChange)](https://searchfox.org/mozilla-central/rev/4e87b5392eafe1f1d49017e76f7317b06ec0b1d8/accessible/base/DocManager.cpp#238).
+2. It tries to get an existing DocAccessible, but it doesn't exist yet, so it [creates a DocAccessible (DocManager::CreateDocOrRootAccessible)](https://searchfox.org/mozilla-central/rev/4e87b5392eafe1f1d49017e76f7317b06ec0b1d8/accessible/base/DocManager.cpp#395).
+
+### Scenario 3: Accessibility service is already started, child document is reached while building accessibility tree for parent document
+Child document here refers to a child document in the same process; i.e. an in-process iframe or a parent process document such as an about: page.
+Note that scenario 1 or 2 could happen for a child document as well.
+
+1. While building the accessibility tree for the parent document, an OuterDocAccessible is created (e.g. for a XUL browser or iframe).
+2. The [OuterDocAccessible constructor gets the DocAccessible for the child document (DocManager::GetDocAccessible)](https://searchfox.org/mozilla-central/rev/36f79bed679ad7ec46f7cd05868a8f6dc823e1be/accessible/generic/OuterDocAccessible.cpp#56).
+3. Because it doesn't exist yet, [the DocAccessible gets created (DocManager::CreateDocOrRootAccessible)](https://searchfox.org/mozilla-central/rev/4e87b5392eafe1f1d49017e76f7317b06ec0b1d8/accessible/base/DocManager.cpp#62).
+
+### Scenario 4: Accessibility service starts after top level document is loaded
+1. When the accessibility service starts, it [initializes the ApplicationAccessible (ApplicationAccessible::Init)](https://searchfox.org/mozilla-central/rev/36f79bed679ad7ec46f7cd05868a8f6dc823e1be/accessible/base/nsAccessibilityService.cpp#1219).
+2. As part of that, all documents are iterated.
+3. For each top level document, [the DocAccessible is retrieved (DocManager::GetDocAccessible)](https://searchfox.org/mozilla-central/rev/36f79bed679ad7ec46f7cd05868a8f6dc823e1be/accessible/generic/ApplicationAccessible.cpp#130) and [thus created (DocManager::CreateDocOrRootAccessible)](https://searchfox.org/mozilla-central/rev/4e87b5392eafe1f1d49017e76f7317b06ec0b1d8/accessible/base/DocManager.cpp#62).
+
+Scenario 3 would then apply for any child documents encountered while building the accessibility trees for these top level DocAccessibles.
+
+### Scenario 5: Document gets focus before layout begins
+It is possible for a document to get focus before layout has begun and before DOM loading is complete.
+In that case, there will be a PresShell, but it will have no root frame.
+Despite this, it is necessary to create the document because otherwise, a11y focus would go nowhere while the document had DOM focus.
+
+1. [a11y::FocusManager gets notified of a DOM focus change (FocusManager::NotifyOfDOMFocus)](https://searchfox.org/mozilla-central/rev/04dbb1a865894aec20eb02585aa75acccc0b72d5/accessible/base/FocusManager.cpp#126).
+2. It gets the DocAccessible for the child document (DocManager::GetDocAccessible).
+3. Because it doesn't exist yet, [the DocAccessible gets created (DocManager::CreateDocOrRootAccessible)](https://searchfox.org/mozilla-central/rev/4e87b5392eafe1f1d49017e76f7317b06ec0b1d8/accessible/base/DocManager.cpp#62).
+
+## 2. Initial tree creation
+1. When a DocAccessible is created, it [creates a refresh observer (NotificationController)](https://searchfox.org/mozilla-central/rev/36f79bed679ad7ec46f7cd05868a8f6dc823e1be/accessible/generic/DocAccessible.cpp#368) which performs various processing asynchronously.
+2. When the NotificationController is created, it [schedules processing for the next possible refresh tick](https://searchfox.org/mozilla-central/rev/36f79bed679ad7ec46f7cd05868a8f6dc823e1be/accessible/base/NotificationController.cpp#39).
+3. Once a refresh tick occurs, [while there is not an interruptible reflow in progress](https://searchfox.org/mozilla-central/rev/36f79bed679ad7ec46f7cd05868a8f6dc823e1be/accessible/base/NotificationController.cpp#615) and there is an initialized PresShell, the DocAccessible's [initial update is triggered (DocAccessible::DoInitialUpdate)](https://searchfox.org/mozilla-central/source/accessible/base/NotificationController.cpp#649).
+4. For a top level document, the [DocAccessibleChild IPC actor is created](https://searchfox.org/mozilla-central/rev/36f79bed679ad7ec46f7cd05868a8f6dc823e1be/accessible/generic/DocAccessible.cpp#1752). See the section on IPC actor creation below.
+5. The [DOM tree is walked and the accessibility tree is built for the document down (DocAccessible::CacheChildrenInSubtree)](https://searchfox.org/mozilla-central/rev/36f79bed679ad7ec46f7cd05868a8f6dc823e1be/accessible/generic/DocAccessible.cpp#1789).
+
+Note that the document might still have no layout frame if the PresShell still has no frame; see scenario 5 in DocAccessible creation above.
+Nevertheless, DoInitialUpdate must be called because otherwise, we wouldn't create the IPC actor, which would in turn mean remote documents in this state couldn't get a11y focus.
+
+## 3. Child document binding
+Child document here refers to a child document in the same process; e.g. an in-process iframe or a parent process document such as an about: page.
+
+Child documents need to be a child of their OuterDocAccessible; e.g. the iframe.
+However, the child document might be ready before the parent document is.
+To deal with this:
+
+1. When a DocAccessible is created for a child document (DocManager::CreateDocOrRootAccessible), it is [scheduled for binding to its parent (DocAccessible::BindChildDocument)](https://searchfox.org/mozilla-central/rev/1758450798ae14492ba28b695f48143840ad6c5b/accessible/base/DocManager.cpp#505).
+2. NotificationController [does not handle any updates for child documents until they are bound to their parent](https://searchfox.org/mozilla-central/rev/1758450798ae14492ba28b695f48143840ad6c5b/accessible/base/NotificationController.cpp#638).
+3. After the initial tree creation for the parent document, NotificationController [binds the document scheduled in 1)](https://searchfox.org/mozilla-central/source/accessible/base/NotificationController.cpp#783).
+
+## 4. IPC actor (DocAccessibleChild/Parent) creation
+
+### Scenario 1: Top level document
+1. As the first part of the DocAccessible's initial update (DocAccessible::DoInitialUpdate), if the document is a top level document, it [creates a DocAccessibleChild](https://searchfox.org/mozilla-central/rev/1758450798ae14492ba28b695f48143840ad6c5b/accessible/generic/DocAccessible.cpp#1757).
+ - There is also a [code path to handle the case where the DocAccessibleChild has already been created](https://searchfox.org/mozilla-central/source/accessible/generic/DocAccessible.cpp#1755). However, it doesn't seem like this should happen and code coverage information suggests that it doesn't.
+2. It then [sends a message to the parent process to construct the DocAccessibleParent (BrowserChild::SendPDocAccessibleConstructor)](https://searchfox.org/mozilla-central/source/accessible/generic/DocAccessible.cpp#1771).
+
+### Scenario 2: Child document
+The [DocAccessibleChild for a child document is created](https://searchfox.org/mozilla-central/source/accessible/base/NotificationController.cpp#909) when the child document is bound to its parent by NotificationController.
+There is also a [code path to handle the case where the DocAccessibleChild has already been created](https://searchfox.org/mozilla-central/source/accessible/base/NotificationController.cpp#905).
+However, it doesn't seem like this should happen and code coverage information suggests that it doesn't.
+
+## 5. Document load events
+
+### Scenario 1: DocAccessible is already created, DOM loading completes
+1. [a11y::DocManager gets notified that the document has stopped loading (DocManager::OnStateChange)](https://searchfox.org/mozilla-central/rev/4e87b5392eafe1f1d49017e76f7317b06ec0b1d8/accessible/base/DocManager.cpp#238).
+2. It [notifies the DocAccessible (DocAccessible::NotifyOfLoad](https://searchfox.org/mozilla-central/source/accessible/base/DocManager.cpp#399), passing EVENT_DOCUMENT_LOAD_COMPLETE.
+3. That [sets the eDOMLoaded LoadState](https://searchfox.org/mozilla-central/rev/4e87b5392eafe1f1d49017e76f7317b06ec0b1d8/accessible/generic/DocAccessible-inl.h#93) and mLoadEventType on the DocAccessible.
+4. Something schedules NotificationController processing. This could be the initial update, an insertion, etc.
+5. Because the DocAccessible has been marked as loaded, the initial tree has been built and all child documents are loded, [NotificationController calls DocAccessible::ProcessLoad](https://searchfox.org/mozilla-central/source/accessible/base/NotificationController.cpp#815).
+6. ProcessLoad [fires the EVENT_DOCUMENT_LOAD_COMPLETE event](https://searchfox.org/mozilla-central/source/accessible/generic/DocAccessible.cpp#1841) as set in 3).
+
+### Scenario 2: DocAccessible is created some time after DOM loading completed
+This can happen if the accessibility service is started late.
+It can also happen if a DocAccessible couldn't be created earlier because the PresShell or containre Accessible wasn't created yet.
+
+1. The [DocAccessible is initialized (DocAccessible::Init)](https://searchfox.org/mozilla-central/rev/8db61933e64b13c4a0ae456bcaccbd86a519ccc5/accessible/generic/DocAccessible.cpp#359).
+2. It [detects that DOM loading is already complete](https://searchfox.org/mozilla-central/rev/8db61933e64b13c4a0ae456bcaccbd86a519ccc5/accessible/generic/DocAccessible.cpp#379).
+3. In response, it [sets the eDOMLoaded state and mLoadEventType on the DocAccessible](https://searchfox.org/mozilla-central/rev/8db61933e64b13c4a0ae456bcaccbd86a519ccc5/accessible/generic/DocAccessible.cpp#381).
+4. Something schedules NotificationController processing. This could be the initial update, an insertion, etc.
+5. Because the DocAccessible has been marked as loaded, the initial tree has been built and all child documents are loded, [NotificationController calls DocAccessible::ProcessLoad](https://searchfox.org/mozilla-central/source/accessible/base/NotificationController.cpp#815).
+6. ProcessLoad [fires the EVENT_DOCUMENT_LOAD_COMPLETE event](https://searchfox.org/mozilla-central/source/accessible/generic/DocAccessible.cpp#1841) as set in 3).
+
+### Suppression of document load events for parent process iframes
+Note that for documents loaded directly in the parent process, ProcessLoad won't fire a load event for a child document whose parent is still loading.
+This is old behavior which does not work in the content process and will probably be removed in future.
+See [bug 1700362](https://bugzilla.mozilla.org/show_bug.cgi?id=1700362).
diff --git a/accessible/docs/GeckoViewThreadTopography.md b/accessible/docs/GeckoViewThreadTopography.md
new file mode 100644
index 0000000000..43265a3b16
--- /dev/null
+++ b/accessible/docs/GeckoViewThreadTopography.md
@@ -0,0 +1,51 @@
+# GeckoView Thread Topography
+Unlike conventional Gecko, GeckoView utilizes two main threads in the parent process. A thread called the "UI" thread that runs the native Java event loop, and a "Gecko" thread that spawns an instance of Gecko. Note, this "Gecko" thread is considered the main thread in conventional Gecko and throughout the source is referred to as such. For example `NS_IsMainThread()` returns `true` when called on the Gecko thread.
+
+Android's accessibility API's entry point is the UI thread. The most common methods for an accessibility service to query and interact with Gecko have a return value and thus must be performed synchronously. For example [performAction()](https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeProvider#performAction(int,%20int,%20android.os.Bundle)) and [createAccessibilityNodeInfo()](https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeProvider#createAccessibilityNodeInfo(int)).
+
+## Thread Safety Concerns
+As a rule, every data structure and method in Gecko should be considered unsafe to access from GeckoView's UI thread. For example the default reference counting implementation is not thread safe and should not be used outside the Gecko thread. Cross-process messaging must be done from the Gecko thread as well.
+
+Since the accessibility consumer is in the UI thread we need to either call into the Gecko thread while blocking the UI thread, or make a limited subset of the accessibility API thread safe. We employ both methods for different types of content.
+
+## In-Process Content
+Content that is rendered in the top-level process is rare and mostly consists of "about" pages that give insight to browser internals, like `about:support`.
+
+Since our Gecko accessibility API queries DOM and Layout, and includes many complex, thread-unsafe data structures, we call into the Gecko thread to retrieve what is needed and block the UI until we get a response from the Gecko thread. For example, to retrieve a "class name" enum that is calculated for a certain accessible the method will look something like this:
+```cpp
+int SessionAccessibility::GetNodeClassName(int32_t aID) {
+ int32_t classNameEnum = java::SessionAccessibility::CLASSNAME_VIEW;
+ RefPtr<SessionAccessibility> self(this);
+ nsAppShell::SyncRunEvent([this, self, aID, &classNameEnum] {
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ classNameEnum = AccessibleWrap::AndroidClass(acc);
+ }
+ });
+
+ return classNameEnum;
+}
+```
+
+## Out-of-Process Content
+Most web content will be rendered in a child process. Historically, we would cache the accessible tree hierarchy and object roles in the parent process and use synchronous IPC to query individual objects for more properties. We are transitioning to a fully cached approach where all fields are either stored or calculated in remote proxy objects in the parent process.
+
+The "cache" in our definition is any data member associated with the remote `Accessible` or its subclasses, such as `mParent`, `mChildren` and `mCachedFields` in `RemoteAccessibleBase`, or `mAccessibles` in `DocAccessibleParent`. In addition to all those members the cache includes the accessible/id table in `SessionAccessibility`.
+
+The cache is initialized and modified exclusively by messages from the child process to the parent process. Since the cache is limited to our module and is relatively well scoped, it is possible to synchronize access to it and allow it to be consumed by the UI thread.
+
+### Global Accessibility Thread Monitor
+We have global thread monitor that can be acquired by calling `nsAccessibilityService::GetAndroidMonitor()`. For example, if we were to retrieve the class name enum we would use an RAII wrapped monitor to exclusively access the cache:
+```cpp
+int SessionAccessibility::GetNodeClassName(int32_t aID) {
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+ if (Accessible* acc = GetAccessibleByID(aID)) {
+ return AccessibleWrap::AndroidClass(acc);
+ }
+}
+```
+
+#### Gecko Thread Access
+As a rule, all incoming IPC messages (ie. all `DocAccessibleParent::Recv` methods) should hold the global monitor. This is because any `Recv` method may indirectly modify the cache, for example a certain event might invalidate or update a cached property. There are currently no deferred tasks that happen as a result of a `Recv` method, so holding the monitor at the start of these methods will assure that the cache is being read or written in a synchronized fashion.
+
+#### UI Thread Access
+All methods that are called in via JNI should be assumed are in the UI thread and should acquire the monitor, unless the are annotated with `@WrapForJNI(dispatchTo = "gecko")`. This assures us that they are synchronized with any potential modifications to the cache happening in the Gecko thread.
diff --git a/accessible/docs/HCMMediaQueries.md b/accessible/docs/HCMMediaQueries.md
new file mode 100644
index 0000000000..c4b57de281
--- /dev/null
+++ b/accessible/docs/HCMMediaQueries.md
@@ -0,0 +1,306 @@
+# High Contrast Mode (HCM) Media Queries
+
+Firefox supports both the `prefers-contrast` and `forced-colors` media queries. These queries detect a user's display settings (in the browser and/or in their OS), and allow authors to adjust content styling accordingly. These queries are not interchangeable, and we should be intentional about using one or the other (or both!) when designing for accessibility.
+
+## What activates these queries?
+
+### Forced Colors
+The `forced-colors` media query has two values: `active` and `none`. The resolved value of this media query is determined by the browser based on user settings (OS and Firefox).
+* The `none` state indicates documents can use any colors to render content.
+* The `active` state indicates a user has OS or browser settings that limit the color palette available to the User Agent.
+
+In Firefox's default configuration, the `active` state is triggered when a user has enabled Windows High Contrast Mode or [Firefox High Contrast Mode](https://firefox-source-docs.mozilla.org/accessible/ColorsAndHighContrastMode.html#the-colors-dialog) (FF HCM). Both of these forced-colors modes offer users a limited amount of color customisation. In Windows, users can override seven colors, and with Firefox High Contrast Mode users can override four. MacOS's "Increase Contrast" setting does **not** trigger `forced-colors: active` because it does not limit the color palette available to UA's.
+> **NOTE:** MacOS's "Increase Contrast" setting **will** trigger `forced-colors: active` (in non-chrome web content) if it is enabled in combination with FF HCM's "Only with High Contrast Themes" option. This is also true on Linux. Because [FF HCM defaults to "Never" on non-Windows platforms](https://searchfox.org/mozilla-central/rev/896042a1a71066254ceb5291f016ca3dbca21cb7/modules/libpref/init/StaticPrefList.yaml#1195-1199), enabling "Increase Contrast" or Linux High Contrast Themes alone will not trigger `forced-colors: active`. Similarly, if a user disables FF HCM while Win HCM is enabled, `forced-colors` (in content) will evaluate to `none`.
+
+#### CSS System Colors
+
+Colors that are overridden by the forced-colors modes list above are inherited into the browser's [CSS System Colors](https://www.w3.org/TR/css-color-4/#css-system-colors). Designers must use these colors exclusively in their designs when designing for forced-colors modes. CSS System Colors are intended to be used in pairs: for example the `ButtonFace` and `ButtonText` colors should be used together to create styling for interactive controls. The full pairings are listed in the [system color pairings](https://www.w3.org/TR/css-color-4/#system-color-pairings) section of the CSS spec. Mozilla has additional design guidance for using CSS System Colors in our [HCM Design Guide](https://wiki.mozilla.org/Accessibility/Design_Guide). You can also check out our other docs to [learn more about color overriding](https://firefox-source-docs.mozilla.org/accessible/ColorsAndHighContrastMode.html).
+
+### Prefers Contrast
+The `prefers-contrast` media query has four values: `more`, `less`, `custom`, and `no-preference`.
+
+* The `more` state is triggered when a user has macOS's "Increase Contrast" setting enabled, or when a forced-colors mode is active **and** the ratio between a user's [chosen foreground and background](https://searchfox.org/mozilla-central/rev/655f49c541108e3d0a232aa7173fbcb9af88d80b/layout/style/PreferenceSheet.cpp#123-126) is higher than 7:1. This check is done [in our style system](https://searchfox.org/mozilla-central/rev/655f49c541108e3d0a232aa7173fbcb9af88d80b/layout/style/nsMediaFeatures.cpp#287-308).
+* The `less` state is triggered when a forced-colors mode is active and the foreground/background ratio is less than 4.5.
+* When a forced-colors mode is enabled with a ratio between these two values, we return `custom`.
+* Otherwise, if no forced-colors mode is active, and macOS's "Increase Contrast" is off, the media query resolves to `no-preference`.
+
+## So what do I use?
+
+Generally, this boils down to which operating systems you want your styling to apply to. In the past, we've often written code to target _all_ platforms within `prefers-contrast` blocks because the kinds of changes we make to target forced-colors mode also benefit "Increase Contrast" users on macOS; they allow system colors to propagate to the UA. The palette that macOS supplies when "Increase Contrast" is enabled is _also_ inherited to CSS System Colors, and may offer improvements over the standard palette for a design. It is still possible that changes made for forced-colors modes benefit "Increase Contrast" users, however we strongly encourage developers to consider whether diversifying our designs for these modes provides better UX.
+
+On macOS, we aren't limited to the OS palette. Instead of modifying a design to use fewer colors, we can simply increase the contrast ratio of the existing colors to 7:1 to satisfy [the enhanced contrast criterion of Web Content Accessibility Guidelines (WCAG)](https://www.w3.org/WAI/WCAG22/Understanding/contrast-enhanced.html) for better accessibility. Consider the following designs.
+
+Standard UX on macOS:
+
+![Firefox view page. The navigation tabs are rendered to match the custom browser theme -- yellow on dark grey, with the active tab rendered light Firefox blue on blue-grey. Page content is rendered yellow on dark grey. Interactive components are a slightly lighter grey, difficult to distinguish against the page background.](fxview.jpg)
+
+UX with increased contrast of existing colors on macOS:
+
+![Firefox view page. The navigation tabs are rendered yellow on darker grey, with the active tab rendered white on darker blue. Page content is rendered yellow on dark grey with white borders around interactive components.](fxview_ic.jpg)
+
+Changes present in this example belong in a `prefers-contrast: more` block. This is **assuming these changes are appropriately overridden in the cascade for `forced-colors: active` users**. If they are not overridden, an additional query like `@media (-moz-platform: macos)` or `.. and (not (forced-colors))` should be combined with `prefers-contrast: more` to ensure these changes only apply to macOS.
+For users who prefer less contrast, we can supply colors that lower the contrast ratios in the existing design and encase those changes in a `prefers-contrast: less` block.
+
+UX with limited color palette in FF HCM on macOS:
+
+![Firefox view page. The navigation tabs are rendered white on dark grey, with the active tab rendered black on white. Page content is rendered white on black with borders around interactive components.](fx_view_fc.jpg)
+
+In forced-colors modes, we require that designs reduce their palettes for compatibility. To accomplish this, we use CSS System Colors instead of the original design colors. This allows the overridden palette to "show through", since colors from the various forced-colors modes are inherited into CSS System Colors. It is possible that using CSS System Colors also increases contrast for users on macOS, but this more drastically changes the design at hand, and can detract from the overall user experience for users who are simply looking for more contrast. Designers should consider the additional guidelines on border use, removing gradients, etc. in our [HCM Design Guide](https://wiki.mozilla.org/Accessibility/Design_Guide).
+
+> **NOTE:** Firefox's chrome style sheets are subject to different rules than web-author style sheets.
+
+Firefox's chrome style sheets (including those in our `about:` pages) are not prevented from using colors that aren't system colors, even if a forced colors mode is enabled. In webpages, however, Firefox forces CSS System Colors regardless of the author's original specification. This is a safety mechanism to ensure pages render appropriately even if no `@media (prefers-contrast)` or `@media (forced-colors)` styling was specified. To avoid this overriding, web authors can use the [forced-color-adjust: none;](https://developer.mozilla.org/en-US/docs/Web/CSS/forced-color-adjust) CSS property.
+
+## Writing Maintainable Frontend Code
+
+Where possible, we prefer color overriding for `prefers-contrast` and `forced-colors` take place in one block at the `root` level rather than in many blocks on an element-by-element basis. We encourage developers to use [design systems tokens](https://searchfox.org/mozilla-central/rev/c130c69b7b863d5e28ab9524b65c27c7a9507c48/toolkit/themes/shared/design-system/tokens-shared.css) with semantic naming when overriding. Consider the following.
+
+```css
+:root {
+ --page-background: #cccccc;
+ --page-text-color: #000000;
+
+ @media (forced-colors) {
+ --page-background: Canvas;
+ --page-text-color: CanvasText;
+ }
+}
+
+body {
+ color: var(--page-text-color);
+ background-color: var(--page-background);
+}
+```
+
+Here, we've defined one set of color variables on `:root` and subsequently overridden them in a `forced-colors` block. We don't need to re-write the styling for `body` within our media query because the vars it references will correctly adapt when `forced-colors` is active.
+
+Though `forced-colors` overriding doesn't happen in Firefox chrome style sheets, our style system _does_ force all instances of `transparent` to the [default color](https://searchfox.org/mozilla-central/rev/655f49c541108e3d0a232aa7173fbcb9af88d80b/servo/components/style/properties/cascade.rs#468-481) for a color attribute (usually `CanvasText`). This allows us to specify styles that appear only in forced-colors mode without an additional CSS keyword. It is also sometimes beneficial to define vars at the `:root` level with an initial value (eg. `transparent`, `0px`, `none`) so the vars exist for HCM overriding later (see: `--dialog-border-width`).
+
+```css
+:root {
+ /* ... */
+ --dialog-background: #ffffff;
+ --dialog-color: #bbbbbb;
+ --dialog-border-color: transparent;
+ --dialog-border-width: 0px;
+
+ @media (forced-colors) {
+ /* ... */
+ --dialog-background: Canvas;
+ --dialog-color: CanvasText;
+ /* No override for --dialog-borer-color, since it'll become
+ * CanvasText when HCM is enabled, which matches the HCM spec. */
+ --dialog-border-width: 1px;
+ }
+}
+
+.dialog {
+ color: var(--dialog-color);
+ background-color: var(--dialog-background);
+ border: var(--dialog-borer-width) solid var(--dialog-border-color);
+}
+
+/* ... */
+```
+
+In general, it is best to do overriding at the `:root` level, even if additional variables are required. It is _not_ recommended to do overriding on a class-by-class or element-by-element basis, like below:
+
+```css
+:root {
+ /* ... */
+ --button-background: #ffffff;
+ --button-color: #bbbbbb;
+
+ @media (forced-colors) {
+ /* ... */
+ --button-background: ButtonFace;
+ --button-color: ButtonText;
+ --button-border-color: var(--button-color);
+ }
+}
+
+@media (forced-colors) {
+ /* BAD: These rules are generic and should be outside of a @media block */
+ .destroyButton {
+ color: var(--button-color);
+ background-color: var(--button-background);
+ border: 1px solid var(--button-border-color);
+ }
+}
+
+@media (not (forced-colors)) {
+ /* BAD: These rules are generic and should be outside of a @media block */
+ .destroyButton {
+ color: var(--light-grey-20);
+ background-color: var(--red-60);
+ }
+}
+/*...*/
+```
+
+## Putting it all together!
+
+Let's walk through an example on this sample website.
+
+<iframe src="https://mreschenberg.com/sample_site.html" style="width: 100%; height: 60vh;"></iframe>
+
+The majority of our site styling is done via color overriding on the root block.
+```css
+:root {
+ /* General */
+ --background: #202124;
+ --color: #cccccc;
+ --border-color: transparent;
+ --border-size: 0;
+ --table-background: #303134;
+ /* Note */
+ --note-background: gold;
+ --note-color: var(--table-background);
+ --note-opacity: .7;
+ /* General Controls */
+ --control-background: gray;
+ --control-color: var(--color);
+ --control-border-size: var(--border-size);
+ --control-border-color: var(--border-color);
+ /* Destructive Button */
+ --destructive-button-background: tomato;
+ --destructive-button-color: var(--table-background);
+}
+```
+
+How might we adapt this website for users who prefer increased contrast? What about for users with forced colors?
+
+Let's start with users who prefer increased contrast. We might decide to make the background and foreground easier to read by making the foreground text lighter, increasing the contrast ratio. We might also remove the note's opacity and darken its text color. We don't need to remove the note's golden background, since prefers-contrast doesn't require a reduced color palette.
+```css
+:root {
+ /* ... */
+ @media (prefers-contrast) and (not (forced-colors)) {
+ /* General */
+ --color: white;
+ /* Note */
+ --note-color: black;
+ --note-opacity: 1;
+ /* ... */
+ }
+}
+```
+To address these same issues in forced-colors mode, we should replace the colors with the semantically appropriate system color. Unlike our work in the `prefers-contrast` block above, we do need to modify the note's background, since `gold` is not a system color. We might end up with something like this:
+```css
+:root {
+ /* ... */
+ @media (forced-colors) {
+ /* General */
+ --background: Canvas;
+ --color: CanvasText;
+ --table-background: var(--background);
+ /* Note */
+ --note-background: var(--background);
+ --note-color: var(--color);
+ --note-opacity: 1;
+ /* ... */
+ }
+}
+```
+
+After this change, you'll notice our page background, table background, and note background all share the same color. This makes them difficult to differentiate. To address this, we can add a contrasting border and override the previously transparent `--border-color` variable and its corresponding `--border-size`. This var applies to content areas but not controls.
+```css
+:root {
+ /* ... */
+ @media (forced-colors) {
+ /* General */
+ --background: Canvas;
+ --color: CanvasText;
+ --border-color: var(--color);
+ --border-size: 1px;
+ --table-background: var(--background);
+ /* Note */
+ --note-background: var(--background);
+ --note-color: var(--color);
+ --note-opacity: 1;
+ /* ... */
+ }
+}
+```
+
+Next, let's look at the controls this page uses for the web form. We've got an input, a checkbox, a submit button, and a clear button. In the prefers-contrast case, we should ensure the controls contrast from the background as much as possible. It's possible to do this via color alone, but it can help to add borders for additional contrast. Here, again, we don't need to get rid of the clear button's `tomato` background, but we can update it to something brighter.
+```css
+:root {
+ /* ... */
+ @media (prefers-contrast) and (not (forced-colors)) {
+ /* ... */
+ /* General Controls */
+ --control-background: color-mix(in srgb, gray, lightgray);
+ --control-color: black;
+ --control-border-size: 1px;
+ --control-border-color: gray;
+ /* Destructive Button */
+ --destructive-button-background: red;
+ --destructive-button-color: white;
+ /* ... */
+ }
+}
+```
+
+Finally, let's style the page's controls for `forced-colors`. Unlike in prefers-contrast, we can't use color to differentiate between the submit button and the clear form button -- both should inherit from our button CSS system colors.
+```css
+:root {
+ /* ... */
+ @media (forced-colors) {
+ /* General */
+ --background: Canvas;
+ --color: CanvasText;
+ --border-color: var(--color);
+ --border-size: 1px;
+ --table-background: var(--background);
+ /* Note */
+ --note-background: var(--background);
+ --note-color: var(--color);
+ --note-opacity: 1;
+ /* General Controls */
+ --control-background: ButtonFace;
+ --control-color: ButtonText;
+ --control-border-color: ButtonText;
+ /* Destructive Button */
+ --destructive-button-background: var(--control-background);
+ --destructive-button-color: var(--control-color);
+ }
+}
+```
+
+If we wanted to make the submit button stand out as a primary button, we could invert the styling on that button in particular. Something like:
+```css
+:root {
+ /* ... */
+ @media (forced-colors) {
+ /* General */
+ --background: Canvas;
+ --color: CanvasText;
+ --border-color: var(--color);
+ --border-size: 1px;
+ --table-background: var(--background);
+ /* Note */
+ --note-background: var(--background);
+ --note-color: var(--color);
+ --note-opacity: 1;
+ /* General Controls */
+ --control-background: ButtonFace;
+ --control-color: ButtonText;
+ --control-border-color: ButtonText;
+ /* Destructive Button */
+ --destructive-button-background: var(--control-background);
+ --destructive-button-color: var(--control-color);
+ }
+
+ #submit {
+ background: var(--control-color);
+ color: var(--control-background);
+ border: var(--control-border-size) solid currentColor;
+ }
+}
+```
+
+Now we've got a site that functions in multiple HCM scenerios :) Visit the <a href="https://mreschenberg.com/sample_site.html">live site</a> with HCM or Increase Contrast enabled to test it for yourself.
+A few takeaways:
+- `prefers-contrast` and `forced-colors` are not mutually exclusive. If you write two independent `forced-colors` and `prefers-contrast` media query blocks, they'll both apply when FF HCM is enabled (assuming your foreground/background contrast ratio is high, which it is by default). Adding an `and (not (forced-colors))` clause to your `@media (prefers-contrast)` declaration can help make the two blocks distinct if you'd like mac-centric styling in one and `forced-colors` styling in the other.
+- `prefers-contrast` requires specific, case-by-case overriding, whereas `forced-colors` is largely about inheretince. In the former, page regions that are differentiated with color should stay that way (albeit with more contrasting colors). In `forced-colors` all page regions that aren't interactive should use `Canvas`/`CanvasText` and rely on borders for distinction. Once you've set `--background: Canvas;` at the root level, for ex. subsequent background vars should inherit from it.
+- Where possible, aim to do overriding at the `:root` level using CSS variables, this makes it easier to update code in the future.
diff --git a/accessible/docs/HCMSettings.md b/accessible/docs/HCMSettings.md
new file mode 100644
index 0000000000..014019828b
--- /dev/null
+++ b/accessible/docs/HCMSettings.md
@@ -0,0 +1,45 @@
+# HCM Settings
+
+Several Firefox settings work together to determine how web content and browser chrome are rendered. They can be hard to keep track of! Use the flowcharts below for quick reference.
+
+## Settings that control color usage in browser chrome
+- OS HCM:
+ - Windows: High Contrast Mode in OS accessibility settings
+ - macOS: Increase Contrast in OS accessibility settings
+ - Linux: High Contrast Theme in OS accessibility settings
+- FF Theme (AKA FF Colorway)
+Note: OS HCM settings will only trigger HCM color usage in chrome if a user's FF theme is set to "system auto". If they have a pre-selected colorway or other FF theme (including explicit "Dark" or "Light") they will not see color changes upon enabling OS HCM.
+
+```{mermaid}
+flowchart TD
+A[Is OS HCM enabeld?]
+A -->|Yes| B[Is FF's theme set to System Auto?]
+B --> |Yes| C[Use OS HCM colors to render browser chrome]
+B -->|No| D[Use FF theme colors to render browser chrome]
+A -->|No| D
+```
+
+## Settings that control color usage in content
+- Colors Dialog (about:preferences > Manage Colors)
+ - Dropdown with options: Always, Only with High Contrast Themes, and Never
+ - Use System Colors checkbox
+ - Text, Background, Visited and Unvisited Link color inputs
+- Extensions like Dark Reader, or changes to user.css, may override author specified colors independent of HCM.
+
+```{mermaid}
+flowchart TD
+A[What is the value of the dropdown in the colors dialog?]
+A -->|Always|C
+
+A -->|Only with High Contrast Themes| B[Is a OS HCM enabled?]
+B -->|Yes| C[Is the Use System Colors checkbox checked?]
+C -->|Yes, and OS HCM is on| D[Use OS HCM colors to render web content]
+C -->|Yes, and OS HCM is off| D2[Use OS dark/light colors to render web content]
+C-->|No| E[Use colors dialog colors to render web content]
+
+B -->|No| F[Is a color-modifying web extension or color-modifying user.css change active?]
+F -->|Yes| G[Use web extension/user.css provided colors to render web content]
+F -->|No| H[Use author-provided colors to render web content]
+
+A -->|Never|F
+```
diff --git a/accessible/docs/fx_view_fc.jpg b/accessible/docs/fx_view_fc.jpg
new file mode 100644
index 0000000000..6ba63d2742
--- /dev/null
+++ b/accessible/docs/fx_view_fc.jpg
Binary files differ
diff --git a/accessible/docs/fxview.jpg b/accessible/docs/fxview.jpg
new file mode 100644
index 0000000000..c889feae92
--- /dev/null
+++ b/accessible/docs/fxview.jpg
Binary files differ
diff --git a/accessible/docs/fxview_ic.jpg b/accessible/docs/fxview_ic.jpg
new file mode 100644
index 0000000000..1f9b85f8dd
--- /dev/null
+++ b/accessible/docs/fxview_ic.jpg
Binary files differ
diff --git a/accessible/docs/index.rst b/accessible/docs/index.rst
new file mode 100644
index 0000000000..881d49ca9b
--- /dev/null
+++ b/accessible/docs/index.rst
@@ -0,0 +1,17 @@
+Accessibility
+=============
+
+These pages contain design documents for the accessibility implementation in Gecko.
+They live in the mozilla-central repository under the accessible/docs directory.
+
+The `Accessibility page on the Mozilla Wiki <https://wiki.mozilla.org/Accessibility>`__ contains general information about accessibility and the accessibility team at Mozilla.
+
+.. toctree::
+ :maxdepth: 1
+
+ Architecture
+ DocumentAccessibilityLifecycle
+ GeckoViewThreadTopography
+ ColorsAndHighContrastMode
+ HCMSettings
+ HCMMediaQueries
diff --git a/accessible/generic/ARIAGridAccessible.cpp b/accessible/generic/ARIAGridAccessible.cpp
new file mode 100644
index 0000000000..58e49bee1f
--- /dev/null
+++ b/accessible/generic/ARIAGridAccessible.cpp
@@ -0,0 +1,77 @@
+/* -*- 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 "ARIAGridAccessible.h"
+
+#include <stdint.h>
+#include "LocalAccessible-inl.h"
+#include "AccAttributes.h"
+#include "mozilla/a11y/TableAccessible.h"
+#include "mozilla/a11y/TableCellAccessible.h"
+#include "nsAccessibilityService.h"
+#include "nsAccUtils.h"
+#include "nsGkAtoms.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// ARIAGridCellAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+// Constructor
+
+ARIAGridCellAccessible::ARIAGridCellAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mGenericTypes |= eTableCell;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible
+
+void ARIAGridCellAccessible::ApplyARIAState(uint64_t* aState) const {
+ HyperTextAccessible::ApplyARIAState(aState);
+
+ // Return if the gridcell has aria-selected="true".
+ if (*aState & states::SELECTED) return;
+
+ // Check aria-selected="true" on the row.
+ LocalAccessible* row = LocalParent();
+ if (!row || row->Role() != roles::ROW) return;
+
+ nsIContent* rowContent = row->GetContent();
+ if (nsAccUtils::HasDefinedARIAToken(rowContent, nsGkAtoms::aria_selected) &&
+ !nsAccUtils::ARIAAttrValueIs(rowContent->AsElement(),
+ nsGkAtoms::aria_selected, nsGkAtoms::_false,
+ eCaseMatters)) {
+ *aState |= states::SELECTABLE | states::SELECTED;
+ }
+}
+
+already_AddRefed<AccAttributes> ARIAGridCellAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = HyperTextAccessible::NativeAttributes();
+
+ // We only need to expose table-cell-index to clients. If we're in the content
+ // process, we don't need this, so building a CachedTableAccessible is very
+ // wasteful. This will be exposed by RemoteAccessible in the parent process
+ // instead.
+ if (!IPCAccessibilityActive()) {
+ if (const TableCellAccessible* cell = AsTableCell()) {
+ TableAccessible* table = cell->Table();
+ const uint32_t row = cell->RowIdx();
+ const uint32_t col = cell->ColIdx();
+ const int32_t cellIdx = table->CellIndexAt(row, col);
+ if (cellIdx != -1) {
+ attributes->SetAttribute(nsGkAtoms::tableCellIndex, cellIdx);
+ }
+ }
+ }
+
+ return attributes.forget();
+}
diff --git a/accessible/generic/ARIAGridAccessible.h b/accessible/generic/ARIAGridAccessible.h
new file mode 100644
index 0000000000..35590b0446
--- /dev/null
+++ b/accessible/generic/ARIAGridAccessible.h
@@ -0,0 +1,35 @@
+/* -*- 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_ARIAGridAccessible_h_
+#define MOZILLA_A11Y_ARIAGridAccessible_h_
+
+#include "HyperTextAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Accessible for ARIA gridcell and rowheader/columnheader.
+ */
+class ARIAGridCellAccessible : public HyperTextAccessible {
+ public:
+ ARIAGridCellAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(ARIAGridCellAccessible,
+ HyperTextAccessible)
+
+ // LocalAccessible
+ virtual void ApplyARIAState(uint64_t* aState) const override;
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+
+ protected:
+ virtual ~ARIAGridCellAccessible() {}
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/ApplicationAccessible.cpp b/accessible/generic/ApplicationAccessible.cpp
new file mode 100644
index 0000000000..90539cf2bd
--- /dev/null
+++ b/accessible/generic/ApplicationAccessible.cpp
@@ -0,0 +1,142 @@
+/* -*- 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 "ApplicationAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "nsAccessibilityService.h"
+#include "Relation.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+
+#include "nsServiceManagerUtils.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/Components.h"
+#include "nsGlobalWindowOuter.h"
+#include "nsIStringBundle.h"
+
+using namespace mozilla::a11y;
+
+ApplicationAccessible::ApplicationAccessible()
+ : AccessibleWrap(nullptr, nullptr) {
+ mType = eApplicationType;
+ mAppInfo = do_GetService("@mozilla.org/xre/app-info;1");
+ MOZ_ASSERT(mAppInfo, "no application info");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessible
+
+ENameValueFlag ApplicationAccessible::Name(nsString& aName) const {
+ aName.Truncate();
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+
+ NS_ASSERTION(bundleService, "String bundle service must be present!");
+ if (!bundleService) return eNameOK;
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = bundleService->CreateBundle(
+ "chrome://branding/locale/brand.properties", getter_AddRefs(bundle));
+ if (NS_FAILED(rv)) return eNameOK;
+
+ nsAutoString appName;
+ rv = bundle->GetStringFromName("brandShortName", appName);
+ if (NS_FAILED(rv) || appName.IsEmpty()) {
+ NS_WARNING("brandShortName not found, using default app name");
+ appName.AssignLiteral("Gecko based application");
+ }
+
+ aName.Assign(appName);
+ return eNameOK;
+}
+
+void ApplicationAccessible::Description(nsString& aDescription) const {
+ aDescription.Truncate();
+}
+
+void ApplicationAccessible::Value(nsString& aValue) const { aValue.Truncate(); }
+
+uint64_t ApplicationAccessible::State() {
+ return IsDefunct() ? states::DEFUNCT : 0;
+}
+
+already_AddRefed<AccAttributes> ApplicationAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+ return attributes.forget();
+}
+
+GroupPos ApplicationAccessible::GroupPosition() { return GroupPos(); }
+
+LocalAccessible* ApplicationAccessible::LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) {
+ return nullptr;
+}
+
+Accessible* ApplicationAccessible::FocusedChild() {
+ LocalAccessible* focus = FocusMgr()->FocusedLocalAccessible();
+ if (focus && focus->LocalParent() == this) {
+ return focus;
+ }
+
+ return nullptr;
+}
+
+Relation ApplicationAccessible::RelationByType(
+ RelationType aRelationType) const {
+ return Relation();
+}
+
+mozilla::LayoutDeviceIntRect ApplicationAccessible::Bounds() const {
+ return mozilla::LayoutDeviceIntRect();
+}
+
+nsRect ApplicationAccessible::BoundsInAppUnits() const { return nsRect(); }
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible public methods
+
+void ApplicationAccessible::Shutdown() { mAppInfo = nullptr; }
+
+void ApplicationAccessible::ApplyARIAState(uint64_t* aState) const {}
+
+role ApplicationAccessible::NativeRole() const { return roles::APP_ROOT; }
+
+uint64_t ApplicationAccessible::NativeState() const { return 0; }
+
+KeyBinding ApplicationAccessible::AccessKey() const { return KeyBinding(); }
+
+void ApplicationAccessible::Init() {
+ // Basically children are kept updated by Append/RemoveChild method calls.
+ // However if there are open windows before accessibility was started
+ // then we need to make sure root accessibles for open windows are created so
+ // that all root accessibles are stored in application accessible children
+ // array.
+
+ nsGlobalWindowOuter::OuterWindowByIdTable* windowsById =
+ nsGlobalWindowOuter::GetWindowsTable();
+
+ if (!windowsById) {
+ return;
+ }
+
+ for (const auto& window : windowsById->Values()) {
+ if (window->GetDocShell() && window->IsRootOuterWindow()) {
+ if (RefPtr<dom::Document> docNode = window->GetExtantDoc()) {
+ GetAccService()->GetDocAccessible(docNode); // ensure creation
+ }
+ }
+ }
+}
+
+LocalAccessible* ApplicationAccessible::GetSiblingAtOffset(
+ int32_t aOffset, nsresult* aError) const {
+ if (aError) *aError = NS_OK; // fail peacefully
+
+ return nullptr;
+}
diff --git a/accessible/generic/ApplicationAccessible.h b/accessible/generic/ApplicationAccessible.h
new file mode 100644
index 0000000000..1b21ca8e8b
--- /dev/null
+++ b/accessible/generic/ApplicationAccessible.h
@@ -0,0 +1,109 @@
+/* -*- 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_ApplicationAccessible_h__
+#define mozilla_a11y_ApplicationAccessible_h__
+
+#include "AccessibleWrap.h"
+
+#include "nsIXULAppInfo.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * ApplicationAccessible is for the whole application of Mozilla.
+ * Only one instance of ApplicationAccessible exists for one Mozilla instance.
+ * And this one should be created when Mozilla Startup (if accessibility
+ * feature has been enabled) and destroyed when Mozilla Shutdown.
+ *
+ * All the accessibility objects for toplevel windows are direct children of
+ * the ApplicationAccessible instance.
+ */
+
+class ApplicationAccessible : public AccessibleWrap {
+ public:
+ ApplicationAccessible();
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(ApplicationAccessible, AccessibleWrap)
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual LayoutDeviceIntRect Bounds() const override;
+ virtual nsRect BoundsInAppUnits() const override;
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+ virtual GroupPos GroupPosition() override;
+ virtual ENameValueFlag Name(nsString& aName) const override;
+ virtual void ApplyARIAState(uint64_t* aState) const override;
+ virtual void Description(nsString& aDescription) const override;
+ virtual void Value(nsString& aValue) const override;
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t State() override;
+ virtual uint64_t NativeState() const override;
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ virtual LocalAccessible* LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override;
+ virtual Accessible* FocusedChild() override;
+
+ // ActionAccessible
+ virtual KeyBinding AccessKey() const override;
+
+ // ApplicationAccessible
+ void Init();
+
+ void AppName(nsAString& aName) const {
+ MOZ_ASSERT(mAppInfo, "no application info");
+
+ if (mAppInfo) {
+ nsAutoCString cname;
+ mAppInfo->GetName(cname);
+ AppendUTF8toUTF16(cname, aName);
+ }
+ }
+
+ void AppVersion(nsAString& aVersion) const {
+ MOZ_ASSERT(mAppInfo, "no application info");
+
+ if (mAppInfo) {
+ nsAutoCString cversion;
+ mAppInfo->GetVersion(cversion);
+ AppendUTF8toUTF16(cversion, aVersion);
+ }
+ }
+
+ void PlatformName(nsAString& aName) const { aName.AssignLiteral("Gecko"); }
+
+ void PlatformVersion(nsAString& aVersion) const {
+ MOZ_ASSERT(mAppInfo, "no application info");
+
+ if (mAppInfo) {
+ nsAutoCString cversion;
+ mAppInfo->GetPlatformVersion(cversion);
+ AppendUTF8toUTF16(cversion, aVersion);
+ }
+ }
+
+ protected:
+ virtual ~ApplicationAccessible() {}
+
+ // LocalAccessible
+ virtual LocalAccessible* GetSiblingAtOffset(
+ int32_t aOffset, nsresult* aError = nullptr) const override;
+
+ private:
+ nsCOMPtr<nsIXULAppInfo> mAppInfo;
+};
+
+inline ApplicationAccessible* LocalAccessible::AsApplication() {
+ return IsApplication() ? static_cast<ApplicationAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/BaseAccessibles.cpp b/accessible/generic/BaseAccessibles.cpp
new file mode 100644
index 0000000000..520f54e96b
--- /dev/null
+++ b/accessible/generic/BaseAccessibles.cpp
@@ -0,0 +1,142 @@
+/* -*- 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 "BaseAccessibles.h"
+
+#include "States.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// LeafAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+LeafAccessible::LeafAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {
+ mStateFlags |= eNoKidsFromDOM;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LeafAccessible: LocalAccessible public
+
+LocalAccessible* LeafAccessible::LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) {
+ // Don't walk into leaf accessibles.
+ return this;
+}
+
+bool LeafAccessible::InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) {
+ MOZ_ASSERT_UNREACHABLE("InsertChildAt called on leaf accessible!");
+ return false;
+}
+
+bool LeafAccessible::RemoveChild(LocalAccessible* aChild) {
+ MOZ_ASSERT_UNREACHABLE("RemoveChild called on leaf accessible!");
+ return false;
+}
+
+bool LeafAccessible::IsAcceptableChild(nsIContent* aEl) const {
+ // No children for leaf accessible.
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LinkableAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+// LinkableAccessible. nsIAccessible
+
+void LinkableAccessible::TakeFocus() const {
+ if (const LocalAccessible* actionAcc = ActionWalk()) {
+ actionAcc->TakeFocus();
+ } else {
+ AccessibleWrap::TakeFocus();
+ }
+}
+
+uint64_t LinkableAccessible::NativeLinkState() const {
+ bool isLink;
+ const LocalAccessible* actionAcc = ActionWalk(&isLink);
+ if (isLink) {
+ return states::LINKED | (actionAcc->LinkState() & states::TRAVERSED);
+ }
+
+ return 0;
+}
+
+void LinkableAccessible::Value(nsString& aValue) const {
+ aValue.Truncate();
+
+ LocalAccessible::Value(aValue);
+ if (!aValue.IsEmpty()) {
+ return;
+ }
+
+ bool isLink;
+ const LocalAccessible* actionAcc = ActionWalk(&isLink);
+ if (isLink) {
+ actionAcc->Value(aValue);
+ }
+}
+
+const LocalAccessible* LinkableAccessible::ActionWalk(bool* aIsLink,
+ bool* aIsOnclick) const {
+ if (aIsOnclick) {
+ *aIsOnclick = false;
+ }
+ if (aIsLink) {
+ *aIsLink = false;
+ }
+
+ if (HasPrimaryAction()) {
+ if (aIsOnclick) {
+ *aIsOnclick = true;
+ }
+
+ return nullptr;
+ }
+
+ const Accessible* actionAcc = ActionAncestor();
+
+ const LocalAccessible* localAction =
+ actionAcc ? const_cast<Accessible*>(actionAcc)->AsLocal() : nullptr;
+
+ if (!localAction) {
+ return nullptr;
+ }
+
+ if (localAction->LinkState() & states::LINKED) {
+ if (aIsLink) {
+ *aIsLink = true;
+ }
+ } else if (aIsOnclick) {
+ *aIsOnclick = true;
+ }
+
+ return localAction;
+}
+
+KeyBinding LinkableAccessible::AccessKey() const {
+ if (const LocalAccessible* actionAcc =
+ const_cast<LinkableAccessible*>(this)->ActionWalk()) {
+ return actionAcc->AccessKey();
+ }
+
+ return LocalAccessible::AccessKey();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DummyAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+uint64_t DummyAccessible::NativeState() const { return 0; }
+uint64_t DummyAccessible::NativeInteractiveState() const { return 0; }
+
+uint64_t DummyAccessible::NativeLinkState() const { return 0; }
+
+bool DummyAccessible::NativelyUnavailable() const { return false; }
+
+void DummyAccessible::ApplyARIAState(uint64_t* aState) const {}
diff --git a/accessible/generic/BaseAccessibles.h b/accessible/generic/BaseAccessibles.h
new file mode 100644
index 0000000000..e67d208786
--- /dev/null
+++ b/accessible/generic/BaseAccessibles.h
@@ -0,0 +1,136 @@
+/* -*- 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_BaseAccessibles_h__
+#define mozilla_a11y_BaseAccessibles_h__
+
+#include "AccessibleWrap.h"
+#include "HyperTextAccessible.h"
+
+class nsIContent;
+
+/**
+ * This file contains a number of classes that are used as base
+ * classes for the different accessibility implementations of
+ * the HTML and XUL widget sets. --jgaunt
+ */
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Leaf version of DOM Accessible -- has no children
+ */
+class LeafAccessible : public AccessibleWrap {
+ public:
+ LeafAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(LeafAccessible, AccessibleWrap)
+
+ // LocalAccessible
+ virtual LocalAccessible* LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override;
+ bool InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) final;
+ bool RemoveChild(LocalAccessible* aChild) final;
+
+ virtual bool IsAcceptableChild(nsIContent* aEl) const override;
+
+ protected:
+ virtual ~LeafAccessible() {}
+};
+
+/**
+ * Used for text or image accessible nodes contained by link accessibles or
+ * accessibles for nodes with registered click event handler. It knows how to
+ * report the state of the host link (traveled or not) and can focus the host
+ * accessible programmatically.
+ */
+class LinkableAccessible : public AccessibleWrap {
+ public:
+ LinkableAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {}
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(LinkableAccessible, AccessibleWrap)
+
+ // LocalAccessible
+ virtual void Value(nsString& aValue) const override;
+ virtual uint64_t NativeLinkState() const override;
+ virtual void TakeFocus() const override;
+
+ // ActionAccessible
+ virtual KeyBinding AccessKey() const override;
+
+ // ActionAccessible helpers
+ const LocalAccessible* ActionWalk(bool* aIsLink = nullptr,
+ bool* aIsOnclick = nullptr) const;
+
+ protected:
+ virtual ~LinkableAccessible() {}
+};
+
+/**
+ * A simple accessible that gets its enumerated role.
+ */
+template <a11y::role R>
+class EnumRoleAccessible : public AccessibleWrap {
+ public:
+ EnumRoleAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {}
+
+ NS_IMETHOD QueryInterface(REFNSIID aIID, void** aPtr) override {
+ return LocalAccessible::QueryInterface(aIID, aPtr);
+ }
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override { return R; }
+
+ protected:
+ virtual ~EnumRoleAccessible() {}
+};
+
+/**
+ * Like EnumRoleAccessible, but with text support.
+ */
+template <a11y::role R>
+class EnumRoleHyperTextAccessible : public HyperTextAccessible {
+ public:
+ EnumRoleHyperTextAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override { return R; }
+
+ protected:
+ virtual ~EnumRoleHyperTextAccessible() {}
+};
+
+/**
+ * A wrapper accessible around native accessible to connect it with
+ * crossplatform accessible tree.
+ */
+class DummyAccessible : public AccessibleWrap {
+ public:
+ explicit DummyAccessible(DocAccessible* aDocument = nullptr)
+ : AccessibleWrap(nullptr, aDocument) {
+ // IsDefunct() asserts if mContent is null, which is always true for
+ // DummyAccessible. We can prevent this by setting eSharedNode.
+ mStateFlags |= eSharedNode;
+ }
+
+ uint64_t NativeState() const final;
+ uint64_t NativeInteractiveState() const final;
+ uint64_t NativeLinkState() const final;
+ bool NativelyUnavailable() const final;
+ void ApplyARIAState(uint64_t* aState) const final;
+
+ protected:
+ virtual ~DummyAccessible() {}
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/DocAccessible-inl.h b/accessible/generic/DocAccessible-inl.h
new file mode 100644
index 0000000000..2e358724cd
--- /dev/null
+++ b/accessible/generic/DocAccessible-inl.h
@@ -0,0 +1,183 @@
+/* -*- 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_DocAccessible_inl_h_
+#define mozilla_a11y_DocAccessible_inl_h_
+
+#include "DocAccessible.h"
+#include "LocalAccessible-inl.h"
+#include "nsAccessibilityService.h"
+#include "NotificationController.h"
+#include "States.h"
+#include "nsIScrollableFrame.h"
+#include "mozilla/dom/DocumentInlines.h"
+
+#ifdef A11Y_LOG
+# include "Logging.h"
+#endif
+
+namespace mozilla {
+namespace a11y {
+
+inline LocalAccessible* DocAccessible::AccessibleOrTrueContainer(
+ nsINode* aNode, bool aNoContainerIfPruned) const {
+ // HTML comboboxes have no-content list accessible as an intermediate
+ // containing all options.
+ LocalAccessible* container =
+ GetAccessibleOrContainer(aNode, aNoContainerIfPruned);
+ if (container && container->IsHTMLCombobox()) {
+ return container->LocalFirstChild();
+ }
+ return container;
+}
+
+inline bool DocAccessible::IsContentLoaded() const {
+ // eDOMLoaded flag check is used for error pages as workaround to make this
+ // method return correct result since error pages do not receive 'pageshow'
+ // event and as consequence Document::IsShowing() returns false.
+ return mDocumentNode && mDocumentNode->IsVisible() &&
+ (mDocumentNode->IsShowing() || HasLoadState(eDOMLoaded));
+}
+
+inline bool DocAccessible::IsHidden() const { return mDocumentNode->Hidden(); }
+
+inline void DocAccessible::FireDelayedEvent(AccEvent* aEvent) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocLoad)) logging::DocLoadEventFired(aEvent);
+#endif
+
+ mNotificationController->QueueEvent(aEvent);
+}
+
+inline void DocAccessible::FireDelayedEvent(uint32_t aEventType,
+ LocalAccessible* aTarget) {
+ RefPtr<AccEvent> event = new AccEvent(aEventType, aTarget);
+ FireDelayedEvent(event);
+}
+
+inline void DocAccessible::BindChildDocument(DocAccessible* aDocument) {
+ mNotificationController->ScheduleChildDocBinding(aDocument);
+}
+
+template <class Class, class... Args>
+inline void DocAccessible::HandleNotification(
+ Class* aInstance, typename TNotification<Class, Args...>::Callback aMethod,
+ Args*... aArgs) {
+ if (mNotificationController) {
+ mNotificationController->HandleNotification<Class, Args...>(
+ aInstance, aMethod, aArgs...);
+ }
+}
+
+inline void DocAccessible::UpdateText(nsIContent* aTextNode) {
+ NS_ASSERTION(mNotificationController, "The document was shut down!");
+
+ // Ignore the notification if initial tree construction hasn't been done yet.
+ if (mNotificationController && HasLoadState(eTreeConstructed)) {
+ mNotificationController->ScheduleTextUpdate(aTextNode);
+ }
+}
+
+inline void DocAccessible::NotifyOfLoad(uint32_t aLoadEventType) {
+ mLoadState |= eDOMLoaded;
+ mLoadEventType = aLoadEventType;
+
+ // If the document is loaded completely then network activity was presumingly
+ // caused by file loading. Fire busy state change event.
+ if (HasLoadState(eCompletelyLoaded) && IsLoadEventTarget()) {
+ RefPtr<AccEvent> stateEvent =
+ new AccStateChangeEvent(this, states::BUSY, false);
+ FireDelayedEvent(stateEvent);
+ }
+}
+
+inline void DocAccessible::MaybeNotifyOfValueChange(
+ LocalAccessible* aAccessible) {
+ if (aAccessible->IsCombobox() || aAccessible->Role() == roles::ENTRY ||
+ aAccessible->Role() == roles::SPINBUTTON) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE, aAccessible);
+ }
+}
+
+inline LocalAccessible* DocAccessible::GetAccessibleEvenIfNotInMapOrContainer(
+ nsINode* aNode) const {
+ LocalAccessible* acc = GetAccessibleEvenIfNotInMap(aNode);
+ return acc ? acc : GetContainerAccessible(aNode);
+}
+
+inline void DocAccessible::CreateSubtree(LocalAccessible* aChild) {
+ // If a focused node has been shown then it could mean its frame was recreated
+ // while the node stays focused and we need to fire focus event on
+ // the accessible we just created. If the queue contains a focus event for
+ // this node already then it will be suppressed by this one.
+ LocalAccessible* focusedAcc = nullptr;
+ CacheChildrenInSubtree(aChild, &focusedAcc);
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eVerbose)) {
+ logging::Tree("TREE", "Created subtree", aChild);
+ }
+#endif
+
+ // Fire events for ARIA elements.
+ if (aChild->HasARIARole()) {
+ roles::Role role = aChild->ARIARole();
+ if (role == roles::MENUPOPUP) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START, aChild);
+ } else if (role == roles::ALERT) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, aChild);
+ }
+ }
+
+ // XXX: do we really want to send focus to focused DOM node not taking into
+ // account active item?
+ if (focusedAcc) {
+ FocusMgr()->DispatchFocusEvent(this, focusedAcc);
+ SelectionMgr()->SetControlSelectionListener(
+ focusedAcc->GetNode()->AsElement());
+ }
+}
+
+inline DocAccessible::AttrRelProviders* DocAccessible::GetRelProviders(
+ dom::Element* aElement, const nsAString& aID) const {
+ DependentIDsHashtable* hash = mDependentIDsHashes.Get(
+ aElement->GetUncomposedDocOrConnectedShadowRoot());
+ if (hash) {
+ return hash->Get(aID);
+ }
+ return nullptr;
+}
+
+inline DocAccessible::AttrRelProviders* DocAccessible::GetOrCreateRelProviders(
+ dom::Element* aElement, const nsAString& aID) {
+ dom::DocumentOrShadowRoot* docOrShadowRoot =
+ aElement->GetUncomposedDocOrConnectedShadowRoot();
+ DependentIDsHashtable* hash =
+ mDependentIDsHashes.GetOrInsertNew(docOrShadowRoot);
+
+ return hash->GetOrInsertNew(aID);
+}
+
+inline void DocAccessible::RemoveRelProvidersIfEmpty(dom::Element* aElement,
+ const nsAString& aID) {
+ dom::DocumentOrShadowRoot* docOrShadowRoot =
+ aElement->GetUncomposedDocOrConnectedShadowRoot();
+ DependentIDsHashtable* hash = mDependentIDsHashes.Get(docOrShadowRoot);
+ if (hash) {
+ AttrRelProviders* providers = hash->Get(aID);
+ if (providers && providers->Length() == 0) {
+ hash->Remove(aID);
+ if (mDependentIDsHashes.IsEmpty()) {
+ mDependentIDsHashes.Remove(docOrShadowRoot);
+ }
+ }
+ }
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp
new file mode 100644
index 0000000000..28ed8bcbb4
--- /dev/null
+++ b/accessible/generic/DocAccessible.cpp
@@ -0,0 +1,2847 @@
+/* -*- 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/. */
+
+#include "LocalAccessible-inl.h"
+#include "AccIterator.h"
+#include "AccAttributes.h"
+#include "CachedTableAccessible.h"
+#include "DocAccessible-inl.h"
+#include "EventTree.h"
+#include "HTMLImageMapAccessible.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "nsAccUtils.h"
+#include "nsEventShell.h"
+#include "nsIIOService.h"
+#include "nsLayoutUtils.h"
+#include "nsTextEquivUtils.h"
+#include "mozilla/a11y/Role.h"
+#include "TreeWalker.h"
+#include "xpcAccessibleDocument.h"
+
+#include "nsIDocShell.h"
+#include "mozilla/dom/Document.h"
+#include "nsPIDOMWindow.h"
+#include "nsIContentInlines.h"
+#include "nsIEditingSession.h"
+#include "nsIFrame.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsImageFrame.h"
+#include "nsViewManager.h"
+#include "nsIScrollableFrame.h"
+#include "nsIURI.h"
+#include "nsIWebNavigation.h"
+#include "nsFocusManager.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Components.h" // for mozilla::components
+#include "mozilla/EditorBase.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/ipc/ProcessChild.h"
+#include "mozilla/PerfStats.h"
+#include "mozilla/PresShell.h"
+#include "nsAccessibilityService.h"
+#include "mozilla/a11y/DocAccessibleChild.h"
+#include "mozilla/dom/AncestorIterator.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/DocumentType.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLSelectElement.h"
+#include "mozilla/dom/MutationEventBinding.h"
+#include "mozilla/dom/UserActivation.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// Static member initialization
+
+static nsStaticAtom* const kRelationAttrs[] = {
+ nsGkAtoms::aria_labelledby, nsGkAtoms::aria_describedby,
+ nsGkAtoms::aria_details, nsGkAtoms::aria_owns,
+ nsGkAtoms::aria_controls, nsGkAtoms::aria_flowto,
+ nsGkAtoms::aria_errormessage, nsGkAtoms::_for,
+ nsGkAtoms::control, nsGkAtoms::popovertarget};
+
+static const uint32_t kRelationAttrsLen = ArrayLength(kRelationAttrs);
+
+////////////////////////////////////////////////////////////////////////////////
+// Constructor/desctructor
+
+DocAccessible::DocAccessible(dom::Document* aDocument,
+ PresShell* aPresShell)
+ : // XXX don't pass a document to the LocalAccessible constructor so that
+ // we don't set mDoc until our vtable is fully setup. If we set mDoc
+ // before setting up the vtable we will call LocalAccessible::AddRef()
+ // but not the overrides of it for subclasses. It is important to call
+ // those overrides to avoid confusing leak checking machinary.
+ HyperTextAccessible(nullptr, nullptr),
+ // XXX aaronl should we use an algorithm for the initial cache size?
+ mAccessibleCache(kDefaultCacheLength),
+ mNodeToAccessibleMap(kDefaultCacheLength),
+ mDocumentNode(aDocument),
+ mLoadState(eTreeConstructionPending),
+ mDocFlags(0),
+ mViewportCacheDirty(false),
+ mLoadEventType(0),
+ mPrevStateBits(0),
+ mPresShell(aPresShell),
+ mIPCDoc(nullptr) {
+ mGenericTypes |= eDocument;
+ mStateFlags |= eNotNodeMapEntry;
+ mDoc = this;
+
+ MOZ_ASSERT(mPresShell, "should have been given a pres shell");
+ mPresShell->SetDocAccessible(this);
+}
+
+DocAccessible::~DocAccessible() {
+ NS_ASSERTION(!mPresShell, "LastRelease was never called!?!");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(DocAccessible)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible,
+ LocalAccessible)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotificationController)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildDocuments)
+ for (const auto& hashEntry : tmp->mDependentIDsHashes.Values()) {
+ for (const auto& providers : hashEntry->Values()) {
+ for (int32_t provIdx = providers->Length() - 1; provIdx >= 0; provIdx--) {
+ NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(
+ cb, "content of dependent ids hash entry of document accessible");
+
+ const auto& provider = (*providers)[provIdx];
+ cb.NoteXPCOMChild(provider->mContent);
+ }
+ }
+ }
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInvalidationList)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingUpdates)
+ for (const auto& ar : tmp->mARIAOwnsHash.Values()) {
+ for (uint32_t i = 0; i < ar->Length(); i++) {
+ NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mARIAOwnsHash entry item");
+ cb.NoteXPCOMChild(ar->ElementAt(i));
+ }
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocAccessible, LocalAccessible)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mNotificationController)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildDocuments)
+ tmp->mDependentIDsHashes.Clear();
+ tmp->mNodeToAccessibleMap.Clear();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessibleCache)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchorJumpElm)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mInvalidationList)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingUpdates)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
+ tmp->mARIAOwnsHash.Clear();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocAccessible)
+ NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END_INHERITING(HyperTextAccessible)
+
+NS_IMPL_ADDREF_INHERITED(DocAccessible, HyperTextAccessible)
+NS_IMPL_RELEASE_INHERITED(DocAccessible, HyperTextAccessible)
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessible
+
+ENameValueFlag DocAccessible::Name(nsString& aName) const {
+ aName.Truncate();
+
+ if (mParent) {
+ mParent->Name(aName); // Allow owning iframe to override the name
+ }
+ if (aName.IsEmpty()) {
+ // Allow name via aria-labelledby or title attribute
+ LocalAccessible::Name(aName);
+ }
+ if (aName.IsEmpty()) {
+ Title(aName); // Try title element
+ }
+ if (aName.IsEmpty()) { // Last resort: use URL
+ URL(aName);
+ }
+
+ return eNameOK;
+}
+
+// LocalAccessible public method
+role DocAccessible::NativeRole() const {
+ nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mDocumentNode);
+ if (docShell) {
+ nsCOMPtr<nsIDocShellTreeItem> sameTypeRoot;
+ docShell->GetInProcessSameTypeRootTreeItem(getter_AddRefs(sameTypeRoot));
+ int32_t itemType = docShell->ItemType();
+ if (sameTypeRoot == docShell) {
+ // Root of content or chrome tree
+ if (itemType == nsIDocShellTreeItem::typeChrome) {
+ return roles::CHROME_WINDOW;
+ }
+
+ if (itemType == nsIDocShellTreeItem::typeContent) {
+ return roles::DOCUMENT;
+ }
+ } else if (itemType == nsIDocShellTreeItem::typeContent) {
+ return roles::DOCUMENT;
+ }
+ }
+
+ return roles::PANE; // Fall back;
+}
+
+void DocAccessible::Description(nsString& aDescription) const {
+ if (mParent) mParent->Description(aDescription);
+
+ if (HasOwnContent() && aDescription.IsEmpty()) {
+ nsTextEquivUtils::GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby,
+ aDescription);
+ }
+}
+
+// LocalAccessible public method
+uint64_t DocAccessible::NativeState() const {
+ // Document is always focusable.
+ uint64_t state =
+ states::FOCUSABLE; // keep in sync with NativeInteractiveState() impl
+ if (FocusMgr()->IsFocused(this)) state |= states::FOCUSED;
+
+ // Expose stale state until the document is ready (DOM is loaded and tree is
+ // constructed).
+ if (!HasLoadState(eReady)) state |= states::STALE;
+
+ // Expose state busy until the document and all its subdocuments is completely
+ // loaded.
+ if (!HasLoadState(eCompletelyLoaded)) state |= states::BUSY;
+
+ nsIFrame* frame = GetFrame();
+ if (!frame || !frame->IsVisibleConsideringAncestors(
+ nsIFrame::VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) {
+ state |= states::INVISIBLE | states::OFFSCREEN;
+ }
+
+ RefPtr<EditorBase> editorBase = GetEditor();
+ state |= editorBase ? states::EDITABLE : states::READONLY;
+
+ return state;
+}
+
+uint64_t DocAccessible::NativeInteractiveState() const {
+ // Document is always focusable.
+ return states::FOCUSABLE;
+}
+
+bool DocAccessible::NativelyUnavailable() const { return false; }
+
+// LocalAccessible public method
+void DocAccessible::ApplyARIAState(uint64_t* aState) const {
+ // Grab states from content element.
+ if (mContent) LocalAccessible::ApplyARIAState(aState);
+
+ // Allow iframe/frame etc. to have final state override via ARIA.
+ if (mParent) mParent->ApplyARIAState(aState);
+}
+
+Accessible* DocAccessible::FocusedChild() {
+ // Return an accessible for the current global focus, which does not have to
+ // be contained within the current document.
+ return FocusMgr()->FocusedAccessible();
+}
+
+void DocAccessible::TakeFocus() const {
+ // Focus the document.
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ RefPtr<dom::Element> newFocus;
+ dom::AutoHandlingUserInputStatePusher inputStatePusher(true);
+ fm->MoveFocus(mDocumentNode->GetWindow(), nullptr,
+ nsFocusManager::MOVEFOCUS_ROOT, 0, getter_AddRefs(newFocus));
+}
+
+// HyperTextAccessible method
+already_AddRefed<EditorBase> DocAccessible::GetEditor() const {
+ // Check if document is editable (designMode="on" case). Otherwise check if
+ // the html:body (for HTML document case) or document element is editable.
+ if (!mDocumentNode->IsInDesignMode() &&
+ (!mContent || !mContent->HasFlag(NODE_IS_EDITABLE))) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = mDocumentNode->GetDocShell();
+ if (!docShell) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIEditingSession> editingSession;
+ docShell->GetEditingSession(getter_AddRefs(editingSession));
+ if (!editingSession) return nullptr; // No editing session interface
+
+ RefPtr<HTMLEditor> htmlEditor =
+ editingSession->GetHTMLEditorForWindow(mDocumentNode->GetWindow());
+ if (!htmlEditor) {
+ return nullptr;
+ }
+
+ bool isEditable = false;
+ htmlEditor->GetIsDocumentEditable(&isEditable);
+ if (isEditable) {
+ return htmlEditor.forget();
+ }
+
+ return nullptr;
+}
+
+// DocAccessible public method
+
+void DocAccessible::URL(nsAString& aURL) const {
+ aURL.Truncate();
+ nsCOMPtr<nsISupports> container = mDocumentNode->GetContainer();
+ nsCOMPtr<nsIWebNavigation> webNav(do_GetInterface(container));
+ if (MOZ_UNLIKELY(!webNav)) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ webNav->GetCurrentURI(getter_AddRefs(uri));
+ if (MOZ_UNLIKELY(!uri)) {
+ return;
+ }
+ // Let's avoid treating too long URI in the main process for avoiding
+ // memory fragmentation as far as possible.
+ if (uri->SchemeIs("data") || uri->SchemeIs("blob")) {
+ return;
+ }
+
+ nsCOMPtr<nsIIOService> io = mozilla::components::IO::Service();
+ if (NS_WARN_IF(!io)) {
+ return;
+ }
+ nsCOMPtr<nsIURI> exposableURI;
+ if (NS_FAILED(io->CreateExposableURI(uri, getter_AddRefs(exposableURI))) ||
+ MOZ_UNLIKELY(!exposableURI)) {
+ return;
+ }
+ nsAutoCString theURL;
+ if (NS_SUCCEEDED(exposableURI->GetSpec(theURL))) {
+ CopyUTF8toUTF16(theURL, aURL);
+ }
+}
+
+void DocAccessible::Title(nsString& aTitle) const {
+ mDocumentNode->GetTitle(aTitle);
+}
+
+void DocAccessible::MimeType(nsAString& aType) const {
+ mDocumentNode->GetContentType(aType);
+}
+
+void DocAccessible::DocType(nsAString& aType) const {
+ dom::DocumentType* docType = mDocumentNode->GetDoctype();
+ if (docType) docType->GetPublicId(aType);
+}
+
+void DocAccessible::QueueCacheUpdate(LocalAccessible* aAcc,
+ uint64_t aNewDomain) {
+ if (!mIPCDoc) {
+ return;
+ }
+ // These strong references aren't necessary because WithEntryHandle is
+ // guaranteed to run synchronously. However, static analysis complains without
+ // them.
+ RefPtr<DocAccessible> self = this;
+ RefPtr<LocalAccessible> acc = aAcc;
+ size_t arrayIndex =
+ mQueuedCacheUpdatesHash.WithEntryHandle(aAcc, [self, acc](auto&& entry) {
+ if (entry.HasEntry()) {
+ // This LocalAccessible has already been queued. Return its index in
+ // the queue array so we can update its queued domains.
+ return entry.Data();
+ }
+ // Add this LocalAccessible to the queue array.
+ size_t index = self->mQueuedCacheUpdatesArray.Length();
+ self->mQueuedCacheUpdatesArray.EmplaceBack(std::make_pair(acc, 0));
+ // Also add it to the hash map so we can avoid processing the same
+ // LocalAccessible twice.
+ return entry.Insert(index);
+ });
+ auto& [arrayAcc, domain] = mQueuedCacheUpdatesArray[arrayIndex];
+ MOZ_ASSERT(arrayAcc == aAcc);
+ domain |= aNewDomain;
+ Controller()->ScheduleProcessing();
+}
+
+void DocAccessible::QueueCacheUpdateForDependentRelations(
+ LocalAccessible* aAcc) {
+ if (!mIPCDoc || !aAcc || !aAcc->Elm() || !aAcc->IsInDocument() ||
+ aAcc->IsDefunct()) {
+ return;
+ }
+ nsAutoString ID;
+ aAcc->DOMNodeID(ID);
+ if (AttrRelProviders* list = GetRelProviders(aAcc->Elm(), ID)) {
+ // We call this function when we've noticed an ID change, or when an acc
+ // is getting bound to its document. We need to ensure any existing accs
+ // that depend on this acc's ID have their rel cache entries updated.
+ for (const auto& provider : *list) {
+ LocalAccessible* relatedAcc = GetAccessible(provider->mContent);
+ if (!relatedAcc || relatedAcc->IsDefunct() ||
+ !relatedAcc->IsInDocument() ||
+ mInsertedAccessibles.Contains(relatedAcc)) {
+ continue;
+ }
+ QueueCacheUpdate(relatedAcc, CacheDomain::Relations);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible
+
+void DocAccessible::Init() {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocCreate)) {
+ logging::DocCreate("document initialize", mDocumentNode, this);
+ }
+#endif
+
+ // Initialize notification controller.
+ mNotificationController = new NotificationController(this, mPresShell);
+
+ // Mark the DocAccessible as loaded if its DOM document is already loaded at
+ // this point. This can happen for one of three reasons:
+ // 1. A11y was started late.
+ // 2. DOM loading for a document (probably an in-process iframe) completed
+ // before its Accessible container was created.
+ // 3. The PresShell for the document was created after DOM loading completed.
+ // In that case, we tried to create the DocAccessible when DOM loading
+ // completed, but we can't create a DocAccessible without a PresShell, so
+ // this failed. The DocAccessible was subsequently created due to a layout
+ // notification.
+ if (mDocumentNode->GetReadyStateEnum() ==
+ dom::Document::READYSTATE_COMPLETE) {
+ mLoadState |= eDOMLoaded;
+ // If this happened due to reasons 1 or 2, it isn't *necessary* to fire a
+ // doc load complete event. If it happened due to reason 3, we need to fire
+ // doc load complete because clients (especially tests) might be waiting
+ // for the document to load using this event. We can't distinguish why this
+ // happened at this point, so just fire it regardless. It won't do any
+ // harm even if it isn't necessary. We set mLoadEventType here and it will
+ // be fired in ProcessLoad as usual.
+ mLoadEventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE;
+ } else if (mDocumentNode->IsInitialDocument()) {
+ // The initial about:blank document will never finish loading, so we can
+ // immediately mark it loaded to avoid waiting for its load.
+ mLoadState |= eDOMLoaded;
+ }
+
+ AddEventListeners();
+}
+
+void DocAccessible::Shutdown() {
+ if (!mPresShell) { // already shutdown
+ return;
+ }
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocDestroy)) {
+ logging::DocDestroy("document shutdown", mDocumentNode, this);
+ }
+#endif
+
+ // Mark the document as shutdown before AT is notified about the document
+ // removal from its container (valid for root documents on ATK and due to
+ // some reason for MSAA, refer to bug 757392 for details).
+ mStateFlags |= eIsDefunct;
+
+ if (mNotificationController) {
+ mNotificationController->Shutdown();
+ mNotificationController = nullptr;
+ }
+
+ RemoveEventListeners();
+
+ // mParent->RemoveChild clears mParent, but we need to know whether we were a
+ // child later, so use a flag.
+ const bool isChild = !!mParent;
+ if (mParent) {
+ DocAccessible* parentDocument = mParent->Document();
+ if (parentDocument) parentDocument->RemoveChildDocument(this);
+
+ mParent->RemoveChild(this);
+ MOZ_ASSERT(!mParent, "Parent has to be null!");
+ }
+
+ mPresShell->SetDocAccessible(nullptr);
+ mPresShell = nullptr; // Avoid reentrancy
+
+ // Walk the array backwards because child documents remove themselves from the
+ // array as they are shutdown.
+ int32_t childDocCount = mChildDocuments.Length();
+ for (int32_t idx = childDocCount - 1; idx >= 0; idx--) {
+ mChildDocuments[idx]->Shutdown();
+ }
+
+ mChildDocuments.Clear();
+ // mQueuedCacheUpdates* can contain a reference to this document (ex. if the
+ // doc is scrollable and we're sending a scroll position update). Clear the
+ // map here to avoid creating ref cycles.
+ mQueuedCacheUpdatesArray.Clear();
+ mQueuedCacheUpdatesHash.Clear();
+
+ // XXX thinking about ordering?
+ if (mIPCDoc) {
+ MOZ_ASSERT(IPCAccessibilityActive());
+ mIPCDoc->Shutdown();
+ MOZ_ASSERT(!mIPCDoc);
+ }
+
+ mDependentIDsHashes.Clear();
+ mNodeToAccessibleMap.Clear();
+
+ mAnchorJumpElm = nullptr;
+ mInvalidationList.Clear();
+ mPendingUpdates.Clear();
+
+ for (auto iter = mAccessibleCache.Iter(); !iter.Done(); iter.Next()) {
+ LocalAccessible* accessible = iter.Data();
+ MOZ_ASSERT(accessible);
+ if (accessible) {
+ // This might have been focused with FocusManager::ActiveItemChanged. In
+ // that case, we must notify FocusManager so that it clears the active
+ // item. Otherwise, it will hold on to a defunct Accessible. Normally,
+ // this happens in UnbindFromDocument, but we don't call that when the
+ // whole document shuts down.
+ if (FocusMgr()->WasLastFocused(accessible)) {
+ FocusMgr()->ActiveItemChanged(nullptr);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("doc shutdown", accessible);
+ }
+#endif
+ }
+ if (!accessible->IsDefunct()) {
+ // Unlink parent to avoid its cleaning overhead in shutdown.
+ accessible->mParent = nullptr;
+ accessible->Shutdown();
+ }
+ }
+ iter.Remove();
+ }
+
+ HyperTextAccessible::Shutdown();
+
+ MOZ_ASSERT(GetAccService());
+ GetAccService()->NotifyOfDocumentShutdown(
+ this, mDocumentNode,
+ // Make sure we don't shut down AccService while a parent document is
+ // still shutting down. The parent will allow service shutdown when it
+ // reaches this point.
+ /* aAllowServiceShutdown */ !isChild);
+ mDocumentNode = nullptr;
+}
+
+nsIFrame* DocAccessible::GetFrame() const {
+ nsIFrame* root = nullptr;
+ if (mPresShell) {
+ root = mPresShell->GetRootFrame();
+ }
+
+ return root;
+}
+
+nsINode* DocAccessible::GetNode() const { return mDocumentNode; }
+
+// DocAccessible protected member
+nsRect DocAccessible::RelativeBounds(nsIFrame** aRelativeFrame) const {
+ *aRelativeFrame = GetFrame();
+
+ dom::Document* document = mDocumentNode;
+ dom::Document* parentDoc = nullptr;
+
+ nsRect bounds;
+ while (document) {
+ PresShell* presShell = document->GetPresShell();
+ if (!presShell) {
+ return nsRect();
+ }
+
+ nsRect scrollPort;
+ nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable();
+ if (sf) {
+ scrollPort = sf->GetScrollPortRect();
+ } else {
+ nsIFrame* rootFrame = presShell->GetRootFrame();
+ if (!rootFrame) return nsRect();
+
+ scrollPort = rootFrame->GetRect();
+ }
+
+ if (parentDoc) { // After first time thru loop
+ // XXXroc bogus code! scrollPort is relative to the viewport of
+ // this document, but we're intersecting rectangles derived from
+ // multiple documents and assuming they're all in the same coordinate
+ // system. See bug 514117.
+ bounds.IntersectRect(scrollPort, bounds);
+ } else { // First time through loop
+ bounds = scrollPort;
+ }
+
+ document = parentDoc = document->GetInProcessParentDocument();
+ }
+
+ return bounds;
+}
+
+// DocAccessible protected member
+nsresult DocAccessible::AddEventListeners() {
+ SelectionMgr()->AddDocSelectionListener(mPresShell);
+
+ // Add document observer.
+ mDocumentNode->AddObserver(this);
+ return NS_OK;
+}
+
+// DocAccessible protected member
+nsresult DocAccessible::RemoveEventListeners() {
+ // Remove listeners associated with content documents
+ NS_ASSERTION(mDocumentNode, "No document during removal of listeners.");
+
+ if (mDocumentNode) {
+ mDocumentNode->RemoveObserver(this);
+ }
+
+ if (mScrollWatchTimer) {
+ mScrollWatchTimer->Cancel();
+ mScrollWatchTimer = nullptr;
+ NS_RELEASE_THIS(); // Kung fu death grip
+ }
+
+ SelectionMgr()->RemoveDocSelectionListener(mPresShell);
+ return NS_OK;
+}
+
+void DocAccessible::ScrollTimerCallback(nsITimer* aTimer, void* aClosure) {
+ DocAccessible* docAcc = reinterpret_cast<DocAccessible*>(aClosure);
+
+ if (docAcc) {
+ // Dispatch a scroll-end for all entries in table. They have not
+ // been scrolled in at least `kScrollEventInterval`.
+ for (auto iter = docAcc->mLastScrollingDispatch.Iter(); !iter.Done();
+ iter.Next()) {
+ docAcc->DispatchScrollingEvent(iter.Key(),
+ nsIAccessibleEvent::EVENT_SCROLLING_END);
+ iter.Remove();
+ }
+
+ if (docAcc->mScrollWatchTimer) {
+ docAcc->mScrollWatchTimer = nullptr;
+ NS_RELEASE(docAcc); // Release kung fu death grip
+ }
+ }
+}
+
+void DocAccessible::HandleScroll(nsINode* aTarget) {
+ nsINode* target = aTarget;
+ LocalAccessible* targetAcc = GetAccessible(target);
+ if (!targetAcc && target->IsInNativeAnonymousSubtree()) {
+ // The scroll event for textareas comes from a native anonymous div. We need
+ // the closest non-anonymous ancestor to get the right Accessible.
+ target = target->GetClosestNativeAnonymousSubtreeRootParentOrHost();
+ targetAcc = GetAccessible(target);
+ }
+ // Regardless of our scroll timer, we need to send a cache update
+ // to ensure the next Bounds() query accurately reflects our position
+ // after scrolling.
+ if (targetAcc) {
+ QueueCacheUpdate(targetAcc, CacheDomain::ScrollPosition);
+ }
+
+ const uint32_t kScrollEventInterval = 100;
+ // If we haven't dispatched a scrolling event for a target in at least
+ // kScrollEventInterval milliseconds, dispatch one now.
+ mLastScrollingDispatch.WithEntryHandle(target, [&](auto&& lastDispatch) {
+ const TimeStamp now = TimeStamp::Now();
+
+ if (!lastDispatch ||
+ (now - lastDispatch.Data()).ToMilliseconds() >= kScrollEventInterval) {
+ // We can't fire events on a document whose tree isn't constructed yet.
+ if (HasLoadState(eTreeConstructed)) {
+ DispatchScrollingEvent(target, nsIAccessibleEvent::EVENT_SCROLLING);
+ }
+ lastDispatch.InsertOrUpdate(now);
+ }
+ });
+
+ // If timer callback is still pending, push it 100ms into the future.
+ // When scrolling ends and we don't fire this callback anymore, the
+ // timer callback will fire and dispatch an EVENT_SCROLLING_END.
+ if (mScrollWatchTimer) {
+ mScrollWatchTimer->SetDelay(kScrollEventInterval);
+ } else {
+ NS_NewTimerWithFuncCallback(getter_AddRefs(mScrollWatchTimer),
+ ScrollTimerCallback, this, kScrollEventInterval,
+ nsITimer::TYPE_ONE_SHOT,
+ "a11y::DocAccessible::ScrollPositionDidChange");
+ if (mScrollWatchTimer) {
+ NS_ADDREF_THIS(); // Kung fu death grip
+ }
+ }
+}
+
+std::pair<nsPoint, nsRect> DocAccessible::ComputeScrollData(
+ LocalAccessible* aAcc) {
+ nsPoint scrollPoint;
+ nsRect scrollRange;
+
+ if (nsIFrame* frame = aAcc->GetFrame()) {
+ nsIScrollableFrame* sf = aAcc == this
+ ? mPresShell->GetRootScrollFrameAsScrollable()
+ : frame->GetScrollTargetFrame();
+
+ // If there is no scrollable frame, it's likely a scroll in a popup, like
+ // <select>. Return a scroll offset and range of 0. The scroll info
+ // is currently only used on Android, and popups are rendered natively
+ // there.
+ if (sf) {
+ scrollPoint = sf->GetScrollPosition() * mPresShell->GetResolution();
+ scrollRange = sf->GetScrollRange();
+ scrollRange.ScaleRoundOut(mPresShell->GetResolution());
+ }
+ }
+
+ return {scrollPoint, scrollRange};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIDocumentObserver
+
+NS_IMPL_NSIDOCUMENTOBSERVER_CORE_STUB(DocAccessible)
+NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible)
+
+void DocAccessible::AttributeWillChange(dom::Element* aElement,
+ int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType) {
+ LocalAccessible* accessible = GetAccessible(aElement);
+ if (!accessible) {
+ if (aElement != mContent) return;
+
+ accessible = this;
+ }
+
+ // Update dependent IDs cache. Take care of elements that are accessible
+ // because dependent IDs cache doesn't contain IDs from non accessible
+ // elements.
+ if (aModType != dom::MutationEvent_Binding::ADDITION) {
+ RemoveDependentIDsFor(accessible, aAttribute);
+ }
+
+ if (aAttribute == nsGkAtoms::id) {
+ if (accessible->IsActiveDescendant()) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::ACTIVE, false);
+ FireDelayedEvent(event);
+ }
+
+ RelocateARIAOwnedIfNeeded(aElement);
+ }
+
+ if (aAttribute == nsGkAtoms::aria_activedescendant) {
+ if (LocalAccessible* activeDescendant = accessible->CurrentItem()) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(activeDescendant, states::ACTIVE, false);
+ FireDelayedEvent(event);
+ }
+ }
+
+ // If attribute affects accessible's state, store the old state so we can
+ // later compare it against the state of the accessible after the attribute
+ // change.
+ if (accessible->AttributeChangesState(aAttribute)) {
+ mPrevStateBits = accessible->State();
+ } else {
+ mPrevStateBits = 0;
+ }
+}
+
+void DocAccessible::AttributeChanged(dom::Element* aElement,
+ int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue) {
+ NS_ASSERTION(!IsDefunct(),
+ "Attribute changed called on defunct document accessible!");
+
+ // Proceed even if the element is not accessible because element may become
+ // accessible if it gets certain attribute.
+ if (UpdateAccessibleOnAttrChange(aElement, aAttribute)) return;
+
+ // Update the accessible tree on aria-hidden change. Make sure to not create
+ // a tree under aria-hidden='true'.
+ if (aAttribute == nsGkAtoms::aria_hidden) {
+ if (aria::HasDefinedARIAHidden(aElement)) {
+ ContentRemoved(aElement);
+ } else {
+ ContentInserted(aElement, aElement->GetNextSibling());
+ }
+ return;
+ }
+
+ LocalAccessible* accessible = GetAccessible(aElement);
+ if (!accessible) {
+ if (mContent == aElement) {
+ // The attribute change occurred on the root content of this
+ // DocAccessible, so handle it as an attribute change on this.
+ accessible = this;
+ } else {
+ if (aModType == dom::MutationEvent_Binding::ADDITION &&
+ aria::AttrCharacteristicsFor(aAttribute) & ATTR_GLOBAL) {
+ // The element doesn't have an Accessible, but a global ARIA attribute
+ // was just added, which means we should probably create an Accessible.
+ ContentInserted(aElement, aElement->GetNextSibling());
+ return;
+ }
+ // The element doesn't have an Accessible, so ignore the attribute
+ // change.
+ return;
+ }
+ }
+
+ MOZ_ASSERT(accessible->IsBoundToParent() || accessible->IsDoc(),
+ "DOM attribute change on an accessible detached from the tree");
+
+ if (aAttribute == nsGkAtoms::id) {
+ dom::Element* elm = accessible->Elm();
+ RelocateARIAOwnedIfNeeded(elm);
+ ARIAActiveDescendantIDMaybeMoved(accessible);
+ QueueCacheUpdate(accessible, CacheDomain::DOMNodeIDAndClass);
+ QueueCacheUpdateForDependentRelations(accessible);
+ }
+
+ // The activedescendant universal property redirects accessible focus events
+ // to the element with the id that activedescendant points to. Make sure
+ // the tree up to date before processing. In other words, when a node has just
+ // been inserted, the tree won't be up to date yet, so we must always schedule
+ // an async notification so that a newly inserted node will be present in
+ // the tree.
+ if (aAttribute == nsGkAtoms::aria_activedescendant) {
+ mNotificationController
+ ->ScheduleNotification<DocAccessible, LocalAccessible>(
+ this, &DocAccessible::ARIAActiveDescendantChanged, accessible);
+ return;
+ }
+
+ // Defer to accessible any needed actions like changing states or emiting
+ // events.
+ accessible->DOMAttributeChanged(aNameSpaceID, aAttribute, aModType, aOldValue,
+ mPrevStateBits);
+
+ // Update dependent IDs cache. We handle elements with accessibles.
+ // If the accessible or element with the ID doesn't exist yet the cache will
+ // be updated when they are added.
+ if (aModType == dom::MutationEvent_Binding::MODIFICATION ||
+ aModType == dom::MutationEvent_Binding::ADDITION) {
+ AddDependentIDsFor(accessible, aAttribute);
+ }
+}
+
+void DocAccessible::ARIAAttributeDefaultWillChange(dom::Element* aElement,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ NS_ASSERTION(!IsDefunct(),
+ "Attribute changed called on defunct document accessible!");
+
+ if (aElement->HasAttr(aAttribute)) {
+ return;
+ }
+
+ AttributeWillChange(aElement, kNameSpaceID_None, aAttribute, aModType);
+}
+
+void DocAccessible::ARIAAttributeDefaultChanged(dom::Element* aElement,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ NS_ASSERTION(!IsDefunct(),
+ "Attribute changed called on defunct document accessible!");
+
+ if (aElement->HasAttr(aAttribute)) {
+ return;
+ }
+
+ AttributeChanged(aElement, kNameSpaceID_None, aAttribute, aModType, nullptr);
+}
+
+void DocAccessible::ARIAActiveDescendantChanged(LocalAccessible* aAccessible) {
+ if (dom::Element* elm = aAccessible->Elm()) {
+ nsAutoString id;
+ if (elm->GetAttr(nsGkAtoms::aria_activedescendant, id)) {
+ dom::Element* activeDescendantElm = IDRefsIterator::GetElem(elm, id);
+ if (activeDescendantElm) {
+ LocalAccessible* activeDescendant = GetAccessible(activeDescendantElm);
+ if (activeDescendant) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(activeDescendant, states::ACTIVE, true);
+ FireDelayedEvent(event);
+ if (aAccessible->IsActiveWidget()) {
+ FocusMgr()->ActiveItemChanged(activeDescendant, false);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("ARIA activedescedant changed",
+ activeDescendant);
+ }
+#endif
+ }
+ return;
+ }
+ }
+ }
+
+ // aria-activedescendant was cleared or changed to a non-existent node.
+ // Move focus back to the element itself if it has DOM focus.
+ if (aAccessible->IsActiveWidget()) {
+ FocusMgr()->ActiveItemChanged(aAccessible, false);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("ARIA activedescedant cleared",
+ aAccessible);
+ }
+#endif
+ }
+ }
+}
+
+void DocAccessible::ContentAppended(nsIContent* aFirstNewContent) {
+ MaybeHandleChangeToHiddenNameOrDescription(aFirstNewContent);
+}
+
+void DocAccessible::ElementStateChanged(dom::Document* aDocument,
+ dom::Element* aElement,
+ dom::ElementState aStateMask) {
+ if (aStateMask.HasState(dom::ElementState::READWRITE) &&
+ aElement == mDocumentNode->GetRootElement()) {
+ // This handles changes to designMode. contentEditable is handled by
+ // LocalAccessible::AttributeChangesState and
+ // LocalAccessible::DOMAttributeChanged.
+ const bool isEditable =
+ aElement->State().HasState(dom::ElementState::READWRITE);
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(this, states::EDITABLE, isEditable);
+ FireDelayedEvent(event);
+ event = new AccStateChangeEvent(this, states::READONLY, !isEditable);
+ FireDelayedEvent(event);
+ }
+
+ LocalAccessible* accessible = GetAccessible(aElement);
+ if (!accessible) return;
+
+ if (aStateMask.HasState(dom::ElementState::CHECKED)) {
+ LocalAccessible* widget = accessible->ContainerWidget();
+ if (widget && widget->IsSelect()) {
+ // Changing selection here changes what we cache for
+ // the viewport.
+ SetViewportCacheDirty(true);
+ AccSelChangeEvent::SelChangeType selChangeType =
+ aElement->State().HasState(dom::ElementState::CHECKED)
+ ? AccSelChangeEvent::eSelectionAdd
+ : AccSelChangeEvent::eSelectionRemove;
+ RefPtr<AccEvent> event =
+ new AccSelChangeEvent(widget, accessible, selChangeType);
+ FireDelayedEvent(event);
+ return;
+ }
+
+ RefPtr<AccEvent> event = new AccStateChangeEvent(
+ accessible, states::CHECKED,
+ aElement->State().HasState(dom::ElementState::CHECKED));
+ FireDelayedEvent(event);
+ }
+
+ if (aStateMask.HasState(dom::ElementState::INVALID)) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::INVALID);
+ FireDelayedEvent(event);
+ }
+
+ if (aStateMask.HasState(dom::ElementState::REQUIRED)) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::REQUIRED);
+ FireDelayedEvent(event);
+ }
+
+ if (aStateMask.HasState(dom::ElementState::VISITED)) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::TRAVERSED, true);
+ FireDelayedEvent(event);
+ }
+
+ // We only expose dom::ElementState::DEFAULT on buttons, but we can get
+ // notifications for other controls like checkboxes.
+ if (aStateMask.HasState(dom::ElementState::DEFAULT) &&
+ accessible->IsButton()) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::DEFAULT);
+ FireDelayedEvent(event);
+ }
+}
+
+void DocAccessible::CharacterDataWillChange(nsIContent* aContent,
+ const CharacterDataChangeInfo&) {}
+
+void DocAccessible::CharacterDataChanged(nsIContent* aContent,
+ const CharacterDataChangeInfo&) {}
+
+void DocAccessible::ContentInserted(nsIContent* aChild) {
+ MaybeHandleChangeToHiddenNameOrDescription(aChild);
+}
+
+void DocAccessible::ContentRemoved(nsIContent* aChildNode,
+ nsIContent* aPreviousSiblingNode) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eTree)) {
+ logging::MsgBegin("TREE", "DOM content removed; doc: %p", this);
+ logging::Node("container node", aChildNode->GetParent());
+ logging::Node("content node", aChildNode);
+ logging::MsgEnd();
+ }
+#endif
+ ContentRemoved(aChildNode);
+}
+
+void DocAccessible::ParentChainChanged(nsIContent* aContent) {}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible
+
+#ifdef A11Y_LOG
+nsresult DocAccessible::HandleAccEvent(AccEvent* aEvent) {
+ if (logging::IsEnabled(logging::eDocLoad)) {
+ logging::DocLoadEventHandled(aEvent);
+ }
+
+ return HyperTextAccessible::HandleAccEvent(aEvent);
+}
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+// Public members
+
+nsPresContext* DocAccessible::PresContext() const {
+ return mPresShell->GetPresContext();
+}
+
+void* DocAccessible::GetNativeWindow() const {
+ if (!mPresShell) {
+ return nullptr;
+ }
+
+ nsViewManager* vm = mPresShell->GetViewManager();
+ if (!vm) return nullptr;
+
+ nsCOMPtr<nsIWidget> widget = vm->GetRootWidget();
+ if (widget) return widget->GetNativeData(NS_NATIVE_WINDOW);
+
+ return nullptr;
+}
+
+LocalAccessible* DocAccessible::GetAccessibleByUniqueIDInSubtree(
+ void* aUniqueID) {
+ LocalAccessible* child = GetAccessibleByUniqueID(aUniqueID);
+ if (child) return child;
+
+ uint32_t childDocCount = mChildDocuments.Length();
+ for (uint32_t childDocIdx = 0; childDocIdx < childDocCount; childDocIdx++) {
+ DocAccessible* childDocument = mChildDocuments.ElementAt(childDocIdx);
+ child = childDocument->GetAccessibleByUniqueIDInSubtree(aUniqueID);
+ if (child) return child;
+ }
+
+ return nullptr;
+}
+
+LocalAccessible* DocAccessible::GetAccessibleOrContainer(
+ nsINode* aNode, bool aNoContainerIfPruned) const {
+ if (!aNode || !aNode->GetComposedDoc()) {
+ return nullptr;
+ }
+
+ nsINode* start = aNode;
+ if (auto* shadowRoot = dom::ShadowRoot::FromNode(aNode)) {
+ // This can happen, for example, when called within
+ // SelectionManager::ProcessSelectionChanged due to focusing a direct
+ // child of a shadow root.
+ // GetFlattenedTreeParent works on children of a shadow root, but not the
+ // shadow root itself.
+ start = shadowRoot->GetHost();
+ if (!start) {
+ return nullptr;
+ }
+ }
+
+ for (nsINode* currNode : dom::InclusiveFlatTreeAncestors(*start)) {
+ // No container if is inside of aria-hidden subtree.
+ if (aNoContainerIfPruned && currNode->IsElement() &&
+ aria::HasDefinedARIAHidden(currNode->AsElement())) {
+ return nullptr;
+ }
+
+ // Check if node is in zero-sized map
+ if (aNoContainerIfPruned && currNode->IsHTMLElement(nsGkAtoms::map)) {
+ if (nsIFrame* frame = currNode->AsContent()->GetPrimaryFrame()) {
+ if (nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame->GetParent())
+ .IsEmpty()) {
+ return nullptr;
+ }
+ }
+ }
+
+ if (LocalAccessible* accessible = GetAccessible(currNode)) {
+ return accessible;
+ }
+ }
+
+ return nullptr;
+}
+
+LocalAccessible* DocAccessible::GetContainerAccessible(nsINode* aNode) const {
+ return aNode ? GetAccessibleOrContainer(aNode->GetFlattenedTreeParentNode())
+ : nullptr;
+}
+
+LocalAccessible* DocAccessible::GetAccessibleOrDescendant(
+ nsINode* aNode) const {
+ LocalAccessible* acc = GetAccessible(aNode);
+ if (acc) return acc;
+
+ if (aNode == mContent || aNode == mDocumentNode->GetRootElement()) {
+ // If the node is the doc's body or root element, return the doc accessible.
+ return const_cast<DocAccessible*>(this);
+ }
+
+ acc = GetContainerAccessible(aNode);
+ if (acc) {
+ TreeWalker walker(acc, aNode->AsContent(),
+ TreeWalker::eWalkCache | TreeWalker::eScoped);
+ return walker.Next();
+ }
+
+ return nullptr;
+}
+
+void DocAccessible::BindToDocument(LocalAccessible* aAccessible,
+ const nsRoleMapEntry* aRoleMapEntry) {
+ // Put into DOM node cache.
+ if (aAccessible->IsNodeMapEntry()) {
+ mNodeToAccessibleMap.InsertOrUpdate(aAccessible->GetNode(), aAccessible);
+ }
+
+ // Put into unique ID cache.
+ mAccessibleCache.InsertOrUpdate(aAccessible->UniqueID(), RefPtr{aAccessible});
+
+ aAccessible->SetRoleMapEntry(aRoleMapEntry);
+
+ if (aAccessible->HasOwnContent()) {
+ AddDependentIDsFor(aAccessible);
+
+ nsIContent* content = aAccessible->GetContent();
+ if (content->IsElement() &&
+ content->AsElement()->HasAttr(nsGkAtoms::aria_owns)) {
+ mNotificationController->ScheduleRelocation(aAccessible);
+ }
+ }
+
+ if (mIPCDoc) {
+ mInsertedAccessibles.EnsureInserted(aAccessible);
+ }
+
+ QueueCacheUpdateForDependentRelations(aAccessible);
+}
+
+void DocAccessible::UnbindFromDocument(LocalAccessible* aAccessible) {
+ NS_ASSERTION(mAccessibleCache.GetWeak(aAccessible->UniqueID()),
+ "Unbinding the unbound accessible!");
+
+ // Fire focus event on accessible having DOM focus if last focus was removed
+ // from the tree.
+ if (FocusMgr()->WasLastFocused(aAccessible)) {
+ FocusMgr()->ActiveItemChanged(nullptr);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("tree shutdown", aAccessible);
+ }
+#endif
+ }
+
+ // Remove an accessible from node-to-accessible map if it exists there.
+ if (aAccessible->IsNodeMapEntry() &&
+ mNodeToAccessibleMap.Get(aAccessible->GetNode()) == aAccessible) {
+ mNodeToAccessibleMap.Remove(aAccessible->GetNode());
+ }
+
+ aAccessible->mStateFlags |= eIsNotInDocument;
+
+ // Update XPCOM part.
+ xpcAccessibleDocument* xpcDoc = GetAccService()->GetCachedXPCDocument(this);
+ if (xpcDoc) xpcDoc->NotifyOfShutdown(aAccessible);
+
+ void* uniqueID = aAccessible->UniqueID();
+
+ NS_ASSERTION(!aAccessible->IsDefunct(), "Shutdown the shutdown accessible!");
+ aAccessible->Shutdown();
+
+ mAccessibleCache.Remove(uniqueID);
+}
+
+void DocAccessible::ContentInserted(nsIContent* aStartChildNode,
+ nsIContent* aEndChildNode) {
+ // Ignore content insertions until we constructed accessible tree. Otherwise
+ // schedule tree update on content insertion after layout.
+ if (!mNotificationController || !HasLoadState(eTreeConstructed)) {
+ return;
+ }
+
+ // The frame constructor guarantees that only ranges with the same parent
+ // arrive here in presence of dynamic changes to the page, see
+ // nsCSSFrameConstructor::IssueSingleInsertNotifications' callers.
+ nsINode* parent = aStartChildNode->GetFlattenedTreeParentNode();
+ if (!parent) {
+ return;
+ }
+
+ LocalAccessible* container = AccessibleOrTrueContainer(parent);
+ if (!container) {
+ return;
+ }
+
+ AutoTArray<nsCOMPtr<nsIContent>, 10> list;
+ for (nsIContent* node = aStartChildNode; node != aEndChildNode;
+ node = node->GetNextSibling()) {
+ MOZ_ASSERT(parent == node->GetFlattenedTreeParentNode());
+ if (PruneOrInsertSubtree(node)) {
+ list.AppendElement(node);
+ }
+ }
+
+ mNotificationController->ScheduleContentInsertion(container, list);
+}
+
+void DocAccessible::ScheduleTreeUpdate(nsIContent* aContent) {
+ if (mPendingUpdates.Contains(aContent)) {
+ return;
+ }
+ mPendingUpdates.AppendElement(aContent);
+ mNotificationController->ScheduleProcessing();
+}
+
+void DocAccessible::ProcessPendingUpdates() {
+ auto updates = std::move(mPendingUpdates);
+ for (auto update : updates) {
+ if (update->GetComposedDoc() != mDocumentNode) {
+ continue;
+ }
+ // The pruning logic will take care of avoiding unnecessary notifications.
+ ContentInserted(update, update->GetNextSibling());
+ }
+}
+
+bool DocAccessible::PruneOrInsertSubtree(nsIContent* aRoot) {
+ bool insert = false;
+
+ // In the case that we are, or are in, a shadow host, we need to assure
+ // some accessibles are removed if they are not rendered anymore.
+ nsIContent* shadowHost =
+ aRoot->GetShadowRoot() ? aRoot : aRoot->GetContainingShadowHost();
+ if (shadowHost) {
+ // Check all explicit children in the host, if they are not slotted
+ // then remove their accessibles and subtrees.
+ for (nsIContent* childNode = shadowHost->GetFirstChild(); childNode;
+ childNode = childNode->GetNextSibling()) {
+ if (!childNode->GetPrimaryFrame() &&
+ !nsCoreUtils::CanCreateAccessibleWithoutFrame(childNode)) {
+ ContentRemoved(childNode);
+ }
+ }
+
+ // If this is a slot, check to see if its fallback content is rendered,
+ // if not - remove it.
+ if (aRoot->IsHTMLElement(nsGkAtoms::slot)) {
+ for (nsIContent* childNode = aRoot->GetFirstChild(); childNode;
+ childNode = childNode->GetNextSibling()) {
+ if (!childNode->GetPrimaryFrame() &&
+ !nsCoreUtils::CanCreateAccessibleWithoutFrame(childNode)) {
+ ContentRemoved(childNode);
+ }
+ }
+ }
+ }
+
+ // If we already have an accessible, check if we need to remove it, recreate
+ // it, or keep it in place.
+ LocalAccessible* acc = GetAccessible(aRoot);
+ if (acc) {
+ MOZ_ASSERT(aRoot == acc->GetContent(),
+ "LocalAccessible has differing content!");
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eTree)) {
+ logging::MsgBegin(
+ "TREE", "inserted content already has accessible; doc: %p", this);
+ logging::Node("content node", aRoot);
+ logging::AccessibleInfo("accessible node", acc);
+ logging::MsgEnd();
+ }
+#endif
+
+ nsIFrame* frame = acc->GetFrame();
+ if (frame) {
+ acc->MaybeQueueCacheUpdateForStyleChanges();
+ }
+
+ // LocalAccessible has no frame and it's not display:contents. Remove it.
+ // As well as removing the a11y subtree, we must also remove Accessibles
+ // for DOM descendants, since some of these might be relocated Accessibles
+ // and their DOM nodes are now hidden as well.
+ if (!frame && !nsCoreUtils::CanCreateAccessibleWithoutFrame(aRoot)) {
+ ContentRemoved(aRoot);
+ return false;
+ }
+
+ // If the frame is hidden because its ancestor is specified with
+ // `content-visibility: hidden`, remove its Accessible.
+ if (frame && frame->IsHiddenByContentVisibilityOnAnyAncestor(
+ nsIFrame::IncludeContentVisibility::Hidden)) {
+ ContentRemoved(aRoot);
+ return false;
+ }
+
+ // If it's a XULLabel it was probably reframed because a `value` attribute
+ // was added. The accessible creates its text leaf upon construction, so we
+ // need to recreate. Remove it, and schedule for reconstruction.
+ if (acc->IsXULLabel()) {
+ ContentRemoved(acc);
+ return true;
+ }
+
+ // It is a broken image that is being reframed because it either got
+ // or lost an `alt` tag that would rerender this node as text.
+ if (frame && (acc->IsImage() != (frame->AccessibleType() == eImageType))) {
+ ContentRemoved(aRoot);
+ return true;
+ }
+
+ // If the frame is an OuterDoc frame but this isn't an OuterDocAccessible,
+ // we need to recreate the LocalAccessible. This can happen for embed or
+ // object elements if their embedded content changes to be web content.
+ if (frame && !acc->IsOuterDoc() &&
+ frame->AccessibleType() == eOuterDocType) {
+ ContentRemoved(aRoot);
+ return true;
+ }
+
+ // If the content is focused, and is being re-framed, reset the selection
+ // listener for the node because the previous selection listener is on the
+ // old frame.
+ if (aRoot->IsElement() && FocusMgr()->HasDOMFocus(aRoot)) {
+ SelectionMgr()->SetControlSelectionListener(aRoot->AsElement());
+ }
+
+ // If the accessible is a table, or table part, its layout table
+ // status may have changed. We need to invalidate the associated
+ // mac table cache, which listens for the following event. We don't
+ // use this cache when the core cache is enabled, so to minimise event
+ // traffic only fire this event when that cache is off.
+ if (acc->IsTable() || acc->IsTableRow() || acc->IsTableCell()) {
+ LocalAccessible* table = nsAccUtils::TableFor(acc);
+ if (table && table->IsTable()) {
+ QueueCacheUpdate(table, CacheDomain::Table);
+ }
+ }
+
+ // The accessible can be reparented or reordered in its parent.
+ // We schedule it for reinsertion. For example, a slotted element
+ // can change its slot attribute to a different slot.
+ insert = true;
+
+ // If the frame is invisible, remove it.
+ // Normally, layout sends explicit a11y notifications for visibility
+ // changes (see SendA11yNotifications in RestyleManager). However, if a
+ // visibility change also reconstructs the frame, we must handle it here.
+ if (frame && !frame->StyleVisibility()->IsVisible()) {
+ ContentRemoved(aRoot);
+ // There might be visible descendants, so we want to walk the subtree.
+ // However, we know we don't want to reinsert this node, so we set insert
+ // to false.
+ insert = false;
+ }
+ } else {
+ // If there is no current accessible, and the node has a frame, or is
+ // display:contents, schedule it for insertion.
+ if (aRoot->GetPrimaryFrame() ||
+ nsCoreUtils::CanCreateAccessibleWithoutFrame(aRoot)) {
+ // This may be a new subtree, the insertion process will recurse through
+ // its descendants.
+ if (!GetAccessibleOrDescendant(aRoot)) {
+ return true;
+ }
+
+ // Content is not an accessible, but has accessible descendants.
+ // We schedule this container for insertion strictly for the case where it
+ // itself now needs an accessible. We will still need to recurse into the
+ // descendant content to prune accessibles, and in all likelyness to
+ // insert accessibles since accessible insertions will likeley get missed
+ // in an existing subtree.
+ insert = true;
+ }
+ }
+
+ if (LocalAccessible* container = AccessibleOrTrueContainer(aRoot)) {
+ AutoTArray<nsCOMPtr<nsIContent>, 10> list;
+ dom::AllChildrenIterator iter =
+ dom::AllChildrenIterator(aRoot, nsIContent::eAllChildren, true);
+ while (nsIContent* childNode = iter.GetNextChild()) {
+ if (PruneOrInsertSubtree(childNode)) {
+ list.AppendElement(childNode);
+ }
+ }
+
+ if (!list.IsEmpty()) {
+ mNotificationController->ScheduleContentInsertion(container, list);
+ }
+ }
+
+ return insert;
+}
+
+void DocAccessible::RecreateAccessible(nsIContent* aContent) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eTree)) {
+ logging::MsgBegin("TREE", "accessible recreated");
+ logging::Node("content", aContent);
+ logging::MsgEnd();
+ }
+#endif
+
+ // XXX: we shouldn't recreate whole accessible subtree, instead we should
+ // subclass hide and show events to handle them separately and implement their
+ // coalescence with normal hide and show events. Note, in this case they
+ // should be coalesced with normal show/hide events.
+ ContentRemoved(aContent);
+ ContentInserted(aContent, aContent->GetNextSibling());
+}
+
+void DocAccessible::ProcessInvalidationList() {
+ // Invalidate children of container accessible for each element in
+ // invalidation list. Allow invalidation list insertions while container
+ // children are recached.
+ for (uint32_t idx = 0; idx < mInvalidationList.Length(); idx++) {
+ nsIContent* content = mInvalidationList[idx];
+ if (!HasAccessible(content) && content->HasID()) {
+ LocalAccessible* container = GetContainerAccessible(content);
+ if (container) {
+ // Check if the node is a target of aria-owns, and if so, don't process
+ // it here and let DoARIAOwnsRelocation process it.
+ AttrRelProviders* list = GetRelProviders(
+ content->AsElement(), nsDependentAtomString(content->GetID()));
+ bool shouldProcess = !!list;
+ if (shouldProcess) {
+ for (uint32_t idx = 0; idx < list->Length(); idx++) {
+ if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) {
+ shouldProcess = false;
+ break;
+ }
+ }
+
+ if (shouldProcess) {
+ ProcessContentInserted(container, content);
+ }
+ }
+ }
+ }
+ }
+
+ mInvalidationList.Clear();
+}
+
+void DocAccessible::ProcessQueuedCacheUpdates() {
+ AUTO_PROFILER_MARKER_TEXT("DocAccessible::ProcessQueuedCacheUpdates", A11Y,
+ {}, ""_ns);
+ PerfStats::AutoMetricRecording<
+ PerfStats::Metric::A11Y_ProcessQueuedCacheUpdate>
+ autoRecording;
+ // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS.
+
+ nsTArray<CacheData> data;
+ for (auto [acc, domain] : mQueuedCacheUpdatesArray) {
+ if (acc && acc->IsInDocument() && !acc->IsDefunct()) {
+ RefPtr<AccAttributes> fields =
+ acc->BundleFieldsForCache(domain, CacheUpdateType::Update);
+
+ if (fields->Count()) {
+ data.AppendElement(CacheData(
+ acc->IsDoc() ? 0 : reinterpret_cast<uint64_t>(acc->UniqueID()),
+ fields));
+ }
+ }
+ }
+
+ mQueuedCacheUpdatesArray.Clear();
+ mQueuedCacheUpdatesHash.Clear();
+
+ if (mViewportCacheDirty) {
+ RefPtr<AccAttributes> fields =
+ BundleFieldsForCache(CacheDomain::Viewport, CacheUpdateType::Update);
+ if (fields->Count()) {
+ data.AppendElement(CacheData(0, fields));
+ }
+ mViewportCacheDirty = false;
+ }
+
+ if (data.Length()) {
+ IPCDoc()->SendCache(CacheUpdateType::Update, data);
+ }
+}
+
+void DocAccessible::SendAccessiblesWillMove() {
+ if (!mIPCDoc) {
+ return;
+ }
+ nsTArray<uint64_t> ids;
+ for (LocalAccessible* acc : mMovedAccessibles) {
+ // If acc is defunct or not in a document, it was removed after it was
+ // moved.
+ if (!acc->IsDefunct() && acc->IsInDocument()) {
+ ids.AppendElement(reinterpret_cast<uintptr_t>(acc->UniqueID()));
+ // acc might have been re-parented. Since we cache bounds relative to the
+ // parent, we need to update the cache.
+ QueueCacheUpdate(acc, CacheDomain::Bounds);
+ }
+ }
+ if (!ids.IsEmpty()) {
+ mIPCDoc->SendAccessiblesWillMove(ids);
+ }
+}
+
+LocalAccessible* DocAccessible::GetAccessibleEvenIfNotInMap(
+ nsINode* aNode) const {
+ if (!aNode->IsContent() ||
+ !aNode->AsContent()->IsHTMLElement(nsGkAtoms::area)) {
+ return GetAccessible(aNode);
+ }
+
+ // XXX Bug 135040, incorrect when multiple images use the same map.
+ nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
+ nsImageFrame* imageFrame = do_QueryFrame(frame);
+ if (imageFrame) {
+ LocalAccessible* parent = GetAccessible(imageFrame->GetContent());
+ if (parent) {
+ if (HTMLImageMapAccessible* imageMap = parent->AsImageMap()) {
+ return imageMap->GetChildAccessibleFor(aNode);
+ }
+ return nullptr;
+ }
+ }
+
+ return GetAccessible(aNode);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Protected members
+
+void DocAccessible::NotifyOfLoading(bool aIsReloading) {
+ // Mark the document accessible as loading, if it stays alive then we'll mark
+ // it as loaded when we receive proper notification.
+ mLoadState &= ~eDOMLoaded;
+
+ if (!IsLoadEventTarget()) return;
+
+ if (aIsReloading && !mLoadEventType &&
+ // We can't fire events on a document whose tree isn't constructed yet.
+ HasLoadState(eTreeConstructed)) {
+ // Fire reload and state busy events on existing document accessible while
+ // event from user input flag can be calculated properly and accessible
+ // is alive. When new document gets loaded then this one is destroyed.
+ RefPtr<AccEvent> reloadEvent =
+ new AccEvent(nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD, this);
+ nsEventShell::FireEvent(reloadEvent);
+ }
+
+ // Fire state busy change event. Use delayed event since we don't care
+ // actually if event isn't delivered when the document goes away like a shot.
+ RefPtr<AccEvent> stateEvent =
+ new AccStateChangeEvent(this, states::BUSY, true);
+ FireDelayedEvent(stateEvent);
+}
+
+void DocAccessible::DoInitialUpdate() {
+ AUTO_PROFILER_MARKER_TEXT("DocAccessible::DoInitialUpdate", A11Y, {}, ""_ns);
+ PerfStats::AutoMetricRecording<PerfStats::Metric::A11Y_DoInitialUpdate>
+ autoRecording;
+ // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS.
+
+ if (nsCoreUtils::IsTopLevelContentDocInProcess(mDocumentNode)) {
+ mDocFlags |= eTopLevelContentDocInProcess;
+ if (IPCAccessibilityActive()) {
+ nsIDocShell* docShell = mDocumentNode->GetDocShell();
+ if (RefPtr<dom::BrowserChild> browserChild =
+ dom::BrowserChild::GetFrom(docShell)) {
+ // In content processes, top level content documents are always
+ // RootAccessibles.
+ MOZ_ASSERT(IsRoot());
+ DocAccessibleChild* ipcDoc = IPCDoc();
+ if (!ipcDoc) {
+ ipcDoc = new DocAccessibleChild(this, browserChild);
+ MOZ_RELEASE_ASSERT(browserChild->SendPDocAccessibleConstructor(
+ ipcDoc, nullptr, 0, mDocumentNode->GetBrowsingContext()));
+ // trying to recover from this failing is problematic
+ SetIPCDoc(ipcDoc);
+ }
+ }
+ }
+ }
+
+ mLoadState |= eTreeConstructed;
+
+ // Set up a root element and ARIA role mapping.
+ UpdateRootElIfNeeded();
+
+ // Build initial tree.
+ CacheChildrenInSubtree(this);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eVerbose)) {
+ logging::Tree("TREE", "Initial subtree", this);
+ }
+ if (logging::IsEnabled(logging::eTreeSize)) {
+ logging::TreeSize("TREE SIZE", "Initial subtree", this);
+ }
+#endif
+
+ // Fire reorder event after the document tree is constructed. Note, since
+ // this reorder event is processed by parent document then events targeted to
+ // this document may be fired prior to this reorder event. If this is
+ // a problem then consider to keep event processing per tab document.
+ if (!IsRoot()) {
+ RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(LocalParent());
+ ParentDocument()->FireDelayedEvent(reorderEvent);
+ }
+
+ if (ipc::ProcessChild::ExpectingShutdown()) {
+ return;
+ }
+ if (IPCAccessibilityActive()) {
+ DocAccessibleChild* ipcDoc = IPCDoc();
+ MOZ_ASSERT(ipcDoc);
+ if (ipcDoc) {
+ // Send an initial update for this document and its attributes. Each acc
+ // contained in this doc will have its initial update sent in
+ // `InsertIntoIpcTree`.
+ SendCache(CacheDomain::All, CacheUpdateType::Initial);
+
+ for (auto idx = 0U; idx < mChildren.Length(); idx++) {
+ ipcDoc->InsertIntoIpcTree(mChildren.ElementAt(idx), true);
+ }
+ }
+ }
+}
+
+void DocAccessible::ProcessLoad() {
+ mLoadState |= eCompletelyLoaded;
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocLoad)) {
+ logging::DocCompleteLoad(this, IsLoadEventTarget());
+ }
+#endif
+
+ // Do not fire document complete/stop events for root chrome document
+ // accessibles and for frame/iframe documents because
+ // a) screen readers start working on focus event in the case of root chrome
+ // documents
+ // b) document load event on sub documents causes screen readers to act is if
+ // entire page is reloaded.
+ if (!IsLoadEventTarget()) return;
+
+ // Fire complete/load stopped if the load event type is given.
+ if (mLoadEventType) {
+ RefPtr<AccEvent> loadEvent = new AccEvent(mLoadEventType, this);
+ FireDelayedEvent(loadEvent);
+
+ mLoadEventType = 0;
+ }
+
+ // Fire busy state change event.
+ RefPtr<AccEvent> stateEvent =
+ new AccStateChangeEvent(this, states::BUSY, false);
+ FireDelayedEvent(stateEvent);
+}
+
+void DocAccessible::AddDependentIDsFor(LocalAccessible* aRelProvider,
+ nsAtom* aRelAttr) {
+ dom::Element* relProviderEl = aRelProvider->Elm();
+ if (!relProviderEl) return;
+
+ for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) {
+ nsStaticAtom* relAttr = kRelationAttrs[idx];
+ if (aRelAttr && aRelAttr != relAttr) continue;
+
+ if (relAttr == nsGkAtoms::_for) {
+ if (!relProviderEl->IsAnyOfHTMLElements(nsGkAtoms::label,
+ nsGkAtoms::output)) {
+ continue;
+ }
+
+ } else if (relAttr == nsGkAtoms::control) {
+ if (!relProviderEl->IsAnyOfXULElements(nsGkAtoms::label,
+ nsGkAtoms::description)) {
+ continue;
+ }
+ }
+
+ IDRefsIterator iter(this, relProviderEl, relAttr);
+ while (true) {
+ const nsDependentSubstring id = iter.NextID();
+ if (id.IsEmpty()) break;
+
+ AttrRelProviders* providers = GetOrCreateRelProviders(relProviderEl, id);
+ if (providers) {
+ AttrRelProvider* provider = new AttrRelProvider(relAttr, relProviderEl);
+ if (provider) {
+ providers->AppendElement(provider);
+
+ // We've got here during the children caching. If the referenced
+ // content is not accessible then store it to pend its container
+ // children invalidation (this happens immediately after the caching
+ // is finished).
+ nsIContent* dependentContent = iter.GetElem(id);
+ if (dependentContent) {
+ if (!HasAccessible(dependentContent)) {
+ mInvalidationList.AppendElement(dependentContent);
+ }
+ }
+ }
+ }
+ }
+
+ // If the relation attribute is given then we don't have anything else to
+ // check.
+ if (aRelAttr) break;
+ }
+
+ // Make sure to schedule the tree update if needed.
+ mNotificationController->ScheduleProcessing();
+}
+
+void DocAccessible::RemoveDependentIDsFor(LocalAccessible* aRelProvider,
+ nsAtom* aRelAttr) {
+ dom::Element* relProviderElm = aRelProvider->Elm();
+ if (!relProviderElm) return;
+
+ for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) {
+ nsStaticAtom* relAttr = kRelationAttrs[idx];
+ if (aRelAttr && aRelAttr != kRelationAttrs[idx]) continue;
+
+ IDRefsIterator iter(this, relProviderElm, relAttr);
+ while (true) {
+ const nsDependentSubstring id = iter.NextID();
+ if (id.IsEmpty()) break;
+
+ AttrRelProviders* providers = GetRelProviders(relProviderElm, id);
+ if (providers) {
+ providers->RemoveElementsBy(
+ [relAttr, relProviderElm](const auto& provider) {
+ return provider->mRelAttr == relAttr &&
+ provider->mContent == relProviderElm;
+ });
+
+ RemoveRelProvidersIfEmpty(relProviderElm, id);
+ }
+ }
+
+ // If the relation attribute is given then we don't have anything else to
+ // check.
+ if (aRelAttr) break;
+ }
+}
+
+bool DocAccessible::UpdateAccessibleOnAttrChange(dom::Element* aElement,
+ nsAtom* aAttribute) {
+ if (aAttribute == nsGkAtoms::role) {
+ // It is common for js libraries to set the role on the body element after
+ // the document has loaded. In this case we just update the role map entry.
+ if (mContent == aElement) {
+ SetRoleMapEntryForDoc(aElement);
+ if (mIPCDoc) {
+ mIPCDoc->SendRoleChangedEvent(Role(), mRoleMapEntryIndex);
+ }
+
+ return true;
+ }
+
+ // Recreate the accessible when role is changed because we might require a
+ // different accessible class for the new role or the accessible may expose
+ // a different sets of interfaces (COM restriction).
+ RecreateAccessible(aElement);
+
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::multiple) {
+ if (dom::HTMLSelectElement* select =
+ dom::HTMLSelectElement::FromNode(aElement)) {
+ if (select->Size() <= 1) {
+ // Adding the 'multiple' attribute to a select that has a size of 1
+ // creates a listbox as opposed to a combobox with a popup combobox
+ // list. Removing the attribute does the opposite.
+ RecreateAccessible(aElement);
+ return true;
+ }
+ }
+ }
+
+ if (aAttribute == nsGkAtoms::size &&
+ aElement->IsHTMLElement(nsGkAtoms::select)) {
+ // Changing the size of a select element can potentially change it from a
+ // combobox button to a listbox with different underlying implementations.
+ RecreateAccessible(aElement);
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::type) {
+ // If the input[type] changes, we should recreate the accessible.
+ RecreateAccessible(aElement);
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::href &&
+ !nsCoreUtils::HasClickListener(aElement)) {
+ // If the href is added or removed for a or area elements without click
+ // listeners, we need to recreate the accessible since the role might have
+ // changed. Without an href or click listener, the accessible must be a
+ // generic.
+ if (aElement->IsHTMLElement(nsGkAtoms::a)) {
+ LocalAccessible* acc = GetAccessible(aElement);
+ if (!acc) {
+ return false;
+ }
+ if (acc->IsHTMLLink() != aElement->HasAttr(nsGkAtoms::href)) {
+ RecreateAccessible(aElement);
+ return true;
+ }
+ } else if (aElement->IsHTMLElement(nsGkAtoms::area)) {
+ // For area accessibles, we have to recreate the entire image map, since
+ // the image map accessible manages the tree itself.
+ LocalAccessible* areaAcc = GetAccessibleEvenIfNotInMap(aElement);
+ if (!areaAcc || !areaAcc->LocalParent()) {
+ return false;
+ }
+ RecreateAccessible(areaAcc->LocalParent()->GetContent());
+ return true;
+ }
+ }
+
+ if (aElement->IsHTMLElement(nsGkAtoms::img) && aAttribute == nsGkAtoms::alt) {
+ // If alt text changes on an img element, we may want to create or remove an
+ // accessible for that img.
+ if (nsAccessibilityService::ShouldCreateImgAccessible(aElement, this)) {
+ if (GetAccessible(aElement)) {
+ // If the accessible already exists, there's no need to create one.
+ return false;
+ }
+ ContentInserted(aElement, aElement->GetNextSibling());
+ } else {
+ ContentRemoved(aElement);
+ }
+ return true;
+ }
+
+ if (aAttribute == nsGkAtoms::popover && aElement->IsHTMLElement()) {
+ // Changing the popover attribute might change the role.
+ RecreateAccessible(aElement);
+ return true;
+ }
+
+ return false;
+}
+
+void DocAccessible::UpdateRootElIfNeeded() {
+ dom::Element* rootEl = mDocumentNode->GetBodyElement();
+ if (!rootEl) {
+ rootEl = mDocumentNode->GetRootElement();
+ }
+ if (rootEl != mContent) {
+ mContent = rootEl;
+ SetRoleMapEntryForDoc(rootEl);
+ if (mIPCDoc) {
+ mIPCDoc->SendRoleChangedEvent(Role(), mRoleMapEntryIndex);
+ }
+ }
+}
+
+/**
+ * Content insertion helper.
+ */
+class InsertIterator final {
+ public:
+ InsertIterator(LocalAccessible* aContext,
+ const nsTArray<nsCOMPtr<nsIContent>>* aNodes)
+ : mChild(nullptr),
+ mChildBefore(nullptr),
+ mWalker(aContext),
+ mNodes(aNodes),
+ mNodesIdx(0) {
+ MOZ_ASSERT(aContext, "No context");
+ MOZ_ASSERT(aNodes, "No nodes to search for accessible elements");
+ MOZ_COUNT_CTOR(InsertIterator);
+ }
+ MOZ_COUNTED_DTOR(InsertIterator)
+
+ LocalAccessible* Context() const { return mWalker.Context(); }
+ LocalAccessible* Child() const { return mChild; }
+ LocalAccessible* ChildBefore() const { return mChildBefore; }
+ DocAccessible* Document() const { return mWalker.Document(); }
+
+ /**
+ * Iterates to a next accessible within the inserted content.
+ */
+ bool Next();
+
+ void Rejected() {
+ mChild = nullptr;
+ mChildBefore = nullptr;
+ }
+
+ private:
+ LocalAccessible* mChild;
+ LocalAccessible* mChildBefore;
+ TreeWalker mWalker;
+
+ const nsTArray<nsCOMPtr<nsIContent>>* mNodes;
+ nsTHashSet<nsPtrHashKey<const nsIContent>> mProcessedNodes;
+ uint32_t mNodesIdx;
+};
+
+bool InsertIterator::Next() {
+ if (mNodesIdx > 0) {
+ // If we already processed the first node in the mNodes list,
+ // check if we can just use the walker to get its next sibling.
+ LocalAccessible* nextChild = mWalker.Next();
+ if (nextChild) {
+ mChildBefore = mChild;
+ mChild = nextChild;
+ return true;
+ }
+ }
+
+ while (mNodesIdx < mNodes->Length()) {
+ nsIContent* node = mNodes->ElementAt(mNodesIdx++);
+ // Check to see if we already processed this node with this iterator.
+ // this can happen if we get two redundant insertions in the case of a
+ // text and frame insertion.
+ if (!mProcessedNodes.EnsureInserted(node)) {
+ continue;
+ }
+
+ LocalAccessible* container = Document()->AccessibleOrTrueContainer(
+ node->GetFlattenedTreeParentNode(), true);
+ // Ignore nodes that are not contained by the container anymore.
+ // The container might be changed, for example, because of the subsequent
+ // overlapping content insertion (i.e. other content was inserted between
+ // this inserted content and its container or the content was reinserted
+ // into different container of unrelated part of tree). To avoid a double
+ // processing of the content insertion ignore this insertion notification.
+ // Note, the inserted content might be not in tree at all at this point
+ // what means there's no container. Ignore the insertion too.
+ if (container != Context()) {
+ continue;
+ }
+
+ // HTML comboboxes have no-content list accessible as an intermediate
+ // containing all options.
+ if (container->IsHTMLCombobox()) {
+ container = container->LocalFirstChild();
+ }
+
+ if (!container->IsAcceptableChild(node)) {
+ continue;
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("traversing an inserted node", logging::eVerbose,
+ "container", container, "node", node);
+#endif
+
+ nsIContent* prevNode = mChild ? mChild->GetContent() : nullptr;
+ if (prevNode && prevNode->GetNextSibling() == node) {
+ // If inserted nodes are siblings then just move the walker next.
+ LocalAccessible* nextChild = mWalker.Scope(node);
+ if (nextChild) {
+ mChildBefore = mChild;
+ mChild = nextChild;
+ return true;
+ }
+ } else {
+ // Otherwise use a new walker to find this node in the container's
+ // subtree, and retrieve its preceding sibling.
+ TreeWalker finder(container);
+ if (finder.Seek(node)) {
+ mChild = mWalker.Scope(node);
+ if (mChild) {
+ MOZ_ASSERT(!mChild->IsRelocated(), "child cannot be aria owned");
+ mChildBefore = finder.Prev();
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+void DocAccessible::MaybeFireEventsForChangedPopover(LocalAccessible *aAcc) {
+ dom::Element* el = aAcc->Elm();
+ if (!el || !el->IsHTMLElement() || !el->HasAttr(nsGkAtoms::popover)) {
+ return; // Not a popover.
+ }
+ // A popover has just been inserted into or removed from the a11y tree, which
+ // means it just appeared or disappeared. Fire expanded state changes on its
+ // invokers.
+ RelatedAccIterator invokers(mDoc, el, nsGkAtoms::popovertarget);
+ while (Accessible* invoker = invokers.Next()) {
+ RefPtr<AccEvent> expandedChangeEvent =
+ new AccStateChangeEvent(invoker->AsLocal(), states::EXPANDED);
+ FireDelayedEvent(expandedChangeEvent);
+ }
+}
+
+void DocAccessible::ProcessContentInserted(
+ LocalAccessible* aContainer, const nsTArray<nsCOMPtr<nsIContent>>* aNodes) {
+ // Process insertions if the container accessible is still in tree.
+ if (!aContainer->IsInDocument()) {
+ return;
+ }
+
+ // If new root content has been inserted then update it.
+ if (aContainer == this) {
+ UpdateRootElIfNeeded();
+ }
+
+ InsertIterator iter(aContainer, aNodes);
+ if (!iter.Next()) {
+ return;
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("children before insertion", logging::eVerbose, aContainer);
+#endif
+
+ TreeMutation mt(aContainer);
+ bool inserted = false;
+ do {
+ LocalAccessible* parent = iter.Child()->LocalParent();
+ if (parent) {
+ LocalAccessible* previousSibling = iter.ChildBefore();
+ if (parent != aContainer ||
+ iter.Child()->LocalPrevSibling() != previousSibling) {
+ if (previousSibling && previousSibling->LocalParent() != aContainer) {
+ // previousSibling hasn't been moved into aContainer yet.
+ // previousSibling should be later in the insertion list, so the tree
+ // will get adjusted when we process it later.
+ MOZ_DIAGNOSTIC_ASSERT(parent == aContainer,
+ "Child moving to new parent, but previous "
+ "sibling in wrong parent");
+ continue;
+ }
+#ifdef A11Y_LOG
+ logging::TreeInfo("relocating accessible", 0, "old parent", parent,
+ "new parent", aContainer, "child", iter.Child(),
+ nullptr);
+#endif
+ MoveChild(iter.Child(), aContainer,
+ previousSibling ? previousSibling->IndexInParent() + 1 : 0);
+ inserted = true;
+ }
+ continue;
+ }
+
+ if (aContainer->InsertAfter(iter.Child(), iter.ChildBefore())) {
+#ifdef A11Y_LOG
+ logging::TreeInfo("accessible was inserted", 0, "container", aContainer,
+ "child", iter.Child(), nullptr);
+#endif
+
+ CreateSubtree(iter.Child());
+ mt.AfterInsertion(iter.Child());
+ inserted = true;
+ MaybeFireEventsForChangedPopover(iter.Child());
+ continue;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("accessible was rejected");
+ iter.Rejected();
+ } while (iter.Next());
+
+ mt.Done();
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("children after insertion", logging::eVerbose, aContainer);
+#endif
+
+ // We might not have actually inserted anything if layout frame reconstruction
+ // occurred.
+ if (inserted) {
+ FireEventsOnInsertion(aContainer);
+ }
+}
+
+void DocAccessible::ProcessContentInserted(LocalAccessible* aContainer,
+ nsIContent* aNode) {
+ if (!aContainer->IsInDocument()) {
+ return;
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("children before insertion", logging::eVerbose, aContainer);
+#endif
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("traversing an inserted node", logging::eVerbose,
+ "container", aContainer, "node", aNode);
+#endif
+
+ TreeWalker walker(aContainer);
+ if (aContainer->IsAcceptableChild(aNode) && walker.Seek(aNode)) {
+ LocalAccessible* child = GetAccessible(aNode);
+ if (!child) {
+ child = GetAccService()->CreateAccessible(aNode, aContainer);
+ }
+
+ if (child) {
+ TreeMutation mt(aContainer);
+ if (!aContainer->InsertAfter(child, walker.Prev())) {
+ return;
+ }
+ CreateSubtree(child);
+ mt.AfterInsertion(child);
+ mt.Done();
+
+ FireEventsOnInsertion(aContainer);
+ }
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("children after insertion", logging::eVerbose, aContainer);
+#endif
+}
+
+void DocAccessible::FireEventsOnInsertion(LocalAccessible* aContainer) {
+ // Check to see if change occurred inside an alert, and fire an EVENT_ALERT
+ // if it did.
+ if (aContainer->IsAlert() || aContainer->IsInsideAlert()) {
+ LocalAccessible* ancestor = aContainer;
+ do {
+ if (ancestor->IsAlert()) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, ancestor);
+ break;
+ }
+ } while ((ancestor = ancestor->LocalParent()));
+ }
+}
+
+void DocAccessible::ContentRemoved(LocalAccessible* aChild) {
+ MOZ_DIAGNOSTIC_ASSERT(aChild != this, "Should never be called for the doc");
+ LocalAccessible* parent = aChild->LocalParent();
+ MOZ_DIAGNOSTIC_ASSERT(parent, "Unattached accessible from tree");
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("process content removal", 0, "container", parent, "child",
+ aChild, nullptr);
+#endif
+
+ // XXX: event coalescence may kill us
+ RefPtr<LocalAccessible> kungFuDeathGripChild(aChild);
+
+ TreeMutation mt(parent);
+ mt.BeforeRemoval(aChild);
+
+ if (aChild->IsDefunct()) {
+ MOZ_ASSERT_UNREACHABLE("Event coalescence killed the accessible");
+ mt.Done();
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(aChild->LocalParent(), "Alive but unparented #1");
+
+ if (aChild->IsRelocated()) {
+ nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(parent);
+ MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash");
+ owned->RemoveElement(aChild);
+ if (owned->Length() == 0) {
+ mARIAOwnsHash.Remove(parent);
+ }
+ }
+ MOZ_DIAGNOSTIC_ASSERT(aChild->LocalParent(), "Unparented #2");
+ UncacheChildrenInSubtree(aChild);
+ parent->RemoveChild(aChild);
+
+ mt.Done();
+}
+
+void DocAccessible::ContentRemoved(nsIContent* aContentNode) {
+ if (!mRemovedNodes.EnsureInserted(aContentNode)) {
+ return;
+ }
+
+ // If child node is not accessible then look for its accessible children.
+ LocalAccessible* acc = GetAccessible(aContentNode);
+ if (acc) {
+ ContentRemoved(acc);
+ }
+
+ dom::AllChildrenIterator iter =
+ dom::AllChildrenIterator(aContentNode, nsIContent::eAllChildren, true);
+ while (nsIContent* childNode = iter.GetNextChild()) {
+ ContentRemoved(childNode);
+ }
+
+ // If this node has a shadow root, remove its explicit children too.
+ // The host node may be removed after the shadow root was attached, and
+ // before we asynchronously prune the light DOM and construct the shadow DOM.
+ // If this is a case where the node does not have its own accessible, we will
+ // not recurse into its current children, so we need to use an
+ // ExplicitChildIterator in order to get its accessible children in the light
+ // DOM, since they are not accessible anymore via AllChildrenIterator.
+ if (aContentNode->GetShadowRoot()) {
+ for (nsIContent* childNode = aContentNode->GetFirstChild(); childNode;
+ childNode = childNode->GetNextSibling()) {
+ ContentRemoved(childNode);
+ }
+ }
+}
+
+bool DocAccessible::RelocateARIAOwnedIfNeeded(nsIContent* aElement) {
+ if (!aElement->HasID()) return false;
+
+ AttrRelProviders* list = GetRelProviders(
+ aElement->AsElement(), nsDependentAtomString(aElement->GetID()));
+ if (list) {
+ for (uint32_t idx = 0; idx < list->Length(); idx++) {
+ if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) {
+ LocalAccessible* owner = GetAccessible(list->ElementAt(idx)->mContent);
+ if (owner) {
+ mNotificationController->ScheduleRelocation(owner);
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+void DocAccessible::DoARIAOwnsRelocation(LocalAccessible* aOwner) {
+ MOZ_ASSERT(aOwner, "aOwner must be a valid pointer");
+ MOZ_ASSERT(aOwner->Elm(), "aOwner->Elm() must be a valid pointer");
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("aria owns relocation", logging::eVerbose, aOwner);
+#endif
+
+ nsTArray<RefPtr<LocalAccessible>>* owned =
+ mARIAOwnsHash.GetOrInsertNew(aOwner);
+
+ IDRefsIterator iter(this, aOwner->Elm(), nsGkAtoms::aria_owns);
+ uint32_t idx = 0;
+ while (nsIContent* childEl = iter.NextElem()) {
+ LocalAccessible* child = GetAccessible(childEl);
+ auto insertIdx = aOwner->ChildCount() - owned->Length() + idx;
+
+ // Make an attempt to create an accessible if it wasn't created yet.
+ if (!child) {
+ // An owned child cannot be an ancestor of the owner.
+ bool ok = true;
+ bool check = true;
+ for (LocalAccessible* parent = aOwner; parent && !parent->IsDoc();
+ parent = parent->LocalParent()) {
+ if (check) {
+ if (parent->Elm()->IsInclusiveDescendantOf(childEl)) {
+ ok = false;
+ break;
+ }
+ }
+ // We need to do the DOM descendant check again whenever the DOM
+ // lineage changes. If parent is relocated, that means the next
+ // ancestor will have a different DOM lineage.
+ check = parent->IsRelocated();
+ }
+ if (!ok) {
+ continue;
+ }
+
+ if (aOwner->IsAcceptableChild(childEl)) {
+ child = GetAccService()->CreateAccessible(childEl, aOwner);
+ if (child) {
+ TreeMutation imut(aOwner);
+ aOwner->InsertChildAt(insertIdx, child);
+ imut.AfterInsertion(child);
+ imut.Done();
+
+ child->SetRelocated(true);
+ owned->InsertElementAt(idx, child);
+ idx++;
+
+ // Create subtree before adjusting the insertion index, since subtree
+ // creation may alter children in the container.
+ CreateSubtree(child);
+ FireEventsOnInsertion(aOwner);
+ }
+ }
+ continue;
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("aria owns traversal", logging::eVerbose, "candidate",
+ child, nullptr);
+#endif
+
+ if (owned->IndexOf(child) < idx) {
+ continue; // ignore second entry of same ID
+ }
+
+ // Same child on same position, no change.
+ if (child->LocalParent() == aOwner) {
+ int32_t indexInParent = child->IndexInParent();
+
+ // The child is being placed in its current index,
+ // eg. aria-owns='id1 id2 id3' is changed to aria-owns='id3 id2 id1'.
+ if (indexInParent == static_cast<int32_t>(insertIdx)) {
+ MOZ_ASSERT(child->IsRelocated(),
+ "A child, having an index in parent from aria ownded "
+ "indices range, has to be aria owned");
+ MOZ_ASSERT(owned->ElementAt(idx) == child,
+ "Unexpected child in ARIA owned array");
+ idx++;
+ continue;
+ }
+
+ // The child is being inserted directly after its current index,
+ // resulting in a no-move case. This will happen when a parent aria-owns
+ // its last ordinal child:
+ // <ul aria-owns='id2'><li id='id1'></li><li id='id2'></li></ul>
+ if (indexInParent == static_cast<int32_t>(insertIdx) - 1) {
+ MOZ_ASSERT(!child->IsRelocated(),
+ "Child should be in its ordinal position");
+ child->SetRelocated(true);
+ owned->InsertElementAt(idx, child);
+ idx++;
+ continue;
+ }
+ }
+
+ MOZ_ASSERT(owned->SafeElementAt(idx) != child, "Already in place!");
+
+ // A new child is found, check for loops.
+ if (child->LocalParent() != aOwner) {
+ // Child is aria-owned by another container, skip.
+ if (child->IsRelocated()) {
+ continue;
+ }
+
+ LocalAccessible* parent = aOwner;
+ while (parent && parent != child && !parent->IsDoc()) {
+ parent = parent->LocalParent();
+ }
+ // A referred child cannot be a parent of the owner.
+ if (parent == child) {
+ continue;
+ }
+ }
+
+ if (MoveChild(child, aOwner, insertIdx)) {
+ child->SetRelocated(true);
+ MOZ_ASSERT(owned == mARIAOwnsHash.Get(aOwner));
+ owned = mARIAOwnsHash.GetOrInsertNew(aOwner);
+ owned->InsertElementAt(idx, child);
+ idx++;
+ }
+ }
+
+ // Put back children that are not seized anymore.
+ PutChildrenBack(owned, idx);
+ if (owned->Length() == 0) {
+ mARIAOwnsHash.Remove(aOwner);
+ }
+}
+
+void DocAccessible::PutChildrenBack(
+ nsTArray<RefPtr<LocalAccessible>>* aChildren, uint32_t aStartIdx) {
+ MOZ_ASSERT(aStartIdx <= aChildren->Length(), "Wrong removal index");
+
+ for (auto idx = aStartIdx; idx < aChildren->Length(); idx++) {
+ LocalAccessible* child = aChildren->ElementAt(idx);
+ if (!child->IsInDocument()) {
+ continue;
+ }
+
+ // Remove the child from the owner
+ LocalAccessible* owner = child->LocalParent();
+ if (!owner) {
+ NS_ERROR("Cannot put the child back. No parent, a broken tree.");
+ continue;
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("aria owns put child back", 0, "old parent", owner,
+ "child", child, nullptr);
+#endif
+
+ // Unset relocated flag to find an insertion point for the child.
+ child->SetRelocated(false);
+
+ nsIContent* content = child->GetContent();
+ int32_t idxInParent = -1;
+ LocalAccessible* origContainer =
+ AccessibleOrTrueContainer(content->GetFlattenedTreeParentNode());
+ // This node has probably been detached or removed from the DOM, so we have
+ // nowhere to move it.
+ if (!origContainer) {
+ continue;
+ }
+
+ // If the target container or any of its ancestors aren't in the document,
+ // there's no need to determine where the child should go for relocation
+ // since the target tree is going away.
+ bool origContainerHasOutOfDocAncestor = false;
+ LocalAccessible* ancestor = origContainer;
+ while (ancestor) {
+ if (ancestor->IsDoc()) {
+ break;
+ }
+ if (!ancestor->IsInDocument()) {
+ origContainerHasOutOfDocAncestor = true;
+ break;
+ }
+ ancestor = ancestor->LocalParent();
+ }
+ if (origContainerHasOutOfDocAncestor) {
+ continue;
+ }
+
+ TreeWalker walker(origContainer);
+ if (!walker.Seek(content)) {
+ continue;
+ }
+ LocalAccessible* prevChild = walker.Prev();
+ if (prevChild) {
+ idxInParent = prevChild->IndexInParent() + 1;
+ MOZ_DIAGNOSTIC_ASSERT(origContainer == prevChild->LocalParent(),
+ "Broken tree");
+ origContainer = prevChild->LocalParent();
+ } else {
+ idxInParent = 0;
+ }
+
+ // The child may have already be in its ordinal place for 2 reasons:
+ // 1. It was the last ordinal child, and the first aria-owned child.
+ // given: <ul id="list" aria-owns="b"><li id="a"></li><li
+ // id="b"></li></ul> after load: $("list").setAttribute("aria-owns", "");
+ // 2. The preceding adopted children were just reclaimed, eg:
+ // given: <ul id="list"><li id="b"></li></ul>
+ // after load: $("list").setAttribute("aria-owns", "a b");
+ // later: $("list").setAttribute("aria-owns", "");
+ if (origContainer != owner || child->IndexInParent() != idxInParent) {
+ // Only attempt to move the child if the target container would accept it.
+ // Otherwise, just allow it to be removed from the tree, since it would
+ // not be allowed in normal tree creation.
+ if (origContainer->IsAcceptableChild(child->GetContent())) {
+ DebugOnly<bool> moved = MoveChild(child, origContainer, idxInParent);
+ MOZ_ASSERT(moved, "Failed to put child back.");
+ }
+ } else {
+ MOZ_ASSERT(!child->LocalPrevSibling() ||
+ !child->LocalPrevSibling()->IsRelocated(),
+ "No relocated child should appear before this one");
+ MOZ_ASSERT(!child->LocalNextSibling() ||
+ child->LocalNextSibling()->IsRelocated(),
+ "No ordinal child should appear after this one");
+ }
+ }
+
+ aChildren->RemoveLastElements(aChildren->Length() - aStartIdx);
+}
+
+void DocAccessible::TrackMovedAccessible(LocalAccessible* aAcc) {
+ MOZ_ASSERT(aAcc->mDoc == this);
+ // If an Accessible is inserted and moved during the same tick, don't track
+ // it as a move because it hasn't been shown yet.
+ if (!mInsertedAccessibles.Contains(aAcc)) {
+ mMovedAccessibles.EnsureInserted(aAcc);
+ }
+ // When we move an Accessible, we're also moving its descendants.
+ if (aAcc->IsOuterDoc()) {
+ // Don't descend into other documents.
+ return;
+ }
+ for (uint32_t c = 0, count = aAcc->ContentChildCount(); c < count; ++c) {
+ TrackMovedAccessible(aAcc->ContentChildAt(c));
+ }
+}
+
+bool DocAccessible::MoveChild(LocalAccessible* aChild,
+ LocalAccessible* aNewParent,
+ int32_t aIdxInParent) {
+ MOZ_ASSERT(aChild, "No child");
+ MOZ_ASSERT(aChild->LocalParent(), "No parent");
+ // We can't guarantee MoveChild works correctly for accessibilities storing
+ // children outside mChildren.
+ MOZ_ASSERT(
+ aIdxInParent <= static_cast<int32_t>(aNewParent->mChildren.Length()),
+ "Wrong insertion point for a moving child");
+
+ LocalAccessible* curParent = aChild->LocalParent();
+
+ if (!aNewParent->IsAcceptableChild(aChild->GetContent())) {
+ return false;
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("move child", 0, "old parent", curParent, "new parent",
+ aNewParent, "child", aChild, nullptr);
+#endif
+
+ // Forget aria-owns info in case of ARIA owned element. The caller is expected
+ // to update it if needed.
+ if (aChild->IsRelocated()) {
+ aChild->SetRelocated(false);
+ nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(curParent);
+ MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash");
+ owned->RemoveElement(aChild);
+ if (owned->Length() == 0) {
+ mARIAOwnsHash.Remove(curParent);
+ }
+ }
+
+ if (curParent == aNewParent) {
+ MOZ_ASSERT(aChild->IndexInParent() != aIdxInParent, "No move case");
+ curParent->RelocateChild(aIdxInParent, aChild);
+ if (mIPCDoc) {
+ TrackMovedAccessible(aChild);
+ }
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("move child: parent tree after", logging::eVerbose,
+ curParent);
+#endif
+ return true;
+ }
+
+ // If the child cannot be re-inserted into the tree, then make sure to remove
+ // it from its present parent and then shutdown it.
+ bool hasInsertionPoint =
+ (aIdxInParent >= 0) &&
+ (aIdxInParent <= static_cast<int32_t>(aNewParent->mChildren.Length()));
+
+ TreeMutation rmut(curParent);
+ rmut.BeforeRemoval(aChild, hasInsertionPoint && TreeMutation::kNoShutdown);
+ curParent->RemoveChild(aChild);
+ rmut.Done();
+
+ // No insertion point for the child.
+ if (!hasInsertionPoint) {
+ return true;
+ }
+
+ TreeMutation imut(aNewParent);
+ aNewParent->InsertChildAt(aIdxInParent, aChild);
+ if (mIPCDoc) {
+ TrackMovedAccessible(aChild);
+ }
+ imut.AfterInsertion(aChild);
+ imut.Done();
+
+#ifdef A11Y_LOG
+ logging::TreeInfo("move child: old parent tree after", logging::eVerbose,
+ curParent);
+ logging::TreeInfo("move child: new parent tree after", logging::eVerbose,
+ aNewParent);
+#endif
+
+ return true;
+}
+
+void DocAccessible::CacheChildrenInSubtree(LocalAccessible* aRoot,
+ LocalAccessible** aFocusedAcc) {
+ // If the accessible is focused then report a focus event after all related
+ // mutation events.
+ if (aFocusedAcc && !*aFocusedAcc &&
+ FocusMgr()->HasDOMFocus(aRoot->GetContent())) {
+ *aFocusedAcc = aRoot;
+ }
+
+ LocalAccessible* root =
+ aRoot->IsHTMLCombobox() ? aRoot->LocalFirstChild() : aRoot;
+ if (root->KidsFromDOM()) {
+ TreeMutation mt(root, TreeMutation::kNoEvents);
+ TreeWalker walker(root);
+ while (LocalAccessible* child = walker.Next()) {
+ if (child->IsBoundToParent()) {
+ MoveChild(child, root, root->mChildren.Length());
+ continue;
+ }
+
+ root->AppendChild(child);
+ mt.AfterInsertion(child);
+
+ CacheChildrenInSubtree(child, aFocusedAcc);
+ }
+ mt.Done();
+ }
+
+ // Fire events for ARIA elements.
+ if (!aRoot->HasARIARole()) {
+ return;
+ }
+
+ // XXX: we should delay document load complete event if the ARIA document
+ // has aria-busy.
+ roles::Role role = aRoot->ARIARole();
+ if (!aRoot->IsDoc() &&
+ (role == roles::DIALOG || role == roles::NON_NATIVE_DOCUMENT)) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE, aRoot);
+ }
+}
+
+void DocAccessible::UncacheChildrenInSubtree(LocalAccessible* aRoot) {
+ MaybeFireEventsForChangedPopover(aRoot);
+ aRoot->mStateFlags |= eIsNotInDocument;
+ RemoveDependentIDsFor(aRoot);
+
+ // The parent of the removed subtree is about to be cleared, so we must do
+ // this here rather than in LocalAccessible::UnbindFromParent because we need
+ // the ancestry for this to work.
+ if (aRoot->IsTable() || aRoot->IsTableCell()) {
+ CachedTableAccessible::Invalidate(aRoot);
+ }
+
+ // Put relocated children back in their original places instead of removing
+ // them from the tree.
+ nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(aRoot);
+ if (owned) {
+ PutChildrenBack(owned, 0);
+ MOZ_ASSERT(owned->IsEmpty(),
+ "Owned Accessibles should be cleared after PutChildrenBack.");
+ mARIAOwnsHash.Remove(aRoot);
+ owned = nullptr;
+ }
+
+ const uint32_t count = aRoot->ContentChildCount();
+ for (uint32_t idx = 0; idx < count; ++idx) {
+ LocalAccessible* child = aRoot->ContentChildAt(idx);
+
+ MOZ_ASSERT(!child->IsRelocated(),
+ "No children should be relocated here. They should all have "
+ "been relocated by PutChildrenBack.");
+
+ // Removing this accessible from the document doesn't mean anything about
+ // accessibles for subdocuments, so skip removing those from the tree.
+ if (!child->IsDoc()) {
+ UncacheChildrenInSubtree(child);
+ }
+ }
+
+ if (aRoot->IsNodeMapEntry() &&
+ mNodeToAccessibleMap.Get(aRoot->GetNode()) == aRoot) {
+ mNodeToAccessibleMap.Remove(aRoot->GetNode());
+ }
+}
+
+void DocAccessible::ShutdownChildrenInSubtree(LocalAccessible* aAccessible) {
+ MOZ_ASSERT(!nsAccessibilityService::IsShutdown());
+ // Traverse through children and shutdown them before this accessible. When
+ // child gets shutdown then it removes itself from children array of its
+ // parent. Use jdx index to process the cases if child is not attached to the
+ // parent and as result doesn't remove itself from its children.
+ uint32_t count = aAccessible->ContentChildCount();
+ for (uint32_t idx = 0, jdx = 0; idx < count; idx++) {
+ LocalAccessible* child = aAccessible->ContentChildAt(jdx);
+ if (!child->IsBoundToParent()) {
+ NS_ERROR("Parent refers to a child, child doesn't refer to parent!");
+ jdx++;
+ }
+
+ // Don't cross document boundaries. The outerdoc shutdown takes care about
+ // its subdocument.
+ if (!child->IsDoc()) {
+ ShutdownChildrenInSubtree(child);
+ if (nsAccessibilityService::IsShutdown()) {
+ // If XPCOM is the only consumer (devtools & mochitests), shutting down
+ // the child's subtree can cause a11y to shut down because the last
+ // xpcom accessibles will be removed. In that case, return early, our
+ // work is done.
+ return;
+ }
+ }
+ }
+
+ UnbindFromDocument(aAccessible);
+}
+
+bool DocAccessible::IsLoadEventTarget() const {
+ nsCOMPtr<nsIDocShellTreeItem> treeItem = mDocumentNode->GetDocShell();
+ if (!treeItem) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDocShellTreeItem> parentTreeItem;
+ treeItem->GetInProcessParent(getter_AddRefs(parentTreeItem));
+
+ // Not a root document.
+ if (parentTreeItem) {
+ // Return true if it's either:
+ // a) tab document;
+ nsCOMPtr<nsIDocShellTreeItem> rootTreeItem;
+ treeItem->GetInProcessRootTreeItem(getter_AddRefs(rootTreeItem));
+ if (parentTreeItem == rootTreeItem) return true;
+
+ // b) frame/iframe document and its parent document is not in loading state
+ // Note: we can get notifications while document is loading (and thus
+ // while there's no parent document yet).
+ DocAccessible* parentDoc = ParentDocument();
+ return parentDoc && parentDoc->HasLoadState(eCompletelyLoaded);
+ }
+
+ // It's content (not chrome) root document.
+ return (treeItem->ItemType() == nsIDocShellTreeItem::typeContent);
+}
+
+void DocAccessible::SetIPCDoc(DocAccessibleChild* aIPCDoc) {
+ MOZ_ASSERT(!mIPCDoc || !aIPCDoc, "Clobbering an attached IPCDoc!");
+ mIPCDoc = aIPCDoc;
+}
+
+void DocAccessible::DispatchScrollingEvent(nsINode* aTarget,
+ uint32_t aEventType) {
+ LocalAccessible* acc = GetAccessible(aTarget);
+ if (!acc) {
+ return;
+ }
+
+ nsIFrame* frame = acc->GetFrame();
+ if (!frame) {
+ // Although the accessible had a frame at scroll time, it may now be gone
+ // because of display: contents.
+ return;
+ }
+
+ auto [scrollPoint, scrollRange] = ComputeScrollData(acc);
+
+ int32_t appUnitsPerDevPixel =
+ mPresShell->GetPresContext()->AppUnitsPerDevPixel();
+
+ LayoutDeviceIntPoint scrollPointDP = LayoutDevicePoint::FromAppUnitsToNearest(
+ scrollPoint, appUnitsPerDevPixel);
+ LayoutDeviceIntRect scrollRangeDP =
+ LayoutDeviceRect::FromAppUnitsToNearest(scrollRange, appUnitsPerDevPixel);
+
+ RefPtr<AccEvent> event =
+ new AccScrollingEvent(aEventType, acc, scrollPointDP.x, scrollPointDP.y,
+ scrollRangeDP.width, scrollRangeDP.height);
+ nsEventShell::FireEvent(event);
+}
+
+void DocAccessible::ARIAActiveDescendantIDMaybeMoved(
+ LocalAccessible* aAccessible) {
+ LocalAccessible* widget = nullptr;
+ if (aAccessible->IsActiveDescendant(&widget) && widget) {
+ // The active descendant might have just been inserted and may not be in the
+ // tree yet. Therefore, schedule this async to ensure the tree is up to
+ // date.
+ mNotificationController
+ ->ScheduleNotification<DocAccessible, LocalAccessible>(
+ this, &DocAccessible::ARIAActiveDescendantChanged, widget);
+ }
+}
+
+void DocAccessible::SetRoleMapEntryForDoc(dom::Element* aElement) {
+ const nsRoleMapEntry* entry = aria::GetRoleMap(aElement);
+ if (!entry || entry->role == roles::APPLICATION ||
+ entry->role == roles::DIALOG ||
+ // Role alert isn't valid on the body element according to the ARIA spec,
+ // but it's useful for our UI; e.g. the WebRTC sharing indicator.
+ (entry->role == roles::ALERT && !mDocumentNode->IsContentDocument())) {
+ SetRoleMapEntry(entry);
+ return;
+ }
+ // No other ARIA roles are valid on body elements.
+ SetRoleMapEntry(nullptr);
+}
+
+LocalAccessible* DocAccessible::GetAccessible(nsINode* aNode) const {
+ return aNode == mDocumentNode ? const_cast<DocAccessible*>(this)
+ : mNodeToAccessibleMap.Get(aNode);
+}
+
+bool DocAccessible::HasPrimaryAction() const {
+ if (HyperTextAccessible::HasPrimaryAction()) {
+ return true;
+ }
+ // mContent is normally the body, but there might be a click listener on the
+ // root.
+ dom::Element* root = mDocumentNode->GetRootElement();
+ if (mContent != root) {
+ return nsCoreUtils::HasClickListener(root);
+ }
+ return false;
+}
+
+void DocAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ aName.Truncate();
+ if (aIndex != 0) {
+ return;
+ }
+ if (HasPrimaryAction()) {
+ aName.AssignLiteral("click");
+ }
+}
+
+void DocAccessible::MaybeHandleChangeToHiddenNameOrDescription(
+ nsIContent* aChild) {
+ if (!HasLoadState(eTreeConstructed)) {
+ return;
+ }
+ for (nsIContent* content = aChild; content; content = content->GetParent()) {
+ if (HasAccessible(content)) {
+ // This node isn't hidden. Events for name/description dependents will be
+ // fired elsewhere.
+ break;
+ }
+ nsAtom* id = content->GetID();
+ if (!id) {
+ continue;
+ }
+ auto* providers =
+ GetRelProviders(content->AsElement(), nsDependentAtomString(id));
+ if (!providers) {
+ continue;
+ }
+ for (auto& provider : *providers) {
+ if (provider->mRelAttr != nsGkAtoms::aria_labelledby &&
+ provider->mRelAttr != nsGkAtoms::aria_describedby) {
+ continue;
+ }
+ LocalAccessible* dependentAcc = GetAccessible(provider->mContent);
+ if (!dependentAcc) {
+ continue;
+ }
+ FireDelayedEvent(provider->mRelAttr == nsGkAtoms::aria_labelledby
+ ? nsIAccessibleEvent::EVENT_NAME_CHANGE
+ : nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE,
+ dependentAcc);
+ }
+ }
+}
diff --git a/accessible/generic/DocAccessible.h b/accessible/generic/DocAccessible.h
new file mode 100644
index 0000000000..52cbdd68cf
--- /dev/null
+++ b/accessible/generic/DocAccessible.h
@@ -0,0 +1,825 @@
+/* -*- 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_DocAccessible_h__
+#define mozilla_a11y_DocAccessible_h__
+
+#include "HyperTextAccessible.h"
+#include "AccEvent.h"
+
+#include "nsClassHashtable.h"
+#include "nsTHashMap.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIDocumentObserver.h"
+#include "nsITimer.h"
+#include "nsTHashSet.h"
+#include "nsWeakReference.h"
+
+const uint32_t kDefaultCacheLength = 128;
+
+namespace mozilla {
+
+class EditorBase;
+class PresShell;
+
+namespace dom {
+class Document;
+}
+
+namespace a11y {
+
+class DocManager;
+class NotificationController;
+class DocAccessibleChild;
+class RelatedAccIterator;
+template <class Class, class... Args>
+class TNotification;
+
+/**
+ * An accessibility tree node that originated in a content process and
+ * represents a document. Tabs, in-process iframes, and out-of-process iframes
+ * all use this class to represent the doc they contain.
+ */
+class DocAccessible : public HyperTextAccessible,
+ public nsIDocumentObserver,
+ public nsSupportsWeakReference {
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DocAccessible, LocalAccessible)
+
+ protected:
+ typedef mozilla::dom::Document Document;
+
+ public:
+ DocAccessible(Document* aDocument, PresShell* aPresShell);
+
+ // nsIDocumentObserver
+ NS_DECL_NSIDOCUMENTOBSERVER
+
+ // LocalAccessible
+ virtual void Init();
+ virtual void Shutdown() override;
+ virtual nsIFrame* GetFrame() const override;
+ virtual nsINode* GetNode() const override;
+ Document* DocumentNode() const { return mDocumentNode; }
+
+ virtual mozilla::a11y::ENameValueFlag Name(nsString& aName) const override;
+ virtual void Description(nsString& aDescription) const override;
+ virtual Accessible* FocusedChild() override;
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual uint64_t NativeInteractiveState() const override;
+ virtual bool NativelyUnavailable() const override;
+ virtual void ApplyARIAState(uint64_t* aState) const override;
+
+ virtual void TakeFocus() const override;
+
+#ifdef A11Y_LOG
+ virtual nsresult HandleAccEvent(AccEvent* aEvent) override;
+#endif
+
+ virtual nsRect RelativeBounds(nsIFrame** aRelativeFrame) const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+
+ // HyperTextAccessible
+ virtual already_AddRefed<EditorBase> GetEditor() const override;
+
+ // DocAccessible
+
+ /**
+ * Return document URL.
+ */
+ void URL(nsAString& aURL) const;
+
+ /**
+ * Return DOM document title.
+ */
+ void Title(nsString& aTitle) const;
+
+ /**
+ * Return DOM document mime type.
+ */
+ void MimeType(nsAString& aType) const;
+ /**
+ * Return DOM document type.
+ */
+ void DocType(nsAString& aType) const;
+
+ /**
+ * Adds an entry to queued cache updates indicating aAcc requires
+ * a cache update on domain aNewDomain. If we've already queued an update
+ * for aAcc, aNewDomain is or'd with the existing domain(s)
+ * and the map is updated. Otherwise, the entry is simply inserted.
+ * This function also schedules processing on the controller.
+ * Note that this CANNOT be used for anything which fires events, since events
+ * must be fired after their associated cache update.
+ */
+ void QueueCacheUpdate(LocalAccessible* aAcc, uint64_t aNewDomain);
+
+ /**
+ * Walks the mDependentIDsHashes list for the given accessible and
+ * queues a CacheDomain::Relations cache update fore each related acc.
+ * We call this when we observe an ID mutation or when an acc is bound
+ * to its document.
+ */
+ void QueueCacheUpdateForDependentRelations(LocalAccessible* aAcc);
+
+ /**
+ * Returns true if the instance has shutdown.
+ */
+ bool HasShutdown() const { return !mPresShell; }
+
+ /**
+ * Return presentation shell for this document accessible.
+ */
+ PresShell* PresShellPtr() const {
+ MOZ_DIAGNOSTIC_ASSERT(!HasShutdown());
+ return mPresShell;
+ }
+
+ /**
+ * Return the presentation shell's context.
+ */
+ nsPresContext* PresContext() const;
+
+ /**
+ * Return true if associated DOM document was loaded and isn't unloading.
+ */
+ bool IsContentLoaded() const;
+
+ bool IsHidden() const;
+
+ void SetViewportCacheDirty(bool aDirty) { mViewportCacheDirty = aDirty; }
+
+ /**
+ * Document load states.
+ */
+ enum LoadState {
+ // initial tree construction is pending
+ eTreeConstructionPending = 0,
+ // initial tree construction done
+ eTreeConstructed = 1,
+ // DOM document is loaded.
+ eDOMLoaded = 1 << 1,
+ // document is ready
+ eReady = eTreeConstructed | eDOMLoaded,
+ // document and all its subdocuments are ready
+ eCompletelyLoaded = eReady | 1 << 2
+ };
+
+ /**
+ * Return true if the document has given document state.
+ */
+ bool HasLoadState(LoadState aState) const {
+ return (mLoadState & static_cast<uint32_t>(aState)) ==
+ static_cast<uint32_t>(aState);
+ }
+
+ /**
+ * Return a native window handler or pointer depending on platform.
+ */
+ virtual void* GetNativeWindow() const;
+
+ /**
+ * Return the parent document.
+ */
+ DocAccessible* ParentDocument() const {
+ return mParent ? mParent->Document() : nullptr;
+ }
+
+ /**
+ * Return the child document count.
+ */
+ uint32_t ChildDocumentCount() const { return mChildDocuments.Length(); }
+
+ /**
+ * Return the child document at the given index.
+ */
+ DocAccessible* GetChildDocumentAt(uint32_t aIndex) const {
+ return mChildDocuments.SafeElementAt(aIndex, nullptr);
+ }
+
+ /**
+ * Fire accessible event asynchronously.
+ */
+ void FireDelayedEvent(AccEvent* aEvent);
+ void FireDelayedEvent(uint32_t aEventType, LocalAccessible* aTarget);
+ void FireEventsOnInsertion(LocalAccessible* aContainer);
+
+ /**
+ * Fire value change event on the given accessible if applicable.
+ */
+ void MaybeNotifyOfValueChange(LocalAccessible* aAccessible);
+
+ /**
+ * Get/set the anchor jump.
+ */
+ LocalAccessible* AnchorJump() {
+ return GetAccessibleOrContainer(mAnchorJumpElm);
+ }
+
+ void SetAnchorJump(nsIContent* aTargetNode) { mAnchorJumpElm = aTargetNode; }
+
+ /**
+ * Bind the child document to the tree.
+ */
+ void BindChildDocument(DocAccessible* aDocument);
+
+ /**
+ * Process the generic notification.
+ *
+ * @note The caller must guarantee that the given instance still exists when
+ * notification is processed.
+ * @see NotificationController::HandleNotification
+ */
+ template <class Class, class... Args>
+ void HandleNotification(
+ Class* aInstance,
+ typename TNotification<Class, Args...>::Callback aMethod, Args*... aArgs);
+
+ /**
+ * Return the cached accessible by the given DOM node if it's in subtree of
+ * this document accessible or the document accessible itself, otherwise null.
+ *
+ * @return the accessible object
+ */
+ LocalAccessible* GetAccessible(nsINode* aNode) const;
+
+ /**
+ * Return an accessible for the given node even if the node is not in
+ * document's node map cache (like HTML area element).
+ *
+ * XXX: it should be really merged with GetAccessible().
+ */
+ LocalAccessible* GetAccessibleEvenIfNotInMap(nsINode* aNode) const;
+ LocalAccessible* GetAccessibleEvenIfNotInMapOrContainer(nsINode* aNode) const;
+
+ /**
+ * Return whether the given DOM node has an accessible or not.
+ */
+ bool HasAccessible(nsINode* aNode) const { return GetAccessible(aNode); }
+
+ /**
+ * Return the cached accessible by the given unique ID within this document.
+ *
+ * @note the unique ID matches with the uniqueID() of Accessible
+ *
+ * @param aUniqueID [in] the unique ID used to cache the node.
+ */
+ LocalAccessible* GetAccessibleByUniqueID(void* aUniqueID) {
+ return UniqueID() == aUniqueID ? this : mAccessibleCache.GetWeak(aUniqueID);
+ }
+
+ /**
+ * Return the cached accessible by the given unique ID looking through
+ * this and nested documents.
+ */
+ LocalAccessible* GetAccessibleByUniqueIDInSubtree(void* aUniqueID);
+
+ /**
+ * Return an accessible for the given DOM node or container accessible if
+ * the node is not accessible. If aNoContainerIfPruned is true it will return
+ * null if the node is in a pruned subtree (eg. aria-hidden or unselected deck
+ * panel)
+ */
+ LocalAccessible* GetAccessibleOrContainer(
+ nsINode* aNode, bool aNoContainerIfPruned = false) const;
+
+ /**
+ * Return a container accessible for the given DOM node.
+ */
+ LocalAccessible* GetContainerAccessible(nsINode* aNode) const;
+
+ /**
+ * Return an accessible for the given node if any, or an immediate accessible
+ * container for it.
+ */
+ LocalAccessible* AccessibleOrTrueContainer(
+ nsINode* aNode, bool aNoContainerIfPruned = false) const;
+
+ /**
+ * Return an accessible for the given node or its first accessible descendant.
+ */
+ LocalAccessible* GetAccessibleOrDescendant(nsINode* aNode) const;
+
+ /**
+ * Returns aria-owns seized child at the given index.
+ */
+ LocalAccessible* ARIAOwnedAt(LocalAccessible* aParent,
+ uint32_t aIndex) const {
+ nsTArray<RefPtr<LocalAccessible>>* children = mARIAOwnsHash.Get(aParent);
+ if (children) {
+ return children->SafeElementAt(aIndex);
+ }
+ return nullptr;
+ }
+ uint32_t ARIAOwnedCount(LocalAccessible* aParent) const {
+ nsTArray<RefPtr<LocalAccessible>>* children = mARIAOwnsHash.Get(aParent);
+ return children ? children->Length() : 0;
+ }
+
+ /**
+ * Return true if the given ID is referred by relation attribute.
+ */
+ bool IsDependentID(dom::Element* aElement, const nsAString& aID) const {
+ return GetRelProviders(aElement, aID);
+ }
+
+ /**
+ * Initialize the newly created accessible and put it into document caches.
+ *
+ * @param aAccessible [in] created accessible
+ * @param aRoleMapEntry [in] the role map entry role the ARIA role or
+ * nullptr if none
+ */
+ void BindToDocument(LocalAccessible* aAccessible,
+ const nsRoleMapEntry* aRoleMapEntry);
+
+ /**
+ * Remove from document and shutdown the given accessible.
+ */
+ void UnbindFromDocument(LocalAccessible* aAccessible);
+
+ /**
+ * Notify the document accessible that content was inserted.
+ */
+ void ContentInserted(nsIContent* aStartChildNode, nsIContent* aEndChildNode);
+
+ /**
+ * @see nsAccessibilityService::ScheduleAccessibilitySubtreeUpdate
+ */
+ void ScheduleTreeUpdate(nsIContent* aContent);
+
+ /**
+ * Update the tree on content removal.
+ */
+ void ContentRemoved(LocalAccessible* aAccessible);
+ void ContentRemoved(nsIContent* aContentNode);
+
+ /**
+ * Updates accessible tree when rendered text is changed.
+ */
+ void UpdateText(nsIContent* aTextNode);
+
+ /**
+ * Recreate an accessible, results in hide/show events pair.
+ */
+ void RecreateAccessible(nsIContent* aContent);
+
+ /**
+ * Schedule ARIA owned element relocation if needed. Return true if relocation
+ * was scheduled.
+ */
+ bool RelocateARIAOwnedIfNeeded(nsIContent* aEl);
+
+ /**
+ * Return a notification controller associated with the document.
+ */
+ NotificationController* Controller() const { return mNotificationController; }
+
+ /**
+ * If this document is in a content process return the object responsible for
+ * communicating with the main process for it.
+ */
+ DocAccessibleChild* IPCDoc() const { return mIPCDoc; }
+
+ /**
+ * Notify the document that a DOM node has been scrolled. document will
+ * dispatch throttled accessibility events for scrolling, and a scroll-end
+ * event. This function also queues a cache update for ScrollPosition.
+ */
+ void HandleScroll(nsINode* aTarget);
+
+ /**
+ * Retrieves the scroll frame (if it exists) for the given accessible
+ * and returns its scroll position and scroll range. If the given
+ * accessible is `this`, return the scroll position and range of
+ * the root scroll frame. Return values have been scaled by the
+ * PresShell's resolution.
+ */
+ std::pair<nsPoint, nsRect> ComputeScrollData(LocalAccessible* aAcc);
+
+ /**
+ * Only works in content process documents.
+ */
+ bool IsAccessibleBeingMoved(LocalAccessible* aAcc) {
+ return mMovedAccessibles.Contains(aAcc);
+ }
+
+ protected:
+ virtual ~DocAccessible();
+
+ void LastRelease();
+
+ // DocAccessible
+ virtual nsresult AddEventListeners();
+ virtual nsresult RemoveEventListeners();
+
+ /**
+ * Marks this document as loaded or loading.
+ */
+ void NotifyOfLoad(uint32_t aLoadEventType);
+ void NotifyOfLoading(bool aIsReloading);
+
+ friend class DocManager;
+
+ /**
+ * Perform initial update (create accessible tree).
+ * Can be overridden by wrappers to prepare initialization work.
+ */
+ virtual void DoInitialUpdate();
+
+ /**
+ * Updates root element and picks up ARIA role on it if any.
+ */
+ void UpdateRootElIfNeeded();
+
+ /**
+ * Process document load notification, fire document load and state busy
+ * events if applicable.
+ */
+ void ProcessLoad();
+
+ /**
+ * Append the given document accessible to this document's child document
+ * accessibles.
+ */
+ bool AppendChildDocument(DocAccessible* aChildDocument) {
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier, or change the return type to void.
+ mChildDocuments.AppendElement(aChildDocument);
+ return true;
+ }
+
+ /**
+ * Remove the given document accessible from this document's child document
+ * accessibles.
+ */
+ void RemoveChildDocument(DocAccessible* aChildDocument) {
+ mChildDocuments.RemoveElement(aChildDocument);
+ }
+
+ /**
+ * Add dependent IDs pointed by accessible element by relation attribute to
+ * cache. If the relation attribute is missed then all relation attributes
+ * are checked.
+ *
+ * @param aRelProvider [in] accessible that element has relation attribute
+ * @param aRelAttr [in, optional] relation attribute
+ */
+ void AddDependentIDsFor(LocalAccessible* aRelProvider,
+ nsAtom* aRelAttr = nullptr);
+
+ /**
+ * Remove dependent IDs pointed by accessible element by relation attribute
+ * from cache. If the relation attribute is absent then all relation
+ * attributes are checked.
+ *
+ * @param aRelProvider [in] accessible that element has relation attribute
+ * @param aRelAttr [in, optional] relation attribute
+ */
+ void RemoveDependentIDsFor(LocalAccessible* aRelProvider,
+ nsAtom* aRelAttr = nullptr);
+
+ /**
+ * Update or recreate an accessible depending on a changed attribute.
+ *
+ * @param aElement [in] the element the attribute was changed on
+ * @param aAttribute [in] the changed attribute
+ * @return true if an action was taken on the attribute change
+ */
+ bool UpdateAccessibleOnAttrChange(mozilla::dom::Element* aElement,
+ nsAtom* aAttribute);
+
+ /**
+ * Process ARIA active-descendant attribute change.
+ */
+ void ARIAActiveDescendantChanged(LocalAccessible* aAccessible);
+
+ /**
+ * Update the accessible tree for inserted content.
+ */
+ void ProcessContentInserted(
+ LocalAccessible* aContainer,
+ const nsTArray<nsCOMPtr<nsIContent>>* aInsertedContent);
+ void ProcessContentInserted(LocalAccessible* aContainer,
+ nsIContent* aInsertedContent);
+
+ /**
+ * Used to notify the document to make it process the invalidation list.
+ *
+ * While children are cached we may encounter the case there's no accessible
+ * for referred content by related accessible. Store these related nodes to
+ * invalidate their containers later.
+ */
+ void ProcessInvalidationList();
+
+ /**
+ * Process mPendingUpdates
+ */
+ void ProcessPendingUpdates();
+
+ /**
+ * Called from NotificationController to process this doc's
+ * queued cache updates. For each acc in the map, this function
+ * sends a cache update with its corresponding CacheDomain.
+ */
+ void ProcessQueuedCacheUpdates();
+
+ /**
+ * Called from NotificationController before mutation events are processed to
+ * notify the parent process which Accessibles are being moved (if any).
+ */
+ void SendAccessiblesWillMove();
+
+ /**
+ * Called from NotificationController after all mutation events have been
+ * processed to clear our data about mutations during this tick.
+ */
+ void ClearMutationData() {
+ mMovedAccessibles.Clear();
+ mInsertedAccessibles.Clear();
+ mRemovedNodes.Clear();
+ }
+
+ /**
+ * Steals or puts back accessible subtrees.
+ */
+ void DoARIAOwnsRelocation(LocalAccessible* aOwner);
+
+ /**
+ * Moves children back under their original parents.
+ */
+ void PutChildrenBack(nsTArray<RefPtr<LocalAccessible>>* aChildren,
+ uint32_t aStartIdx);
+
+ bool MoveChild(LocalAccessible* aChild, LocalAccessible* aNewParent,
+ int32_t aIdxInParent);
+
+ /**
+ * Create accessible tree.
+ *
+ * @param aRoot [in] a root of subtree to create
+ * @param aFocusedAcc [in, optional] a focused accessible under created
+ * subtree if any
+ */
+ void CacheChildrenInSubtree(LocalAccessible* aRoot,
+ LocalAccessible** aFocusedAcc = nullptr);
+ void CreateSubtree(LocalAccessible* aRoot);
+
+ /**
+ * Remove accessibles in subtree from node to accessible map.
+ */
+ void UncacheChildrenInSubtree(LocalAccessible* aRoot);
+
+ /**
+ * Shutdown any cached accessible in the subtree.
+ *
+ * @param aAccessible [in] the root of the subrtee to invalidate accessible
+ * child/parent refs in
+ */
+ void ShutdownChildrenInSubtree(LocalAccessible* aAccessible);
+
+ /**
+ * Return true if the document is a target of document loading events
+ * (for example, state busy change or document reload events).
+ *
+ * Rules: The root chrome document accessible is never an event target
+ * (for example, Firefox UI window). If the sub document is loaded within its
+ * parent document then the parent document is a target only (aka events
+ * coalescence).
+ */
+ bool IsLoadEventTarget() const;
+
+ /*
+ * Set the object responsible for communicating with the main process on
+ * behalf of this document.
+ */
+ void SetIPCDoc(DocAccessibleChild* aIPCDoc);
+
+ friend class DocAccessibleChild;
+
+ /**
+ * Used to fire scrolling end event after page scroll.
+ *
+ * @param aTimer [in] the timer object
+ * @param aClosure [in] the document accessible where scrolling happens
+ */
+ static void ScrollTimerCallback(nsITimer* aTimer, void* aClosure);
+
+ void DispatchScrollingEvent(nsINode* aTarget, uint32_t aEventType);
+
+ /**
+ * Check if an id attribute change affects aria-activedescendant and handle
+ * the aria-activedescendant change if appropriate.
+ * If the currently focused element has aria-activedescendant and an
+ * element's id changes to match this, the id was probably moved from the
+ * previous active descendant, thus making this element the new active
+ * descendant. In that case, accessible focus must be changed accordingly.
+ */
+ void ARIAActiveDescendantIDMaybeMoved(LocalAccessible* aAccessible);
+
+ /**
+ * Traverse content subtree and for each node do one of 3 things:
+ * 1. Check if content node has an accessible that should be removed and
+ * remove it.
+ * 2. Check if content node has an accessible that needs to be recreated.
+ * Remove it and schedule it for reinsertion.
+ * 3. Check if content node has no accessible but needs one. Schedule one for
+ * insertion.
+ *
+ * Returns true if the root node should be reinserted.
+ */
+ bool PruneOrInsertSubtree(nsIContent* aRoot);
+
+ protected:
+ /**
+ * State and property flags, kept by mDocFlags.
+ */
+ enum {
+ // Whether the document is a top level content document in this process.
+ eTopLevelContentDocInProcess = 1 << 0
+ };
+
+ /**
+ * Cache of accessibles within this document accessible.
+ */
+ AccessibleHashtable mAccessibleCache;
+ nsTHashMap<nsPtrHashKey<const nsINode>, LocalAccessible*>
+ mNodeToAccessibleMap;
+
+ Document* mDocumentNode;
+ nsCOMPtr<nsITimer> mScrollWatchTimer;
+ nsTHashMap<nsPtrHashKey<nsINode>, TimeStamp> mLastScrollingDispatch;
+
+ /**
+ * Bit mask of document load states (@see LoadState).
+ */
+ uint32_t mLoadState : 3;
+
+ /**
+ * Bit mask of other states and props.
+ */
+ uint32_t mDocFlags : 27;
+
+ /**
+ * Tracks whether we have seen changes to this document's content that
+ * indicate we should re-send the viewport cache we use for hittesting.
+ * This value is set in `BundleFieldsForCache` and processed in
+ * `ProcessQueuedCacheUpdates`.
+ */
+ bool mViewportCacheDirty : 1;
+
+ /**
+ * Type of document load event fired after the document is loaded completely.
+ */
+ uint32_t mLoadEventType;
+
+ /**
+ * Reference to anchor jump element.
+ */
+ nsCOMPtr<nsIContent> mAnchorJumpElm;
+
+ /**
+ * A generic state (see items below) before the attribute value was changed.
+ * @see AttributeWillChange and AttributeChanged notifications.
+ */
+
+ // Previous state bits before attribute change
+ uint64_t mPrevStateBits;
+
+ nsTArray<RefPtr<DocAccessible>> mChildDocuments;
+
+ /**
+ * A storage class for pairing content with one of its relation attributes.
+ */
+ class AttrRelProvider {
+ public:
+ AttrRelProvider(nsAtom* aRelAttr, nsIContent* aContent)
+ : mRelAttr(aRelAttr), mContent(aContent) {}
+
+ nsAtom* mRelAttr;
+ nsCOMPtr<nsIContent> mContent;
+
+ private:
+ AttrRelProvider();
+ AttrRelProvider(const AttrRelProvider&);
+ AttrRelProvider& operator=(const AttrRelProvider&);
+ };
+
+ typedef nsTArray<mozilla::UniquePtr<AttrRelProvider>> AttrRelProviders;
+ typedef nsClassHashtable<nsStringHashKey, AttrRelProviders>
+ DependentIDsHashtable;
+
+ /**
+ * Returns/creates/removes attribute relation providers associated with
+ * a DOM document if the element is in uncomposed document or associated
+ * with shadow DOM the element is in.
+ */
+ AttrRelProviders* GetRelProviders(dom::Element* aElement,
+ const nsAString& aID) const;
+ AttrRelProviders* GetOrCreateRelProviders(dom::Element* aElement,
+ const nsAString& aID);
+ void RemoveRelProvidersIfEmpty(dom::Element* aElement, const nsAString& aID);
+
+ /**
+ * The cache of IDs pointed by relation attributes.
+ */
+ nsClassHashtable<nsPtrHashKey<dom::DocumentOrShadowRoot>,
+ DependentIDsHashtable>
+ mDependentIDsHashes;
+
+ friend class RelatedAccIterator;
+
+ /**
+ * Used for our caching algorithm. We store the list of nodes that should be
+ * invalidated.
+ *
+ * @see ProcessInvalidationList
+ */
+ nsTArray<RefPtr<nsIContent>> mInvalidationList;
+
+ /**
+ * Holds a list of aria-owns relocations.
+ */
+ nsClassHashtable<nsPtrHashKey<LocalAccessible>,
+ nsTArray<RefPtr<LocalAccessible>>>
+ mARIAOwnsHash;
+
+ /**
+ * Keeps a list of pending subtrees to update post-refresh.
+ */
+ nsTArray<RefPtr<nsIContent>> mPendingUpdates;
+
+ /**
+ * Used to process notification from core and accessible events.
+ */
+ RefPtr<NotificationController> mNotificationController;
+ friend class EventTree;
+ friend class NotificationController;
+
+ private:
+ void SetRoleMapEntryForDoc(dom::Element* aElement);
+
+ /**
+ * This must be called whenever an Accessible is moved in a content process.
+ * It keeps track of Accessibles moved during this tick.
+ */
+ void TrackMovedAccessible(LocalAccessible* aAcc);
+
+ /**
+ * For hidden subtrees, fire a name/description change event if the subtree
+ * is a target of aria-labelledby/describedby.
+ * This does nothing if it is called on a node which is not part of a hidden
+ * aria-labelledby/describedby target.
+ */
+ void MaybeHandleChangeToHiddenNameOrDescription(nsIContent* aChild);
+
+ void MaybeFireEventsForChangedPopover(LocalAccessible* aAcc);
+
+ PresShell* mPresShell;
+
+ // Exclusively owned by IPDL so don't manually delete it!
+ // Cleared in ActorDestroy
+ DocAccessibleChild* mIPCDoc;
+
+ // These data structures map between LocalAccessibles and CacheDomains,
+ // tracking cache updates that have been queued during the current tick but
+ // not yet sent. If there are a lot of nearby text cache updates (e.g. during
+ // a reflow), it is much more performant to process them in order because we
+ // then benefit from the layout line cursor. However, we still only want to
+ // process each LocalAccessible only once. Therefore, we use an array for
+ // ordering and a hash map to avoid duplicates, since Gecko has no ordered
+ // set data structure. The array contains pairs of LocalAccessible and cache
+ // domain. The hash map maps from LocalAccessible to the corresponding index
+ // in the array. These data structures must be kept in sync. It is possible
+ // for these to contain a reference to the document they live on. We clear
+ // them in Shutdown() to avoid cyclical references.
+ nsTArray<std::pair<RefPtr<LocalAccessible>, uint64_t>>
+ mQueuedCacheUpdatesArray;
+ nsTHashMap<LocalAccessible*, size_t> mQueuedCacheUpdatesHash;
+
+ // A set of Accessibles moved during this tick. Only used in content
+ // processes.
+ nsTHashSet<RefPtr<LocalAccessible>> mMovedAccessibles;
+ // A set of Accessibles inserted during this tick. Only used in content
+ // processes. This is needed to prevent insertions + moves of the same
+ // Accessible in the same tick from being tracked as moves.
+ nsTHashSet<RefPtr<LocalAccessible>> mInsertedAccessibles;
+ // A set of DOM nodes removed during this tick. This avoids a lot of pointless
+ // recursive DOM traversals.
+ nsTHashSet<nsIContent*> mRemovedNodes;
+};
+
+inline DocAccessible* LocalAccessible::AsDoc() {
+ return IsDoc() ? static_cast<DocAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/FormControlAccessible.cpp b/accessible/generic/FormControlAccessible.cpp
new file mode 100644
index 0000000000..dd9a7065cb
--- /dev/null
+++ b/accessible/generic/FormControlAccessible.cpp
@@ -0,0 +1,83 @@
+/* -*- 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/. */
+
+// NOTE: alphabetically ordered
+
+#include "FormControlAccessible.h"
+
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/a11y/Role.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// CheckboxAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role CheckboxAccessible::NativeRole() const { return roles::CHECKBUTTON; }
+
+void CheckboxAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == eAction_Click) {
+ uint64_t state = NativeState();
+ if (state & states::CHECKED) {
+ aName.AssignLiteral("uncheck");
+ } else if (state & states::MIXED) {
+ aName.AssignLiteral("cycle");
+ } else {
+ aName.AssignLiteral("check");
+ }
+ }
+}
+
+bool CheckboxAccessible::HasPrimaryAction() const { return true; }
+
+uint64_t CheckboxAccessible::NativeState() const {
+ uint64_t state = LeafAccessible::NativeState();
+
+ state |= states::CHECKABLE;
+ dom::HTMLInputElement* input = dom::HTMLInputElement::FromNode(mContent);
+ if (input) { // HTML:input@type="checkbox"
+ if (input->Indeterminate()) {
+ return state | states::MIXED;
+ }
+
+ if (input->Checked()) {
+ return state | states::CHECKED;
+ }
+
+ } else if (mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::checked, nsGkAtoms::_true,
+ eCaseMatters)) { // XUL checkbox
+ return state | states::CHECKED;
+ }
+
+ return state;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// CheckboxAccessible: Widgets
+
+bool CheckboxAccessible::IsWidget() const { return true; }
+
+////////////////////////////////////////////////////////////////////////////////
+// RadioButtonAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+RadioButtonAccessible::RadioButtonAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {}
+
+bool RadioButtonAccessible::HasPrimaryAction() const { return true; }
+
+void RadioButtonAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == eAction_Click) aName.AssignLiteral("select");
+}
+
+role RadioButtonAccessible::NativeRole() const { return roles::RADIOBUTTON; }
+
+////////////////////////////////////////////////////////////////////////////////
+// RadioButtonAccessible: Widgets
+
+bool RadioButtonAccessible::IsWidget() const { return true; }
diff --git a/accessible/generic/FormControlAccessible.h b/accessible/generic/FormControlAccessible.h
new file mode 100644
index 0000000000..44c142d4e2
--- /dev/null
+++ b/accessible/generic/FormControlAccessible.h
@@ -0,0 +1,65 @@
+/* -*- 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_FormControlAccessible_H_
+#define MOZILLA_A11Y_FormControlAccessible_H_
+
+#include "BaseAccessibles.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Checkbox accessible.
+ */
+class CheckboxAccessible : public LeafAccessible {
+ public:
+ enum { eAction_Click = 0 };
+
+ CheckboxAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {
+ // Ignore "CheckboxStateChange" DOM event in lieu of document observer
+ // state change notification.
+ if (aContent->IsHTMLElement()) {
+ mStateFlags |= eIgnoreDOMUIEvent;
+ }
+ }
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ // ActionAccessible
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual bool HasPrimaryAction() const override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+};
+
+/**
+ * Generic class used for radio buttons.
+ */
+class RadioButtonAccessible : public LeafAccessible {
+ public:
+ RadioButtonAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+
+ // ActionAccessible
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual bool HasPrimaryAction() const override;
+
+ enum { eAction_Click = 0 };
+
+ // Widgets
+ virtual bool IsWidget() const override;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/HyperTextAccessible-inl.h b/accessible/generic/HyperTextAccessible-inl.h
new file mode 100644
index 0000000000..daba5068b9
--- /dev/null
+++ b/accessible/generic/HyperTextAccessible-inl.h
@@ -0,0 +1,48 @@
+/* -*- 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_HyperTextAccessible_inl_h__
+#define mozilla_a11y_HyperTextAccessible_inl_h__
+
+#include "HyperTextAccessible.h"
+
+#include "nsAccUtils.h"
+
+#include "nsIClipboard.h"
+#include "nsFrameSelection.h"
+
+#include "mozilla/CaretAssociationHint.h"
+#include "mozilla/EditorBase.h"
+
+namespace mozilla::a11y {
+
+inline void HyperTextAccessible::SetCaretOffset(int32_t aOffset) {
+ SetSelectionRange(aOffset, aOffset);
+ // XXX: Force cache refresh until a good solution for AT emulation of user
+ // input is implemented (AccessFu caret movement).
+ SelectionMgr()->UpdateCaretOffset(this, aOffset);
+}
+
+inline bool HyperTextAccessible::IsCaretAtEndOfLine() const {
+ RefPtr<nsFrameSelection> frameSelection = FrameSelection();
+ return frameSelection &&
+ frameSelection->GetHint() == CaretAssociationHint::Before;
+}
+
+inline already_AddRefed<nsFrameSelection> HyperTextAccessible::FrameSelection()
+ const {
+ nsIFrame* frame = GetFrame();
+ return frame ? frame->GetFrameSelection() : nullptr;
+}
+
+inline dom::Selection* HyperTextAccessible::DOMSelection() const {
+ RefPtr<nsFrameSelection> frameSelection = FrameSelection();
+ return frameSelection ? frameSelection->GetSelection(SelectionType::eNormal)
+ : nullptr;
+}
+
+} // namespace mozilla::a11y
+
+#endif
diff --git a/accessible/generic/HyperTextAccessible.cpp b/accessible/generic/HyperTextAccessible.cpp
new file mode 100644
index 0000000000..eaeba0ccf9
--- /dev/null
+++ b/accessible/generic/HyperTextAccessible.cpp
@@ -0,0 +1,1141 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=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/. */
+
+#include "HyperTextAccessible-inl.h"
+
+#include "nsAccessibilityService.h"
+#include "nsIAccessibleTypes.h"
+#include "AccAttributes.h"
+#include "HTMLListAccessible.h"
+#include "LocalAccessible-inl.h"
+#include "Relation.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+#include "TextAttrs.h"
+#include "TextRange.h"
+#include "TreeWalker.h"
+
+#include "nsCaret.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsFocusManager.h"
+#include "nsIEditingSession.h"
+#include "nsContainerFrame.h"
+#include "nsFrameSelection.h"
+#include "nsILineIterator.h"
+#include "nsIScrollableFrame.h"
+#include "nsIMathMLFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsRange.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/EditorBase.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/IntegerRange.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/SelectionMovementUtils.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/Selection.h"
+#include "gfxSkipChars.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// HyperTextAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HyperTextAccessible::HyperTextAccessible(nsIContent* aNode, DocAccessible* aDoc)
+ : AccessibleWrap(aNode, aDoc) {
+ mType = eHyperTextType;
+ mGenericTypes |= eHyperText;
+}
+
+role HyperTextAccessible::NativeRole() const {
+ a11y::role r = GetAccService()->MarkupRole(mContent);
+ if (r != roles::NOTHING) return r;
+
+ nsIFrame* frame = GetFrame();
+ if (frame && frame->IsInlineFrame()) return roles::TEXT;
+
+ return roles::TEXT_CONTAINER;
+}
+
+uint64_t HyperTextAccessible::NativeState() const {
+ uint64_t states = AccessibleWrap::NativeState();
+
+ if (IsEditable()) {
+ states |= states::EDITABLE;
+
+ } else if (mContent->IsHTMLElement(nsGkAtoms::article)) {
+ // We want <article> to behave like a document in terms of readonly state.
+ states |= states::READONLY;
+ }
+
+ nsIFrame* frame = GetFrame();
+ if ((states & states::EDITABLE) || (frame && frame->IsSelectable(nullptr))) {
+ // If the accessible is editable the layout selectable state only disables
+ // mouse selection, but keyboard (shift+arrow) selection is still possible.
+ states |= states::SELECTABLE_TEXT;
+ }
+
+ return states;
+}
+
+bool HyperTextAccessible::IsEditable() const {
+ if (!mContent) {
+ return false;
+ }
+ return mContent->AsElement()->State().HasState(dom::ElementState::READWRITE);
+}
+
+uint32_t HyperTextAccessible::DOMPointToOffset(nsINode* aNode,
+ int32_t aNodeOffset,
+ bool aIsEndOffset) const {
+ if (!aNode) return 0;
+
+ uint32_t offset = 0;
+ nsINode* findNode = nullptr;
+
+ if (aNodeOffset == -1) {
+ findNode = aNode;
+
+ } else if (aNode->IsText()) {
+ // For text nodes, aNodeOffset comes in as a character offset
+ // Text offset will be added at the end, if we find the offset in this
+ // hypertext We want the "skipped" offset into the text (rendered text
+ // without the extra whitespace)
+ nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
+ NS_ENSURE_TRUE(frame, 0);
+
+ nsresult rv = ContentToRenderedOffset(frame, aNodeOffset, &offset);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ findNode = aNode;
+
+ } else {
+ // findNode could be null if aNodeOffset == # of child nodes, which means
+ // one of two things:
+ // 1) there are no children, and the passed-in node is not mContent -- use
+ // parentContent for the node to find
+ // 2) there are no children and the passed-in node is mContent, which means
+ // we're an empty nsIAccessibleText
+ // 3) there are children and we're at the end of the children
+
+ findNode = aNode->GetChildAt_Deprecated(aNodeOffset);
+ if (!findNode) {
+ if (aNodeOffset == 0) {
+ if (aNode == GetNode()) {
+ // Case #1: this accessible has no children and thus has empty text,
+ // we can only be at hypertext offset 0.
+ return 0;
+ }
+
+ // Case #2: there are no children, we're at this node.
+ findNode = aNode;
+ } else if (aNodeOffset == static_cast<int32_t>(aNode->GetChildCount())) {
+ // Case #3: we're after the last child, get next node to this one.
+ for (nsINode* tmpNode = aNode;
+ !findNode && tmpNode && tmpNode != mContent;
+ tmpNode = tmpNode->GetParent()) {
+ findNode = tmpNode->GetNextSibling();
+ }
+ }
+ }
+ }
+
+ // Get accessible for this findNode, or if that node isn't accessible, use the
+ // accessible for the next DOM node which has one (based on forward depth
+ // first search)
+ LocalAccessible* descendant = nullptr;
+ if (findNode) {
+ dom::HTMLBRElement* brElement = dom::HTMLBRElement::FromNode(findNode);
+ if (brElement && brElement->IsPaddingForEmptyEditor()) {
+ // This <br> is the hacky "padding <br> element" used when there is no
+ // text in the editor.
+ return 0;
+ }
+
+ descendant = mDoc->GetAccessible(findNode);
+ if (!descendant && findNode->IsContent()) {
+ LocalAccessible* container = mDoc->GetContainerAccessible(findNode);
+ if (container) {
+ TreeWalker walker(container, findNode->AsContent(),
+ TreeWalker::eWalkContextTree);
+ descendant = walker.Next();
+ if (!descendant) descendant = container;
+ }
+ }
+ }
+
+ return TransformOffset(descendant, offset, aIsEndOffset);
+}
+
+uint32_t HyperTextAccessible::TransformOffset(LocalAccessible* aDescendant,
+ uint32_t aOffset,
+ bool aIsEndOffset) const {
+ // From the descendant, go up and get the immediate child of this hypertext.
+ uint32_t offset = aOffset;
+ LocalAccessible* descendant = aDescendant;
+ while (descendant) {
+ LocalAccessible* parent = descendant->LocalParent();
+ if (parent == this) return GetChildOffset(descendant) + offset;
+
+ // This offset no longer applies because the passed-in text object is not
+ // a child of the hypertext. This happens when there are nested hypertexts,
+ // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset
+ // to make it relative the hypertext.
+ // If the end offset is not supposed to be inclusive and the original point
+ // is not at 0 offset then the returned offset should be after an embedded
+ // character the original point belongs to.
+ if (aIsEndOffset) {
+ // Similar to our special casing in FindOffset, we add handling for
+ // bulleted lists here because PeekOffset returns the inner text node
+ // for a list when it should return the list bullet.
+ // We manually set the offset so the error doesn't propagate up.
+ if (offset == 0 && parent && parent->IsHTMLListItem() &&
+ descendant->LocalPrevSibling() &&
+ descendant->LocalPrevSibling() ==
+ parent->AsHTMLListItem()->Bullet()) {
+ offset = 0;
+ } else {
+ offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0;
+ }
+ } else {
+ offset = 0;
+ }
+
+ descendant = parent;
+ }
+
+ // If the given a11y point cannot be mapped into offset relative this
+ // hypertext offset then return length as fallback value.
+ return CharacterCount();
+}
+
+DOMPoint HyperTextAccessible::OffsetToDOMPoint(int32_t aOffset) const {
+ // 0 offset is valid even if no children. In this case the associated editor
+ // is empty so return a DOM point for editor root element.
+ if (aOffset == 0) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ if (editorBase->IsEmpty()) {
+ return DOMPoint(editorBase->GetRoot(), 0);
+ }
+ }
+ }
+
+ int32_t childIdx = GetChildIndexAtOffset(aOffset);
+ if (childIdx == -1) return DOMPoint();
+
+ LocalAccessible* child = LocalChildAt(childIdx);
+ int32_t innerOffset = aOffset - GetChildOffset(childIdx);
+
+ // A text leaf case.
+ if (child->IsTextLeaf()) {
+ // The point is inside the text node. This is always true for any text leaf
+ // except a last child one. See assertion below.
+ if (aOffset < GetChildOffset(childIdx + 1)) {
+ nsIContent* content = child->GetContent();
+ int32_t idx = 0;
+ if (NS_FAILED(RenderedToContentOffset(content->GetPrimaryFrame(),
+ innerOffset, &idx))) {
+ return DOMPoint();
+ }
+
+ return DOMPoint(content, idx);
+ }
+
+ // Set the DOM point right after the text node.
+ MOZ_ASSERT(static_cast<uint32_t>(aOffset) == CharacterCount());
+ innerOffset = 1;
+ }
+
+ // Case of embedded object. The point is either before or after the element.
+ NS_ASSERTION(innerOffset == 0 || innerOffset == 1, "A wrong inner offset!");
+ nsINode* node = child->GetNode();
+ nsINode* parentNode = node->GetParentNode();
+ return parentNode ? DOMPoint(parentNode,
+ parentNode->ComputeIndexOf_Deprecated(node) +
+ innerOffset)
+ : DOMPoint();
+}
+
+already_AddRefed<AccAttributes> HyperTextAccessible::DefaultTextAttributes() {
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+
+ TextAttrsMgr textAttrsMgr(this);
+ textAttrsMgr.GetAttributes(attributes);
+ return attributes.forget();
+}
+
+void HyperTextAccessible::SetMathMLXMLRoles(AccAttributes* aAttributes) {
+ // Add MathML xmlroles based on the position inside the parent.
+ LocalAccessible* parent = LocalParent();
+ if (parent) {
+ switch (parent->Role()) {
+ case roles::MATHML_CELL:
+ case roles::MATHML_ENCLOSED:
+ case roles::MATHML_ERROR:
+ case roles::MATHML_MATH:
+ case roles::MATHML_ROW:
+ case roles::MATHML_SQUARE_ROOT:
+ case roles::MATHML_STYLE:
+ if (Role() == roles::MATHML_OPERATOR) {
+ // This is an operator inside an <mrow> (or an inferred <mrow>).
+ // See http://www.w3.org/TR/MathML3/chapter3.html#presm.inferredmrow
+ // XXX We should probably do something similar for MATHML_FENCED, but
+ // operators do not appear in the accessible tree. See bug 1175747.
+ nsIMathMLFrame* mathMLFrame = do_QueryFrame(GetFrame());
+ if (mathMLFrame) {
+ nsEmbellishData embellishData;
+ mathMLFrame->GetEmbellishData(embellishData);
+ if (NS_MATHML_EMBELLISH_IS_FENCE(embellishData.flags)) {
+ if (!LocalPrevSibling()) {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ nsGkAtoms::open_fence);
+ } else if (!LocalNextSibling()) {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ nsGkAtoms::close_fence);
+ }
+ }
+ if (NS_MATHML_EMBELLISH_IS_SEPARATOR(embellishData.flags)) {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ nsGkAtoms::separator_);
+ }
+ }
+ }
+ break;
+ case roles::MATHML_FRACTION:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles, IndexInParent() == 0 ? nsGkAtoms::numerator
+ : nsGkAtoms::denominator);
+ break;
+ case roles::MATHML_ROOT:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::root_index);
+ break;
+ case roles::MATHML_SUB:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::subscript);
+ break;
+ case roles::MATHML_SUP:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::superscript);
+ break;
+ case roles::MATHML_SUB_SUP: {
+ int32_t index = IndexInParent();
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ index == 0
+ ? nsGkAtoms::base
+ : (index == 1 ? nsGkAtoms::subscript : nsGkAtoms::superscript));
+ } break;
+ case roles::MATHML_UNDER:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::underscript);
+ break;
+ case roles::MATHML_OVER:
+ aAttributes->SetAttribute(
+ nsGkAtoms::xmlroles,
+ IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::overscript);
+ break;
+ case roles::MATHML_UNDER_OVER: {
+ int32_t index = IndexInParent();
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ index == 0
+ ? nsGkAtoms::base
+ : (index == 1 ? nsGkAtoms::underscript
+ : nsGkAtoms::overscript));
+ } break;
+ case roles::MATHML_MULTISCRIPTS: {
+ // Get the <multiscripts> base.
+ nsIContent* child;
+ bool baseFound = false;
+ for (child = parent->GetContent()->GetFirstChild(); child;
+ child = child->GetNextSibling()) {
+ if (child->IsMathMLElement()) {
+ baseFound = true;
+ break;
+ }
+ }
+ if (baseFound) {
+ nsIContent* content = GetContent();
+ if (child == content) {
+ // We are the base.
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles, nsGkAtoms::base);
+ } else {
+ // Browse the list of scripts to find us and determine our type.
+ bool postscript = true;
+ bool subscript = true;
+ for (child = child->GetNextSibling(); child;
+ child = child->GetNextSibling()) {
+ if (!child->IsMathMLElement()) continue;
+ if (child->IsMathMLElement(nsGkAtoms::mprescripts_)) {
+ postscript = false;
+ subscript = true;
+ continue;
+ }
+ if (child == content) {
+ if (postscript) {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ subscript ? nsGkAtoms::subscript
+ : nsGkAtoms::superscript);
+ } else {
+ aAttributes->SetAttribute(nsGkAtoms::xmlroles,
+ subscript
+ ? nsGkAtoms::presubscript
+ : nsGkAtoms::presuperscript);
+ }
+ break;
+ }
+ subscript = !subscript;
+ }
+ }
+ }
+ } break;
+ default:
+ break;
+ }
+ }
+}
+
+already_AddRefed<AccAttributes> HyperTextAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = AccessibleWrap::NativeAttributes();
+
+ // 'formatting' attribute is deprecated, 'display' attribute should be
+ // instead.
+ nsIFrame* frame = GetFrame();
+ if (frame && frame->IsBlockFrame()) {
+ attributes->SetAttribute(nsGkAtoms::formatting, nsGkAtoms::block);
+ }
+
+ if (FocusMgr()->IsFocused(this)) {
+ int32_t lineNumber = CaretLineNumber();
+ if (lineNumber >= 1) {
+ attributes->SetAttribute(nsGkAtoms::lineNumber, lineNumber);
+ }
+ }
+
+ if (HasOwnContent()) {
+ GetAccService()->MarkupAttributes(this, attributes);
+ if (mContent->IsMathMLElement()) SetMathMLXMLRoles(attributes);
+ }
+
+ return attributes.forget();
+}
+
+int32_t HyperTextAccessible::OffsetAtPoint(int32_t aX, int32_t aY,
+ uint32_t aCoordType) {
+ nsIFrame* hyperFrame = GetFrame();
+ if (!hyperFrame) return -1;
+
+ LayoutDeviceIntPoint coords =
+ nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, this);
+
+ nsPresContext* presContext = mDoc->PresContext();
+ nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits(
+ coords, presContext->AppUnitsPerDevPixel());
+
+ nsRect frameScreenRect = hyperFrame->GetScreenRectInAppUnits();
+ if (!frameScreenRect.Contains(coordsInAppUnits.x, coordsInAppUnits.y)) {
+ return -1; // Not found
+ }
+
+ nsPoint pointInHyperText(coordsInAppUnits.x - frameScreenRect.X(),
+ coordsInAppUnits.y - frameScreenRect.Y());
+
+ // Go through the frames to check if each one has the point.
+ // When one does, add up the character offsets until we have a match
+
+ // We have an point in an accessible child of this, now we need to add up the
+ // offsets before it to what we already have
+ int32_t offset = 0;
+ uint32_t childCount = ChildCount();
+ for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
+ LocalAccessible* childAcc = mChildren[childIdx];
+
+ nsIFrame* primaryFrame = childAcc->GetFrame();
+ NS_ENSURE_TRUE(primaryFrame, -1);
+
+ nsIFrame* frame = primaryFrame;
+ while (frame) {
+ nsIContent* content = frame->GetContent();
+ NS_ENSURE_TRUE(content, -1);
+ nsPoint pointInFrame = pointInHyperText - frame->GetOffsetTo(hyperFrame);
+ nsSize frameSize = frame->GetSize();
+ if (pointInFrame.x < frameSize.width &&
+ pointInFrame.y < frameSize.height) {
+ // Finished
+ if (frame->IsTextFrame()) {
+ nsIFrame::ContentOffsets contentOffsets =
+ frame->GetContentOffsetsFromPointExternal(
+ pointInFrame, nsIFrame::IGNORE_SELECTION_STYLE);
+ if (contentOffsets.IsNull() || contentOffsets.content != content) {
+ return -1; // Not found
+ }
+ uint32_t addToOffset;
+ nsresult rv = ContentToRenderedOffset(
+ primaryFrame, contentOffsets.offset, &addToOffset);
+ NS_ENSURE_SUCCESS(rv, -1);
+ offset += addToOffset;
+ }
+ return offset;
+ }
+ frame = frame->GetNextContinuation();
+ }
+
+ offset += nsAccUtils::TextLength(childAcc);
+ }
+
+ return -1; // Not found
+}
+
+already_AddRefed<EditorBase> HyperTextAccessible::GetEditor() const {
+ if (!mContent->HasFlag(NODE_IS_EDITABLE)) {
+ // If we're inside an editable container, then return that container's
+ // editor
+ LocalAccessible* ancestor = LocalParent();
+ while (ancestor) {
+ HyperTextAccessible* hyperText = ancestor->AsHyperText();
+ if (hyperText) {
+ // Recursion will stop at container doc because it has its own impl
+ // of GetEditor()
+ return hyperText->GetEditor();
+ }
+
+ ancestor = ancestor->LocalParent();
+ }
+
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mContent);
+ nsCOMPtr<nsIEditingSession> editingSession;
+ docShell->GetEditingSession(getter_AddRefs(editingSession));
+ if (!editingSession) return nullptr; // No editing session interface
+
+ dom::Document* docNode = mDoc->DocumentNode();
+ RefPtr<HTMLEditor> htmlEditor =
+ editingSession->GetHTMLEditorForWindow(docNode->GetWindow());
+ return htmlEditor.forget();
+}
+
+/**
+ * =================== Caret & Selection ======================
+ */
+
+nsresult HyperTextAccessible::SetSelectionRange(int32_t aStartPos,
+ int32_t aEndPos) {
+ // Before setting the selection range, we need to ensure that the editor
+ // is initialized. (See bug 804927.)
+ // Otherwise, it's possible that lazy editor initialization will override
+ // the selection we set here and leave the caret at the end of the text.
+ // By calling GetEditor here, we ensure that editor initialization is
+ // completed before we set the selection.
+ RefPtr<EditorBase> editorBase = GetEditor();
+
+ bool isFocusable = InteractiveState() & states::FOCUSABLE;
+
+ // If accessible is focusable then focus it before setting the selection to
+ // neglect control's selection changes on focus if any (for example, inputs
+ // that do select all on focus).
+ // some input controls
+ if (isFocusable) TakeFocus();
+
+ RefPtr<dom::Selection> domSel = DOMSelection();
+ NS_ENSURE_STATE(domSel);
+
+ // Set up the selection.
+ domSel->RemoveAllRanges(IgnoreErrors());
+ SetSelectionBoundsAt(0, aStartPos, aEndPos);
+
+ // Make sure it is visible
+ domSel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
+ ScrollAxis(), ScrollAxis(),
+ dom::Selection::SCROLL_FOR_CARET_MOVE |
+ dom::Selection::SCROLL_OVERFLOW_HIDDEN);
+
+ // When selection is done, move the focus to the selection if accessible is
+ // not focusable. That happens when selection is set within hypertext
+ // accessible.
+ if (isFocusable) return NS_OK;
+
+ nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager();
+ if (DOMFocusManager) {
+ NS_ENSURE_TRUE(mDoc, NS_ERROR_FAILURE);
+ dom::Document* docNode = mDoc->DocumentNode();
+ NS_ENSURE_TRUE(docNode, NS_ERROR_FAILURE);
+ nsCOMPtr<nsPIDOMWindowOuter> window = docNode->GetWindow();
+ RefPtr<dom::Element> result;
+ DOMFocusManager->MoveFocus(
+ window, nullptr, nsIFocusManager::MOVEFOCUS_CARET,
+ nsIFocusManager::FLAG_BYMOVEFOCUS, getter_AddRefs(result));
+ }
+
+ return NS_OK;
+}
+
+int32_t HyperTextAccessible::CaretOffset() const {
+ // Not focused focusable accessible except document accessible doesn't have
+ // a caret.
+ if (!IsDoc() && !FocusMgr()->IsFocused(this) &&
+ (InteractiveState() & states::FOCUSABLE)) {
+ return -1;
+ }
+
+ // Check cached value.
+ int32_t caretOffset = -1;
+ HyperTextAccessible* text = SelectionMgr()->AccessibleWithCaret(&caretOffset);
+
+ // Use cached value if it corresponds to this accessible.
+ if (caretOffset != -1) {
+ if (text == this) return caretOffset;
+
+ nsINode* textNode = text->GetNode();
+ // Ignore offset if cached accessible isn't a text leaf.
+ if (nsCoreUtils::IsAncestorOf(GetNode(), textNode)) {
+ return TransformOffset(text, textNode->IsText() ? caretOffset : 0, false);
+ }
+ }
+
+ // No caret if the focused node is not inside this DOM node and this DOM node
+ // is not inside of focused node.
+ FocusManager::FocusDisposition focusDisp =
+ FocusMgr()->IsInOrContainsFocus(this);
+ if (focusDisp == FocusManager::eNone) return -1;
+
+ // Turn the focus node and offset of the selection into caret hypretext
+ // offset.
+ dom::Selection* domSel = DOMSelection();
+ NS_ENSURE_TRUE(domSel, -1);
+
+ nsINode* focusNode = domSel->GetFocusNode();
+ uint32_t focusOffset = domSel->FocusOffset();
+
+ // No caret if this DOM node is inside of focused node but the selection's
+ // focus point is not inside of this DOM node.
+ if (focusDisp == FocusManager::eContainedByFocus) {
+ nsINode* resultNode =
+ nsCoreUtils::GetDOMNodeFromDOMPoint(focusNode, focusOffset);
+
+ nsINode* thisNode = GetNode();
+ if (resultNode != thisNode &&
+ !nsCoreUtils::IsAncestorOf(thisNode, resultNode)) {
+ return -1;
+ }
+ }
+
+ return DOMPointToOffset(focusNode, focusOffset);
+}
+
+int32_t HyperTextAccessible::CaretLineNumber() {
+ // Provide the line number for the caret, relative to the
+ // currently focused node. Use a 1-based index
+ RefPtr<nsFrameSelection> frameSelection = FrameSelection();
+ if (!frameSelection) return -1;
+
+ dom::Selection* domSel = frameSelection->GetSelection(SelectionType::eNormal);
+ if (!domSel) return -1;
+
+ nsINode* caretNode = domSel->GetFocusNode();
+ if (!caretNode || !caretNode->IsContent()) return -1;
+
+ nsIContent* caretContent = caretNode->AsContent();
+ if (!nsCoreUtils::IsAncestorOf(GetNode(), caretContent)) return -1;
+
+ uint32_t caretOffset = domSel->FocusOffset();
+ CaretAssociationHint hint = frameSelection->GetHint();
+ nsIFrame* caretFrame = SelectionMovementUtils::GetFrameForNodeOffset(
+ caretContent, caretOffset, hint);
+ NS_ENSURE_TRUE(caretFrame, -1);
+
+ AutoAssertNoDomMutations guard; // The nsILineIterators below will break if
+ // the DOM is modified while they're in use!
+ int32_t lineNumber = 1;
+ nsILineIterator* lineIterForCaret = nullptr;
+ nsIContent* hyperTextContent = IsContent() ? mContent.get() : nullptr;
+ while (caretFrame) {
+ if (hyperTextContent == caretFrame->GetContent()) {
+ return lineNumber; // Must be in a single line hyper text, there is no
+ // line iterator
+ }
+ nsContainerFrame* parentFrame = caretFrame->GetParent();
+ if (!parentFrame) break;
+
+ // Add lines for the sibling frames before the caret
+ nsIFrame* sibling = parentFrame->PrincipalChildList().FirstChild();
+ while (sibling && sibling != caretFrame) {
+ nsILineIterator* lineIterForSibling = sibling->GetLineIterator();
+ if (lineIterForSibling) {
+ // For the frames before that grab all the lines
+ int32_t addLines = lineIterForSibling->GetNumLines();
+ lineNumber += addLines;
+ }
+ sibling = sibling->GetNextSibling();
+ }
+
+ // Get the line number relative to the container with lines
+ if (!lineIterForCaret) { // Add the caret line just once
+ lineIterForCaret = parentFrame->GetLineIterator();
+ if (lineIterForCaret) {
+ // Ancestor of caret
+ int32_t addLines = lineIterForCaret->FindLineContaining(caretFrame);
+ lineNumber += addLines;
+ }
+ }
+
+ caretFrame = parentFrame;
+ }
+
+ MOZ_ASSERT_UNREACHABLE(
+ "DOM ancestry had this hypertext but frame ancestry didn't");
+ return lineNumber;
+}
+
+LayoutDeviceIntRect HyperTextAccessible::GetCaretRect(nsIWidget** aWidget) {
+ *aWidget = nullptr;
+
+ RefPtr<nsCaret> caret = mDoc->PresShellPtr()->GetCaret();
+ NS_ENSURE_TRUE(caret, LayoutDeviceIntRect());
+
+ bool isVisible = caret->IsVisible();
+ if (!isVisible) return LayoutDeviceIntRect();
+
+ nsRect rect;
+ nsIFrame* frame = caret->GetGeometry(&rect);
+ if (!frame || rect.IsEmpty()) return LayoutDeviceIntRect();
+
+ PresShell* presShell = mDoc->PresShellPtr();
+ // Transform rect to be relative to the root frame.
+ nsIFrame* rootFrame = presShell->GetRootFrame();
+ rect = nsLayoutUtils::TransformFrameRectToAncestor(frame, rect, rootFrame);
+ // We need to inverse translate with the offset of the edge of the visual
+ // viewport from top edge of the layout viewport.
+ nsPoint viewportOffset = presShell->GetVisualViewportOffset() -
+ presShell->GetLayoutViewportOffset();
+ rect.MoveBy(-viewportOffset);
+ // We need to take into account a non-1 resolution set on the presshell.
+ // This happens with async pinch zooming. Here we scale the bounds before
+ // adding the screen-relative offset.
+ rect.ScaleRoundOut(presShell->GetResolution());
+ // Now we need to put the rect in absolute screen coords.
+ nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits();
+ rect.MoveBy(rootScreenRect.TopLeft());
+ // Finally, convert from app units.
+ auto caretRect = LayoutDeviceIntRect::FromAppUnitsToNearest(
+ rect, presShell->GetPresContext()->AppUnitsPerDevPixel());
+
+ // Correct for character size, so that caret always matches the size of
+ // the character. This is important for font size transitions, and is
+ // necessary because the Gecko caret uses the previous character's size as
+ // the user moves forward in the text by character.
+ int32_t caretOffset = CaretOffset();
+ if (NS_WARN_IF(caretOffset == -1)) {
+ // The caret offset will be -1 if this Accessible isn't focused. Note that
+ // the DOM node contaning the caret might be focused, but the Accessible
+ // might not be; e.g. due to an autocomplete popup suggestion having a11y
+ // focus.
+ return LayoutDeviceIntRect();
+ }
+ LayoutDeviceIntRect charRect = CharBounds(
+ caretOffset, nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE);
+ if (!charRect.IsEmpty()) {
+ caretRect.SetTopEdge(charRect.Y());
+ }
+
+ *aWidget = frame->GetNearestWidget();
+ return caretRect;
+}
+
+void HyperTextAccessible::GetSelectionDOMRanges(SelectionType aSelectionType,
+ nsTArray<nsRange*>* aRanges) {
+ if (IsDoc() && !AsDoc()->HasLoadState(DocAccessible::eTreeConstructed)) {
+ // Rarely, a client query can be handled after a DocAccessible is created
+ // but before the initial tree is constructed, since DoInitialUpdate happens
+ // during a refresh tick. In that case, there might be a DOM selection, but
+ // we can't use it. We will crash if we try due to mContent being null, etc.
+ // This should only happen in the parent process because we should never
+ // try to push the cache in a content process before the initial tree is
+ // constructed.
+ MOZ_ASSERT(XRE_IsParentProcess(), "Query before DoInitialUpdate");
+ return;
+ }
+ // Ignore selection if it is not visible.
+ RefPtr<nsFrameSelection> frameSelection = FrameSelection();
+ if (!frameSelection || frameSelection->GetDisplaySelection() <=
+ nsISelectionController::SELECTION_HIDDEN) {
+ return;
+ }
+
+ dom::Selection* domSel = frameSelection->GetSelection(aSelectionType);
+ if (!domSel) return;
+
+ nsINode* startNode = GetNode();
+
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ startNode = editorBase->GetRoot();
+ }
+
+ if (!startNode) return;
+
+ uint32_t childCount = startNode->GetChildCount();
+ nsresult rv = domSel->GetDynamicRangesForIntervalArray(
+ startNode, 0, startNode, childCount, true, aRanges);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // Remove collapsed ranges
+ aRanges->RemoveElementsBy(
+ [](const auto& range) { return range->Collapsed(); });
+}
+
+int32_t HyperTextAccessible::SelectionCount() {
+ nsTArray<nsRange*> ranges;
+ GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
+ return ranges.Length();
+}
+
+bool HyperTextAccessible::SelectionBoundsAt(int32_t aSelectionNum,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset) {
+ *aStartOffset = *aEndOffset = 0;
+
+ nsTArray<nsRange*> ranges;
+ GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
+
+ uint32_t rangeCount = ranges.Length();
+ if (aSelectionNum < 0 || aSelectionNum >= static_cast<int32_t>(rangeCount)) {
+ return false;
+ }
+
+ nsRange* range = ranges[aSelectionNum];
+
+ // Get start and end points.
+ nsINode* startNode = range->GetStartContainer();
+ nsINode* endNode = range->GetEndContainer();
+ uint32_t startOffset = range->StartOffset();
+ uint32_t endOffset = range->EndOffset();
+
+ // Make sure start is before end, by swapping DOM points. This occurs when
+ // the user selects backwards in the text.
+ const Maybe<int32_t> order =
+ nsContentUtils::ComparePoints(endNode, endOffset, startNode, startOffset);
+
+ if (!order) {
+ MOZ_ASSERT_UNREACHABLE();
+ return false;
+ }
+
+ if (*order < 0) {
+ std::swap(startNode, endNode);
+ std::swap(startOffset, endOffset);
+ }
+
+ if (!startNode->IsInclusiveDescendantOf(mContent)) {
+ *aStartOffset = 0;
+ } else {
+ *aStartOffset =
+ DOMPointToOffset(startNode, AssertedCast<int32_t>(startOffset));
+ }
+
+ if (!endNode->IsInclusiveDescendantOf(mContent)) {
+ *aEndOffset = CharacterCount();
+ } else {
+ *aEndOffset =
+ DOMPointToOffset(endNode, AssertedCast<int32_t>(endOffset), true);
+ }
+ return true;
+}
+
+bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) {
+ RefPtr<dom::Selection> domSel = DOMSelection();
+ if (!domSel) return false;
+
+ if (aSelectionNum < 0 ||
+ aSelectionNum >= static_cast<int32_t>(domSel->RangeCount())) {
+ return false;
+ }
+
+ const RefPtr<nsRange> range{
+ domSel->GetRangeAt(static_cast<uint32_t>(aSelectionNum))};
+ domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
+ IgnoreErrors());
+ return true;
+}
+
+void HyperTextAccessible::ScrollSubstringToPoint(int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aCoordinateType,
+ int32_t aX, int32_t aY) {
+ nsIFrame* frame = GetFrame();
+ if (!frame) return;
+
+ LayoutDeviceIntPoint coords =
+ nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this);
+
+ RefPtr<nsRange> domRange = nsRange::Create(mContent);
+ TextRange range(this, this, aStartOffset, this, aEndOffset);
+ if (!range.AssignDOMRange(domRange)) {
+ return;
+ }
+
+ nsPresContext* presContext = frame->PresContext();
+ nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits(
+ coords, presContext->AppUnitsPerDevPixel());
+
+ bool initialScrolled = false;
+ nsIFrame* parentFrame = frame;
+ while ((parentFrame = parentFrame->GetParent())) {
+ nsIScrollableFrame* scrollableFrame = do_QueryFrame(parentFrame);
+ if (scrollableFrame) {
+ if (!initialScrolled) {
+ // Scroll substring to the given point. Turn the point into percents
+ // relative scrollable area to use nsCoreUtils::ScrollSubstringTo.
+ nsRect frameRect = parentFrame->GetScreenRectInAppUnits();
+ nscoord offsetPointX = coordsInAppUnits.x - frameRect.X();
+ nscoord offsetPointY = coordsInAppUnits.y - frameRect.Y();
+
+ nsSize size(parentFrame->GetSize());
+
+ // avoid divide by zero
+ size.width = size.width ? size.width : 1;
+ size.height = size.height ? size.height : 1;
+
+ int16_t hPercent = offsetPointX * 100 / size.width;
+ int16_t vPercent = offsetPointY * 100 / size.height;
+
+ nsresult rv = nsCoreUtils::ScrollSubstringTo(
+ frame, domRange,
+ ScrollAxis(WhereToScroll(vPercent), WhenToScroll::Always),
+ ScrollAxis(WhereToScroll(hPercent), WhenToScroll::Always));
+ if (NS_FAILED(rv)) return;
+
+ initialScrolled = true;
+ } else {
+ // Substring was scrolled to the given point already inside its closest
+ // scrollable area. If there are nested scrollable areas then make
+ // sure we scroll lower areas to the given point inside currently
+ // traversed scrollable area.
+ nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords);
+ }
+ }
+ frame = parentFrame;
+ }
+}
+
+void HyperTextAccessible::SelectionRanges(
+ nsTArray<a11y::TextRange>* aRanges) const {
+ dom::Selection* sel = DOMSelection();
+ if (!sel) {
+ return;
+ }
+
+ TextRange::TextRangesFromSelection(sel, aRanges);
+}
+
+void HyperTextAccessible::ReplaceText(const nsAString& aText) {
+ if (aText.Length() == 0) {
+ DeleteText(0, CharacterCount());
+ return;
+ }
+
+ SetSelectionRange(0, CharacterCount());
+
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (!editorBase) {
+ return;
+ }
+
+ DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new text");
+}
+
+void HyperTextAccessible::InsertText(const nsAString& aText,
+ int32_t aPosition) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ SetSelectionRange(aPosition, aPosition);
+ DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the text");
+ }
+}
+
+void HyperTextAccessible::CopyText(int32_t aStartPos, int32_t aEndPos) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ SetSelectionRange(aStartPos, aEndPos);
+ editorBase->Copy();
+ }
+}
+
+void HyperTextAccessible::CutText(int32_t aStartPos, int32_t aEndPos) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ SetSelectionRange(aStartPos, aEndPos);
+ editorBase->Cut();
+ }
+}
+
+void HyperTextAccessible::DeleteText(int32_t aStartPos, int32_t aEndPos) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (!editorBase) {
+ return;
+ }
+ SetSelectionRange(aStartPos, aEndPos);
+ DebugOnly<nsresult> rv =
+ editorBase->DeleteSelectionAsAction(nsIEditor::eNone, nsIEditor::eStrip);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to delete text");
+}
+
+void HyperTextAccessible::PasteText(int32_t aPosition) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ SetSelectionRange(aPosition, aPosition);
+ editorBase->PasteAsAction(nsIClipboard::kGlobalClipboard,
+ EditorBase::DispatchPasteEvent::Yes);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible public
+
+// LocalAccessible protected
+ENameValueFlag HyperTextAccessible::NativeName(nsString& aName) const {
+ // Check @alt attribute for invalid img elements.
+ if (mContent->IsHTMLElement(nsGkAtoms::img)) {
+ mContent->AsElement()->GetAttr(nsGkAtoms::alt, aName);
+ if (!aName.IsEmpty()) return eNameOK;
+ }
+
+ ENameValueFlag nameFlag = AccessibleWrap::NativeName(aName);
+ if (!aName.IsEmpty()) return nameFlag;
+
+ // Get name from title attribute for HTML abbr and acronym elements making it
+ // a valid name from markup. Otherwise their name isn't picked up by recursive
+ // name computation algorithm. See NS_OK_NAME_FROM_TOOLTIP.
+ if (IsAbbreviation() && mContent->AsElement()->GetAttr(
+ kNameSpaceID_None, nsGkAtoms::title, aName)) {
+ aName.CompressWhitespace();
+ }
+
+ return eNameOK;
+}
+
+void HyperTextAccessible::Shutdown() {
+ mOffsets.Clear();
+ AccessibleWrap::Shutdown();
+}
+
+bool HyperTextAccessible::RemoveChild(LocalAccessible* aAccessible) {
+ const int32_t childIndex = aAccessible->IndexInParent();
+ if (childIndex < static_cast<int32_t>(mOffsets.Length())) {
+ mOffsets.RemoveLastElements(mOffsets.Length() - childIndex);
+ }
+
+ return AccessibleWrap::RemoveChild(aAccessible);
+}
+
+bool HyperTextAccessible::InsertChildAt(uint32_t aIndex,
+ LocalAccessible* aChild) {
+ if (aIndex < mOffsets.Length()) {
+ mOffsets.RemoveLastElements(mOffsets.Length() - aIndex);
+ }
+
+ return AccessibleWrap::InsertChildAt(aIndex, aChild);
+}
+
+Relation HyperTextAccessible::RelationByType(RelationType aType) const {
+ Relation rel = LocalAccessible::RelationByType(aType);
+
+ switch (aType) {
+ case RelationType::NODE_CHILD_OF:
+ if (HasOwnContent() && mContent->IsMathMLElement()) {
+ LocalAccessible* parent = LocalParent();
+ if (parent) {
+ nsIContent* parentContent = parent->GetContent();
+ if (parentContent &&
+ parentContent->IsMathMLElement(nsGkAtoms::mroot_)) {
+ // Add a relation pointing to the parent <mroot>.
+ rel.AppendTarget(parent);
+ }
+ }
+ }
+ break;
+ case RelationType::NODE_PARENT_OF:
+ if (HasOwnContent() && mContent->IsMathMLElement(nsGkAtoms::mroot_)) {
+ LocalAccessible* base = LocalChildAt(0);
+ LocalAccessible* index = LocalChildAt(1);
+ if (base && index) {
+ // Append the <mroot> children in the order index, base.
+ rel.AppendTarget(index);
+ rel.AppendTarget(base);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ return rel;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HyperTextAccessible public static
+
+nsresult HyperTextAccessible::ContentToRenderedOffset(
+ nsIFrame* aFrame, int32_t aContentOffset, uint32_t* aRenderedOffset) const {
+ if (!aFrame) {
+ // Current frame not rendered -- this can happen if text is set on
+ // something with display: none
+ *aRenderedOffset = 0;
+ return NS_OK;
+ }
+
+ if (IsTextField()) {
+ *aRenderedOffset = aContentOffset;
+ return NS_OK;
+ }
+
+ NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
+ NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
+ "Call on primary frame only");
+
+ nsIFrame::RenderedText text =
+ aFrame->GetRenderedText(aContentOffset, aContentOffset + 1,
+ nsIFrame::TextOffsetType::OffsetsInContentText,
+ nsIFrame::TrailingWhitespace::DontTrim);
+ *aRenderedOffset = text.mOffsetWithinNodeRenderedText;
+
+ return NS_OK;
+}
+
+nsresult HyperTextAccessible::RenderedToContentOffset(
+ nsIFrame* aFrame, uint32_t aRenderedOffset, int32_t* aContentOffset) const {
+ if (IsTextField()) {
+ *aContentOffset = aRenderedOffset;
+ return NS_OK;
+ }
+
+ *aContentOffset = 0;
+ NS_ENSURE_TRUE(aFrame, NS_ERROR_FAILURE);
+
+ NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
+ NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
+ "Call on primary frame only");
+
+ nsIFrame::RenderedText text =
+ aFrame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1,
+ nsIFrame::TextOffsetType::OffsetsInRenderedText,
+ nsIFrame::TrailingWhitespace::DontTrim);
+ *aContentOffset = text.mOffsetWithinNodeText;
+
+ return NS_OK;
+}
diff --git a/accessible/generic/HyperTextAccessible.h b/accessible/generic/HyperTextAccessible.h
new file mode 100644
index 0000000000..e02585e1ca
--- /dev/null
+++ b/accessible/generic/HyperTextAccessible.h
@@ -0,0 +1,266 @@
+/* -*- 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_HyperTextAccessible_h__
+#define mozilla_a11y_HyperTextAccessible_h__
+
+#include "AccessibleWrap.h"
+#include "mozilla/a11y/HyperTextAccessibleBase.h"
+#include "nsIAccessibleText.h"
+#include "nsIAccessibleTypes.h"
+#include "nsIFrame.h" // only for nsSelectionAmount
+#include "nsISelectionController.h"
+
+class nsFrameSelection;
+class nsIFrame;
+class nsRange;
+class nsIWidget;
+
+namespace mozilla {
+class EditorBase;
+namespace dom {
+class Selection;
+}
+
+namespace a11y {
+
+class TextLeafPoint;
+class TextRange;
+
+struct DOMPoint {
+ DOMPoint() : node(nullptr), idx(0) {}
+ DOMPoint(nsINode* aNode, int32_t aIdx) : node(aNode), idx(aIdx) {}
+
+ nsINode* node;
+ int32_t idx;
+};
+
+/**
+ * Special Accessible that knows how contain both text and embedded objects
+ */
+class HyperTextAccessible : public AccessibleWrap,
+ public HyperTextAccessibleBase {
+ public:
+ HyperTextAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HyperTextAccessible, AccessibleWrap)
+
+ // LocalAccessible
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ virtual void Shutdown() override;
+ virtual bool RemoveChild(LocalAccessible* aAccessible) override;
+ virtual bool InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) override;
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ /**
+ * Return whether the associated content is editable.
+ */
+ bool IsEditable() const;
+
+ // HyperTextAccessible (static helper method)
+
+ // Convert content offset to rendered text offset
+ nsresult ContentToRenderedOffset(nsIFrame* aFrame, int32_t aContentOffset,
+ uint32_t* aRenderedOffset) const;
+
+ // Convert rendered text offset to content offset
+ nsresult RenderedToContentOffset(nsIFrame* aFrame, uint32_t aRenderedOffset,
+ int32_t* aContentOffset) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // HyperLinkAccessible
+
+ /**
+ * Return link accessible at the given index.
+ */
+ LocalAccessible* LinkAt(uint32_t aIndex) {
+ Accessible* child = EmbeddedChildAt(aIndex);
+ return child ? child->AsLocal() : nullptr;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // HyperTextAccessible: DOM point to text offset conversions.
+
+ /**
+ * Turn a DOM point (node and offset) into a character offset of this
+ * hypertext. Will look for closest match when the DOM node does not have
+ * an accessible object associated with it. Will return an offset for the end
+ * of the string if the node is not found.
+ *
+ * @param aNode [in] the node to look for
+ * @param aNodeOffset [in] the offset to look for
+ * if -1 just look directly for the node
+ * if >=0 and aNode is text, this represents a char
+ * offset if >=0 and aNode is not text, this represents a child node offset
+ * @param aIsEndOffset [in] if true, then this offset is not inclusive. The
+ * character indicated by the offset returned is at [offset - 1]. This means
+ * if the passed-in offset is really in a descendant, then the offset
+ * returned will come just after the relevant embedded object characer. If
+ * false, then the offset is inclusive. The character indicated by the offset
+ * returned is at [offset]. If the passed-in offset in inside a descendant,
+ * then the returned offset will be on the relevant embedded object char.
+ */
+ uint32_t DOMPointToOffset(nsINode* aNode, int32_t aNodeOffset,
+ bool aIsEndOffset = false) const;
+
+ /**
+ * Transform the given a11y point into the offset relative this hypertext.
+ */
+ uint32_t TransformOffset(LocalAccessible* aDescendant, uint32_t aOffset,
+ bool aIsEndOffset) const;
+
+ /**
+ * Convert the given offset into DOM point.
+ *
+ * If offset is at text leaf then DOM point is (text node, offsetInTextNode),
+ * if before embedded object then (parent node, indexInParent), if after then
+ * (parent node, indexInParent + 1).
+ */
+ DOMPoint OffsetToDOMPoint(int32_t aOffset) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // TextAccessible
+
+ virtual already_AddRefed<AccAttributes> DefaultTextAttributes() override;
+
+ // HyperTextAccessibleBase provides an overload which takes an Accessible.
+ using HyperTextAccessibleBase::GetChildOffset;
+
+ virtual LocalAccessible* GetChildAtOffset(uint32_t aOffset) const override {
+ return LocalChildAt(GetChildIndexAtOffset(aOffset));
+ }
+
+ /**
+ * Return an offset at the given point.
+ */
+ int32_t OffsetAtPoint(int32_t aX, int32_t aY, uint32_t aCoordType) override;
+
+ /**
+ * Get/set caret offset, if no caret then -1.
+ */
+ virtual int32_t CaretOffset() const override;
+ virtual void SetCaretOffset(int32_t aOffset) override;
+
+ virtual int32_t CaretLineNumber() override;
+
+ /**
+ * Return the caret rect and the widget containing the caret within this
+ * text accessible.
+ *
+ * @param [out] the widget containing the caret
+ * @return the caret rect
+ */
+ mozilla::LayoutDeviceIntRect GetCaretRect(nsIWidget** aWidget);
+
+ /**
+ * Return true if caret is at end of line.
+ */
+ bool IsCaretAtEndOfLine() const;
+
+ virtual int32_t SelectionCount() override;
+
+ virtual bool SelectionBoundsAt(int32_t aSelectionNum, int32_t* aStartOffset,
+ int32_t* aEndOffset) override;
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual bool RemoveFromSelection(
+ int32_t aSelectionNum) override;
+
+ virtual void ScrollSubstringToPoint(int32_t aStartOffset, int32_t aEndOffset,
+ uint32_t aCoordinateType, int32_t aX,
+ int32_t aY) override;
+
+ virtual void SelectionRanges(nsTArray<TextRange>* aRanges) const override;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // EditableTextAccessible
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void ReplaceText(
+ const nsAString& aText) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void InsertText(
+ const nsAString& aText, int32_t aPosition) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void CopyText(int32_t aStartPos,
+ int32_t aEndPos) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void CutText(int32_t aStartPos,
+ int32_t aEndPos) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void DeleteText(int32_t aStartPos,
+ int32_t aEndPos) override;
+ MOZ_CAN_RUN_SCRIPT virtual void PasteText(int32_t aPosition) override;
+
+ /**
+ * Return the editor associated with the accessible.
+ * The result may be either TextEditor or HTMLEditor.
+ */
+ virtual already_AddRefed<EditorBase> GetEditor() const;
+
+ /**
+ * Return DOM selection object for the accessible.
+ */
+ dom::Selection* DOMSelection() const;
+
+ protected:
+ virtual ~HyperTextAccessible() {}
+
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+
+ // HyperTextAccessible
+
+ // Selection helpers
+
+ /**
+ * Return frame selection object for the accessible.
+ */
+ already_AddRefed<nsFrameSelection> FrameSelection() const;
+
+ /**
+ * Return selection ranges within the accessible subtree.
+ */
+ void GetSelectionDOMRanges(SelectionType aSelectionType,
+ nsTArray<nsRange*>* aRanges);
+
+ // TODO: annotate this with `MOZ_CAN_RUN_SCRIPT` instead.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult SetSelectionRange(int32_t aStartPos,
+ int32_t aEndPos);
+
+ // Helpers
+
+ /**
+ * Set xml-roles attributes for MathML elements.
+ * @param aAttributes
+ */
+ void SetMathMLXMLRoles(AccAttributes* aAttributes);
+
+ // HyperTextAccessibleBase
+ virtual const Accessible* Acc() const override { return this; }
+
+ virtual nsTArray<int32_t>& GetCachedHyperTextOffsets() override {
+ return mOffsets;
+ }
+
+ private:
+ /**
+ * End text offsets array.
+ */
+ mutable nsTArray<int32_t> mOffsets;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible downcasting method
+
+inline HyperTextAccessible* LocalAccessible::AsHyperText() {
+ return IsHyperText() ? static_cast<HyperTextAccessible*>(this) : nullptr;
+}
+
+inline HyperTextAccessibleBase* LocalAccessible::AsHyperTextBase() {
+ return AsHyperText();
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/ImageAccessible.cpp b/accessible/generic/ImageAccessible.cpp
new file mode 100644
index 0000000000..df964e026c
--- /dev/null
+++ b/accessible/generic/ImageAccessible.cpp
@@ -0,0 +1,260 @@
+/* -*- 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 "ImageAccessible.h"
+
+#include "DocAccessible-inl.h"
+#include "LocalAccessible-inl.h"
+#include "nsAccUtils.h"
+#include "mozilla/a11y/Role.h"
+#include "AccAttributes.h"
+#include "AccIterator.h"
+#include "CacheConstants.h"
+#include "States.h"
+
+#include "imgIRequest.h"
+#include "nsGenericHTMLElement.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/MutationEventBinding.h"
+#include "nsContentUtils.h"
+#include "nsIImageLoadingContent.h"
+#include "nsPIDOMWindow.h"
+#include "nsIURI.h"
+
+namespace mozilla::a11y {
+
+NS_IMPL_ISUPPORTS_INHERITED(ImageAccessible, LinkableAccessible,
+ imgINotificationObserver)
+
+////////////////////////////////////////////////////////////////////////////////
+// ImageAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+ImageAccessible::ImageAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : LinkableAccessible(aContent, aDoc),
+ mImageRequestStatus(imgIRequest::STATUS_NONE) {
+ mType = eImageType;
+ nsCOMPtr<nsIImageLoadingContent> content(do_QueryInterface(mContent));
+ if (content) {
+ content->AddNativeObserver(this);
+ nsCOMPtr<imgIRequest> imageRequest;
+ content->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
+ getter_AddRefs(imageRequest));
+ if (imageRequest) {
+ imageRequest->GetImageStatus(&mImageRequestStatus);
+ }
+ }
+}
+
+ImageAccessible::~ImageAccessible() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible public
+
+void ImageAccessible::Shutdown() {
+ nsCOMPtr<nsIImageLoadingContent> content(do_QueryInterface(mContent));
+ if (content) {
+ content->RemoveNativeObserver(this);
+ }
+
+ LinkableAccessible::Shutdown();
+}
+
+uint64_t ImageAccessible::NativeState() const {
+ // The state is a bitfield, get our inherited state, then logically OR it with
+ // states::ANIMATED if this is an animated image.
+
+ uint64_t state = LinkableAccessible::NativeState();
+
+ if (mImageRequestStatus & imgIRequest::STATUS_IS_ANIMATED) {
+ state |= states::ANIMATED;
+ }
+
+ if (!(mImageRequestStatus & imgIRequest::STATUS_SIZE_AVAILABLE)) {
+ nsIFrame* frame = GetFrame();
+ MOZ_ASSERT(!frame || frame->AccessibleType() == eImageType ||
+ frame->AccessibleType() == a11y::eHTMLImageMapType);
+ if (frame && !frame->HasAnyStateBits(IMAGE_SIZECONSTRAINED)) {
+ // The size of this image hasn't been constrained and we haven't loaded
+ // enough of the image to know its size yet. This means it currently
+ // has 0 width and height.
+ state |= states::INVISIBLE;
+ }
+ }
+
+ return state;
+}
+
+ENameValueFlag ImageAccessible::NativeName(nsString& aName) const {
+ mContent->AsElement()->GetAttr(nsGkAtoms::alt, aName);
+ if (!aName.IsEmpty()) return eNameOK;
+
+ ENameValueFlag nameFlag = LocalAccessible::NativeName(aName);
+ if (!aName.IsEmpty()) return nameFlag;
+
+ return eNameOK;
+}
+
+role ImageAccessible::NativeRole() const { return roles::GRAPHIC; }
+
+void ImageAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ LinkableAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
+ aOldValue, aOldState);
+
+ if (aAttribute == nsGkAtoms::longdesc &&
+ (aModType == dom::MutationEvent_Binding::ADDITION ||
+ aModType == dom::MutationEvent_Binding::REMOVAL)) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Actions);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible
+
+uint8_t ImageAccessible::ActionCount() const {
+ uint8_t actionCount = LinkableAccessible::ActionCount();
+ return HasLongDesc() ? actionCount + 1 : actionCount;
+}
+
+void ImageAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ aName.Truncate();
+ if (IsLongDescIndex(aIndex) && HasLongDesc()) {
+ aName.AssignLiteral("showlongdesc");
+ } else {
+ LinkableAccessible::ActionNameAt(aIndex, aName);
+ }
+}
+
+bool ImageAccessible::DoAction(uint8_t aIndex) const {
+ // Get the long description uri and open in a new window.
+ if (!IsLongDescIndex(aIndex)) return LinkableAccessible::DoAction(aIndex);
+
+ nsCOMPtr<nsIURI> uri = GetLongDescURI();
+ if (!uri) return false;
+
+ nsAutoCString utf8spec;
+ uri->GetSpec(utf8spec);
+ NS_ConvertUTF8toUTF16 spec(utf8spec);
+
+ dom::Document* document = mContent->OwnerDoc();
+ nsCOMPtr<nsPIDOMWindowOuter> piWindow = document->GetWindow();
+ if (!piWindow) return false;
+
+ RefPtr<dom::BrowsingContext> tmp;
+ return NS_SUCCEEDED(piWindow->Open(spec, u""_ns, u""_ns,
+ /* aLoadInfo = */ nullptr,
+ /* aForceNoOpener = */ false,
+ getter_AddRefs(tmp)));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ImageAccessible
+
+LayoutDeviceIntPoint ImageAccessible::Position(uint32_t aCoordType) {
+ LayoutDeviceIntPoint point = Bounds().TopLeft();
+ nsAccUtils::ConvertScreenCoordsTo(&point.x.value, &point.y.value, aCoordType,
+ this);
+ return point;
+}
+
+LayoutDeviceIntSize ImageAccessible::Size() { return Bounds().Size(); }
+
+// LocalAccessible
+already_AddRefed<AccAttributes> ImageAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = LinkableAccessible::NativeAttributes();
+
+ nsString src;
+ mContent->AsElement()->GetAttr(nsGkAtoms::src, src);
+ if (!src.IsEmpty()) attributes->SetAttribute(nsGkAtoms::src, std::move(src));
+
+ return attributes.forget();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Private methods
+
+already_AddRefed<nsIURI> ImageAccessible::GetLongDescURI() const {
+ if (mContent->AsElement()->HasAttr(nsGkAtoms::longdesc)) {
+ // To check if longdesc contains an invalid url.
+ nsAutoString longdesc;
+ mContent->AsElement()->GetAttr(nsGkAtoms::longdesc, longdesc);
+ if (longdesc.FindChar(' ') != -1 || longdesc.FindChar('\t') != -1 ||
+ longdesc.FindChar('\r') != -1 || longdesc.FindChar('\n') != -1) {
+ return nullptr;
+ }
+ nsCOMPtr<nsIURI> uri;
+ nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(uri), longdesc,
+ mContent->OwnerDoc(),
+ mContent->GetBaseURI());
+ return uri.forget();
+ }
+
+ DocAccessible* document = Document();
+ if (document) {
+ IDRefsIterator iter(document, mContent, nsGkAtoms::aria_describedby);
+ while (nsIContent* target = iter.NextElem()) {
+ if ((target->IsHTMLElement(nsGkAtoms::a) ||
+ target->IsHTMLElement(nsGkAtoms::area)) &&
+ target->AsElement()->HasAttr(nsGkAtoms::href)) {
+ nsGenericHTMLElement* element = nsGenericHTMLElement::FromNode(target);
+
+ nsCOMPtr<nsIURI> uri;
+ element->GetURIAttr(nsGkAtoms::href, nullptr, getter_AddRefs(uri));
+ return uri.forget();
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+bool ImageAccessible::IsLongDescIndex(uint8_t aIndex) const {
+ return aIndex == LinkableAccessible::ActionCount();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// imgINotificationObserver
+
+void ImageAccessible::Notify(imgIRequest* aRequest, int32_t aType,
+ const nsIntRect* aData) {
+ if (aType != imgINotificationObserver::FRAME_COMPLETE &&
+ aType != imgINotificationObserver::LOAD_COMPLETE &&
+ aType != imgINotificationObserver::DECODE_COMPLETE) {
+ // We should update our state if the whole image was decoded,
+ // or the first frame in the case of a gif.
+ return;
+ }
+
+ if (IsDefunct() || !mParent) {
+ return;
+ }
+
+ uint32_t status = imgIRequest::STATUS_NONE;
+ aRequest->GetImageStatus(&status);
+
+ if ((status ^ mImageRequestStatus) & imgIRequest::STATUS_SIZE_AVAILABLE) {
+ nsIFrame* frame = GetFrame();
+ if (frame && !frame->HasAnyStateBits(IMAGE_SIZECONSTRAINED)) {
+ RefPtr<AccEvent> event = new AccStateChangeEvent(
+ this, states::INVISIBLE,
+ !(status & imgIRequest::STATUS_SIZE_AVAILABLE));
+ mDoc->FireDelayedEvent(event);
+ }
+ }
+
+ if ((status ^ mImageRequestStatus) & imgIRequest::STATUS_IS_ANIMATED) {
+ RefPtr<AccEvent> event = new AccStateChangeEvent(
+ this, states::ANIMATED, (status & imgIRequest::STATUS_IS_ANIMATED));
+ mDoc->FireDelayedEvent(event);
+ }
+
+ mImageRequestStatus = status;
+}
+
+} // namespace mozilla::a11y
diff --git a/accessible/generic/ImageAccessible.h b/accessible/generic/ImageAccessible.h
new file mode 100644
index 0000000000..1a072c5f90
--- /dev/null
+++ b/accessible/generic/ImageAccessible.h
@@ -0,0 +1,94 @@
+/* -*- 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_ImageAccessible_h__
+#define mozilla_a11y_ImageAccessible_h__
+
+#include "BaseAccessibles.h"
+#include "imgINotificationObserver.h"
+
+namespace mozilla {
+namespace a11y {
+
+/* LocalAccessible for supporting images
+ * supports:
+ * - gets name, role
+ * - support basic state
+ */
+class ImageAccessible : public LinkableAccessible,
+ public imgINotificationObserver {
+ public:
+ ImageAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_IMGINOTIFICATIONOBSERVER
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+
+ // ActionAccessible
+ virtual uint8_t ActionCount() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual bool DoAction(uint8_t aIndex) const override;
+
+ // ImageAccessible
+ LayoutDeviceIntPoint Position(uint32_t aCoordType);
+ LayoutDeviceIntSize Size();
+
+ /**
+ * Return whether the element has a longdesc URI.
+ */
+ bool HasLongDesc() const {
+ nsCOMPtr<nsIURI> uri = GetLongDescURI();
+ return uri;
+ }
+
+ protected:
+ virtual ~ImageAccessible();
+
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+
+ private:
+ /**
+ * Return an URI for showlongdesc action if any.
+ */
+ already_AddRefed<nsIURI> GetLongDescURI() const;
+
+ /**
+ * Used by ActionNameAt and DoAction to ensure the index for opening the
+ * longdesc URL is valid.
+ * It is always assumed that the highest possible index opens the longdesc.
+ * This doesn't check that there is actually a longdesc, just that the index
+ * would be correct if there was one.
+ *
+ * @param aIndex The 0-based index to be tested.
+ *
+ * @returns true if index is valid for longdesc action.
+ */
+ inline bool IsLongDescIndex(uint8_t aIndex) const;
+
+ uint32_t mImageRequestStatus;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible downcasting method
+
+inline ImageAccessible* LocalAccessible::AsImage() {
+ return IsImage() ? static_cast<ImageAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/LocalAccessible-inl.h b/accessible/generic/LocalAccessible-inl.h
new file mode 100644
index 0000000000..24a6169b76
--- /dev/null
+++ b/accessible/generic/LocalAccessible-inl.h
@@ -0,0 +1,107 @@
+/* -*- 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_Accessible_inl_h_
+#define mozilla_a11y_Accessible_inl_h_
+
+#include "DocAccessible.h"
+#include "ARIAMap.h"
+#include "nsCoreUtils.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/PresShell.h"
+
+#ifdef A11Y_LOG
+# include "Logging.h"
+#endif
+
+namespace mozilla {
+namespace a11y {
+
+inline mozilla::a11y::role LocalAccessible::Role() const {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ mozilla::a11y::role r =
+ (!roleMapEntry || roleMapEntry->roleRule != kUseMapRole)
+ ? NativeRole()
+ : roleMapEntry->role;
+ r = ARIATransformRole(r);
+ return GetMinimumRole(r);
+}
+
+inline mozilla::a11y::role LocalAccessible::ARIARole() {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (!roleMapEntry || roleMapEntry->roleRule != kUseMapRole) {
+ return mozilla::a11y::roles::NOTHING;
+ }
+
+ return ARIATransformRole(roleMapEntry->role);
+}
+
+inline void LocalAccessible::SetRoleMapEntry(
+ const nsRoleMapEntry* aRoleMapEntry) {
+ mRoleMapEntryIndex = aria::GetIndexFromRoleMap(aRoleMapEntry);
+}
+
+inline bool LocalAccessible::NativeHasNumericValue() const {
+ return mGenericTypes & eNumericValue;
+}
+
+inline bool LocalAccessible::ARIAHasNumericValue() const {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (!roleMapEntry || roleMapEntry->valueRule == eNoValue) return false;
+
+ if (roleMapEntry->valueRule == eHasValueMinMaxIfFocusable) {
+ return InteractiveState() & states::FOCUSABLE;
+ }
+
+ return true;
+}
+
+inline bool LocalAccessible::HasNumericValue() const {
+ return NativeHasNumericValue() || ARIAHasNumericValue();
+}
+
+inline bool LocalAccessible::IsDefunct() const {
+ MOZ_ASSERT(mStateFlags & eIsDefunct || IsApplication() || IsDoc() ||
+ mStateFlags & eSharedNode || mContent,
+ "No content");
+ return mStateFlags & eIsDefunct;
+}
+
+inline void LocalAccessible::ScrollTo(uint32_t aHow) const {
+ if (mContent) {
+ RefPtr<PresShell> presShell = mDoc->PresShellPtr();
+ nsCOMPtr<nsIContent> content = mContent;
+ nsCoreUtils::ScrollTo(presShell, content, aHow);
+ }
+}
+
+inline bool LocalAccessible::InsertAfter(LocalAccessible* aNewChild,
+ LocalAccessible* aRefChild) {
+ MOZ_ASSERT(aNewChild, "No new child to insert");
+
+ if (aRefChild && aRefChild->LocalParent() != this) {
+#ifdef A11Y_LOG
+ logging::TreeInfo("broken accessible tree", 0, "parent", this,
+ "prev sibling parent", aRefChild->LocalParent(), "child",
+ aNewChild, nullptr);
+ if (logging::IsEnabled(logging::eVerbose)) {
+ logging::Tree("TREE", "Document tree", mDoc);
+ logging::DOMTree("TREE", "DOM document tree", mDoc);
+ }
+#endif
+ MOZ_ASSERT_UNREACHABLE("Broken accessible tree");
+ mDoc->UnbindFromDocument(aNewChild);
+ return false;
+ }
+
+ return InsertChildAt(aRefChild ? aRefChild->IndexInParent() + 1 : 0,
+ aNewChild);
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/LocalAccessible.cpp b/accessible/generic/LocalAccessible.cpp
new file mode 100644
index 0000000000..f02f68faa6
--- /dev/null
+++ b/accessible/generic/LocalAccessible.cpp
@@ -0,0 +1,4255 @@
+/* -*- 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 "AccEvent.h"
+#include "LocalAccessible-inl.h"
+
+#include "EmbeddedObjCollector.h"
+#include "AccGroupInfo.h"
+#include "AccIterator.h"
+#include "CachedTableAccessible.h"
+#include "DocAccessible-inl.h"
+#include "mozilla/a11y/AccAttributes.h"
+#include "mozilla/a11y/DocAccessibleChild.h"
+#include "mozilla/a11y/Platform.h"
+#include "nsAccUtils.h"
+#include "nsAccessibilityService.h"
+#include "ApplicationAccessible.h"
+#include "nsGenericHTMLElement.h"
+#include "NotificationController.h"
+#include "nsEventShell.h"
+#include "nsTextEquivUtils.h"
+#include "EventTree.h"
+#include "OuterDocAccessible.h"
+#include "Pivot.h"
+#include "Relation.h"
+#include "mozilla/a11y/Role.h"
+#include "RootAccessible.h"
+#include "States.h"
+#include "TextLeafRange.h"
+#include "TextRange.h"
+#include "HTMLElementAccessibles.h"
+#include "HTMLSelectAccessible.h"
+#include "HTMLTableAccessible.h"
+#include "ImageAccessible.h"
+
+#include "nsComputedDOMStyle.h"
+#include "nsGkAtoms.h"
+#include "nsIDOMXULButtonElement.h"
+#include "nsIDOMXULSelectCntrlEl.h"
+#include "nsIDOMXULSelectCntrlItemEl.h"
+#include "nsINodeList.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLFormElement.h"
+#include "mozilla/dom/HTMLAnchorElement.h"
+#include "mozilla/gfx/Matrix.h"
+#include "nsIContent.h"
+#include "nsIFormControl.h"
+
+#include "nsDisplayList.h"
+#include "nsLayoutUtils.h"
+#include "nsPresContext.h"
+#include "nsIFrame.h"
+#include "nsTextFrame.h"
+#include "nsView.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIScrollableFrame.h"
+#include "nsStyleStructInlines.h"
+#include "nsFocusManager.h"
+
+#include "nsString.h"
+#include "nsAtom.h"
+#include "nsContainerFrame.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLLabelElement.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+#include "mozilla/dom/TreeWalker.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/MutationEventBinding.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible: nsISupports and cycle collection
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(LocalAccessible)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(LocalAccessible)
+ tmp->Shutdown();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(LocalAccessible)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContent, mDoc)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LocalAccessible)
+ NS_INTERFACE_MAP_ENTRY_CONCRETE(LocalAccessible)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, LocalAccessible)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(LocalAccessible)
+NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_DESTROY(LocalAccessible, LastRelease())
+
+LocalAccessible::LocalAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : mContent(aContent),
+ mDoc(aDoc),
+ mParent(nullptr),
+ mIndexInParent(-1),
+ mFirstLineStart(-1),
+ mStateFlags(0),
+ mContextFlags(0),
+ mReorderEventTarget(false),
+ mShowEventTarget(false),
+ mHideEventTarget(false),
+ mIndexOfEmbeddedChild(-1),
+ mGroupInfo(nullptr) {}
+
+LocalAccessible::~LocalAccessible() {
+ NS_ASSERTION(!mDoc, "LastRelease was never called!?!");
+}
+
+ENameValueFlag LocalAccessible::Name(nsString& aName) const {
+ aName.Truncate();
+
+ if (!HasOwnContent()) return eNameOK;
+
+ ARIAName(aName);
+ if (!aName.IsEmpty()) return eNameOK;
+
+ ENameValueFlag nameFlag = NativeName(aName);
+ if (!aName.IsEmpty()) return nameFlag;
+
+ // In the end get the name from tooltip.
+ if (mContent->IsHTMLElement()) {
+ if (mContent->AsElement()->GetAttr(nsGkAtoms::title, aName)) {
+ aName.CompressWhitespace();
+ return eNameFromTooltip;
+ }
+ } else if (mContent->IsXULElement()) {
+ if (mContent->AsElement()->GetAttr(nsGkAtoms::tooltiptext, aName)) {
+ aName.CompressWhitespace();
+ return eNameFromTooltip;
+ }
+ } else if (mContent->IsSVGElement()) {
+ // If user agents need to choose among multiple 'desc' or 'title'
+ // elements for processing, the user agent shall choose the first one.
+ for (nsIContent* childElm = mContent->GetFirstChild(); childElm;
+ childElm = childElm->GetNextSibling()) {
+ if (childElm->IsSVGElement(nsGkAtoms::desc)) {
+ nsTextEquivUtils::AppendTextEquivFromContent(this, childElm, &aName);
+ return eNameFromTooltip;
+ }
+ }
+ }
+
+ aName.SetIsVoid(true);
+
+ return nameFlag;
+}
+
+void LocalAccessible::Description(nsString& aDescription) const {
+ // There are 4 conditions that make an accessible have no accDescription:
+ // 1. it's a text node; or
+ // 2. It has no ARIA describedby or description property
+ // 3. it doesn't have an accName; or
+ // 4. its title attribute already equals to its accName nsAutoString name;
+
+ if (!HasOwnContent() || mContent->IsText()) return;
+
+ ARIADescription(aDescription);
+
+ if (aDescription.IsEmpty()) {
+ NativeDescription(aDescription);
+
+ if (aDescription.IsEmpty()) {
+ // Keep the Name() method logic.
+ if (mContent->IsHTMLElement()) {
+ mContent->AsElement()->GetAttr(nsGkAtoms::title, aDescription);
+ } else if (mContent->IsXULElement()) {
+ mContent->AsElement()->GetAttr(nsGkAtoms::tooltiptext, aDescription);
+ } else if (mContent->IsSVGElement()) {
+ for (nsIContent* childElm = mContent->GetFirstChild(); childElm;
+ childElm = childElm->GetNextSibling()) {
+ if (childElm->IsSVGElement(nsGkAtoms::desc)) {
+ nsTextEquivUtils::AppendTextEquivFromContent(this, childElm,
+ &aDescription);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (!aDescription.IsEmpty()) {
+ aDescription.CompressWhitespace();
+ nsAutoString name;
+ Name(name);
+ // Don't expose a description if it is the same as the name.
+ if (aDescription.Equals(name)) aDescription.Truncate();
+ }
+}
+
+KeyBinding LocalAccessible::AccessKey() const {
+ if (!HasOwnContent()) return KeyBinding();
+
+ uint32_t key = nsCoreUtils::GetAccessKeyFor(mContent);
+ if (!key && mContent->IsElement()) {
+ LocalAccessible* label = nullptr;
+
+ // Copy access key from label node.
+ if (mContent->IsHTMLElement()) {
+ // Unless it is labeled via an ancestor <label>, in which case that would
+ // be redundant.
+ HTMLLabelIterator iter(Document(), this,
+ HTMLLabelIterator::eSkipAncestorLabel);
+ label = iter.Next();
+ }
+ if (!label) {
+ XULLabelIterator iter(Document(), mContent);
+ label = iter.Next();
+ }
+
+ if (label) key = nsCoreUtils::GetAccessKeyFor(label->GetContent());
+ }
+
+ if (!key) return KeyBinding();
+
+ // Get modifier mask. Use ui.key.generalAccessKey (unless it is -1).
+ switch (StaticPrefs::ui_key_generalAccessKey()) {
+ case -1:
+ break;
+ case dom::KeyboardEvent_Binding::DOM_VK_SHIFT:
+ return KeyBinding(key, KeyBinding::kShift);
+ case dom::KeyboardEvent_Binding::DOM_VK_CONTROL:
+ return KeyBinding(key, KeyBinding::kControl);
+ case dom::KeyboardEvent_Binding::DOM_VK_ALT:
+ return KeyBinding(key, KeyBinding::kAlt);
+ case dom::KeyboardEvent_Binding::DOM_VK_META:
+ return KeyBinding(key, KeyBinding::kMeta);
+ default:
+ return KeyBinding();
+ }
+
+ // Determine the access modifier used in this context.
+ dom::Document* document = mContent->GetComposedDoc();
+ if (!document) return KeyBinding();
+
+ nsCOMPtr<nsIDocShellTreeItem> treeItem(document->GetDocShell());
+ if (!treeItem) return KeyBinding();
+
+ nsresult rv = NS_ERROR_FAILURE;
+ int32_t modifierMask = 0;
+ switch (treeItem->ItemType()) {
+ case nsIDocShellTreeItem::typeChrome:
+ modifierMask = StaticPrefs::ui_key_chromeAccess();
+ rv = NS_OK;
+ break;
+ case nsIDocShellTreeItem::typeContent:
+ modifierMask = StaticPrefs::ui_key_contentAccess();
+ rv = NS_OK;
+ break;
+ }
+
+ return NS_SUCCEEDED(rv) ? KeyBinding(key, modifierMask) : KeyBinding();
+}
+
+KeyBinding LocalAccessible::KeyboardShortcut() const { return KeyBinding(); }
+
+uint64_t LocalAccessible::VisibilityState() const {
+ if (IPCAccessibilityActive()) {
+ // Visibility states must be calculated by RemoteAccessible, so there's no
+ // point calculating them here.
+ return 0;
+ }
+ nsIFrame* frame = GetFrame();
+ if (!frame) {
+ // Element having display:contents is considered visible semantically,
+ // despite it doesn't have a visually visible box.
+ if (nsCoreUtils::IsDisplayContents(mContent)) {
+ return states::OFFSCREEN;
+ }
+ return states::INVISIBLE;
+ }
+
+ if (!frame->StyleVisibility()->IsVisible()) return states::INVISIBLE;
+
+ // It's invisible if the presshell is hidden by a visibility:hidden element in
+ // an ancestor document.
+ if (frame->PresShell()->IsUnderHiddenEmbedderElement()) {
+ return states::INVISIBLE;
+ }
+
+ // Offscreen state if the document's visibility state is not visible.
+ if (Document()->IsHidden()) return states::OFFSCREEN;
+
+ // Walk the parent frame chain to see if the frame is in background tab or
+ // scrolled out.
+ nsIFrame* curFrame = frame;
+ do {
+ nsView* view = curFrame->GetView();
+ if (view && view->GetVisibility() == ViewVisibility::Hide) {
+ return states::INVISIBLE;
+ }
+
+ if (nsLayoutUtils::IsPopup(curFrame)) {
+ return 0;
+ }
+
+ if (curFrame->StyleUIReset()->mMozSubtreeHiddenOnlyVisually) {
+ // Offscreen state for background tab content.
+ return states::OFFSCREEN;
+ }
+
+ nsIFrame* parentFrame = curFrame->GetParent();
+ // If contained by scrollable frame then check that at least 12 pixels
+ // around the object is visible, otherwise the object is offscreen.
+ nsIScrollableFrame* scrollableFrame = do_QueryFrame(parentFrame);
+ const nscoord kMinPixels = nsPresContext::CSSPixelsToAppUnits(12);
+ if (scrollableFrame) {
+ nsRect scrollPortRect = scrollableFrame->GetScrollPortRect();
+ nsRect frameRect = nsLayoutUtils::TransformFrameRectToAncestor(
+ frame, frame->GetRectRelativeToSelf(), parentFrame);
+ if (!scrollPortRect.Contains(frameRect)) {
+ scrollPortRect.Deflate(kMinPixels, kMinPixels);
+ if (!scrollPortRect.Intersects(frameRect)) return states::OFFSCREEN;
+ }
+ }
+
+ if (!parentFrame) {
+ parentFrame = nsLayoutUtils::GetCrossDocParentFrameInProcess(curFrame);
+ // Even if we couldn't find the parent frame, it might mean we are in an
+ // out-of-process iframe, try to see if |frame| is scrolled out in an
+ // scrollable frame in a cross-process ancestor document.
+ if (!parentFrame &&
+ nsLayoutUtils::FrameIsMostlyScrolledOutOfViewInCrossProcess(
+ frame, kMinPixels)) {
+ return states::OFFSCREEN;
+ }
+ }
+
+ curFrame = parentFrame;
+ } while (curFrame);
+
+ // Zero area rects can occur in the first frame of a multi-frame text flow,
+ // in which case the rendered text is not empty and the frame should not be
+ // marked invisible.
+ // XXX Can we just remove this check? Why do we need to mark empty
+ // text invisible?
+ if (frame->IsTextFrame() && !frame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW) &&
+ frame->GetRect().IsEmpty()) {
+ nsIFrame::RenderedText text = frame->GetRenderedText(
+ 0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText,
+ nsIFrame::TrailingWhitespace::DontTrim);
+ if (text.mString.IsEmpty()) {
+ return states::INVISIBLE;
+ }
+ }
+
+ return 0;
+}
+
+uint64_t LocalAccessible::NativeState() const {
+ uint64_t state = 0;
+
+ if (!IsInDocument()) state |= states::STALE;
+
+ if (HasOwnContent() && mContent->IsElement()) {
+ dom::ElementState elementState = mContent->AsElement()->State();
+
+ if (elementState.HasState(dom::ElementState::INVALID)) {
+ state |= states::INVALID;
+ }
+
+ if (elementState.HasState(dom::ElementState::REQUIRED)) {
+ state |= states::REQUIRED;
+ }
+
+ state |= NativeInteractiveState();
+ }
+
+ // Gather states::INVISIBLE and states::OFFSCREEN flags for this object.
+ state |= VisibilityState();
+
+ nsIFrame* frame = GetFrame();
+ if (frame && frame->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW)) {
+ state |= states::FLOATING;
+ }
+
+ // Check if a XUL element has the popup attribute (an attached popup menu).
+ if (HasOwnContent() && mContent->IsXULElement() &&
+ mContent->AsElement()->HasAttr(nsGkAtoms::popup)) {
+ state |= states::HASPOPUP;
+ }
+
+ // Bypass the link states specialization for non links.
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (!roleMapEntry || roleMapEntry->roleRule == kUseNativeRole ||
+ roleMapEntry->role == roles::LINK) {
+ state |= NativeLinkState();
+ }
+
+ return state;
+}
+
+uint64_t LocalAccessible::NativeInteractiveState() const {
+ if (!mContent->IsElement()) return 0;
+
+ if (NativelyUnavailable()) return states::UNAVAILABLE;
+
+ nsIFrame* frame = GetFrame();
+ // If we're caching this remote document in the parent process, we
+ // need to cache focusability irrespective of visibility. Otherwise,
+ // if this document is invisible when it first loads, we'll cache that
+ // all descendants are unfocusable and this won't get updated when the
+ // document becomes visible. Even if we did get notified when the
+ // document becomes visible, it would be wasteful to walk the entire
+ // tree to figure out what is now focusable and push cache updates.
+ // Although ignoring visibility means IsFocusable will return true for
+ // visibility: hidden, etc., this isn't a problem because we don't include
+ // those hidden elements in the a11y tree anyway.
+ const bool ignoreVisibility = mDoc->IPCDoc();
+ if (frame && frame->IsFocusable(
+ /* aWithMouse */ false,
+ /* aCheckVisibility */ !ignoreVisibility)) {
+ return states::FOCUSABLE;
+ }
+
+ return 0;
+}
+
+uint64_t LocalAccessible::NativeLinkState() const { return 0; }
+
+bool LocalAccessible::NativelyUnavailable() const {
+ if (mContent->IsHTMLElement()) return mContent->AsElement()->IsDisabled();
+
+ return mContent->IsElement() && mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::disabled,
+ nsGkAtoms::_true, eCaseMatters);
+}
+
+Accessible* LocalAccessible::ChildAtPoint(int32_t aX, int32_t aY,
+ EWhichChildAtPoint aWhichChild) {
+ Accessible* child = LocalChildAtPoint(aX, aY, aWhichChild);
+ if (aWhichChild != EWhichChildAtPoint::DirectChild && child &&
+ child->IsOuterDoc()) {
+ child = child->ChildAtPoint(aX, aY, aWhichChild);
+ }
+
+ return child;
+}
+
+LocalAccessible* LocalAccessible::LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) {
+ // If we can't find the point in a child, we will return the fallback answer:
+ // we return |this| if the point is within it, otherwise nullptr.
+ LocalAccessible* fallbackAnswer = nullptr;
+ LayoutDeviceIntRect rect = Bounds();
+ if (rect.Contains(aX, aY)) fallbackAnswer = this;
+
+ if (nsAccUtils::MustPrune(this)) { // Do not dig any further
+ return fallbackAnswer;
+ }
+
+ // Search an accessible at the given point starting from accessible document
+ // because containing block (see CSS2) for out of flow element (for example,
+ // absolutely positioned element) may be different from its DOM parent and
+ // therefore accessible for containing block may be different from accessible
+ // for DOM parent but GetFrameForPoint() should be called for containing block
+ // to get an out of flow element.
+ DocAccessible* accDocument = Document();
+ NS_ENSURE_TRUE(accDocument, nullptr);
+
+ nsIFrame* rootFrame = accDocument->GetFrame();
+ NS_ENSURE_TRUE(rootFrame, nullptr);
+
+ nsIFrame* startFrame = rootFrame;
+
+ // Check whether the point is at popup content.
+ nsIWidget* rootWidget = rootFrame->GetView()->GetNearestWidget(nullptr);
+ NS_ENSURE_TRUE(rootWidget, nullptr);
+
+ LayoutDeviceIntRect rootRect = rootWidget->GetScreenBounds();
+
+ auto point = LayoutDeviceIntPoint(aX - rootRect.X(), aY - rootRect.Y());
+
+ nsIFrame* popupFrame = nsLayoutUtils::GetPopupFrameForPoint(
+ accDocument->PresContext()->GetRootPresContext(), rootWidget, point);
+ if (popupFrame) {
+ // If 'this' accessible is not inside the popup then ignore the popup when
+ // searching an accessible at point.
+ DocAccessible* popupDoc =
+ GetAccService()->GetDocAccessible(popupFrame->GetContent()->OwnerDoc());
+ LocalAccessible* popupAcc =
+ popupDoc->GetAccessibleOrContainer(popupFrame->GetContent());
+ LocalAccessible* popupChild = this;
+ while (popupChild && !popupChild->IsDoc() && popupChild != popupAcc) {
+ popupChild = popupChild->LocalParent();
+ }
+
+ if (popupChild == popupAcc) startFrame = popupFrame;
+ }
+
+ nsPresContext* presContext = startFrame->PresContext();
+ nsRect screenRect = startFrame->GetScreenRectInAppUnits();
+ nsPoint offset(presContext->DevPixelsToAppUnits(aX) - screenRect.X(),
+ presContext->DevPixelsToAppUnits(aY) - screenRect.Y());
+
+ nsIFrame* foundFrame = nsLayoutUtils::GetFrameForPoint(
+ RelativeTo{startFrame, ViewportType::Visual}, offset);
+
+ nsIContent* content = nullptr;
+ if (!foundFrame || !(content = foundFrame->GetContent())) {
+ return fallbackAnswer;
+ }
+
+ // Get accessible for the node with the point or the first accessible in
+ // the DOM parent chain.
+ DocAccessible* contentDocAcc =
+ GetAccService()->GetDocAccessible(content->OwnerDoc());
+
+ // contentDocAcc in some circumstances can be nullptr. See bug 729861
+ NS_ASSERTION(contentDocAcc, "could not get the document accessible");
+ if (!contentDocAcc) return fallbackAnswer;
+
+ LocalAccessible* accessible =
+ contentDocAcc->GetAccessibleOrContainer(content);
+ if (!accessible) return fallbackAnswer;
+
+ // Hurray! We have an accessible for the frame that layout gave us.
+ // Since DOM node of obtained accessible may be out of flow then we should
+ // ensure obtained accessible is a child of this accessible.
+ LocalAccessible* child = accessible;
+ while (child != this) {
+ LocalAccessible* parent = child->LocalParent();
+ if (!parent) {
+ // Reached the top of the hierarchy. These bounds were inside an
+ // accessible that is not a descendant of this one.
+ return fallbackAnswer;
+ }
+
+ // If we landed on a legitimate child of |this|, and we want the direct
+ // child, return it here.
+ if (parent == this && aWhichChild == EWhichChildAtPoint::DirectChild) {
+ return child;
+ }
+
+ child = parent;
+ }
+
+ // Manually walk through accessible children and see if the are within this
+ // point. Skip offscreen or invisible accessibles. This takes care of cases
+ // where layout won't walk into things for us, such as image map areas and
+ // sub documents (XXX: subdocuments should be handled by methods of
+ // OuterDocAccessibles).
+ uint32_t childCount = accessible->ChildCount();
+ if (childCount == 1 && accessible->IsOuterDoc() &&
+ accessible->FirstChild()->IsRemote()) {
+ // No local children.
+ return accessible;
+ }
+ for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
+ LocalAccessible* child = accessible->LocalChildAt(childIdx);
+
+ LayoutDeviceIntRect childRect = child->Bounds();
+ if (childRect.Contains(aX, aY) &&
+ (child->State() & states::INVISIBLE) == 0) {
+ if (aWhichChild == EWhichChildAtPoint::DeepestChild) {
+ return child->LocalChildAtPoint(aX, aY,
+ EWhichChildAtPoint::DeepestChild);
+ }
+
+ return child;
+ }
+ }
+
+ return accessible;
+}
+
+nsIFrame* LocalAccessible::FindNearestAccessibleAncestorFrame() {
+ nsIFrame* frame = GetFrame();
+ if (frame->StyleDisplay()->mPosition == StylePositionProperty::Fixed &&
+ nsLayoutUtils::IsReallyFixedPos(frame)) {
+ return mDoc->PresShellPtr()->GetRootFrame();
+ }
+
+ if (IsDoc()) {
+ // We bound documents by their own frame, which is their PresShell's root
+ // frame. We cache the document offset elsewhere in BundleFieldsForCache
+ // using the nsGkAtoms::crossorigin attribute.
+ MOZ_ASSERT(frame, "DocAccessibles should always have a frame");
+ return frame;
+ }
+
+ // Iterate through accessible's ancestors to find one with a frame.
+ LocalAccessible* ancestor = mParent;
+ while (ancestor) {
+ if (nsIFrame* boundingFrame = ancestor->GetFrame()) {
+ return boundingFrame;
+ }
+ ancestor = ancestor->LocalParent();
+ }
+
+ MOZ_ASSERT_UNREACHABLE("No ancestor with frame?");
+ return nsLayoutUtils::GetContainingBlockForClientRect(frame);
+}
+
+nsRect LocalAccessible::ParentRelativeBounds() {
+ nsIFrame* frame = GetFrame();
+ if (frame && mContent) {
+ nsIFrame* boundingFrame = FindNearestAccessibleAncestorFrame();
+ nsRect result = nsLayoutUtils::GetAllInFlowRectsUnion(frame, boundingFrame);
+
+ if (result.IsEmpty()) {
+ // If we end up with a 0x0 rect from above (or one with negative
+ // height/width) we should try using the ink overflow rect instead. If we
+ // use this rect, our relative bounds will match the bounds of what
+ // appears visually. We do this because some web authors (icloud.com for
+ // example) employ things like 0x0 buttons with visual overflow. Without
+ // this, such frames aren't navigable by screen readers.
+ result = frame->InkOverflowRectRelativeToSelf();
+ result.MoveBy(frame->GetOffsetTo(boundingFrame));
+ }
+
+ if (boundingFrame->GetRect().IsEmpty() ||
+ nsLayoutUtils::GetNextContinuationOrIBSplitSibling(boundingFrame)) {
+ // Constructing a bounding box across a frame that has an IB split means
+ // the origin is likely be different from that of boundingFrame.
+ // Descendants will need their parent-relative bounds adjusted
+ // accordingly, since parent-relative bounds are constructed to the
+ // bounding box of the entire element and not each individual IB split
+ // frame. In the case that boundingFrame's rect is empty,
+ // GetAllInFlowRectsUnion might exclude its origin. For example, if
+ // boundingFrame is empty with an origin of (0, -840) but has a non-empty
+ // ib-split-sibling with (0, 0), the union rect will originate at (0, 0).
+ // This means the bounds returned for our parent Accessible might be
+ // offset from boundingFrame's rect. Since result is currently relative to
+ // boundingFrame's rect, we might need to adjust it to make it parent
+ // relative.
+ nsRect boundingUnion =
+ nsLayoutUtils::GetAllInFlowRectsUnion(boundingFrame, boundingFrame);
+ if (!boundingUnion.IsEmpty()) {
+ // The origin of boundingUnion is relative to boundingFrame, meaning
+ // when we call MoveBy on result with this value we're offsetting
+ // `result` by the distance boundingFrame's origin was moved to
+ // construct its bounding box.
+ result.MoveBy(-boundingUnion.TopLeft());
+ } else {
+ // Since GetAllInFlowRectsUnion returned an empty rect on our parent
+ // Accessible, we would have used the ink overflow rect. However,
+ // GetAllInFlowRectsUnion calculates relative to the bounding frame's
+ // main rect, not its ink overflow rect. We need to adjust for the ink
+ // overflow offset to make our result parent relative.
+ nsRect boundingOverflow =
+ boundingFrame->InkOverflowRectRelativeToSelf();
+ result.MoveBy(-boundingOverflow.TopLeft());
+ }
+ }
+
+ if (frame->StyleDisplay()->mPosition == StylePositionProperty::Fixed &&
+ nsLayoutUtils::IsReallyFixedPos(frame)) {
+ // If we're dealing with a fixed position frame, we've already made it
+ // relative to the document which should have gotten rid of its scroll
+ // offset.
+ return result;
+ }
+
+ if (nsIScrollableFrame* sf =
+ mParent == mDoc
+ ? mDoc->PresShellPtr()->GetRootScrollFrameAsScrollable()
+ : boundingFrame->GetScrollTargetFrame()) {
+ // If boundingFrame has a scroll position, result is currently relative
+ // to that. Instead, we want result to remain the same regardless of
+ // scrolling. We then subtract the scroll position later when
+ // calculating absolute bounds. We do this because we don't want to push
+ // cache updates for the bounds of all descendants every time we scroll.
+ nsPoint scrollPos = sf->GetScrollPosition().ApplyResolution(
+ mDoc->PresShellPtr()->GetResolution());
+ result.MoveBy(scrollPos.x, scrollPos.y);
+ }
+
+ return result;
+ }
+
+ return nsRect();
+}
+
+nsRect LocalAccessible::RelativeBounds(nsIFrame** aBoundingFrame) const {
+ nsIFrame* frame = GetFrame();
+ if (frame && mContent) {
+ *aBoundingFrame = nsLayoutUtils::GetContainingBlockForClientRect(frame);
+ nsRect unionRect = nsLayoutUtils::GetAllInFlowRectsUnion(
+ frame, *aBoundingFrame, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
+
+ if (unionRect.IsEmpty()) {
+ // If we end up with a 0x0 rect from above (or one with negative
+ // height/width) we should try using the ink overflow rect instead. If we
+ // use this rect, our relative bounds will match the bounds of what
+ // appears visually. We do this because some web authors (icloud.com for
+ // example) employ things like 0x0 buttons with visual overflow. Without
+ // this, such frames aren't navigable by screen readers.
+ nsRect overflow = frame->InkOverflowRectRelativeToSelf();
+ nsLayoutUtils::TransformRect(frame, *aBoundingFrame, overflow);
+ return overflow;
+ }
+
+ return unionRect;
+ }
+
+ return nsRect();
+}
+
+nsRect LocalAccessible::BoundsInAppUnits() const {
+ nsIFrame* boundingFrame = nullptr;
+ nsRect unionRectTwips = RelativeBounds(&boundingFrame);
+ if (!boundingFrame) {
+ return nsRect();
+ }
+
+ PresShell* presShell = mDoc->PresContext()->PresShell();
+
+ // We need to inverse translate with the offset of the edge of the visual
+ // viewport from top edge of the layout viewport.
+ nsPoint viewportOffset = presShell->GetVisualViewportOffset() -
+ presShell->GetLayoutViewportOffset();
+ unionRectTwips.MoveBy(-viewportOffset);
+
+ // We need to take into account a non-1 resolution set on the presshell.
+ // This happens with async pinch zooming. Here we scale the bounds before
+ // adding the screen-relative offset.
+ unionRectTwips.ScaleRoundOut(presShell->GetResolution());
+ // We have the union of the rectangle, now we need to put it in absolute
+ // screen coords.
+ nsRect orgRectPixels = boundingFrame->GetScreenRectInAppUnits();
+ unionRectTwips.MoveBy(orgRectPixels.X(), orgRectPixels.Y());
+
+ return unionRectTwips;
+}
+
+LayoutDeviceIntRect LocalAccessible::Bounds() const {
+ return LayoutDeviceIntRect::FromAppUnitsToNearest(
+ BoundsInAppUnits(), mDoc->PresContext()->AppUnitsPerDevPixel());
+}
+
+void LocalAccessible::SetSelected(bool aSelect) {
+ if (!HasOwnContent()) return;
+
+ LocalAccessible* select = nsAccUtils::GetSelectableContainer(this, State());
+ if (select) {
+ if (select->State() & states::MULTISELECTABLE) {
+ if (mContent->IsElement() && ARIARoleMap()) {
+ if (aSelect) {
+ mContent->AsElement()->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::aria_selected, u"true"_ns, true);
+ } else {
+ mContent->AsElement()->UnsetAttr(kNameSpaceID_None,
+ nsGkAtoms::aria_selected, true);
+ }
+ }
+ return;
+ }
+
+ if (aSelect) TakeFocus();
+ }
+}
+
+void LocalAccessible::TakeSelection() {
+ LocalAccessible* select = nsAccUtils::GetSelectableContainer(this, State());
+ if (select) {
+ if (select->State() & states::MULTISELECTABLE) select->UnselectAll();
+ SetSelected(true);
+ }
+}
+
+void LocalAccessible::TakeFocus() const {
+ nsIFrame* frame = GetFrame();
+ if (!frame) return;
+
+ nsIContent* focusContent = mContent;
+
+ // If the accessible focus is managed by container widget then focus the
+ // widget and set the accessible as its current item.
+ if (!frame->IsFocusable()) {
+ LocalAccessible* widget = ContainerWidget();
+ if (widget && widget->AreItemsOperable()) {
+ nsIContent* widgetElm = widget->GetContent();
+ nsIFrame* widgetFrame = widgetElm->GetPrimaryFrame();
+ if (widgetFrame && widgetFrame->IsFocusable()) {
+ focusContent = widgetElm;
+ widget->SetCurrentItem(this);
+ }
+ }
+ }
+
+ if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
+ dom::AutoHandlingUserInputStatePusher inputStatePusher(true);
+ // XXXbz: Can we actually have a non-element content here?
+ RefPtr<dom::Element> element = dom::Element::FromNodeOrNull(focusContent);
+ fm->SetFocus(element, 0);
+ }
+}
+
+void LocalAccessible::NameFromAssociatedXULLabel(DocAccessible* aDocument,
+ nsIContent* aElm,
+ nsString& aName) {
+ LocalAccessible* label = nullptr;
+ XULLabelIterator iter(aDocument, aElm);
+ while ((label = iter.Next())) {
+ // Check if label's value attribute is used
+ label->Elm()->GetAttr(nsGkAtoms::value, aName);
+ if (aName.IsEmpty()) {
+ // If no value attribute, a non-empty label must contain
+ // children that define its text -- possibly using HTML
+ nsTextEquivUtils::AppendTextEquivFromContent(label, label->Elm(), &aName);
+ }
+ }
+ aName.CompressWhitespace();
+}
+
+void LocalAccessible::XULElmName(DocAccessible* aDocument, nsIContent* aElm,
+ nsString& aName) {
+ /**
+ * 3 main cases for XUL Controls to be labeled
+ * 1 - control contains label="foo"
+ * 2 - non-child label contains control="controlID"
+ * - label has either value="foo" or children
+ * 3 - name from subtree; e.g. a child label element
+ * Cases 1 and 2 are handled here.
+ * Case 3 is handled by GetNameFromSubtree called in NativeName.
+ * Once a label is found, the search is discontinued, so a control
+ * that has a label attribute as well as having a label external to
+ * the control that uses the control="controlID" syntax will use
+ * the label attribute for its Name.
+ */
+
+ // CASE #1 (via label attribute) -- great majority of the cases
+ // Only do this if this is not a select control element, which uses label
+ // attribute to indicate, which option is selected.
+ nsCOMPtr<nsIDOMXULSelectControlElement> select =
+ aElm->AsElement()->AsXULSelectControl();
+ if (!select) {
+ aElm->AsElement()->GetAttr(nsGkAtoms::label, aName);
+ }
+
+ // CASE #2 -- label as <label control="id" ... ></label>
+ if (aName.IsEmpty()) {
+ NameFromAssociatedXULLabel(aDocument, aElm, aName);
+ }
+
+ aName.CompressWhitespace();
+}
+
+nsresult LocalAccessible::HandleAccEvent(AccEvent* aEvent) {
+ NS_ENSURE_ARG_POINTER(aEvent);
+
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsAutoCString strEventType;
+ GetAccService()->GetStringEventType(aEvent->GetEventType(), strEventType);
+ nsAutoCString strMarker;
+ strMarker.AppendLiteral("A11y Event - ");
+ strMarker.Append(strEventType);
+ PROFILER_MARKER_UNTYPED(strMarker, A11Y);
+ }
+
+ if (IPCAccessibilityActive() && Document()) {
+ DocAccessibleChild* ipcDoc = mDoc->IPCDoc();
+ // If ipcDoc is null, we can't fire the event to the client. We shouldn't
+ // have fired the event in the first place, since this makes events
+ // inconsistent for local and remote documents. To avoid this, don't call
+ // nsEventShell::FireEvent on a DocAccessible for which
+ // HasLoadState(eTreeConstructed) is false.
+ MOZ_ASSERT(ipcDoc);
+ if (ipcDoc) {
+ uint64_t id = aEvent->GetAccessible()->ID();
+
+ switch (aEvent->GetEventType()) {
+ case nsIAccessibleEvent::EVENT_SHOW:
+ ipcDoc->ShowEvent(downcast_accEvent(aEvent));
+ break;
+
+ case nsIAccessibleEvent::EVENT_HIDE:
+ ipcDoc->SendHideEvent(id, aEvent->IsFromUserInput());
+ break;
+
+ case nsIAccessibleEvent::EVENT_INNER_REORDER:
+ case nsIAccessibleEvent::EVENT_REORDER:
+ if (IsTable()) {
+ SendCache(CacheDomain::Table, CacheUpdateType::Update);
+ }
+
+#if defined(XP_WIN)
+ if (HasOwnContent() && mContent->IsMathMLElement()) {
+ // For any change in a MathML subtree, update the innerHTML cache on
+ // the root math element.
+ for (LocalAccessible* acc = this; acc; acc = acc->LocalParent()) {
+ if (acc->HasOwnContent() &&
+ acc->mContent->IsMathMLElement(nsGkAtoms::math)) {
+ mDoc->QueueCacheUpdate(acc, CacheDomain::InnerHTML);
+ }
+ }
+ }
+#endif // defined(XP_WIN)
+
+ // reorder events on the application acc aren't necessary to tell the
+ // parent about new top level documents.
+ if (!aEvent->GetAccessible()->IsApplication()) {
+ ipcDoc->SendEvent(id, aEvent->GetEventType());
+ }
+ break;
+ case nsIAccessibleEvent::EVENT_STATE_CHANGE: {
+ AccStateChangeEvent* event = downcast_accEvent(aEvent);
+ ipcDoc->SendStateChangeEvent(id, event->GetState(),
+ event->IsStateEnabled());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
+ AccCaretMoveEvent* event = downcast_accEvent(aEvent);
+ ipcDoc->SendCaretMoveEvent(
+ id, event->GetCaretOffset(), event->IsSelectionCollapsed(),
+ event->IsAtEndOfLine(), event->GetGranularity(),
+ event->IsFromUserInput());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
+ case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
+ AccTextChangeEvent* event = downcast_accEvent(aEvent);
+ const nsString& text = event->ModifiedText();
+ ipcDoc->SendTextChangeEvent(
+ id, text, event->GetStartOffset(), event->GetLength(),
+ event->IsTextInserted(), event->IsFromUserInput());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_SELECTION:
+ case nsIAccessibleEvent::EVENT_SELECTION_ADD:
+ case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: {
+ AccSelChangeEvent* selEvent = downcast_accEvent(aEvent);
+ ipcDoc->SendSelectionEvent(id, selEvent->Widget()->ID(),
+ aEvent->GetEventType());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_FOCUS:
+ ipcDoc->SendFocusEvent(id);
+ break;
+ case nsIAccessibleEvent::EVENT_SCROLLING_END:
+ case nsIAccessibleEvent::EVENT_SCROLLING: {
+ AccScrollingEvent* scrollingEvent = downcast_accEvent(aEvent);
+ ipcDoc->SendScrollingEvent(
+ id, aEvent->GetEventType(), scrollingEvent->ScrollX(),
+ scrollingEvent->ScrollY(), scrollingEvent->MaxScrollX(),
+ scrollingEvent->MaxScrollY());
+ break;
+ }
+#if !defined(XP_WIN)
+ case nsIAccessibleEvent::EVENT_ANNOUNCEMENT: {
+ AccAnnouncementEvent* announcementEvent = downcast_accEvent(aEvent);
+ ipcDoc->SendAnnouncementEvent(id, announcementEvent->Announcement(),
+ announcementEvent->Priority());
+ break;
+ }
+#endif // !defined(XP_WIN)
+ case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED: {
+ AccTextSelChangeEvent* textSelChangeEvent = downcast_accEvent(aEvent);
+ AutoTArray<TextRange, 1> ranges;
+ textSelChangeEvent->SelectionRanges(&ranges);
+ nsTArray<TextRangeData> textRangeData(ranges.Length());
+ for (size_t i = 0; i < ranges.Length(); i++) {
+ const TextRange& range = ranges.ElementAt(i);
+ LocalAccessible* start = range.StartContainer()->AsLocal();
+ LocalAccessible* end = range.EndContainer()->AsLocal();
+ textRangeData.AppendElement(TextRangeData(start->ID(), end->ID(),
+ range.StartOffset(),
+ range.EndOffset()));
+ }
+ ipcDoc->SendTextSelectionChangeEvent(id, textRangeData);
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE:
+ case nsIAccessibleEvent::EVENT_NAME_CHANGE: {
+ SendCache(CacheDomain::NameAndDescription, CacheUpdateType::Update);
+ ipcDoc->SendEvent(id, aEvent->GetEventType());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE:
+ case nsIAccessibleEvent::EVENT_VALUE_CHANGE: {
+ SendCache(CacheDomain::Value, CacheUpdateType::Update);
+ ipcDoc->SendEvent(id, aEvent->GetEventType());
+ break;
+ }
+ default:
+ ipcDoc->SendEvent(id, aEvent->GetEventType());
+ }
+ }
+ }
+
+ if (nsCoreUtils::AccEventObserversExist()) {
+ nsCoreUtils::DispatchAccEvent(MakeXPCEvent(aEvent));
+ }
+
+ if (IPCAccessibilityActive()) {
+ return NS_OK;
+ }
+
+ if (IsDefunct()) {
+ // This could happen if there is an XPCOM observer, since script might run
+ // which mutates the tree.
+ return NS_OK;
+ }
+
+ LocalAccessible* target = aEvent->GetAccessible();
+ switch (aEvent->GetEventType()) {
+ case nsIAccessibleEvent::EVENT_SHOW:
+ PlatformShowHideEvent(target, target->LocalParent(), true,
+ aEvent->IsFromUserInput());
+ break;
+ case nsIAccessibleEvent::EVENT_HIDE:
+ PlatformShowHideEvent(target, target->LocalParent(), false,
+ aEvent->IsFromUserInput());
+ break;
+ case nsIAccessibleEvent::EVENT_STATE_CHANGE: {
+ AccStateChangeEvent* event = downcast_accEvent(aEvent);
+ PlatformStateChangeEvent(target, event->GetState(),
+ event->IsStateEnabled());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
+ AccCaretMoveEvent* event = downcast_accEvent(aEvent);
+ LayoutDeviceIntRect rect;
+ // The caret rect is only used on Windows, so just pass an empty rect on
+ // other platforms.
+ // XXX We pass an empty rect on Windows as well because
+ // AccessibleWrap::UpdateSystemCaretFor currently needs to call
+ // HyperTextAccessible::GetCaretRect again to get the widget and there's
+ // no point calling it twice.
+ PlatformCaretMoveEvent(
+ target, event->GetCaretOffset(), event->IsSelectionCollapsed(),
+ event->GetGranularity(), rect, event->IsFromUserInput());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
+ case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
+ AccTextChangeEvent* event = downcast_accEvent(aEvent);
+ const nsString& text = event->ModifiedText();
+ PlatformTextChangeEvent(target, text, event->GetStartOffset(),
+ event->GetLength(), event->IsTextInserted(),
+ event->IsFromUserInput());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_SELECTION:
+ case nsIAccessibleEvent::EVENT_SELECTION_ADD:
+ case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: {
+ AccSelChangeEvent* selEvent = downcast_accEvent(aEvent);
+ PlatformSelectionEvent(target, selEvent->Widget(),
+ aEvent->GetEventType());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_FOCUS: {
+ LayoutDeviceIntRect rect;
+ // The caret rect is only used on Windows, so just pass an empty rect on
+ // other platforms.
+#ifdef XP_WIN
+ if (HyperTextAccessible* text = target->AsHyperText()) {
+ nsIWidget* widget = nullptr;
+ rect = text->GetCaretRect(&widget);
+ }
+#endif
+ PlatformFocusEvent(target, rect);
+ break;
+ }
+#if defined(ANDROID)
+ case nsIAccessibleEvent::EVENT_SCROLLING_END:
+ case nsIAccessibleEvent::EVENT_SCROLLING: {
+ AccScrollingEvent* scrollingEvent = downcast_accEvent(aEvent);
+ PlatformScrollingEvent(
+ target, aEvent->GetEventType(), scrollingEvent->ScrollX(),
+ scrollingEvent->ScrollY(), scrollingEvent->MaxScrollX(),
+ scrollingEvent->MaxScrollY());
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_ANNOUNCEMENT: {
+ AccAnnouncementEvent* announcementEvent = downcast_accEvent(aEvent);
+ PlatformAnnouncementEvent(target, announcementEvent->Announcement(),
+ announcementEvent->Priority());
+ break;
+ }
+#endif // defined(ANDROID)
+#if defined(MOZ_WIDGET_COCOA)
+ case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED: {
+ AccTextSelChangeEvent* textSelChangeEvent = downcast_accEvent(aEvent);
+ AutoTArray<TextRange, 1> ranges;
+ textSelChangeEvent->SelectionRanges(&ranges);
+ PlatformTextSelectionChangeEvent(target, ranges);
+ break;
+ }
+#endif // defined(MOZ_WIDGET_COCOA)
+ default:
+ PlatformEvent(target, aEvent->GetEventType());
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<AccAttributes> LocalAccessible::Attributes() {
+ RefPtr<AccAttributes> attributes = NativeAttributes();
+ if (!HasOwnContent() || !mContent->IsElement()) return attributes.forget();
+
+ // 'xml-roles' attribute coming from ARIA.
+ nsString xmlRoles;
+ if (nsAccUtils::GetARIAAttr(mContent->AsElement(), nsGkAtoms::role,
+ xmlRoles) &&
+ !xmlRoles.IsEmpty()) {
+ attributes->SetAttribute(nsGkAtoms::xmlroles, std::move(xmlRoles));
+ } else if (nsAtom* landmark = LandmarkRole()) {
+ // 'xml-roles' attribute for landmark.
+ attributes->SetAttribute(nsGkAtoms::xmlroles, landmark);
+ }
+
+ // Expose object attributes from ARIA attributes.
+ aria::AttrIterator attribIter(mContent);
+ while (attribIter.Next()) {
+ if (attribIter.AttrName() == nsGkAtoms::aria_placeholder &&
+ attributes->HasAttribute(nsGkAtoms::placeholder)) {
+ // If there is an HTML placeholder attribute exposed by
+ // HTMLTextFieldAccessible::NativeAttributes, don't expose
+ // aria-placeholder.
+ continue;
+ }
+ attribIter.ExposeAttr(attributes);
+ }
+
+ // If there is no aria-live attribute then expose default value of 'live'
+ // object attribute used for ARIA role of this accessible.
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (roleMapEntry) {
+ if (roleMapEntry->Is(nsGkAtoms::searchbox)) {
+ attributes->SetAttribute(nsGkAtoms::textInputType, nsGkAtoms::search);
+ }
+
+ if (!attributes->HasAttribute(nsGkAtoms::aria_live)) {
+ nsString live;
+ if (nsAccUtils::GetLiveAttrValue(roleMapEntry->liveAttRule, live)) {
+ attributes->SetAttribute(nsGkAtoms::aria_live, std::move(live));
+ }
+ }
+ }
+
+ return attributes.forget();
+}
+
+already_AddRefed<AccAttributes> LocalAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+
+ // We support values, so expose the string value as well, via the valuetext
+ // object attribute. We test for the value interface because we don't want
+ // to expose traditional Value() information such as URL's on links and
+ // documents, or text in an input.
+ if (HasNumericValue()) {
+ nsString valuetext;
+ Value(valuetext);
+ attributes->SetAttribute(nsGkAtoms::aria_valuetext, std::move(valuetext));
+ }
+
+ // Expose checkable object attribute if the accessible has checkable state
+ if (State() & states::CHECKABLE) {
+ attributes->SetAttribute(nsGkAtoms::checkable, true);
+ }
+
+ // Expose 'explicit-name' attribute.
+ nsAutoString name;
+ if (Name(name) != eNameFromSubtree && !name.IsVoid()) {
+ attributes->SetAttribute(nsGkAtoms::explicit_name, true);
+ }
+
+ // Group attributes (level/setsize/posinset)
+ GroupPos groupPos = GroupPosition();
+ nsAccUtils::SetAccGroupAttrs(attributes, groupPos.level, groupPos.setSize,
+ groupPos.posInSet);
+
+ bool hierarchical = false;
+ uint32_t itemCount = AccGroupInfo::TotalItemCount(this, &hierarchical);
+ if (itemCount) {
+ attributes->SetAttribute(nsGkAtoms::child_item_count,
+ static_cast<int32_t>(itemCount));
+ }
+
+ if (hierarchical) {
+ attributes->SetAttribute(nsGkAtoms::tree, true);
+ }
+
+ // If the accessible doesn't have own content (such as list item bullet or
+ // xul tree item) then don't calculate content based attributes.
+ if (!HasOwnContent()) return attributes.forget();
+
+ nsEventShell::GetEventAttributes(GetNode(), attributes);
+
+ // Get container-foo computed live region properties based on the closest
+ // container with the live region attribute. Inner nodes override outer nodes
+ // within the same document. The inner nodes can be used to override live
+ // region behavior on more general outer nodes.
+ nsAccUtils::SetLiveContainerAttributes(attributes, this);
+
+ if (!mContent->IsElement()) return attributes.forget();
+
+ nsString id;
+ if (nsCoreUtils::GetID(mContent, id)) {
+ attributes->SetAttribute(nsGkAtoms::id, std::move(id));
+ }
+
+ // Expose class because it may have useful microformat information.
+ nsString _class;
+ if (mContent->AsElement()->GetAttr(nsGkAtoms::_class, _class)) {
+ attributes->SetAttribute(nsGkAtoms::_class, std::move(_class));
+ }
+
+ // Expose tag.
+ attributes->SetAttribute(nsGkAtoms::tag, mContent->NodeInfo()->NameAtom());
+
+ if (auto htmlElement = nsGenericHTMLElement::FromNode(mContent)) {
+ // Expose draggable object attribute.
+ if (htmlElement->Draggable()) {
+ attributes->SetAttribute(nsGkAtoms::draggable, true);
+ }
+ nsString popover;
+ htmlElement->GetPopover(popover);
+ if (!popover.IsEmpty()) {
+ attributes->SetAttribute(nsGkAtoms::ispopup, std::move(popover));
+ }
+ }
+
+ // Don't calculate CSS-based object attributes when:
+ // 1. There is no frame (e.g. the accessible is unattached from the tree).
+ // 2. This is an image map area. CSS is irrelevant here. Furthermore, we won't
+ // be able to get the computed style if the map is unslotted in a shadow host.
+ nsIFrame* f = mContent->GetPrimaryFrame();
+ if (!f || mContent->IsHTMLElement(nsGkAtoms::area)) {
+ return attributes.forget();
+ }
+
+ // Expose 'display' attribute.
+ if (RefPtr<nsAtom> display = DisplayStyle()) {
+ attributes->SetAttribute(nsGkAtoms::display, display);
+ }
+
+ const ComputedStyle& style = *f->Style();
+ auto Atomize = [&](nsCSSPropertyID aId) -> RefPtr<nsAtom> {
+ nsAutoCString value;
+ style.GetComputedPropertyValue(aId, value);
+ return NS_Atomize(value);
+ };
+
+ // Expose 'text-align' attribute.
+ attributes->SetAttribute(nsGkAtoms::textAlign,
+ Atomize(eCSSProperty_text_align));
+
+ // Expose 'text-indent' attribute.
+ attributes->SetAttribute(nsGkAtoms::textIndent,
+ Atomize(eCSSProperty_text_indent));
+
+ auto GetMargin = [&](mozilla::Side aSide) -> CSSCoord {
+ // This is here only to guarantee that we do the same as getComputedStyle
+ // does, so that we don't hit precision errors in tests.
+ auto& margin = f->StyleMargin()->mMargin.Get(aSide);
+ if (margin.ConvertsToLength()) {
+ return margin.AsLengthPercentage().ToLengthInCSSPixels();
+ }
+
+ nscoord coordVal = f->GetUsedMargin().Side(aSide);
+ return CSSPixel::FromAppUnits(coordVal);
+ };
+
+ // Expose 'margin-left' attribute.
+ attributes->SetAttribute(nsGkAtoms::marginLeft, GetMargin(eSideLeft));
+
+ // Expose 'margin-right' attribute.
+ attributes->SetAttribute(nsGkAtoms::marginRight, GetMargin(eSideRight));
+
+ // Expose 'margin-top' attribute.
+ attributes->SetAttribute(nsGkAtoms::marginTop, GetMargin(eSideTop));
+
+ // Expose 'margin-bottom' attribute.
+ attributes->SetAttribute(nsGkAtoms::marginBottom, GetMargin(eSideBottom));
+
+ // Expose data-at-shortcutkeys attribute for web applications and virtual
+ // cursors. Currently mostly used by JAWS.
+ nsString atShortcutKeys;
+ if (mContent->AsElement()->GetAttr(
+ kNameSpaceID_None, nsGkAtoms::dataAtShortcutkeys, atShortcutKeys)) {
+ attributes->SetAttribute(nsGkAtoms::dataAtShortcutkeys,
+ std::move(atShortcutKeys));
+ }
+
+ return attributes.forget();
+}
+
+bool LocalAccessible::AttributeChangesState(nsAtom* aAttribute) {
+ return aAttribute == nsGkAtoms::aria_disabled ||
+ aAttribute == nsGkAtoms::disabled ||
+ aAttribute == nsGkAtoms::tabindex ||
+ aAttribute == nsGkAtoms::aria_required ||
+ aAttribute == nsGkAtoms::aria_invalid ||
+ aAttribute == nsGkAtoms::aria_expanded ||
+ aAttribute == nsGkAtoms::aria_checked ||
+ (aAttribute == nsGkAtoms::aria_pressed && IsButton()) ||
+ aAttribute == nsGkAtoms::aria_readonly ||
+ aAttribute == nsGkAtoms::aria_current ||
+ aAttribute == nsGkAtoms::aria_haspopup ||
+ aAttribute == nsGkAtoms::aria_busy ||
+ aAttribute == nsGkAtoms::aria_multiline ||
+ aAttribute == nsGkAtoms::aria_multiselectable ||
+ aAttribute == nsGkAtoms::contenteditable ||
+ aAttribute == nsGkAtoms::popovertarget;
+}
+
+void LocalAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ // Fire accessible event after short timer, because we need to wait for
+ // DOM attribute & resulting layout to actually change. Otherwise,
+ // assistive technology will retrieve the wrong state/value/selection info.
+
+ // XXX todo
+ // We still need to handle special HTML cases here
+ // For example, if an <img>'s usemap attribute is modified
+ // Otherwise it may just be a state change, for example an object changing
+ // its visibility
+ //
+ // XXX todo: report aria state changes for "undefined" literal value changes
+ // filed as bug 472142
+ //
+ // XXX todo: invalidate accessible when aria state changes affect exposed
+ // role filed as bug 472143
+
+ if (AttributeChangesState(aAttribute)) {
+ uint64_t currState = State();
+ uint64_t diffState = currState ^ aOldState;
+ if (diffState) {
+ for (uint64_t state = 1; state <= states::LAST_ENTRY; state <<= 1) {
+ if (diffState & state) {
+ RefPtr<AccEvent> stateChangeEvent =
+ new AccStateChangeEvent(this, state, (currState & state));
+ mDoc->FireDelayedEvent(stateChangeEvent);
+ }
+ }
+ }
+ }
+
+ if (aAttribute == nsGkAtoms::_class) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::DOMNodeIDAndClass);
+ return;
+ }
+
+ // When a details object has its open attribute changed
+ // we should fire a state-change event on the accessible of
+ // its main summary
+ if (aAttribute == nsGkAtoms::open) {
+ // FromDetails checks if the given accessible belongs to
+ // a details frame and also locates the accessible of its
+ // main summary.
+ if (HTMLSummaryAccessible* summaryAccessible =
+ HTMLSummaryAccessible::FromDetails(this)) {
+ RefPtr<AccEvent> expandedChangeEvent =
+ new AccStateChangeEvent(summaryAccessible, states::EXPANDED);
+ mDoc->FireDelayedEvent(expandedChangeEvent);
+ return;
+ }
+ }
+
+ // Check for namespaced ARIA attribute
+ if (aNameSpaceID == kNameSpaceID_None) {
+ // Check for hyphenated aria-foo property?
+ if (StringBeginsWith(nsDependentAtomString(aAttribute), u"aria-"_ns)) {
+ uint8_t attrFlags = aria::AttrCharacteristicsFor(aAttribute);
+ if (!(attrFlags & ATTR_BYPASSOBJ)) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::ARIA);
+ // For aria attributes like drag and drop changes we fire a generic
+ // attribute change event; at least until native API comes up with a
+ // more meaningful event.
+ RefPtr<AccEvent> event =
+ new AccObjectAttrChangedEvent(this, aAttribute);
+ mDoc->FireDelayedEvent(event);
+ }
+ }
+ }
+
+ dom::Element* elm = Elm();
+
+ if (HasNumericValue() &&
+ (aAttribute == nsGkAtoms::aria_valuemax ||
+ aAttribute == nsGkAtoms::aria_valuemin || aAttribute == nsGkAtoms::min ||
+ aAttribute == nsGkAtoms::max || aAttribute == nsGkAtoms::step)) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Value);
+ return;
+ }
+
+ // Fire text value change event whenever aria-valuetext is changed.
+ if (aAttribute == nsGkAtoms::aria_valuetext) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE, this);
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::aria_valuenow) {
+ if (!nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_valuetext) ||
+ nsAccUtils::ARIAAttrValueIs(elm, nsGkAtoms::aria_valuetext,
+ nsGkAtoms::_empty, eCaseMatters)) {
+ // Fire numeric value change event when aria-valuenow is changed and
+ // aria-valuetext is empty
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, this);
+ } else {
+ // We need to update the cache here since we won't get an event if
+ // aria-valuenow is shadowed by aria-valuetext.
+ mDoc->QueueCacheUpdate(this, CacheDomain::Value);
+ }
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::aria_owns) {
+ mDoc->Controller()->ScheduleRelocation(this);
+ }
+
+ // Fire name change and description change events.
+ if (aAttribute == nsGkAtoms::aria_label) {
+ // A valid aria-labelledby would take precedence so an aria-label change
+ // won't change the name.
+ IDRefsIterator iter(mDoc, elm, nsGkAtoms::aria_labelledby);
+ if (!iter.NextElem()) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ }
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::aria_description) {
+ // A valid aria-describedby would take precedence so an aria-description
+ // change won't change the description.
+ IDRefsIterator iter(mDoc, elm, nsGkAtoms::aria_describedby);
+ if (!iter.NextElem()) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE,
+ this);
+ }
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::aria_describedby) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, this);
+ if (aModType == dom::MutationEvent_Binding::MODIFICATION ||
+ aModType == dom::MutationEvent_Binding::ADDITION) {
+ // The subtrees of the new aria-describedby targets might be used to
+ // compute the description for this. Therefore, we need to set
+ // the eHasDescriptionDependent flag on all Accessibles in these subtrees.
+ IDRefsIterator iter(mDoc, elm, nsGkAtoms::aria_describedby);
+ while (LocalAccessible* target = iter.Next()) {
+ target->ModifySubtreeContextFlags(eHasDescriptionDependent, true);
+ }
+ }
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::aria_labelledby) {
+ // We only queue cache updates for explicit relations. Implicit, reverse
+ // relations are handled in ApplyCache and stored in a map on the remote
+ // document itself.
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ if (aModType == dom::MutationEvent_Binding::MODIFICATION ||
+ aModType == dom::MutationEvent_Binding::ADDITION) {
+ // The subtrees of the new aria-labelledby targets might be used to
+ // compute the name for this. Therefore, we need to set
+ // the eHasNameDependent flag on all Accessibles in these subtrees.
+ IDRefsIterator iter(mDoc, elm, nsGkAtoms::aria_labelledby);
+ while (LocalAccessible* target = iter.Next()) {
+ target->ModifySubtreeContextFlags(eHasNameDependent, true);
+ }
+ }
+ return;
+ }
+
+ if ((aAttribute == nsGkAtoms::aria_expanded ||
+ aAttribute == nsGkAtoms::href) &&
+ (aModType == dom::MutationEvent_Binding::ADDITION ||
+ aModType == dom::MutationEvent_Binding::REMOVAL)) {
+ // The presence of aria-expanded adds an expand/collapse action.
+ mDoc->QueueCacheUpdate(this, CacheDomain::Actions);
+ }
+
+ if (aAttribute == nsGkAtoms::href || aAttribute == nsGkAtoms::src) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Value);
+ }
+
+ if (aAttribute == nsGkAtoms::aria_controls ||
+ aAttribute == nsGkAtoms::aria_flowto ||
+ aAttribute == nsGkAtoms::aria_details ||
+ aAttribute == nsGkAtoms::aria_errormessage) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
+ }
+
+ if (aAttribute == nsGkAtoms::popovertarget) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::alt &&
+ !nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_label) &&
+ !elm->HasAttr(nsGkAtoms::aria_labelledby)) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::title) {
+ nsAutoString name;
+ ARIAName(name);
+ if (name.IsEmpty()) {
+ NativeName(name);
+ if (name.IsEmpty()) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ return;
+ }
+ }
+
+ if (!elm->HasAttr(nsGkAtoms::aria_describedby)) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE,
+ this);
+ }
+
+ return;
+ }
+
+ // ARIA or XUL selection
+ if ((mContent->IsXULElement() && aAttribute == nsGkAtoms::selected) ||
+ aAttribute == nsGkAtoms::aria_selected) {
+ LocalAccessible* widget = nsAccUtils::GetSelectableContainer(this, State());
+ if (widget) {
+ AccSelChangeEvent::SelChangeType selChangeType;
+ if (aNameSpaceID != kNameSpaceID_None) {
+ selChangeType = elm->AttrValueIs(aNameSpaceID, aAttribute,
+ nsGkAtoms::_true, eCaseMatters)
+ ? AccSelChangeEvent::eSelectionAdd
+ : AccSelChangeEvent::eSelectionRemove;
+ } else {
+ selChangeType = nsAccUtils::ARIAAttrValueIs(
+ elm, aAttribute, nsGkAtoms::_true, eCaseMatters)
+ ? AccSelChangeEvent::eSelectionAdd
+ : AccSelChangeEvent::eSelectionRemove;
+ }
+
+ RefPtr<AccEvent> event =
+ new AccSelChangeEvent(widget, this, selChangeType);
+ mDoc->FireDelayedEvent(event);
+ if (aAttribute == nsGkAtoms::aria_selected) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::State);
+ }
+ }
+
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::aria_level ||
+ aAttribute == nsGkAtoms::aria_setsize ||
+ aAttribute == nsGkAtoms::aria_posinset) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::GroupInfo);
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::accesskey) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Actions);
+ }
+
+ if (aAttribute == nsGkAtoms::name &&
+ (mContent && mContent->IsHTMLElement(nsGkAtoms::a))) {
+ // If an anchor's name changed, it's possible a LINKS_TO relation
+ // also changed. Push a cache update for Relations.
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
+ }
+
+ if (aAttribute == nsGkAtoms::slot &&
+ !mContent->GetFlattenedTreeParentNode() && this != mDoc) {
+ // This is inside a shadow host but is no longer slotted.
+ mDoc->ContentRemoved(this);
+ }
+}
+
+void LocalAccessible::ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize,
+ int32_t* aPosInSet) const {
+ if (!mContent) {
+ return;
+ }
+
+ if (aLevel) {
+ nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_level, aLevel);
+ }
+ if (aSetSize) {
+ nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_setsize, aSetSize);
+ }
+ if (aPosInSet) {
+ nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_posinset, aPosInSet);
+ }
+}
+
+uint64_t LocalAccessible::State() {
+ if (IsDefunct()) return states::DEFUNCT;
+
+ uint64_t state = NativeState();
+ // Apply ARIA states to be sure accessible states will be overridden.
+ ApplyARIAState(&state);
+
+ const uint32_t kExpandCollapseStates = states::COLLAPSED | states::EXPANDED;
+ if ((state & kExpandCollapseStates) == kExpandCollapseStates) {
+ // Cannot be both expanded and collapsed -- this happens in ARIA expanded
+ // combobox because of limitation of ARIAMap.
+ // XXX: Perhaps we will be able to make this less hacky if we support
+ // extended states in ARIAMap, e.g. derive COLLAPSED from
+ // EXPANDABLE && !EXPANDED.
+ state &= ~states::COLLAPSED;
+ }
+
+ if (!(state & states::UNAVAILABLE)) {
+ state |= states::ENABLED | states::SENSITIVE;
+
+ // If the object is a current item of container widget then mark it as
+ // ACTIVE. This allows screen reader virtual buffer modes to know which
+ // descendant is the current one that would get focus if the user navigates
+ // to the container widget.
+ LocalAccessible* widget = ContainerWidget();
+ if (widget && widget->CurrentItem() == this) state |= states::ACTIVE;
+ }
+
+ if ((state & states::COLLAPSED) || (state & states::EXPANDED)) {
+ state |= states::EXPANDABLE;
+ }
+
+ ApplyImplicitState(state);
+ return state;
+}
+
+void LocalAccessible::ApplyARIAState(uint64_t* aState) const {
+ if (!mContent->IsElement()) return;
+
+ dom::Element* element = mContent->AsElement();
+
+ // Test for universal states first
+ *aState |= aria::UniversalStatesFor(element);
+
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (roleMapEntry) {
+ // We only force the readonly bit off if we have a real mapping for the aria
+ // role. This preserves the ability for screen readers to use readonly
+ // (primarily on the document) as the hint for creating a virtual buffer.
+ if (roleMapEntry->role != roles::NOTHING) *aState &= ~states::READONLY;
+
+ if (mContent->HasID()) {
+ // If has a role & ID and aria-activedescendant on the container, assume
+ // focusable.
+ const LocalAccessible* ancestor = this;
+ while ((ancestor = ancestor->LocalParent()) && !ancestor->IsDoc()) {
+ dom::Element* el = ancestor->Elm();
+ if (el && el->HasAttr(nsGkAtoms::aria_activedescendant)) {
+ *aState |= states::FOCUSABLE;
+ break;
+ }
+ }
+ }
+ }
+
+ if (*aState & states::FOCUSABLE) {
+ // Propogate aria-disabled from ancestors down to any focusable descendant.
+ const LocalAccessible* ancestor = this;
+ while ((ancestor = ancestor->LocalParent()) && !ancestor->IsDoc()) {
+ dom::Element* el = ancestor->Elm();
+ if (el && nsAccUtils::ARIAAttrValueIs(el, nsGkAtoms::aria_disabled,
+ nsGkAtoms::_true, eCaseMatters)) {
+ *aState |= states::UNAVAILABLE;
+ break;
+ }
+ }
+ } else {
+ // Sometimes, we use aria-activedescendant targeting something which isn't
+ // actually a descendant. This is technically a spec violation, but it's a
+ // useful hack which makes certain things much easier. For example, we use
+ // this for "fake focus" for multi select browser tabs and Quantumbar
+ // autocomplete suggestions.
+ // In these cases, the aria-activedescendant code above won't make the
+ // active item focusable. It doesn't make sense for something to have
+ // focus when it isn't focusable, so fix that here.
+ if (FocusMgr()->IsActiveItem(this)) {
+ *aState |= states::FOCUSABLE;
+ }
+ }
+
+ // special case: A native button element whose role got transformed by ARIA to
+ // a toggle button Also applies to togglable button menus, like in the Dev
+ // Tools Web Console.
+ if (IsButton() || IsMenuButton()) {
+ aria::MapToState(aria::eARIAPressed, element, aState);
+ }
+
+ if (!roleMapEntry) return;
+
+ *aState |= roleMapEntry->state;
+
+ if (aria::MapToState(roleMapEntry->attributeMap1, element, aState) &&
+ aria::MapToState(roleMapEntry->attributeMap2, element, aState) &&
+ aria::MapToState(roleMapEntry->attributeMap3, element, aState)) {
+ aria::MapToState(roleMapEntry->attributeMap4, element, aState);
+ }
+
+ // ARIA gridcell inherits readonly state from the grid until it's overridden.
+ if ((roleMapEntry->Is(nsGkAtoms::gridcell) ||
+ roleMapEntry->Is(nsGkAtoms::columnheader) ||
+ roleMapEntry->Is(nsGkAtoms::rowheader)) &&
+ // Don't recurse infinitely for an authoring error like
+ // <table role="gridcell">. Without this check, we'd call TableFor(this)
+ // below, which would return this.
+ !IsTable() &&
+ !nsAccUtils::HasDefinedARIAToken(mContent, nsGkAtoms::aria_readonly)) {
+ if (const LocalAccessible* grid = nsAccUtils::TableFor(this)) {
+ uint64_t gridState = 0;
+ grid->ApplyARIAState(&gridState);
+ *aState |= gridState & states::READONLY;
+ }
+ }
+}
+
+void LocalAccessible::Value(nsString& aValue) const {
+ if (HasNumericValue()) {
+ // aria-valuenow is a number, and aria-valuetext is the optional text
+ // equivalent. For the string value, we will try the optional text
+ // equivalent first.
+ if (!mContent->IsElement()) {
+ return;
+ }
+
+ if (!nsAccUtils::GetARIAAttr(mContent->AsElement(),
+ nsGkAtoms::aria_valuetext, aValue)) {
+ if (!NativeHasNumericValue()) {
+ double checkValue = CurValue();
+ if (!std::isnan(checkValue)) {
+ aValue.AppendFloat(checkValue);
+ }
+ }
+ }
+ return;
+ }
+
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (!roleMapEntry) {
+ return;
+ }
+
+ // Value of textbox is a textified subtree.
+ if (roleMapEntry->Is(nsGkAtoms::textbox)) {
+ nsTextEquivUtils::GetTextEquivFromSubtree(this, aValue);
+ return;
+ }
+
+ // Value of combobox is a text of current or selected item.
+ if (roleMapEntry->Is(nsGkAtoms::combobox)) {
+ LocalAccessible* option = CurrentItem();
+ if (!option) {
+ uint32_t childCount = ChildCount();
+ for (uint32_t idx = 0; idx < childCount; idx++) {
+ LocalAccessible* child = mChildren.ElementAt(idx);
+ if (child->IsListControl()) {
+ Accessible* acc = child->GetSelectedItem(0);
+ option = acc ? acc->AsLocal() : nullptr;
+ break;
+ }
+ }
+ }
+
+ // If there's a selected item, get the value from it. Otherwise, determine
+ // the value from descendant elements.
+ nsTextEquivUtils::GetTextEquivFromSubtree(option ? option : this, aValue);
+ }
+}
+
+double LocalAccessible::MaxValue() const {
+ double checkValue = AttrNumericValue(nsGkAtoms::aria_valuemax);
+ if (std::isnan(checkValue) && !NativeHasNumericValue()) {
+ // aria-valuemax isn't present and this element doesn't natively provide a
+ // maximum value. Use the ARIA default.
+ const nsRoleMapEntry* roleMap = ARIARoleMap();
+ if (roleMap && roleMap->role == roles::SPINBUTTON) {
+ return UnspecifiedNaN<double>();
+ }
+ return 100;
+ }
+ return checkValue;
+}
+
+double LocalAccessible::MinValue() const {
+ double checkValue = AttrNumericValue(nsGkAtoms::aria_valuemin);
+ if (std::isnan(checkValue) && !NativeHasNumericValue()) {
+ // aria-valuemin isn't present and this element doesn't natively provide a
+ // minimum value. Use the ARIA default.
+ const nsRoleMapEntry* roleMap = ARIARoleMap();
+ if (roleMap && roleMap->role == roles::SPINBUTTON) {
+ return UnspecifiedNaN<double>();
+ }
+ return 0;
+ }
+ return checkValue;
+}
+
+double LocalAccessible::Step() const {
+ return UnspecifiedNaN<double>(); // no mimimum increment (step) in ARIA.
+}
+
+double LocalAccessible::CurValue() const {
+ double checkValue = AttrNumericValue(nsGkAtoms::aria_valuenow);
+ if (std::isnan(checkValue) && !NativeHasNumericValue()) {
+ // aria-valuenow isn't present and this element doesn't natively provide a
+ // current value. Use the ARIA default.
+ const nsRoleMapEntry* roleMap = ARIARoleMap();
+ if (roleMap && roleMap->role == roles::SPINBUTTON) {
+ return UnspecifiedNaN<double>();
+ }
+ double minValue = MinValue();
+ return minValue + ((MaxValue() - minValue) / 2);
+ }
+
+ return checkValue;
+}
+
+bool LocalAccessible::SetCurValue(double aValue) {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (!roleMapEntry || roleMapEntry->valueRule == eNoValue) return false;
+
+ const uint32_t kValueCannotChange = states::READONLY | states::UNAVAILABLE;
+ if (State() & kValueCannotChange) return false;
+
+ double checkValue = MinValue();
+ if (!std::isnan(checkValue) && aValue < checkValue) return false;
+
+ checkValue = MaxValue();
+ if (!std::isnan(checkValue) && aValue > checkValue) return false;
+
+ nsAutoString strValue;
+ strValue.AppendFloat(aValue);
+
+ if (!mContent->IsElement()) return true;
+
+ return NS_SUCCEEDED(mContent->AsElement()->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::aria_valuenow, strValue, true));
+}
+
+role LocalAccessible::ARIATransformRole(role aRole) const {
+ // Beginning with ARIA 1.1, user agents are expected to use the native host
+ // language role of the element when the region role is used without a name.
+ // https://rawgit.com/w3c/aria/master/core-aam/core-aam.html#role-map-region
+ //
+ // XXX: While the name computation algorithm can be non-trivial in the general
+ // case, it should not be especially bad here: If the author hasn't used the
+ // region role, this calculation won't occur. And the region role's name
+ // calculation rule excludes name from content. That said, this use case is
+ // another example of why we should consider caching the accessible name. See:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1378235.
+ if (aRole == roles::REGION) {
+ nsAutoString name;
+ Name(name);
+ return name.IsEmpty() ? NativeRole() : aRole;
+ }
+
+ // XXX: these unfortunate exceptions don't fit into the ARIA table. This is
+ // where the accessible role depends on both the role and ARIA state.
+ if (aRole == roles::PUSHBUTTON) {
+ if (nsAccUtils::HasDefinedARIAToken(mContent, nsGkAtoms::aria_pressed)) {
+ // For simplicity, any existing pressed attribute except "" or "undefined"
+ // indicates a toggle.
+ return roles::TOGGLE_BUTTON;
+ }
+
+ if (mContent->IsElement() &&
+ nsAccUtils::ARIAAttrValueIs(mContent->AsElement(),
+ nsGkAtoms::aria_haspopup, nsGkAtoms::_true,
+ eCaseMatters)) {
+ // For button with aria-haspopup="true".
+ return roles::BUTTONMENU;
+ }
+
+ } else if (aRole == roles::LISTBOX) {
+ // A listbox inside of a combobox needs a special role because of ATK
+ // mapping to menu.
+ if (mParent && mParent->IsCombobox()) {
+ return roles::COMBOBOX_LIST;
+ }
+
+ } else if (aRole == roles::OPTION) {
+ if (mParent && mParent->Role() == roles::COMBOBOX_LIST) {
+ return roles::COMBOBOX_OPTION;
+ }
+
+ } else if (aRole == roles::MENUITEM) {
+ // Menuitem has a submenu.
+ if (mContent->IsElement() &&
+ nsAccUtils::ARIAAttrValueIs(mContent->AsElement(),
+ nsGkAtoms::aria_haspopup, nsGkAtoms::_true,
+ eCaseMatters)) {
+ return roles::PARENT_MENUITEM;
+ }
+
+ } else if (aRole == roles::CELL) {
+ // A cell inside an ancestor table element that has a grid role needs a
+ // gridcell role
+ // (https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings).
+ const LocalAccessible* table = nsAccUtils::TableFor(this);
+ if (table && table->IsARIARole(nsGkAtoms::grid)) {
+ return roles::GRID_CELL;
+ }
+ }
+
+ return aRole;
+}
+
+role LocalAccessible::GetMinimumRole(role aRole) const {
+ if (aRole != roles::TEXT && aRole != roles::TEXT_CONTAINER &&
+ aRole != roles::SECTION) {
+ // This isn't a generic role, so aRole is specific enough.
+ return aRole;
+ }
+ dom::Element* el = Elm();
+ if (el && el->IsHTMLElement() && el->HasAttr(nsGkAtoms::popover)) {
+ return roles::GROUPING;
+ }
+ return aRole;
+}
+
+role LocalAccessible::NativeRole() const { return roles::NOTHING; }
+
+uint8_t LocalAccessible::ActionCount() const {
+ return HasPrimaryAction() || ActionAncestor() ? 1 : 0;
+}
+
+void LocalAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ aName.Truncate();
+
+ if (aIndex != 0) return;
+
+ uint32_t actionRule = GetActionRule();
+
+ switch (actionRule) {
+ case eActivateAction:
+ aName.AssignLiteral("activate");
+ return;
+
+ case eClickAction:
+ aName.AssignLiteral("click");
+ return;
+
+ case ePressAction:
+ aName.AssignLiteral("press");
+ return;
+
+ case eCheckUncheckAction: {
+ uint64_t state = State();
+ if (state & states::CHECKED) {
+ aName.AssignLiteral("uncheck");
+ } else if (state & states::MIXED) {
+ aName.AssignLiteral("cycle");
+ } else {
+ aName.AssignLiteral("check");
+ }
+ return;
+ }
+
+ case eJumpAction:
+ aName.AssignLiteral("jump");
+ return;
+
+ case eOpenCloseAction:
+ if (State() & states::COLLAPSED) {
+ aName.AssignLiteral("open");
+ } else {
+ aName.AssignLiteral("close");
+ }
+ return;
+
+ case eSelectAction:
+ aName.AssignLiteral("select");
+ return;
+
+ case eSwitchAction:
+ aName.AssignLiteral("switch");
+ return;
+
+ case eSortAction:
+ aName.AssignLiteral("sort");
+ return;
+
+ case eExpandAction:
+ if (State() & states::COLLAPSED) {
+ aName.AssignLiteral("expand");
+ } else {
+ aName.AssignLiteral("collapse");
+ }
+ return;
+ }
+
+ if (ActionAncestor()) {
+ aName.AssignLiteral("click ancestor");
+ return;
+ }
+}
+
+bool LocalAccessible::DoAction(uint8_t aIndex) const {
+ if (aIndex != 0) return false;
+
+ if (HasPrimaryAction() || ActionAncestor()) {
+ DoCommand();
+ return true;
+ }
+
+ return false;
+}
+
+bool LocalAccessible::HasPrimaryAction() const {
+ return GetActionRule() != eNoAction;
+}
+
+nsIContent* LocalAccessible::GetAtomicRegion() const {
+ nsIContent* loopContent = mContent;
+ nsAutoString atomic;
+ while (loopContent &&
+ (!loopContent->IsElement() ||
+ !nsAccUtils::GetARIAAttr(loopContent->AsElement(),
+ nsGkAtoms::aria_atomic, atomic))) {
+ loopContent = loopContent->GetParent();
+ }
+
+ return atomic.EqualsLiteral("true") ? loopContent : nullptr;
+}
+
+LocalAccessible* LocalAccessible::GetPopoverTargetDetailsRelation() const {
+ dom::Element* targetEl = mContent->GetEffectivePopoverTargetElement();
+ if (!targetEl) {
+ return nullptr;
+ }
+ LocalAccessible* targetAcc = mDoc->GetAccessible(targetEl);
+ if (!targetAcc) {
+ return nullptr;
+ }
+ // Even if the popovertarget is valid, there are a few cases where we must not
+ // expose it via the details relation.
+ if (const nsAttrValue* actionVal =
+ Elm()->GetParsedAttr(nsGkAtoms::popovertargetaction)) {
+ if (static_cast<PopoverTargetAction>(actionVal->GetEnumValue()) ==
+ PopoverTargetAction::Hide) {
+ return nullptr;
+ }
+ }
+ if (targetAcc->NextSibling() == this || targetAcc->PrevSibling() == this) {
+ return nullptr;
+ }
+ return targetAcc;
+}
+
+Relation LocalAccessible::RelationByType(RelationType aType) const {
+ if (!HasOwnContent()) return Relation();
+
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+
+ // Relationships are defined on the same content node that the role would be
+ // defined on.
+ switch (aType) {
+ case RelationType::LABELLED_BY: {
+ Relation rel(
+ new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_labelledby));
+ if (mContent->IsHTMLElement()) {
+ rel.AppendIter(new HTMLLabelIterator(Document(), this));
+ }
+ rel.AppendIter(new XULLabelIterator(Document(), mContent));
+
+ return rel;
+ }
+
+ case RelationType::LABEL_FOR: {
+ Relation rel(new RelatedAccIterator(Document(), mContent,
+ nsGkAtoms::aria_labelledby));
+ if (mContent->IsXULElement(nsGkAtoms::label)) {
+ rel.AppendIter(new IDRefsIterator(mDoc, mContent, nsGkAtoms::control));
+ }
+
+ return rel;
+ }
+
+ case RelationType::DESCRIBED_BY: {
+ Relation rel(
+ new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_describedby));
+ if (mContent->IsXULElement()) {
+ rel.AppendIter(new XULDescriptionIterator(Document(), mContent));
+ }
+
+ return rel;
+ }
+
+ case RelationType::DESCRIPTION_FOR: {
+ Relation rel(new RelatedAccIterator(Document(), mContent,
+ nsGkAtoms::aria_describedby));
+
+ // This affectively adds an optional control attribute to xul:description,
+ // which only affects accessibility, by allowing the description to be
+ // tied to a control.
+ if (mContent->IsXULElement(nsGkAtoms::description)) {
+ rel.AppendIter(new IDRefsIterator(mDoc, mContent, nsGkAtoms::control));
+ }
+
+ return rel;
+ }
+
+ case RelationType::NODE_CHILD_OF: {
+ Relation rel;
+ // This is an ARIA tree or treegrid that doesn't use owns, so we need to
+ // get the parent the hard way.
+ if (roleMapEntry && (roleMapEntry->role == roles::OUTLINEITEM ||
+ roleMapEntry->role == roles::LISTITEM ||
+ roleMapEntry->role == roles::ROW)) {
+ Accessible* parent = const_cast<LocalAccessible*>(this)
+ ->GetOrCreateGroupInfo()
+ ->ConceptualParent();
+ if (parent) {
+ MOZ_ASSERT(parent->IsLocal());
+ rel.AppendTarget(parent->AsLocal());
+ }
+ }
+
+ // If this is an OOP iframe document, we can't support NODE_CHILD_OF
+ // here, since the iframe resides in a different process. This is fine
+ // because the client will then request the parent instead, which will be
+ // correctly handled by platform code.
+ if (XRE_IsContentProcess() && IsRoot()) {
+ dom::Document* doc =
+ const_cast<LocalAccessible*>(this)->AsDoc()->DocumentNode();
+ dom::BrowsingContext* bc = doc->GetBrowsingContext();
+ MOZ_ASSERT(bc);
+ if (!bc->Top()->IsInProcess()) {
+ return rel;
+ }
+ }
+
+ // If accessible is in its own Window, or is the root of a document,
+ // then we should provide NODE_CHILD_OF relation so that MSAA clients
+ // can easily get to true parent instead of getting to oleacc's
+ // ROLE_WINDOW accessible which will prevent us from going up further
+ // (because it is system generated and has no idea about the hierarchy
+ // above it).
+ nsIFrame* frame = GetFrame();
+ if (frame) {
+ nsView* view = frame->GetView();
+ if (view) {
+ nsIScrollableFrame* scrollFrame = do_QueryFrame(frame);
+ if (scrollFrame || view->GetWidget() || !frame->GetParent()) {
+ rel.AppendTarget(LocalParent());
+ }
+ }
+ }
+
+ return rel;
+ }
+
+ case RelationType::NODE_PARENT_OF: {
+ // ARIA tree or treegrid can do the hierarchy by @aria-level, ARIA trees
+ // also can be organized by groups.
+ if (roleMapEntry && (roleMapEntry->role == roles::OUTLINEITEM ||
+ roleMapEntry->role == roles::LISTITEM ||
+ roleMapEntry->role == roles::ROW ||
+ roleMapEntry->role == roles::OUTLINE ||
+ roleMapEntry->role == roles::LIST ||
+ roleMapEntry->role == roles::TREE_TABLE)) {
+ return Relation(new ItemIterator(this));
+ }
+
+ return Relation();
+ }
+
+ case RelationType::CONTROLLED_BY:
+ return Relation(new RelatedAccIterator(Document(), mContent,
+ nsGkAtoms::aria_controls));
+
+ case RelationType::CONTROLLER_FOR: {
+ Relation rel(
+ new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_controls));
+ rel.AppendIter(new HTMLOutputIterator(Document(), mContent));
+ return rel;
+ }
+
+ case RelationType::FLOWS_TO:
+ return Relation(
+ new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_flowto));
+
+ case RelationType::FLOWS_FROM:
+ return Relation(
+ new RelatedAccIterator(Document(), mContent, nsGkAtoms::aria_flowto));
+
+ case RelationType::MEMBER_OF: {
+ if (Role() == roles::RADIOBUTTON) {
+ /* If we see a radio button role here, we're dealing with an aria
+ * radio button (because input=radio buttons are
+ * HTMLRadioButtonAccessibles) */
+ Relation rel = Relation();
+ LocalAccessible* currParent = LocalParent();
+ while (currParent && currParent->Role() != roles::RADIO_GROUP) {
+ currParent = currParent->LocalParent();
+ }
+
+ if (currParent && currParent->Role() == roles::RADIO_GROUP) {
+ /* If we found a radiogroup parent, search for all
+ * roles::RADIOBUTTON children and add them to our relation.
+ * This search will include the radio button this method
+ * was called from, which is expected. */
+ Pivot p = Pivot(currParent);
+ PivotRoleRule rule(roles::RADIOBUTTON);
+ Accessible* match = p.Next(currParent, rule);
+ while (match) {
+ MOZ_ASSERT(match->IsLocal(),
+ "We shouldn't find any remote accs while building our "
+ "relation!");
+ rel.AppendTarget(match->AsLocal());
+ match = p.Next(match, rule);
+ }
+ }
+
+ /* By webkit's standard, aria radio buttons do not get grouped
+ * if they lack a group parent, so we return an empty
+ * relation here if the above check fails. */
+
+ return rel;
+ }
+
+ return Relation(mDoc, GetAtomicRegion());
+ }
+
+ case RelationType::LINKS_TO: {
+ Relation rel = Relation();
+ if (Role() == roles::LINK) {
+ dom::HTMLAnchorElement* anchor =
+ dom::HTMLAnchorElement::FromNode(mContent);
+ if (!anchor) {
+ return rel;
+ }
+ // If this node is an anchor element, query its hash to find the
+ // target.
+ nsAutoString hash;
+ anchor->GetHash(hash);
+ if (hash.IsEmpty()) {
+ return rel;
+ }
+
+ // GetHash returns an ID or name with a leading '#', trim it so we can
+ // search the doc by ID or name alone.
+ hash.Trim("#");
+ if (dom::Element* elm = mContent->OwnerDoc()->GetElementById(hash)) {
+ rel.AppendTarget(mDoc->GetAccessibleOrContainer(elm));
+ } else if (nsCOMPtr<nsINodeList> list =
+ mContent->OwnerDoc()->GetElementsByName(hash)) {
+ // Loop through the named nodes looking for the first anchor
+ uint32_t length = list->Length();
+ for (uint32_t i = 0; i < length; i++) {
+ nsIContent* node = list->Item(i);
+ if (node->IsHTMLElement(nsGkAtoms::a)) {
+ rel.AppendTarget(mDoc->GetAccessibleOrContainer(node));
+ break;
+ }
+ }
+ }
+ }
+
+ return rel;
+ }
+
+ case RelationType::SUBWINDOW_OF:
+ case RelationType::EMBEDS:
+ case RelationType::EMBEDDED_BY:
+ case RelationType::POPUP_FOR:
+ case RelationType::PARENT_WINDOW_OF:
+ return Relation();
+
+ case RelationType::DEFAULT_BUTTON: {
+ if (mContent->IsHTMLElement()) {
+ // HTML form controls implements nsIFormControl interface.
+ nsCOMPtr<nsIFormControl> control(do_QueryInterface(mContent));
+ if (control) {
+ if (dom::HTMLFormElement* form = control->GetForm()) {
+ return Relation(mDoc, form->GetDefaultSubmitElement());
+ }
+ }
+ } else {
+ // In XUL, use first <button default="true" .../> in the document
+ dom::Document* doc = mContent->OwnerDoc();
+ nsIContent* buttonEl = nullptr;
+ if (doc->AllowXULXBL()) {
+ nsCOMPtr<nsIHTMLCollection> possibleDefaultButtons =
+ doc->GetElementsByAttribute(u"default"_ns, u"true"_ns);
+ if (possibleDefaultButtons) {
+ uint32_t length = possibleDefaultButtons->Length();
+ // Check for button in list of default="true" elements
+ for (uint32_t count = 0; count < length && !buttonEl; count++) {
+ nsIContent* item = possibleDefaultButtons->Item(count);
+ RefPtr<nsIDOMXULButtonElement> button =
+ item->IsElement() ? item->AsElement()->AsXULButton()
+ : nullptr;
+ if (button) {
+ buttonEl = item;
+ }
+ }
+ }
+ return Relation(mDoc, buttonEl);
+ }
+ }
+ return Relation();
+ }
+
+ case RelationType::CONTAINING_DOCUMENT:
+ return Relation(mDoc);
+
+ case RelationType::CONTAINING_TAB_PANE: {
+ nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(GetNode());
+ if (docShell) {
+ // Walk up the parent chain without crossing the boundary at which item
+ // types change, preventing us from walking up out of tab content.
+ nsCOMPtr<nsIDocShellTreeItem> root;
+ docShell->GetInProcessSameTypeRootTreeItem(getter_AddRefs(root));
+ if (root) {
+ // If the item type is typeContent, we assume we are in browser tab
+ // content. Note, this includes content such as about:addons,
+ // for consistency.
+ if (root->ItemType() == nsIDocShellTreeItem::typeContent) {
+ return Relation(nsAccUtils::GetDocAccessibleFor(root));
+ }
+ }
+ }
+ return Relation();
+ }
+
+ case RelationType::CONTAINING_APPLICATION:
+ return Relation(ApplicationAcc());
+
+ case RelationType::DETAILS: {
+ if (mContent->IsElement() &&
+ mContent->AsElement()->HasAttr(nsGkAtoms::aria_details)) {
+ return Relation(
+ new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_details));
+ }
+ if (LocalAccessible* target = GetPopoverTargetDetailsRelation()) {
+ return Relation(target);
+ }
+ return Relation();
+ }
+
+ case RelationType::DETAILS_FOR: {
+ Relation rel(
+ new RelatedAccIterator(mDoc, mContent, nsGkAtoms::aria_details));
+ RelatedAccIterator invokers(mDoc, mContent, nsGkAtoms::popovertarget);
+ while (Accessible* invoker = invokers.Next()) {
+ // We should only expose DETAILS_FOR if DETAILS was exposed on the
+ // invoker. However, DETAILS exposure on popover invokers is
+ // conditional.
+ LocalAccessible* popoverTarget =
+ invoker->AsLocal()->GetPopoverTargetDetailsRelation();
+ if (popoverTarget) {
+ MOZ_ASSERT(popoverTarget == this);
+ rel.AppendTarget(invoker);
+ }
+ }
+ return rel;
+ }
+
+ case RelationType::ERRORMSG:
+ return Relation(
+ new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_errormessage));
+
+ case RelationType::ERRORMSG_FOR:
+ return Relation(
+ new RelatedAccIterator(mDoc, mContent, nsGkAtoms::aria_errormessage));
+
+ default:
+ return Relation();
+ }
+}
+
+void LocalAccessible::GetNativeInterface(void** aNativeAccessible) {}
+
+void LocalAccessible::DoCommand(nsIContent* aContent,
+ uint32_t aActionIndex) const {
+ class Runnable final : public mozilla::Runnable {
+ public:
+ Runnable(const LocalAccessible* aAcc, nsIContent* aContent, uint32_t aIdx)
+ : mozilla::Runnable("Runnable"),
+ mAcc(aAcc),
+ mContent(aContent),
+ mIdx(aIdx) {}
+
+ // XXX Cannot mark as MOZ_CAN_RUN_SCRIPT because the base class change
+ // requires too big changes across a lot of modules.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
+ if (mAcc) {
+ MOZ_KnownLive(mAcc)->DispatchClickEvent(MOZ_KnownLive(mContent), mIdx);
+ }
+ return NS_OK;
+ }
+
+ void Revoke() {
+ mAcc = nullptr;
+ mContent = nullptr;
+ }
+
+ private:
+ RefPtr<const LocalAccessible> mAcc;
+ nsCOMPtr<nsIContent> mContent;
+ uint32_t mIdx;
+ };
+
+ nsIContent* content = aContent ? aContent : mContent.get();
+ nsCOMPtr<nsIRunnable> runnable = new Runnable(this, content, aActionIndex);
+ NS_DispatchToMainThread(runnable);
+}
+
+void LocalAccessible::DispatchClickEvent(nsIContent* aContent,
+ uint32_t aActionIndex) const {
+ if (IsDefunct()) return;
+
+ RefPtr<PresShell> presShell = mDoc->PresShellPtr();
+
+ // Scroll into view.
+ presShell->ScrollContentIntoView(aContent, ScrollAxis(), ScrollAxis(),
+ ScrollFlags::ScrollOverflowHidden);
+
+ AutoWeakFrame frame = aContent->GetPrimaryFrame();
+ if (!frame) return;
+
+ // Compute x and y coordinates.
+ nsPoint point;
+ nsCOMPtr<nsIWidget> widget = frame->GetNearestWidget(point);
+ if (!widget) return;
+
+ nsSize size = frame->GetSize();
+
+ RefPtr<nsPresContext> presContext = presShell->GetPresContext();
+ int32_t x = presContext->AppUnitsToDevPixels(point.x + size.width / 2);
+ int32_t y = presContext->AppUnitsToDevPixels(point.y + size.height / 2);
+
+ // Simulate a touch interaction by dispatching touch events with mouse events.
+ nsCoreUtils::DispatchTouchEvent(eTouchStart, x, y, aContent, frame, presShell,
+ widget);
+ nsCoreUtils::DispatchMouseEvent(eMouseDown, x, y, aContent, frame, presShell,
+ widget);
+ nsCoreUtils::DispatchTouchEvent(eTouchEnd, x, y, aContent, frame, presShell,
+ widget);
+ nsCoreUtils::DispatchMouseEvent(eMouseUp, x, y, aContent, frame, presShell,
+ widget);
+}
+
+void LocalAccessible::ScrollToPoint(uint32_t aCoordinateType, int32_t aX,
+ int32_t aY) {
+ nsIFrame* frame = GetFrame();
+ if (!frame) return;
+
+ LayoutDeviceIntPoint coords =
+ nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this);
+
+ nsIFrame* parentFrame = frame;
+ while ((parentFrame = parentFrame->GetParent())) {
+ nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords);
+ }
+}
+
+void LocalAccessible::AppendTextTo(nsAString& aText, uint32_t aStartOffset,
+ uint32_t aLength) {
+ // Return text representation of non-text accessible within hypertext
+ // accessible. Text accessible overrides this method to return enclosed text.
+ if (aStartOffset != 0 || aLength == 0) return;
+
+ MOZ_ASSERT(mParent,
+ "Called on accessible unbound from tree. Result can be wrong.");
+ nsIFrame* frame = GetFrame();
+ // We handle something becoming display: none async, which means we won't have
+ // a frame when we're queuing text removed events. Thus, it's important that
+ // we produce text here even if there's no frame. Otherwise, we won't fire a
+ // text removed event at all, which might leave client caches (e.g. NVDA
+ // virtual buffers) with dead nodes.
+ if (IsHTMLBr() || (frame && frame->IsBrFrame())) {
+ aText += kForcedNewLineChar;
+ } else if (mParent && nsAccUtils::MustPrune(mParent)) {
+ // Expose the embedded object accessible as imaginary embedded object
+ // character if its parent hypertext accessible doesn't expose children to
+ // AT.
+ aText += kImaginaryEmbeddedObjectChar;
+ } else {
+ aText += kEmbeddedObjectChar;
+ }
+}
+
+void LocalAccessible::Shutdown() {
+ // Mark the accessible as defunct, invalidate the child count and pointers to
+ // other accessibles, also make sure none of its children point to this
+ // parent
+ mStateFlags |= eIsDefunct;
+
+ int32_t childCount = mChildren.Length();
+ for (int32_t childIdx = 0; childIdx < childCount; childIdx++) {
+ mChildren.ElementAt(childIdx)->UnbindFromParent();
+ }
+ mChildren.Clear();
+
+ mEmbeddedObjCollector = nullptr;
+
+ if (mParent) mParent->RemoveChild(this);
+
+ mContent = nullptr;
+ mDoc = nullptr;
+ if (SelectionMgr() && SelectionMgr()->AccessibleWithCaret(nullptr) == this) {
+ SelectionMgr()->ResetCaretOffset();
+ }
+}
+
+// LocalAccessible protected
+void LocalAccessible::ARIAName(nsString& aName) const {
+ // aria-labelledby now takes precedence over aria-label
+ nsresult rv = nsTextEquivUtils::GetTextEquivFromIDRefs(
+ this, nsGkAtoms::aria_labelledby, aName);
+ if (NS_SUCCEEDED(rv)) {
+ aName.CompressWhitespace();
+ }
+
+ if (aName.IsEmpty() && mContent->IsElement() &&
+ nsAccUtils::GetARIAAttr(mContent->AsElement(), nsGkAtoms::aria_label,
+ aName)) {
+ aName.CompressWhitespace();
+ }
+}
+
+// LocalAccessible protected
+void LocalAccessible::ARIADescription(nsString& aDescription) const {
+ // aria-describedby takes precedence over aria-description
+ nsresult rv = nsTextEquivUtils::GetTextEquivFromIDRefs(
+ this, nsGkAtoms::aria_describedby, aDescription);
+ if (NS_SUCCEEDED(rv)) {
+ aDescription.CompressWhitespace();
+ }
+
+ if (aDescription.IsEmpty() && mContent->IsElement() &&
+ nsAccUtils::GetARIAAttr(mContent->AsElement(),
+ nsGkAtoms::aria_description, aDescription)) {
+ aDescription.CompressWhitespace();
+ }
+}
+
+// LocalAccessible protected
+ENameValueFlag LocalAccessible::NativeName(nsString& aName) const {
+ if (mContent->IsHTMLElement()) {
+ LocalAccessible* label = nullptr;
+ HTMLLabelIterator iter(Document(), this);
+ while ((label = iter.Next())) {
+ nsTextEquivUtils::AppendTextEquivFromContent(this, label->GetContent(),
+ &aName);
+ aName.CompressWhitespace();
+ }
+
+ if (!aName.IsEmpty()) return eNameOK;
+
+ NameFromAssociatedXULLabel(mDoc, mContent, aName);
+ if (!aName.IsEmpty()) {
+ return eNameOK;
+ }
+
+ nsTextEquivUtils::GetNameFromSubtree(this, aName);
+ return aName.IsEmpty() ? eNameOK : eNameFromSubtree;
+ }
+
+ if (mContent->IsXULElement()) {
+ XULElmName(mDoc, mContent, aName);
+ if (!aName.IsEmpty()) return eNameOK;
+
+ nsTextEquivUtils::GetNameFromSubtree(this, aName);
+ return aName.IsEmpty() ? eNameOK : eNameFromSubtree;
+ }
+
+ if (mContent->IsSVGElement()) {
+ // If user agents need to choose among multiple 'desc' or 'title'
+ // elements for processing, the user agent shall choose the first one.
+ for (nsIContent* childElm = mContent->GetFirstChild(); childElm;
+ childElm = childElm->GetNextSibling()) {
+ if (childElm->IsSVGElement(nsGkAtoms::title)) {
+ nsTextEquivUtils::AppendTextEquivFromContent(this, childElm, &aName);
+ return eNameOK;
+ }
+ }
+ }
+
+ return eNameOK;
+}
+
+// LocalAccessible protected
+void LocalAccessible::NativeDescription(nsString& aDescription) const {
+ bool isXUL = mContent->IsXULElement();
+ if (isXUL) {
+ // Try XUL <description control="[id]">description text</description>
+ XULDescriptionIterator iter(Document(), mContent);
+ LocalAccessible* descr = nullptr;
+ while ((descr = iter.Next())) {
+ nsTextEquivUtils::AppendTextEquivFromContent(this, descr->GetContent(),
+ &aDescription);
+ }
+ }
+}
+
+// LocalAccessible protected
+void LocalAccessible::BindToParent(LocalAccessible* aParent,
+ uint32_t aIndexInParent) {
+ MOZ_ASSERT(aParent, "This method isn't used to set null parent");
+ MOZ_ASSERT(!mParent, "The child was expected to be moved");
+
+#ifdef A11Y_LOG
+ if (mParent) {
+ logging::TreeInfo("BindToParent: stealing accessible", 0, "old parent",
+ mParent, "new parent", aParent, "child", this, nullptr);
+ }
+#endif
+
+ mParent = aParent;
+ mIndexInParent = aIndexInParent;
+
+ if (mParent->HasNameDependent() || mParent->IsXULListItem() ||
+ RelationByType(RelationType::LABEL_FOR).Next() ||
+ nsTextEquivUtils::HasNameRule(mParent, eNameFromSubtreeRule)) {
+ mContextFlags |= eHasNameDependent;
+ } else {
+ mContextFlags &= ~eHasNameDependent;
+ }
+ if (mParent->HasDescriptionDependent() ||
+ RelationByType(RelationType::DESCRIPTION_FOR).Next()) {
+ mContextFlags |= eHasDescriptionDependent;
+ } else {
+ mContextFlags &= ~eHasDescriptionDependent;
+ }
+
+ // Add name/description dependent flags for dependent content once
+ // a name/description provider is added to doc.
+ Relation rel = RelationByType(RelationType::LABELLED_BY);
+ LocalAccessible* relTarget = nullptr;
+ while ((relTarget = rel.LocalNext())) {
+ if (!relTarget->HasNameDependent()) {
+ relTarget->ModifySubtreeContextFlags(eHasNameDependent, true);
+ }
+ }
+
+ rel = RelationByType(RelationType::DESCRIBED_BY);
+ while ((relTarget = rel.LocalNext())) {
+ if (!relTarget->HasDescriptionDependent()) {
+ relTarget->ModifySubtreeContextFlags(eHasDescriptionDependent, true);
+ }
+ }
+
+ mContextFlags |=
+ static_cast<uint32_t>((mParent->IsAlert() || mParent->IsInsideAlert())) &
+ eInsideAlert;
+
+ if (IsTableCell()) {
+ CachedTableAccessible::Invalidate(this);
+ }
+}
+
+// LocalAccessible protected
+void LocalAccessible::UnbindFromParent() {
+ // We do this here to handle document shutdown and an Accessible being moved.
+ // We do this for subtree removal in DocAccessible::UncacheChildrenInSubtree.
+ if (IsTable() || IsTableCell()) {
+ CachedTableAccessible::Invalidate(this);
+ }
+
+ mParent = nullptr;
+ mIndexInParent = -1;
+ mIndexOfEmbeddedChild = -1;
+
+ delete mGroupInfo;
+ mGroupInfo = nullptr;
+ mContextFlags &= ~eHasNameDependent & ~eInsideAlert;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible public methods
+
+RootAccessible* LocalAccessible::RootAccessible() const {
+ nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(GetNode());
+ NS_ASSERTION(docShell, "No docshell for mContent");
+ if (!docShell) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIDocShellTreeItem> root;
+ docShell->GetInProcessRootTreeItem(getter_AddRefs(root));
+ NS_ASSERTION(root, "No root content tree item");
+ if (!root) {
+ return nullptr;
+ }
+
+ DocAccessible* docAcc = nsAccUtils::GetDocAccessibleFor(root);
+ return docAcc ? docAcc->AsRoot() : nullptr;
+}
+
+nsIFrame* LocalAccessible::GetFrame() const {
+ return mContent ? mContent->GetPrimaryFrame() : nullptr;
+}
+
+nsINode* LocalAccessible::GetNode() const { return mContent; }
+
+dom::Element* LocalAccessible::Elm() const {
+ return dom::Element::FromNodeOrNull(mContent);
+}
+
+void LocalAccessible::Language(nsAString& aLanguage) {
+ aLanguage.Truncate();
+
+ if (!mDoc) return;
+
+ nsCoreUtils::GetLanguageFor(mContent, nullptr, aLanguage);
+ if (aLanguage.IsEmpty()) { // Nothing found, so use document's language
+ mDoc->DocumentNode()->GetHeaderData(nsGkAtoms::headerContentLanguage,
+ aLanguage);
+ }
+}
+
+bool LocalAccessible::InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) {
+ if (!aChild) return false;
+
+ if (aIndex == mChildren.Length()) {
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mChildren.AppendElement(aChild);
+ } else {
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mChildren.InsertElementAt(aIndex, aChild);
+
+ MOZ_ASSERT(mStateFlags & eKidsMutating, "Illicit children change");
+
+ for (uint32_t idx = aIndex + 1; idx < mChildren.Length(); idx++) {
+ mChildren[idx]->mIndexInParent = idx;
+ }
+ }
+
+ if (aChild->IsText()) {
+ mStateFlags |= eHasTextKids;
+ }
+
+ aChild->BindToParent(this, aIndex);
+ return true;
+}
+
+bool LocalAccessible::RemoveChild(LocalAccessible* aChild) {
+ MOZ_DIAGNOSTIC_ASSERT(aChild, "No child was given");
+ MOZ_DIAGNOSTIC_ASSERT(aChild->mParent, "No parent");
+ MOZ_DIAGNOSTIC_ASSERT(aChild->mParent == this, "Wrong parent");
+ MOZ_DIAGNOSTIC_ASSERT(aChild->mIndexInParent != -1,
+ "Unbound child was given");
+ MOZ_DIAGNOSTIC_ASSERT((mStateFlags & eKidsMutating) || aChild->IsDefunct() ||
+ aChild->IsDoc() || IsApplication(),
+ "Illicit children change");
+
+ int32_t index = static_cast<uint32_t>(aChild->mIndexInParent);
+ if (mChildren.SafeElementAt(index) != aChild) {
+ MOZ_ASSERT_UNREACHABLE("A wrong child index");
+ index = mChildren.IndexOf(aChild);
+ if (index == -1) {
+ MOZ_ASSERT_UNREACHABLE("No child was found");
+ return false;
+ }
+ }
+
+ aChild->UnbindFromParent();
+ mChildren.RemoveElementAt(index);
+
+ for (uint32_t idx = index; idx < mChildren.Length(); idx++) {
+ mChildren[idx]->mIndexInParent = idx;
+ }
+
+ return true;
+}
+
+void LocalAccessible::RelocateChild(uint32_t aNewIndex,
+ LocalAccessible* aChild) {
+ MOZ_DIAGNOSTIC_ASSERT(aChild, "No child was given");
+ MOZ_DIAGNOSTIC_ASSERT(aChild->mParent == this,
+ "A child from different subtree was given");
+ MOZ_DIAGNOSTIC_ASSERT(aChild->mIndexInParent != -1,
+ "Unbound child was given");
+ MOZ_DIAGNOSTIC_ASSERT(
+ aChild->mParent->LocalChildAt(aChild->mIndexInParent) == aChild,
+ "Wrong index in parent");
+ MOZ_DIAGNOSTIC_ASSERT(
+ static_cast<uint32_t>(aChild->mIndexInParent) != aNewIndex,
+ "No move, same index");
+ MOZ_DIAGNOSTIC_ASSERT(aNewIndex <= mChildren.Length(),
+ "Wrong new index was given");
+
+ RefPtr<AccHideEvent> hideEvent = new AccHideEvent(aChild, false);
+ if (mDoc->Controller()->QueueMutationEvent(hideEvent)) {
+ aChild->SetHideEventTarget(true);
+ }
+
+ mEmbeddedObjCollector = nullptr;
+ mChildren.RemoveElementAt(aChild->mIndexInParent);
+
+ uint32_t startIdx = aNewIndex, endIdx = aChild->mIndexInParent;
+
+ // If the child is moved after its current position.
+ if (static_cast<uint32_t>(aChild->mIndexInParent) < aNewIndex) {
+ startIdx = aChild->mIndexInParent;
+ if (aNewIndex == mChildren.Length() + 1) {
+ // The child is moved to the end.
+ mChildren.AppendElement(aChild);
+ endIdx = mChildren.Length() - 1;
+ } else {
+ mChildren.InsertElementAt(aNewIndex - 1, aChild);
+ endIdx = aNewIndex;
+ }
+ } else {
+ // The child is moved prior its current position.
+ mChildren.InsertElementAt(aNewIndex, aChild);
+ }
+
+ for (uint32_t idx = startIdx; idx <= endIdx; idx++) {
+ mChildren[idx]->mIndexInParent = idx;
+ mChildren[idx]->mIndexOfEmbeddedChild = -1;
+ }
+
+ for (uint32_t idx = 0; idx < mChildren.Length(); idx++) {
+ mChildren[idx]->mStateFlags |= eGroupInfoDirty;
+ }
+
+ RefPtr<AccShowEvent> showEvent = new AccShowEvent(aChild);
+ DebugOnly<bool> added = mDoc->Controller()->QueueMutationEvent(showEvent);
+ MOZ_ASSERT(added);
+ aChild->SetShowEventTarget(true);
+}
+
+LocalAccessible* LocalAccessible::LocalChildAt(uint32_t aIndex) const {
+ LocalAccessible* child = mChildren.SafeElementAt(aIndex, nullptr);
+ if (!child) return nullptr;
+
+#ifdef DEBUG
+ LocalAccessible* realParent = child->mParent;
+ NS_ASSERTION(!realParent || realParent == this,
+ "Two accessibles have the same first child accessible!");
+#endif
+
+ return child;
+}
+
+uint32_t LocalAccessible::ChildCount() const { return mChildren.Length(); }
+
+int32_t LocalAccessible::IndexInParent() const { return mIndexInParent; }
+
+uint32_t LocalAccessible::EmbeddedChildCount() {
+ if (mStateFlags & eHasTextKids) {
+ if (!mEmbeddedObjCollector) {
+ mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this));
+ }
+ return mEmbeddedObjCollector->Count();
+ }
+
+ return ChildCount();
+}
+
+Accessible* LocalAccessible::EmbeddedChildAt(uint32_t aIndex) {
+ if (mStateFlags & eHasTextKids) {
+ if (!mEmbeddedObjCollector) {
+ mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this));
+ }
+ return mEmbeddedObjCollector.get()
+ ? mEmbeddedObjCollector->GetAccessibleAt(aIndex)
+ : nullptr;
+ }
+
+ return ChildAt(aIndex);
+}
+
+int32_t LocalAccessible::IndexOfEmbeddedChild(Accessible* aChild) {
+ MOZ_ASSERT(aChild->IsLocal());
+ if (mStateFlags & eHasTextKids) {
+ if (!mEmbeddedObjCollector) {
+ mEmbeddedObjCollector.reset(new EmbeddedObjCollector(this));
+ }
+ return mEmbeddedObjCollector.get()
+ ? mEmbeddedObjCollector->GetIndexAt(aChild->AsLocal())
+ : -1;
+ }
+
+ return GetIndexOf(aChild->AsLocal());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HyperLinkAccessible methods
+
+bool LocalAccessible::IsLink() const {
+ // Every embedded accessible within hypertext accessible implements
+ // hyperlink interface.
+ return mParent && mParent->IsHyperText() && !IsText();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// SelectAccessible
+
+void LocalAccessible::SelectedItems(nsTArray<Accessible*>* aItems) {
+ AccIterator iter(this, filters::GetSelected);
+ LocalAccessible* selected = nullptr;
+ while ((selected = iter.Next())) aItems->AppendElement(selected);
+}
+
+uint32_t LocalAccessible::SelectedItemCount() {
+ uint32_t count = 0;
+ AccIterator iter(this, filters::GetSelected);
+ LocalAccessible* selected = nullptr;
+ while ((selected = iter.Next())) ++count;
+
+ return count;
+}
+
+Accessible* LocalAccessible::GetSelectedItem(uint32_t aIndex) {
+ AccIterator iter(this, filters::GetSelected);
+ LocalAccessible* selected = nullptr;
+
+ uint32_t index = 0;
+ while ((selected = iter.Next()) && index < aIndex) index++;
+
+ return selected;
+}
+
+bool LocalAccessible::IsItemSelected(uint32_t aIndex) {
+ uint32_t index = 0;
+ AccIterator iter(this, filters::GetSelectable);
+ LocalAccessible* selected = nullptr;
+ while ((selected = iter.Next()) && index < aIndex) index++;
+
+ return selected && selected->State() & states::SELECTED;
+}
+
+bool LocalAccessible::AddItemToSelection(uint32_t aIndex) {
+ uint32_t index = 0;
+ AccIterator iter(this, filters::GetSelectable);
+ LocalAccessible* selected = nullptr;
+ while ((selected = iter.Next()) && index < aIndex) index++;
+
+ if (selected) selected->SetSelected(true);
+
+ return static_cast<bool>(selected);
+}
+
+bool LocalAccessible::RemoveItemFromSelection(uint32_t aIndex) {
+ uint32_t index = 0;
+ AccIterator iter(this, filters::GetSelectable);
+ LocalAccessible* selected = nullptr;
+ while ((selected = iter.Next()) && index < aIndex) index++;
+
+ if (selected) selected->SetSelected(false);
+
+ return static_cast<bool>(selected);
+}
+
+bool LocalAccessible::SelectAll() {
+ bool success = false;
+ LocalAccessible* selectable = nullptr;
+
+ AccIterator iter(this, filters::GetSelectable);
+ while ((selectable = iter.Next())) {
+ success = true;
+ selectable->SetSelected(true);
+ }
+ return success;
+}
+
+bool LocalAccessible::UnselectAll() {
+ bool success = false;
+ LocalAccessible* selected = nullptr;
+
+ AccIterator iter(this, filters::GetSelected);
+ while ((selected = iter.Next())) {
+ success = true;
+ selected->SetSelected(false);
+ }
+ return success;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Widgets
+
+bool LocalAccessible::IsWidget() const { return false; }
+
+bool LocalAccessible::IsActiveWidget() const {
+ if (FocusMgr()->HasDOMFocus(mContent)) return true;
+
+ // If text entry of combobox widget has a focus then the combobox widget is
+ // active.
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::combobox)) {
+ uint32_t childCount = ChildCount();
+ for (uint32_t idx = 0; idx < childCount; idx++) {
+ LocalAccessible* child = mChildren.ElementAt(idx);
+ if (child->Role() == roles::ENTRY) {
+ return FocusMgr()->HasDOMFocus(child->GetContent());
+ }
+ }
+ }
+
+ return false;
+}
+
+bool LocalAccessible::AreItemsOperable() const {
+ return HasOwnContent() && mContent->IsElement() &&
+ mContent->AsElement()->HasAttr(nsGkAtoms::aria_activedescendant);
+}
+
+LocalAccessible* LocalAccessible::CurrentItem() const {
+ // Check for aria-activedescendant, which changes which element has focus.
+ // For activedescendant, the ARIA spec does not require that the user agent
+ // checks whether pointed node is actually a DOM descendant of the element
+ // with the aria-activedescendant attribute.
+ nsAutoString id;
+ if (HasOwnContent() && mContent->IsElement() &&
+ mContent->AsElement()->GetAttr(nsGkAtoms::aria_activedescendant, id)) {
+ dom::Element* activeDescendantElm = IDRefsIterator::GetElem(mContent, id);
+ if (activeDescendantElm) {
+ if (mContent->IsInclusiveDescendantOf(activeDescendantElm)) {
+ // Don't want a cyclical descendant relationship. That would be bad.
+ return nullptr;
+ }
+
+ DocAccessible* document = Document();
+ if (document) return document->GetAccessible(activeDescendantElm);
+ }
+ }
+ return nullptr;
+}
+
+void LocalAccessible::SetCurrentItem(const LocalAccessible* aItem) {
+ nsAtom* id = aItem->GetContent()->GetID();
+ if (id) {
+ nsAutoString idStr;
+ id->ToString(idStr);
+ mContent->AsElement()->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::aria_activedescendant, idStr, true);
+ }
+}
+
+LocalAccessible* LocalAccessible::ContainerWidget() const {
+ if (HasARIARole() && mContent->HasID()) {
+ for (LocalAccessible* parent = LocalParent(); parent;
+ parent = parent->LocalParent()) {
+ nsIContent* parentContent = parent->GetContent();
+ if (parentContent && parentContent->IsElement() &&
+ parentContent->AsElement()->HasAttr(
+ nsGkAtoms::aria_activedescendant)) {
+ return parent;
+ }
+
+ // Don't cross DOM document boundaries.
+ if (parent->IsDoc()) break;
+ }
+ }
+ return nullptr;
+}
+
+bool LocalAccessible::IsActiveDescendant(LocalAccessible** aWidget) const {
+ if (!HasOwnContent() || !mContent->HasID()) {
+ return false;
+ }
+
+ dom::DocumentOrShadowRoot* docOrShadowRoot =
+ mContent->GetUncomposedDocOrConnectedShadowRoot();
+ if (!docOrShadowRoot) {
+ return false;
+ }
+
+ nsAutoCString selector;
+ selector.AppendPrintf(
+ "[aria-activedescendant=\"%s\"]",
+ NS_ConvertUTF16toUTF8(mContent->GetID()->GetUTF16String()).get());
+ IgnoredErrorResult er;
+
+ dom::Element* widgetElm =
+ docOrShadowRoot->AsNode().QuerySelector(selector, er);
+
+ if (!widgetElm || er.Failed()) {
+ return false;
+ }
+
+ if (widgetElm->IsInclusiveDescendantOf(mContent)) {
+ // Don't want a cyclical descendant relationship. That would be bad.
+ return false;
+ }
+
+ LocalAccessible* widget = mDoc->GetAccessible(widgetElm);
+
+ if (aWidget) {
+ *aWidget = widget;
+ }
+
+ return !!widget;
+}
+
+void LocalAccessible::Announce(const nsAString& aAnnouncement,
+ uint16_t aPriority) {
+ RefPtr<AccAnnouncementEvent> event =
+ new AccAnnouncementEvent(this, aAnnouncement, aPriority);
+ nsEventShell::FireEvent(event);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible protected methods
+
+void LocalAccessible::LastRelease() {
+ // First cleanup if needed...
+ if (mDoc) {
+ Shutdown();
+ NS_ASSERTION(!mDoc,
+ "A Shutdown() impl forgot to call its parent's Shutdown?");
+ }
+ // ... then die.
+ delete this;
+}
+
+LocalAccessible* LocalAccessible::GetSiblingAtOffset(int32_t aOffset,
+ nsresult* aError) const {
+ if (!mParent || mIndexInParent == -1) {
+ if (aError) *aError = NS_ERROR_UNEXPECTED;
+
+ return nullptr;
+ }
+
+ if (aError &&
+ mIndexInParent + aOffset >= static_cast<int32_t>(mParent->ChildCount())) {
+ *aError = NS_OK; // fail peacefully
+ return nullptr;
+ }
+
+ LocalAccessible* child = mParent->LocalChildAt(mIndexInParent + aOffset);
+ if (aError && !child) *aError = NS_ERROR_UNEXPECTED;
+
+ return child;
+}
+
+void LocalAccessible::ModifySubtreeContextFlags(uint32_t aContextFlags,
+ bool aAdd) {
+ Pivot pivot(this);
+ LocalAccInSameDocRule rule;
+ for (Accessible* anchor = this; anchor; anchor = pivot.Next(anchor, rule)) {
+ MOZ_ASSERT(anchor->IsLocal());
+ LocalAccessible* acc = anchor->AsLocal();
+ if (aAdd) {
+ acc->mContextFlags |= aContextFlags;
+ } else {
+ acc->mContextFlags &= ~aContextFlags;
+ }
+ }
+}
+
+double LocalAccessible::AttrNumericValue(nsAtom* aAttr) const {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (!roleMapEntry || roleMapEntry->valueRule == eNoValue) {
+ return UnspecifiedNaN<double>();
+ }
+
+ nsAutoString attrValue;
+ if (!mContent->IsElement() ||
+ !nsAccUtils::GetARIAAttr(mContent->AsElement(), aAttr, attrValue)) {
+ return UnspecifiedNaN<double>();
+ }
+
+ nsresult error = NS_OK;
+ double value = attrValue.ToDouble(&error);
+ return NS_FAILED(error) ? UnspecifiedNaN<double>() : value;
+}
+
+uint32_t LocalAccessible::GetActionRule() const {
+ if (!HasOwnContent() || (InteractiveState() & states::UNAVAILABLE)) {
+ return eNoAction;
+ }
+
+ // Return "click" action on elements that have an attached popup menu.
+ if (mContent->IsXULElement()) {
+ if (mContent->AsElement()->HasAttr(nsGkAtoms::popup)) {
+ return eClickAction;
+ }
+ }
+
+ // Has registered 'click' event handler.
+ bool isOnclick = nsCoreUtils::HasClickListener(mContent);
+
+ if (isOnclick) return eClickAction;
+
+ // Get an action based on ARIA role.
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (roleMapEntry && roleMapEntry->actionRule != eNoAction) {
+ return roleMapEntry->actionRule;
+ }
+
+ // Get an action based on ARIA attribute.
+ if (nsAccUtils::HasDefinedARIAToken(mContent, nsGkAtoms::aria_expanded)) {
+ return eExpandAction;
+ }
+
+ return eNoAction;
+}
+
+AccGroupInfo* LocalAccessible::GetGroupInfo() const {
+ if (mGroupInfo && !(mStateFlags & eGroupInfoDirty)) {
+ return mGroupInfo;
+ }
+
+ return nullptr;
+}
+
+AccGroupInfo* LocalAccessible::GetOrCreateGroupInfo() {
+ if (mGroupInfo) {
+ if (mStateFlags & eGroupInfoDirty) {
+ mGroupInfo->Update();
+ mStateFlags &= ~eGroupInfoDirty;
+ }
+
+ return mGroupInfo;
+ }
+
+ mGroupInfo = AccGroupInfo::CreateGroupInfo(this);
+ mStateFlags &= ~eGroupInfoDirty;
+ return mGroupInfo;
+}
+
+void LocalAccessible::SendCache(uint64_t aCacheDomain,
+ CacheUpdateType aUpdateType) {
+ if (!IPCAccessibilityActive() || !Document()) {
+ return;
+ }
+
+ DocAccessibleChild* ipcDoc = mDoc->IPCDoc();
+ if (!ipcDoc) {
+ // This means DocAccessible::DoInitialUpdate hasn't been called yet, which
+ // means the a11y tree hasn't been built yet. Therefore, this should only
+ // be possible if this is a DocAccessible.
+ MOZ_ASSERT(IsDoc(), "Called on a non-DocAccessible but IPCDoc is null");
+ return;
+ }
+
+ RefPtr<AccAttributes> fields =
+ BundleFieldsForCache(aCacheDomain, aUpdateType);
+ if (!fields->Count()) {
+ return;
+ }
+ nsTArray<CacheData> data;
+ data.AppendElement(CacheData(ID(), fields));
+ ipcDoc->SendCache(aUpdateType, data);
+
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsAutoCString updateTypeStr;
+ if (aUpdateType == CacheUpdateType::Initial) {
+ updateTypeStr = "Initial";
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ updateTypeStr = "Update";
+ } else {
+ updateTypeStr = "Other";
+ }
+ PROFILER_MARKER_TEXT("LocalAccessible::SendCache", A11Y, {}, updateTypeStr);
+ }
+}
+
+already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
+ uint64_t aCacheDomain, CacheUpdateType aUpdateType) {
+ RefPtr<AccAttributes> fields = new AccAttributes();
+
+ // Caching name for text leaf Accessibles is redundant, since their name is
+ // always their text. Text gets handled below.
+ if (aCacheDomain & CacheDomain::NameAndDescription && !IsText()) {
+ nsString name;
+ int32_t nameFlag = Name(name);
+ if (nameFlag != eNameOK) {
+ fields->SetAttribute(CacheKey::NameValueFlag, nameFlag);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::NameValueFlag, DeleteEntry());
+ }
+
+ if (IsTextField()) {
+ MOZ_ASSERT(mContent);
+ nsString placeholder;
+ // Only cache the placeholder separately if it isn't used as the name.
+ if (Elm()->GetAttr(nsGkAtoms::placeholder, placeholder) &&
+ name != placeholder) {
+ fields->SetAttribute(CacheKey::HTMLPlaceholder, std::move(placeholder));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::HTMLPlaceholder, DeleteEntry());
+ }
+ }
+
+ if (!name.IsEmpty()) {
+ fields->SetAttribute(CacheKey::Name, std::move(name));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::Name, DeleteEntry());
+ }
+
+ nsString description;
+ Description(description);
+ if (!description.IsEmpty()) {
+ fields->SetAttribute(CacheKey::Description, std::move(description));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::Description, DeleteEntry());
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Value) {
+ // We cache the text value in 3 cases:
+ // 1. Accessible is an HTML input type that holds a number.
+ // 2. Accessible has a numeric value and an aria-valuetext.
+ // 3. Accessible is an HTML input type that holds text.
+ // 4. Accessible is a link, in which case value is the target URL.
+ // ... for all other cases we divine the value remotely.
+ bool cacheValueText = false;
+ if (HasNumericValue()) {
+ fields->SetAttribute(CacheKey::NumericValue, CurValue());
+ fields->SetAttribute(CacheKey::MaxValue, MaxValue());
+ fields->SetAttribute(CacheKey::MinValue, MinValue());
+ fields->SetAttribute(CacheKey::Step, Step());
+ cacheValueText = NativeHasNumericValue() ||
+ (mContent->IsElement() &&
+ nsAccUtils::HasARIAAttr(mContent->AsElement(),
+ nsGkAtoms::aria_valuetext));
+ } else {
+ cacheValueText = IsTextField() || IsHTMLLink();
+ }
+
+ if (cacheValueText) {
+ nsString value;
+ Value(value);
+ if (!value.IsEmpty()) {
+ fields->SetAttribute(CacheKey::TextValue, std::move(value));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::TextValue, DeleteEntry());
+ }
+ }
+
+ if (IsImage()) {
+ // Cache the src of images. This is used by some clients to help remediate
+ // inaccessible images.
+ MOZ_ASSERT(mContent, "Image must have mContent");
+ nsString src;
+ mContent->AsElement()->GetAttr(nsGkAtoms::src, src);
+ if (!src.IsEmpty()) {
+ fields->SetAttribute(CacheKey::SrcURL, std::move(src));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::SrcURL, DeleteEntry());
+ }
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Viewport && IsDoc()) {
+ // Construct the viewport cache for this document. This cache domain will
+ // only be requested after we finish painting.
+ DocAccessible* doc = AsDoc();
+ PresShell* presShell = doc->PresShellPtr();
+
+ if (nsIFrame* rootFrame = presShell->GetRootFrame()) {
+ nsTArray<nsIFrame*> frames;
+ nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable();
+ nsRect scrollPort = sf ? sf->GetScrollPortRect() : rootFrame->GetRect();
+
+ nsLayoutUtils::GetFramesForArea(
+ RelativeTo{rootFrame}, scrollPort, frames,
+ {{// We don't add the ::OnlyVisible option here, because
+ // it means we always consider frames with pointer-events: none.
+ // See usage of HitTestIsForVisibility in nsDisplayList::HitTest.
+ // This flag ensures the display lists are built, even if
+ // the page hasn't finished loading.
+ nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression,
+ // Each doc should have its own viewport cache, so we can
+ // ignore cross-doc content as an optimization.
+ nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc}});
+
+ nsTHashSet<LocalAccessible*> inViewAccs;
+ nsTArray<uint64_t> viewportCache(frames.Length());
+ // Layout considers table rows fully occluded by their containing cells.
+ // This means they don't have their own display list items, and they won't
+ // show up in the list returned from GetFramesForArea. To prevent table
+ // rows from appearing offscreen, we manually add any rows for which we
+ // have on-screen cells.
+ LocalAccessible* prevParentRow = nullptr;
+ for (nsIFrame* frame : frames) {
+ nsIContent* content = frame->GetContent();
+ if (!content) {
+ continue;
+ }
+
+ LocalAccessible* acc = doc->GetAccessible(content);
+ // The document should always be present at the end of the list, so
+ // including it is unnecessary and wasteful. We skip the document here
+ // and handle it as a fallback when hit testing.
+ if (!acc || acc == mDoc) {
+ continue;
+ }
+
+ if (acc->IsTextLeaf() && nsAccUtils::MustPrune(acc->LocalParent())) {
+ acc = acc->LocalParent();
+ }
+ if (acc->IsTableCell()) {
+ LocalAccessible* parent = acc->LocalParent();
+ if (parent && parent->IsTableRow() && parent != prevParentRow) {
+ // If we've entered a new row since the last cell we saw, add the
+ // previous parent row to our viewport cache here to maintain
+ // hittesting order. Keep track of the current parent row.
+ if (prevParentRow && inViewAccs.EnsureInserted(prevParentRow)) {
+ viewportCache.AppendElement(prevParentRow->ID());
+ }
+ prevParentRow = parent;
+ }
+ } else if (acc->IsTable()) {
+ // If we've encountered a table, we know we've already
+ // handled all of this table's content (because we're traversing
+ // in hittesting order). Add our table's final row to the viewport
+ // cache before adding the table itself. Reset our marker for the next
+ // table.
+ if (prevParentRow && inViewAccs.EnsureInserted(prevParentRow)) {
+ viewportCache.AppendElement(prevParentRow->ID());
+ }
+ prevParentRow = nullptr;
+ } else if (acc->IsImageMap()) {
+ // Layout doesn't walk image maps, so we do that
+ // manually here. We do this before adding the map itself
+ // so the children come earlier in the hittesting order.
+ for (uint32_t i = 0; i < acc->ChildCount(); i++) {
+ LocalAccessible* child = acc->LocalChildAt(i);
+ MOZ_ASSERT(child);
+ if (inViewAccs.EnsureInserted(child)) {
+ MOZ_ASSERT(!child->IsDoc());
+ viewportCache.AppendElement(child->ID());
+ }
+ }
+ } else if (acc->IsHTMLCombobox()) {
+ // Layout doesn't consider combobox lists (or their
+ // currently selected items) to be onscreen, but we do.
+ // Add those things manually here.
+ HTMLComboboxAccessible* combobox =
+ static_cast<HTMLComboboxAccessible*>(acc);
+ HTMLComboboxListAccessible* list = combobox->List();
+ LocalAccessible* currItem = combobox->SelectedOption();
+ // Preserve hittesting order by adding the item, then
+ // the list, and finally the combobox itself.
+ if (currItem && inViewAccs.EnsureInserted(currItem)) {
+ viewportCache.AppendElement(currItem->ID());
+ }
+ if (list && inViewAccs.EnsureInserted(list)) {
+ viewportCache.AppendElement(list->ID());
+ }
+ }
+
+ if (inViewAccs.EnsureInserted(acc)) {
+ MOZ_ASSERT(!acc->IsDoc());
+ viewportCache.AppendElement(acc->ID());
+ }
+ }
+
+ if (viewportCache.Length()) {
+ fields->SetAttribute(CacheKey::Viewport, std::move(viewportCache));
+ }
+ }
+ }
+
+ bool boundsChanged = false;
+ nsIFrame* frame = GetFrame();
+ if (aCacheDomain & CacheDomain::Bounds) {
+ nsRect newBoundsRect = ParentRelativeBounds();
+
+ // 1. Layout might notify us of a possible bounds change when the bounds
+ // haven't really changed. Therefore, we cache the last bounds we sent
+ // and don't send an update if they haven't changed.
+ // 2. For an initial cache push, we ignore 1) and always send the bounds.
+ // This handles the case where this LocalAccessible was moved (but not
+ // re-created). In that case, we will have cached bounds, but we currently
+ // do an initial cache push.
+ MOZ_ASSERT(aUpdateType == CacheUpdateType::Initial || mBounds.isSome(),
+ "Incremental cache push but mBounds is not set!");
+
+ if (OuterDocAccessible* doc = AsOuterDoc()) {
+ if (nsIFrame* docFrame = doc->GetFrame()) {
+ const nsMargin& newOffset = docFrame->GetUsedBorderAndPadding();
+ Maybe<nsMargin> currOffset = doc->GetCrossDocOffset();
+ if (!currOffset || *currOffset != newOffset) {
+ // OOP iframe docs can't compute their position within their
+ // cross-proc parent, so we have to manually cache that offset
+ // on the parent (outer doc) itself. For simplicity and consistency,
+ // we do this here for both OOP and in-process iframes. For in-process
+ // iframes, this also avoids the need to push a cache update for the
+ // embedded document when the iframe changes its padding, gets
+ // re-created, etc. Similar to bounds, we maintain a local cache and a
+ // remote cache to avoid sending redundant updates.
+ doc->SetCrossDocOffset(newOffset);
+ nsTArray<int32_t> offsetArray(2);
+ offsetArray.AppendElement(newOffset.Side(eSideLeft)); // X offset
+ offsetArray.AppendElement(newOffset.Side(eSideTop)); // Y offset
+ fields->SetAttribute(CacheKey::CrossDocOffset,
+ std::move(offsetArray));
+ }
+ }
+ }
+
+ boundsChanged = aUpdateType == CacheUpdateType::Initial ||
+ !newBoundsRect.IsEqualEdges(mBounds.value());
+ if (boundsChanged) {
+ mBounds = Some(newBoundsRect);
+
+ nsTArray<int32_t> boundsArray(4);
+
+ boundsArray.AppendElement(newBoundsRect.x);
+ boundsArray.AppendElement(newBoundsRect.y);
+ boundsArray.AppendElement(newBoundsRect.width);
+ boundsArray.AppendElement(newBoundsRect.height);
+
+ fields->SetAttribute(CacheKey::ParentRelativeBounds,
+ std::move(boundsArray));
+ }
+
+ if (frame && frame->ScrollableOverflowRect().IsEmpty()) {
+ fields->SetAttribute(CacheKey::IsClipped, true);
+ } else if (aUpdateType != CacheUpdateType::Initial) {
+ fields->SetAttribute(CacheKey::IsClipped, DeleteEntry());
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Text) {
+ if (!HasChildren()) {
+ // We only cache text and line offsets on leaf Accessibles.
+ // Only text Accessibles can have actual text.
+ if (IsText()) {
+ nsString text;
+ AppendTextTo(text);
+ fields->SetAttribute(CacheKey::Text, std::move(text));
+ TextLeafPoint point(this, 0);
+ RefPtr<AccAttributes> attrs = point.GetTextAttributesLocalAcc(
+ /* aIncludeDefaults */ false);
+ fields->SetAttribute(CacheKey::TextAttributes, std::move(attrs));
+ }
+ }
+ if (HyperTextAccessible* ht = AsHyperText()) {
+ RefPtr<AccAttributes> attrs = ht->DefaultTextAttributes();
+ fields->SetAttribute(CacheKey::TextAttributes, std::move(attrs));
+ }
+ }
+
+ // If text changes, we must also update spelling errors.
+ if (aCacheDomain & (CacheDomain::Spelling | CacheDomain::Text) &&
+ IsTextLeaf()) {
+ auto spellingErrors = TextLeafPoint::GetSpellingErrorOffsets(this);
+ if (!spellingErrors.IsEmpty()) {
+ fields->SetAttribute(CacheKey::SpellingErrors, std::move(spellingErrors));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::SpellingErrors, DeleteEntry());
+ }
+ }
+
+ if (aCacheDomain & (CacheDomain::Text | CacheDomain::Bounds) &&
+ !HasChildren()) {
+ // We cache line start offsets for both text and non-text leaf Accessibles
+ // because non-text leaf Accessibles can still start a line.
+ TextLeafPoint lineStart =
+ TextLeafPoint(this, 0).FindNextLineStartSameLocalAcc(
+ /* aIncludeOrigin */ true);
+ int32_t lineStartOffset = lineStart ? lineStart.mOffset : -1;
+ // We push line starts and text bounds in two cases:
+ // 1. Text or bounds changed, which means it's very likely that line starts
+ // and text bounds changed too.
+ // 2. CacheDomain::Bounds was requested (indicating that the frame was
+ // reflowed) but the bounds didn't actually change. This can happen when
+ // the spanned text is non-rectangular. For example, an Accessible might
+ // cover two characters on one line and a single character on another line.
+ // An insertion in a previous text node might cause it to shift such that it
+ // now covers a single character on the first line and two characters on the
+ // second line. Its bounding rect will be the same both before and after the
+ // insertion. In this case, we use the first line start to determine whether
+ // there was a change. This should be safe because the text didn't change in
+ // this Accessible, so if the first line start doesn't shift, none of them
+ // should shift.
+ if (aCacheDomain & CacheDomain::Text || boundsChanged ||
+ mFirstLineStart != lineStartOffset) {
+ mFirstLineStart = lineStartOffset;
+ nsTArray<int32_t> lineStarts;
+ for (; lineStart;
+ lineStart = lineStart.FindNextLineStartSameLocalAcc(false)) {
+ lineStarts.AppendElement(lineStart.mOffset);
+ }
+ if (!lineStarts.IsEmpty()) {
+ fields->SetAttribute(CacheKey::TextLineStarts, std::move(lineStarts));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::TextLineStarts, DeleteEntry());
+ }
+
+ if (frame && frame->IsTextFrame()) {
+ if (nsTextFrame* currTextFrame = do_QueryFrame(frame)) {
+ nsTArray<int32_t> charData(nsAccUtils::TextLength(this) *
+ kNumbersInRect);
+ // Continuation offsets are calculated relative to the primary frame.
+ // However, the acc's bounds are calculated using
+ // GetAllInFlowRectsUnion. For wrapped text which starts part way
+ // through a line, this might mean the top left of the acc is
+ // different to the top left of the primary frame. This also happens
+ // when the primary frame is empty (e.g. a blank line at the start of
+ // pre-formatted text), since the union rect will exclude the origin
+ // in that case. Calculate the offset from the acc's rect to the
+ // primary frame's rect.
+ nsRect accOffset =
+ nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame);
+ while (currTextFrame) {
+ nsPoint contOffset = currTextFrame->GetOffsetTo(frame);
+ contOffset -= accOffset.TopLeft();
+ int32_t length = currTextFrame->GetContentLength();
+ nsTArray<nsRect> charBounds(length);
+ currTextFrame->GetCharacterRectsInRange(
+ currTextFrame->GetContentOffset(), length, charBounds);
+ for (nsRect& charRect : charBounds) {
+ if (charRect.width == 0 &&
+ !currTextFrame->StyleText()->WhiteSpaceIsSignificant()) {
+ // GetCharacterRectsInRange gives us one rect per content
+ // offset. However, TextLeafAccessibles use rendered offsets;
+ // e.g. they might exclude some content white space. If we get
+ // a 0 width rect and it's white space, skip this rect, since
+ // this character isn't in the rendered text. We do have
+ // a way to convert between content and rendered offsets, but
+ // doing this for every character is expensive.
+ const char16_t contentChar = mContent->GetText()->CharAt(
+ charData.Length() / kNumbersInRect);
+ if (contentChar == u' ' || contentChar == u'\t' ||
+ contentChar == u'\n') {
+ continue;
+ }
+ }
+ // We expect each char rect to be relative to the text leaf
+ // acc this text lives in. Unfortunately, GetCharacterRectsInRange
+ // returns rects relative to their continuation. Add the
+ // continuation's relative position here to make our final
+ // rect relative to the text leaf acc.
+ charRect.MoveBy(contOffset);
+ charData.AppendElement(charRect.x);
+ charData.AppendElement(charRect.y);
+ charData.AppendElement(charRect.width);
+ charData.AppendElement(charRect.height);
+ }
+ currTextFrame = currTextFrame->GetNextContinuation();
+ }
+ if (charData.Length()) {
+ fields->SetAttribute(CacheKey::TextBounds, std::move(charData));
+ }
+ }
+ }
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::TransformMatrix) {
+ bool transformed = false;
+ if (frame && frame->IsTransformed()) {
+ // This matrix is only valid when applied to CSSPixel points/rects
+ // in the coordinate space of `frame`.
+ gfx::Matrix4x4 mtx = nsDisplayTransform::GetResultingTransformMatrix(
+ frame, nsPoint(0, 0), AppUnitsPerCSSPixel(),
+ nsDisplayTransform::INCLUDE_PERSPECTIVE |
+ nsDisplayTransform::OFFSET_BY_ORIGIN);
+ // We might get back the identity matrix. This can happen if there is no
+ // actual transform. For example, if an element has
+ // will-change: transform, nsIFrame::IsTransformed will return true, but
+ // this doesn't necessarily mean there is a transform right now.
+ // Applying the identity matrix is effectively a no-op, so there's no
+ // point caching it.
+ transformed = !mtx.IsIdentity();
+ if (transformed) {
+ UniquePtr<gfx::Matrix4x4> ptr = MakeUnique<gfx::Matrix4x4>(mtx);
+ fields->SetAttribute(CacheKey::TransformMatrix, std::move(ptr));
+ }
+ }
+ if (!transformed && aUpdateType == CacheUpdateType::Update) {
+ // Otherwise, if we're bundling a transform update but this
+ // frame isn't transformed (or doesn't exist), we need
+ // to send a DeleteEntry() to remove any
+ // transform that was previously cached for this frame.
+ fields->SetAttribute(CacheKey::TransformMatrix, DeleteEntry());
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::ScrollPosition && frame) {
+ const auto [scrollPosition, scrollRange] = mDoc->ComputeScrollData(this);
+ if (scrollRange.width || scrollRange.height) {
+ // If the scroll range is 0 by 0, this acc is not scrollable. We
+ // can't simply check scrollPosition != 0, since it's valid for scrollable
+ // frames to have a (0, 0) position. We also can't check IsEmpty or
+ // ZeroArea because frames with only one scrollable dimension will return
+ // a height/width of zero for the non-scrollable dimension, yielding zero
+ // area even if the width/height for the scrollable dimension is nonzero.
+ // We also cache (0, 0) for accs with overflow:auto or overflow:scroll,
+ // even if the content is not currently large enough to be scrollable
+ // right now -- these accs have a non-zero scroll range.
+ nsTArray<int32_t> positionArr(2);
+ positionArr.AppendElement(scrollPosition.x);
+ positionArr.AppendElement(scrollPosition.y);
+ fields->SetAttribute(CacheKey::ScrollPosition, std::move(positionArr));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::ScrollPosition, DeleteEntry());
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::DOMNodeIDAndClass && mContent) {
+ nsAtom* id = mContent->GetID();
+ if (id) {
+ fields->SetAttribute(CacheKey::DOMNodeID, id);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::DOMNodeID, DeleteEntry());
+ }
+ if (auto* el = dom::Element::FromNodeOrNull(mContent)) {
+ nsAutoString className;
+ el->GetClassName(className);
+ if (!className.IsEmpty()) {
+ fields->SetAttribute(CacheKey::DOMNodeClass, std::move(className));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::DOMNodeClass, DeleteEntry());
+ }
+ }
+ }
+
+ // State is only included in the initial push. Thereafter, cached state is
+ // updated via events.
+ if (aCacheDomain & CacheDomain::State) {
+ if (aUpdateType == CacheUpdateType::Initial) {
+ // Most states are updated using state change events, so we only send
+ // these for the initial cache push.
+ uint64_t state = State();
+ // Exclude states which must be calculated by RemoteAccessible.
+ state &= ~kRemoteCalculatedStates;
+ fields->SetAttribute(CacheKey::State, state);
+ }
+ // If aria-selected isn't specified, there may be no SELECTED state.
+ // However, aria-selected can be implicit in some cases when an item is
+ // focused. We don't want to do this if aria-selected is explicitly
+ // set to "false", so we need to differentiate between false and unset.
+ if (auto ariaSelected = ARIASelected()) {
+ fields->SetAttribute(CacheKey::ARIASelected, *ariaSelected);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::ARIASelected, DeleteEntry()); // Unset.
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::GroupInfo && mContent) {
+ for (nsAtom* attr : {nsGkAtoms::aria_level, nsGkAtoms::aria_setsize,
+ nsGkAtoms::aria_posinset}) {
+ int32_t value = 0;
+ if (nsCoreUtils::GetUIntAttr(mContent, attr, &value)) {
+ fields->SetAttribute(attr, value);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(attr, DeleteEntry());
+ }
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Actions) {
+ if (HasPrimaryAction()) {
+ // Here we cache the primary action.
+ nsAutoString actionName;
+ ActionNameAt(0, actionName);
+ RefPtr<nsAtom> actionAtom = NS_Atomize(actionName);
+ fields->SetAttribute(CacheKey::PrimaryAction, actionAtom);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::PrimaryAction, DeleteEntry());
+ }
+
+ if (ImageAccessible* imgAcc = AsImage()) {
+ // Here we cache the showlongdesc action.
+ if (imgAcc->HasLongDesc()) {
+ fields->SetAttribute(CacheKey::HasLongdesc, true);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::HasLongdesc, DeleteEntry());
+ }
+ }
+
+ KeyBinding accessKey = AccessKey();
+ if (!accessKey.IsEmpty()) {
+ fields->SetAttribute(CacheKey::AccessKey, accessKey.Serialize());
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::AccessKey, DeleteEntry());
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Style) {
+ if (RefPtr<nsAtom> display = DisplayStyle()) {
+ fields->SetAttribute(CacheKey::CSSDisplay, display);
+ }
+
+ float opacity = Opacity();
+ if (opacity != 1.0f) {
+ fields->SetAttribute(CacheKey::Opacity, opacity);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::Opacity, DeleteEntry());
+ }
+
+ if (frame &&
+ frame->StyleDisplay()->mPosition == StylePositionProperty::Fixed &&
+ nsLayoutUtils::IsReallyFixedPos(frame)) {
+ fields->SetAttribute(CacheKey::CssPosition, nsGkAtoms::fixed);
+ } else if (aUpdateType != CacheUpdateType::Initial) {
+ fields->SetAttribute(CacheKey::CssPosition, DeleteEntry());
+ }
+
+ if (frame) {
+ nsAutoCString overflow;
+ frame->Style()->GetComputedPropertyValue(eCSSProperty_overflow, overflow);
+ RefPtr<nsAtom> overflowAtom = NS_Atomize(overflow);
+ if (overflowAtom == nsGkAtoms::hidden) {
+ fields->SetAttribute(CacheKey::CSSOverflow, nsGkAtoms::hidden);
+ } else if (aUpdateType != CacheUpdateType::Initial) {
+ fields->SetAttribute(CacheKey::CSSOverflow, DeleteEntry());
+ }
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Table) {
+ if (auto* table = HTMLTableAccessible::GetFrom(this)) {
+ if (table->IsProbablyLayoutTable()) {
+ fields->SetAttribute(CacheKey::TableLayoutGuess, true);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::TableLayoutGuess, DeleteEntry());
+ }
+ } else if (auto* cell = HTMLTableCellAccessible::GetFrom(this)) {
+ // For HTML table cells, we must use the HTMLTableCellAccessible
+ // GetRow/ColExtent methods rather than using the DOM attributes directly.
+ // This is because of things like rowspan="0" which depend on knowing
+ // about thead, tbody, etc., which is info we don't have in the a11y tree.
+ int32_t value = static_cast<int32_t>(cell->RowExtent());
+ MOZ_ASSERT(value > 0);
+ if (value > 1) {
+ fields->SetAttribute(CacheKey::RowSpan, value);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::RowSpan, DeleteEntry());
+ }
+ value = static_cast<int32_t>(cell->ColExtent());
+ MOZ_ASSERT(value > 0);
+ if (value > 1) {
+ fields->SetAttribute(CacheKey::ColSpan, value);
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::ColSpan, DeleteEntry());
+ }
+ if (mContent->AsElement()->HasAttr(nsGkAtoms::headers)) {
+ nsTArray<uint64_t> headers;
+ IDRefsIterator iter(mDoc, mContent, nsGkAtoms::headers);
+ while (LocalAccessible* cell = iter.Next()) {
+ if (cell->IsTableCell()) {
+ headers.AppendElement(cell->ID());
+ }
+ }
+ fields->SetAttribute(CacheKey::CellHeaders, std::move(headers));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::CellHeaders, DeleteEntry());
+ }
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::ARIA && mContent && mContent->IsElement()) {
+ // We use a nested AccAttributes to make cache updates simpler. Rather than
+ // managing individual removals, we just replace or remove the entire set of
+ // ARIA attributes.
+ RefPtr<AccAttributes> ariaAttrs;
+ aria::AttrIterator attrIt(mContent);
+ while (attrIt.Next()) {
+ if (!ariaAttrs) {
+ ariaAttrs = new AccAttributes();
+ }
+ attrIt.ExposeAttr(ariaAttrs);
+ }
+ if (ariaAttrs) {
+ fields->SetAttribute(CacheKey::ARIAAttributes, std::move(ariaAttrs));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(CacheKey::ARIAAttributes, DeleteEntry());
+ }
+ }
+
+ if (aCacheDomain & CacheDomain::Relations && mContent) {
+ if (IsHTMLRadioButton() ||
+ (mContent->IsElement() &&
+ mContent->AsElement()->IsHTMLElement(nsGkAtoms::a))) {
+ // HTML radio buttons with the same name should be grouped
+ // and returned together when their MEMBER_OF relation is
+ // requested. Computing LINKS_TO also requires we cache `name` on
+ // anchor elements.
+ nsString name;
+ mContent->AsElement()->GetAttr(nsGkAtoms::name, name);
+ if (!name.IsEmpty()) {
+ fields->SetAttribute(CacheKey::DOMName, std::move(name));
+ } else if (aUpdateType != CacheUpdateType::Initial) {
+ // It's possible we used to have a name and it's since been
+ // removed. Send a delete entry.
+ fields->SetAttribute(CacheKey::DOMName, DeleteEntry());
+ }
+ }
+
+ for (auto const& data : kRelationTypeAtoms) {
+ nsTArray<uint64_t> ids;
+ nsStaticAtom* const relAtom = data.mAtom;
+
+ Relation rel;
+ if (data.mType == RelationType::LABEL_FOR) {
+ // Labels are a special case -- we need to validate that the target of
+ // their `for` attribute is in fact labelable. DOM checks this when we
+ // call GetControl(). If a label contains an element we will return it
+ // here.
+ if (dom::HTMLLabelElement* labelEl =
+ dom::HTMLLabelElement::FromNode(mContent)) {
+ rel.AppendTarget(mDoc, labelEl->GetControl());
+ }
+ } else if (data.mType == RelationType::DETAILS) {
+ // We need to use RelationByType for details because it might include
+ // popovertarget. Nothing exposes an implicit reverse details
+ // relation, so using RelationByType here is fine.
+ rel = RelationByType(RelationType::DETAILS);
+ } else {
+ // We use an IDRefsIterator here instead of calling RelationByType
+ // directly because we only want to cache explicit relations. Implicit
+ // relations (e.g. LABEL_FOR exposed on the target of aria-labelledby)
+ // will be computed and stored separately in the parent process.
+ rel.AppendIter(new IDRefsIterator(mDoc, mContent, relAtom));
+ }
+
+ while (LocalAccessible* acc = rel.LocalNext()) {
+ ids.AppendElement(acc->ID());
+ }
+ if (ids.Length()) {
+ fields->SetAttribute(relAtom, std::move(ids));
+ } else if (aUpdateType == CacheUpdateType::Update) {
+ fields->SetAttribute(relAtom, DeleteEntry());
+ }
+ }
+ }
+
+#if defined(XP_WIN)
+ if (aCacheDomain & CacheDomain::InnerHTML && HasOwnContent() &&
+ mContent->IsMathMLElement(nsGkAtoms::math)) {
+ nsString innerHTML;
+ mContent->AsElement()->GetInnerHTML(innerHTML, IgnoreErrors());
+ fields->SetAttribute(CacheKey::InnerHTML, std::move(innerHTML));
+ }
+#endif // defined(XP_WIN)
+
+ if (aUpdateType == CacheUpdateType::Initial) {
+ // Add fields which never change and thus only need to be included in the
+ // initial cache push.
+ if (mContent && mContent->IsElement()) {
+ fields->SetAttribute(CacheKey::TagName, mContent->NodeInfo()->NameAtom());
+
+ dom::Element* el = mContent->AsElement();
+ if (IsTextField() || IsDateTimeField()) {
+ // Cache text input types. Accessible is recreated if this changes,
+ // so it is considered immutable.
+ if (const nsAttrValue* attr = el->GetParsedAttr(nsGkAtoms::type)) {
+ RefPtr<nsAtom> inputType = attr->GetAsAtom();
+ if (inputType) {
+ fields->SetAttribute(CacheKey::InputType, inputType);
+ }
+ }
+ }
+
+ // Changing the role attribute currently re-creates the Accessible, so
+ // it's immutable in the cache.
+ if (const nsRoleMapEntry* roleMap = ARIARoleMap()) {
+ // Most of the time, the role attribute is a single, known role. We
+ // already send the map index, so we don't need to double up.
+ if (!nsAccUtils::ARIAAttrValueIs(el, nsGkAtoms::role, roleMap->roleAtom,
+ eIgnoreCase)) {
+ // Multiple roles or unknown roles are rare, so just send them as a
+ // string.
+ nsAutoString role;
+ nsAccUtils::GetARIAAttr(el, nsGkAtoms::role, role);
+ fields->SetAttribute(CacheKey::ARIARole, std::move(role));
+ }
+ }
+
+ if (auto* htmlEl = nsGenericHTMLElement::FromNode(mContent)) {
+ // Changing popover recreates the Accessible, so it's immutable in the
+ // cache.
+ nsAutoString popover;
+ htmlEl->GetPopover(popover);
+ if (!popover.IsEmpty()) {
+ fields->SetAttribute(CacheKey::PopupType,
+ RefPtr{NS_Atomize(popover)});
+ }
+ }
+ }
+
+ if (frame) {
+ // Note our frame's current computed style so we can track style changes
+ // later on.
+ mOldComputedStyle = frame->Style();
+ if (frame->IsTransformed()) {
+ mStateFlags |= eOldFrameHasValidTransformStyle;
+ } else {
+ mStateFlags &= ~eOldFrameHasValidTransformStyle;
+ }
+ }
+
+ if (IsDoc()) {
+ if (PresShell* presShell = AsDoc()->PresShellPtr()) {
+ // Send the initial resolution of the document. When this changes, we
+ // will ne notified via nsAS::NotifyOfResolutionChange
+ float resolution = presShell->GetResolution();
+ fields->SetAttribute(CacheKey::Resolution, resolution);
+ int32_t appUnitsPerDevPixel =
+ presShell->GetPresContext()->AppUnitsPerDevPixel();
+ fields->SetAttribute(CacheKey::AppUnitsPerDevPixel,
+ appUnitsPerDevPixel);
+ }
+
+ nsString mimeType;
+ AsDoc()->MimeType(mimeType);
+ fields->SetAttribute(CacheKey::MimeType, std::move(mimeType));
+ }
+ }
+
+ if ((aCacheDomain & (CacheDomain::Text | CacheDomain::ScrollPosition) ||
+ boundsChanged) &&
+ mDoc) {
+ mDoc->SetViewportCacheDirty(true);
+ }
+
+ return fields.forget();
+}
+
+void LocalAccessible::MaybeQueueCacheUpdateForStyleChanges() {
+ // mOldComputedStyle might be null if the initial cache hasn't been sent yet.
+ // In that case, there is nothing to do here.
+ if (!IPCAccessibilityActive() || !mOldComputedStyle) {
+ return;
+ }
+
+ if (nsIFrame* frame = GetFrame()) {
+ const ComputedStyle* newStyle = frame->Style();
+
+ nsAutoCString oldOverflow, newOverflow;
+ mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_overflow,
+ oldOverflow);
+ newStyle->GetComputedPropertyValue(eCSSProperty_overflow, newOverflow);
+
+ if (oldOverflow != newOverflow) {
+ if (oldOverflow.Equals("hidden"_ns) || newOverflow.Equals("hidden"_ns)) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Style);
+ }
+ if (oldOverflow.Equals("auto"_ns) || newOverflow.Equals("auto"_ns) ||
+ oldOverflow.Equals("scroll"_ns) || newOverflow.Equals("scroll"_ns)) {
+ // We cache a (0,0) scroll position for frames that have overflow
+ // styling which means they _could_ become scrollable, even if the
+ // content within them doesn't currently scroll.
+ mDoc->QueueCacheUpdate(this, CacheDomain::ScrollPosition);
+ }
+ }
+
+ nsAutoCString oldDisplay, newDisplay;
+ mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_display,
+ oldDisplay);
+ newStyle->GetComputedPropertyValue(eCSSProperty_display, newDisplay);
+
+ nsAutoCString oldOpacity, newOpacity;
+ mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_opacity,
+ oldOpacity);
+ newStyle->GetComputedPropertyValue(eCSSProperty_opacity, newOpacity);
+
+ if (oldDisplay != newDisplay || oldOpacity != newOpacity) {
+ // CacheDomain::Style covers both display and opacity, so if
+ // either property has changed, send an update for the entire domain.
+ mDoc->QueueCacheUpdate(this, CacheDomain::Style);
+ }
+
+ nsAutoCString oldPosition, newPosition;
+ mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_position,
+ oldPosition);
+ newStyle->GetComputedPropertyValue(eCSSProperty_position, newPosition);
+
+ if (oldPosition != newPosition) {
+ RefPtr<nsAtom> oldAtom = NS_Atomize(oldPosition);
+ RefPtr<nsAtom> newAtom = NS_Atomize(newPosition);
+ if (oldAtom == nsGkAtoms::fixed || newAtom == nsGkAtoms::fixed) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Style);
+ }
+ }
+
+ bool newHasValidTransformStyle =
+ newStyle->StyleDisplay()->HasTransform(frame);
+ bool oldHasValidTransformStyle =
+ (mStateFlags & eOldFrameHasValidTransformStyle) != 0;
+
+ // We should send a transform update if we're adding or
+ // removing transform styling altogether.
+ bool sendTransformUpdate =
+ newHasValidTransformStyle || oldHasValidTransformStyle;
+
+ if (newHasValidTransformStyle && oldHasValidTransformStyle) {
+ // If we continue to have transform styling, verify
+ // our transform has actually changed.
+ nsChangeHint transformHint =
+ newStyle->StyleDisplay()->CalcTransformPropertyDifference(
+ *mOldComputedStyle->StyleDisplay());
+ // If this hint exists, it implies we found a property difference
+ sendTransformUpdate = !!transformHint;
+ }
+
+ if (sendTransformUpdate) {
+ // If our transform matrix has changed, it's possible our
+ // viewport cache has also changed.
+ mDoc->SetViewportCacheDirty(true);
+ // Queuing a cache update for the TransformMatrix domain doesn't
+ // necessarily mean we'll send the matrix itself, we may
+ // send a DeleteEntry() instead. See BundleFieldsForCache for
+ // more information.
+ mDoc->QueueCacheUpdate(this, CacheDomain::TransformMatrix);
+ }
+
+ mOldComputedStyle = newStyle;
+ if (newHasValidTransformStyle) {
+ mStateFlags |= eOldFrameHasValidTransformStyle;
+ } else {
+ mStateFlags &= ~eOldFrameHasValidTransformStyle;
+ }
+ }
+}
+
+nsAtom* LocalAccessible::TagName() const {
+ return mContent && mContent->IsElement() ? mContent->NodeInfo()->NameAtom()
+ : nullptr;
+}
+
+already_AddRefed<nsAtom> LocalAccessible::InputType() const {
+ if (!IsTextField() && !IsDateTimeField()) {
+ return nullptr;
+ }
+
+ dom::Element* el = mContent->AsElement();
+ if (const nsAttrValue* attr = el->GetParsedAttr(nsGkAtoms::type)) {
+ RefPtr<nsAtom> inputType = attr->GetAsAtom();
+ return inputType.forget();
+ }
+
+ return nullptr;
+}
+
+already_AddRefed<nsAtom> LocalAccessible::DisplayStyle() const {
+ dom::Element* elm = Elm();
+ if (!elm) {
+ return nullptr;
+ }
+ if (elm->IsHTMLElement(nsGkAtoms::area)) {
+ // This is an image map area. CSS is irrelevant here.
+ return nullptr;
+ }
+ static const dom::Element::AttrValuesArray presentationRoles[] = {
+ nsGkAtoms::none, nsGkAtoms::presentation, nullptr};
+ if (nsAccUtils::FindARIAAttrValueIn(elm, nsGkAtoms::role, presentationRoles,
+ eIgnoreCase) != AttrArray::ATTR_MISSING &&
+ IsGeneric()) {
+ // This Accessible has been marked presentational, but we forced a generic
+ // Accessible for some reason; e.g. CSS transform. Don't expose display in
+ // this case, as the author might be explicitly trying to avoid said
+ // exposure.
+ return nullptr;
+ }
+ RefPtr<const ComputedStyle> style =
+ nsComputedDOMStyle::GetComputedStyleNoFlush(elm);
+ if (!style) {
+ // The element is not styled, maybe not in the flat tree?
+ return nullptr;
+ }
+ nsAutoCString value;
+ style->GetComputedPropertyValue(eCSSProperty_display, value);
+ return NS_Atomize(value);
+}
+
+float LocalAccessible::Opacity() const {
+ if (nsIFrame* frame = GetFrame()) {
+ return frame->StyleEffects()->mOpacity;
+ }
+
+ return 1.0f;
+}
+
+void LocalAccessible::DOMNodeID(nsString& aID) const {
+ aID.Truncate();
+ if (mContent) {
+ if (nsAtom* id = mContent->GetID()) {
+ id->ToString(aID);
+ }
+ }
+}
+
+void LocalAccessible::LiveRegionAttributes(nsAString* aLive,
+ nsAString* aRelevant,
+ Maybe<bool>* aAtomic,
+ nsAString* aBusy) const {
+ dom::Element* el = Elm();
+ if (!el) {
+ return;
+ }
+ if (aLive) {
+ nsAccUtils::GetARIAAttr(el, nsGkAtoms::aria_live, *aLive);
+ }
+ if (aRelevant) {
+ nsAccUtils::GetARIAAttr(el, nsGkAtoms::aria_relevant, *aRelevant);
+ }
+ if (aAtomic) {
+ // XXX We ignore aria-atomic="false", but this probably doesn't conform to
+ // the spec.
+ if (nsAccUtils::ARIAAttrValueIs(el, nsGkAtoms::aria_atomic,
+ nsGkAtoms::_true, eCaseMatters)) {
+ *aAtomic = Some(true);
+ }
+ }
+ if (aBusy) {
+ nsAccUtils::GetARIAAttr(el, nsGkAtoms::aria_busy, *aBusy);
+ }
+}
+
+Maybe<bool> LocalAccessible::ARIASelected() const {
+ if (dom::Element* el = Elm()) {
+ nsStaticAtom* atom =
+ nsAccUtils::NormalizeARIAToken(el, nsGkAtoms::aria_selected);
+ if (atom == nsGkAtoms::_true) {
+ return Some(true);
+ }
+ if (atom == nsGkAtoms::_false) {
+ return Some(false);
+ }
+ }
+ return Nothing();
+}
+
+void LocalAccessible::StaticAsserts() const {
+ static_assert(
+ eLastStateFlag <= (1 << kStateFlagsBits) - 1,
+ "LocalAccessible::mStateFlags was oversized by eLastStateFlag!");
+ static_assert(
+ eLastContextFlag <= (1 << kContextFlagsBits) - 1,
+ "LocalAccessible::mContextFlags was oversized by eLastContextFlag!");
+}
+
+TableAccessible* LocalAccessible::AsTable() {
+ if (IsTable() && !mContent->IsXULElement()) {
+ return CachedTableAccessible::GetFrom(this);
+ }
+ return nullptr;
+}
+
+TableCellAccessible* LocalAccessible::AsTableCell() {
+ if (IsTableCell() && !mContent->IsXULElement()) {
+ return CachedTableCellAccessible::GetFrom(this);
+ }
+ return nullptr;
+}
+
+Maybe<int32_t> LocalAccessible::GetIntARIAAttr(nsAtom* aAttrName) const {
+ if (mContent) {
+ int32_t val;
+ if (nsCoreUtils::GetUIntAttr(mContent, aAttrName, &val)) {
+ return Some(val);
+ }
+ // XXX Handle attributes that allow -1; e.g. aria-row/colcount.
+ }
+ return Nothing();
+}
diff --git a/accessible/generic/LocalAccessible.h b/accessible/generic/LocalAccessible.h
new file mode 100644
index 0000000000..a3620f4cbd
--- /dev/null
+++ b/accessible/generic/LocalAccessible.h
@@ -0,0 +1,1028 @@
+/* -*- 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 _LocalAccessible_H_
+#define _LocalAccessible_H_
+
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/a11y/Accessible.h"
+#include "mozilla/a11y/AccTypes.h"
+#include "mozilla/a11y/RelationType.h"
+#include "mozilla/a11y/States.h"
+
+#include "mozilla/UniquePtr.h"
+
+#include "nsIContent.h"
+#include "nsTArray.h"
+#include "nsRefPtrHashtable.h"
+#include "nsRect.h"
+
+struct nsRoleMapEntry;
+
+class nsIFrame;
+
+class nsAttrValue;
+
+namespace mozilla::dom {
+class Element;
+}
+
+namespace mozilla {
+namespace a11y {
+
+class LocalAccessible;
+class AccAttributes;
+class AccEvent;
+class AccGroupInfo;
+class ApplicationAccessible;
+class CacheData;
+class DocAccessible;
+class EmbeddedObjCollector;
+class EventTree;
+class HTMLImageMapAccessible;
+class HTMLLIAccessible;
+class HTMLLinkAccessible;
+class HyperTextAccessible;
+class HyperTextAccessibleBase;
+class ImageAccessible;
+class KeyBinding;
+class OuterDocAccessible;
+class RemoteAccessible;
+class Relation;
+class RootAccessible;
+class TableAccessible;
+class TableCellAccessible;
+class TextLeafAccessible;
+class XULLabelAccessible;
+class XULTreeAccessible;
+
+enum class CacheUpdateType;
+
+#ifdef A11Y_LOG
+namespace logging {
+typedef const char* (*GetTreePrefix)(void* aData, LocalAccessible*);
+void Tree(const char* aTitle, const char* aMsgText, LocalAccessible* aRoot,
+ GetTreePrefix aPrefixFunc, void* GetTreePrefixData);
+void TreeSize(const char* aTitle, const char* aMsgText, LocalAccessible* aRoot);
+}; // namespace logging
+#endif
+
+typedef nsRefPtrHashtable<nsPtrHashKey<const void>, LocalAccessible>
+ AccessibleHashtable;
+
+#define NS_ACCESSIBLE_IMPL_IID \
+ { /* 133c8bf4-4913-4355-bd50-426bd1d6e1ad */ \
+ 0x133c8bf4, 0x4913, 0x4355, { \
+ 0xbd, 0x50, 0x42, 0x6b, 0xd1, 0xd6, 0xe1, 0xad \
+ } \
+ }
+
+/**
+ * An accessibility tree node that originated in mDoc's content process.
+ */
+class LocalAccessible : public nsISupports, public Accessible {
+ public:
+ LocalAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(LocalAccessible)
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_ACCESSIBLE_IMPL_IID)
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Public methods
+
+ /**
+ * Return the document accessible for this accessible.
+ */
+ DocAccessible* Document() const { return mDoc; }
+
+ /**
+ * Return the root document accessible for this accessible.
+ */
+ a11y::RootAccessible* RootAccessible() const;
+
+ /**
+ * Return frame for this accessible.
+ * Note that this will return null for display: contents. Also,
+ * DocAccessible::GetFrame can return null if the frame tree hasn't been
+ * created yet.
+ */
+ virtual nsIFrame* GetFrame() const;
+
+ /**
+ * Return DOM node associated with the accessible.
+ */
+ virtual nsINode* GetNode() const;
+
+ nsIContent* GetContent() const { return mContent; }
+ dom::Element* Elm() const;
+
+ /**
+ * Return node type information of DOM node associated with the accessible.
+ */
+ bool IsContent() const { return GetNode() && GetNode()->IsContent(); }
+
+ /**
+ * Return the unique identifier of the accessible.
+ * ID() should be preferred, but this method still exists because many
+ * LocalAccessible callers expect a void*.
+ */
+ void* UniqueID() { return static_cast<void*>(this); }
+
+ virtual uint64_t ID() const override {
+ return IsDoc() ? 0 : reinterpret_cast<uintptr_t>(this);
+ }
+
+ virtual void Language(nsAString& aLocale) override;
+
+ /**
+ * Get the description of this accessible.
+ */
+ virtual void Description(nsString& aDescription) const override;
+
+ /**
+ * Get the value of this accessible.
+ */
+ virtual void Value(nsString& aValue) const override;
+
+ /**
+ * Get the name of this accessible.
+ */
+ virtual ENameValueFlag Name(nsString& aName) const override;
+
+ /**
+ * Maps ARIA state attributes to state of accessible. Note the given state
+ * argument should hold states for accessible before you pass it into this
+ * method.
+ *
+ * @param [in/out] where to fill the states into.
+ */
+ virtual void ApplyARIAState(uint64_t* aState) const;
+
+ /**
+ * Return enumerated accessible role (see constants in Role.h).
+ */
+ virtual mozilla::a11y::role Role() const override;
+
+ /**
+ * Return accessible role specified by ARIA (see constants in
+ * roles).
+ */
+ inline mozilla::a11y::role ARIARole();
+
+ /**
+ * Returns enumerated accessible role from native markup (see constants in
+ * Role.h). Doesn't take into account ARIA roles.
+ */
+ virtual mozilla::a11y::role NativeRole() const;
+
+ virtual uint64_t State() override;
+
+ /**
+ * Return interactive states present on the accessible
+ * (@see NativeInteractiveState).
+ */
+ uint64_t InteractiveState() const {
+ uint64_t state = NativeInteractiveState();
+ ApplyARIAState(&state);
+ return state;
+ }
+
+ /**
+ * Return link states present on the accessible.
+ */
+ uint64_t LinkState() const {
+ uint64_t state = NativeLinkState();
+ ApplyARIAState(&state);
+ return state;
+ }
+
+ /**
+ * Return the states of accessible, not taking into account ARIA states.
+ * Use State() to get complete set of states.
+ */
+ virtual uint64_t NativeState() const;
+
+ /**
+ * Return native interactice state (unavailable, focusable or selectable).
+ */
+ virtual uint64_t NativeInteractiveState() const;
+
+ /**
+ * Return native link states present on the accessible.
+ */
+ virtual uint64_t NativeLinkState() const;
+
+ /**
+ * Return bit set of invisible and offscreen states.
+ */
+ uint64_t VisibilityState() const;
+
+ /**
+ * Return true if native unavailable state present.
+ */
+ virtual bool NativelyUnavailable() const;
+
+ virtual already_AddRefed<AccAttributes> Attributes() override;
+
+ /**
+ * Return direct or deepest child at the given point.
+ *
+ * @param aX [in] x coordinate relative screen
+ * @param aY [in] y coordinate relative screen
+ * @param aWhichChild [in] flag points if deepest or direct child
+ * should be returned
+ */
+ virtual LocalAccessible* LocalChildAtPoint(int32_t aX, int32_t aY,
+ EWhichChildAtPoint aWhichChild);
+
+ /**
+ * Similar to LocalChildAtPoint but crosses process boundaries.
+ */
+ virtual Accessible* ChildAtPoint(int32_t aX, int32_t aY,
+ EWhichChildAtPoint aWhichChild) override;
+
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Initializing methods
+
+ /**
+ * Shutdown this accessible object.
+ */
+ virtual void Shutdown();
+
+ /**
+ * Set the ARIA role map entry for a new accessible.
+ */
+ inline void SetRoleMapEntry(const nsRoleMapEntry* aRoleMapEntry);
+
+ /**
+ * Append/insert/remove a child. Return true if operation was successful.
+ */
+ bool AppendChild(LocalAccessible* aChild) {
+ return InsertChildAt(mChildren.Length(), aChild);
+ }
+ virtual bool InsertChildAt(uint32_t aIndex, LocalAccessible* aChild);
+
+ /**
+ * Inserts a child after given sibling. If the child cannot be inserted,
+ * then the child is unbound from the document, and false is returned. Make
+ * sure to null out any references on the child object as it may be destroyed.
+ */
+ inline bool InsertAfter(LocalAccessible* aNewChild,
+ LocalAccessible* aRefChild);
+
+ virtual bool RemoveChild(LocalAccessible* aChild);
+
+ /**
+ * Reallocates the child within its parent.
+ */
+ virtual void RelocateChild(uint32_t aNewIndex, LocalAccessible* aChild);
+
+ // Accessible hierarchy method overrides
+
+ virtual Accessible* Parent() const override { return LocalParent(); }
+
+ virtual Accessible* ChildAt(uint32_t aIndex) const override {
+ return LocalChildAt(aIndex);
+ }
+
+ virtual Accessible* NextSibling() const override {
+ return LocalNextSibling();
+ }
+
+ virtual Accessible* PrevSibling() const override {
+ return LocalPrevSibling();
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // LocalAccessible tree traverse methods
+
+ /**
+ * Return parent accessible.
+ */
+ LocalAccessible* LocalParent() const { return mParent; }
+
+ /**
+ * Return child accessible at the given index.
+ */
+ virtual LocalAccessible* LocalChildAt(uint32_t aIndex) const;
+
+ /**
+ * Return child accessible count.
+ */
+ virtual uint32_t ChildCount() const override;
+
+ /**
+ * Return index of the given child accessible.
+ */
+ int32_t GetIndexOf(const LocalAccessible* aChild) const {
+ return (aChild->mParent != this) ? -1 : aChild->IndexInParent();
+ }
+
+ /**
+ * Return index in parent accessible.
+ */
+ virtual int32_t IndexInParent() const override;
+
+ /**
+ * Return first/last/next/previous sibling of the accessible.
+ */
+ inline LocalAccessible* LocalNextSibling() const {
+ return GetSiblingAtOffset(1);
+ }
+ inline LocalAccessible* LocalPrevSibling() const {
+ return GetSiblingAtOffset(-1);
+ }
+ inline LocalAccessible* LocalFirstChild() const { return LocalChildAt(0); }
+ inline LocalAccessible* LocalLastChild() const {
+ uint32_t childCount = ChildCount();
+ return childCount != 0 ? LocalChildAt(childCount - 1) : nullptr;
+ }
+
+ virtual uint32_t EmbeddedChildCount() override;
+
+ /**
+ * Return embedded accessible child at the given index.
+ */
+ virtual Accessible* EmbeddedChildAt(uint32_t aIndex) override;
+
+ virtual int32_t IndexOfEmbeddedChild(Accessible* aChild) override;
+
+ /**
+ * Return number of content children/content child at index. The content
+ * child is created from markup in contrast to it's never constructed by its
+ * parent accessible (like treeitem accessibles for XUL trees).
+ */
+ uint32_t ContentChildCount() const { return mChildren.Length(); }
+ LocalAccessible* ContentChildAt(uint32_t aIndex) const {
+ return mChildren.ElementAt(aIndex);
+ }
+
+ /**
+ * Return true if the accessible is attached to tree.
+ */
+ bool IsBoundToParent() const { return !!mParent; }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Miscellaneous methods
+
+ /**
+ * Handle accessible event, i.e. process it, notifies observers and fires
+ * platform specific event.
+ */
+ virtual nsresult HandleAccEvent(AccEvent* aAccEvent);
+
+ /**
+ * Return true if the accessible is an acceptable child.
+ */
+ virtual bool IsAcceptableChild(nsIContent* aEl) const {
+ return aEl &&
+ !aEl->IsAnyOfHTMLElements(nsGkAtoms::option, nsGkAtoms::optgroup);
+ }
+
+ virtual void AppendTextTo(nsAString& aText, uint32_t aStartOffset = 0,
+ uint32_t aLength = UINT32_MAX) override;
+
+ virtual nsRect BoundsInAppUnits() const override;
+
+ virtual LayoutDeviceIntRect Bounds() const override;
+
+ /**
+ * Return boundaries rect relative the bounding frame.
+ */
+ virtual nsRect RelativeBounds(nsIFrame** aRelativeFrame) const;
+
+ /**
+ * Return boundaries rect relative to the frame of the parent accessible.
+ * The returned bounds are the same regardless of whether the parent is
+ * scrolled. This means the scroll position must be later subtracted to
+ * calculate absolute coordinates.
+ */
+ virtual nsRect ParentRelativeBounds();
+
+ /**
+ * Selects the accessible within its container if applicable.
+ */
+ virtual void SetSelected(bool aSelect) override;
+
+ /**
+ * Select the accessible within its container.
+ */
+ virtual void TakeSelection() override;
+
+ /**
+ * Focus the accessible.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void TakeFocus() const override;
+
+ MOZ_CAN_RUN_SCRIPT
+ virtual void ScrollTo(uint32_t aHow) const override;
+
+ virtual void ScrollToPoint(uint32_t aCoordinateType, int32_t aX,
+ int32_t aY) override;
+
+ /**
+ * Get a pointer to accessibility interface for this node, which is specific
+ * to the OS/accessibility toolkit we're running on.
+ */
+ virtual void GetNativeInterface(void** aNativeAccessible);
+
+ virtual Maybe<int32_t> GetIntARIAAttr(nsAtom* aAttrName) const override;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Downcasting and types
+
+ inline bool IsAbbreviation() const {
+ return mContent &&
+ mContent->IsAnyOfHTMLElements(nsGkAtoms::abbr, nsGkAtoms::acronym);
+ }
+
+ ApplicationAccessible* AsApplication();
+
+ DocAccessible* AsDoc();
+
+ HyperTextAccessible* AsHyperText();
+ virtual HyperTextAccessibleBase* AsHyperTextBase() override;
+
+ HTMLLIAccessible* AsHTMLListItem();
+
+ HTMLLinkAccessible* AsHTMLLink();
+
+ ImageAccessible* AsImage();
+
+ HTMLImageMapAccessible* AsImageMap();
+
+ OuterDocAccessible* AsOuterDoc();
+
+ a11y::RootAccessible* AsRoot();
+
+ virtual TableAccessible* AsTable() override;
+ virtual TableCellAccessible* AsTableCell() override;
+
+ TextLeafAccessible* AsTextLeaf();
+
+ XULLabelAccessible* AsXULLabel();
+
+ XULTreeAccessible* AsXULTree();
+
+ //////////////////////////////////////////////////////////////////////////////
+ // ActionAccessible
+
+ virtual bool HasPrimaryAction() const override;
+
+ virtual uint8_t ActionCount() const override;
+
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+
+ virtual bool DoAction(uint8_t aIndex) const override;
+
+ virtual KeyBinding AccessKey() const override;
+
+ /**
+ * Return global keyboard shortcut for default action, such as Ctrl+O for
+ * Open file menuitem.
+ */
+ virtual KeyBinding KeyboardShortcut() const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // HyperLinkAccessible (any embedded object in text can implement HyperLink,
+ // which helps determine where it is located within containing text).
+
+ /**
+ * Return true if the accessible is hyper link accessible.
+ */
+ virtual bool IsLink() const override;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // SelectAccessible
+
+ /**
+ * Return an array of selected items.
+ */
+ virtual void SelectedItems(nsTArray<Accessible*>* aItems) override;
+
+ /**
+ * Return the number of selected items.
+ */
+ virtual uint32_t SelectedItemCount() override;
+
+ /**
+ * Return selected item at the given index.
+ */
+ virtual Accessible* GetSelectedItem(uint32_t aIndex) override;
+
+ /**
+ * Determine if item at the given index is selected.
+ */
+ virtual bool IsItemSelected(uint32_t aIndex) override;
+
+ /**
+ * Add item at the given index the selection. Return true if success.
+ */
+ virtual bool AddItemToSelection(uint32_t aIndex) override;
+
+ /**
+ * Remove item at the given index from the selection. Return if success.
+ */
+ virtual bool RemoveItemFromSelection(uint32_t aIndex) override;
+
+ /**
+ * Select all items. Return true if success.
+ */
+ virtual bool SelectAll() override;
+
+ /**
+ * Unselect all items. Return true if success.
+ */
+ virtual bool UnselectAll() override;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Value (numeric value interface)
+
+ virtual double MaxValue() const override;
+ virtual double MinValue() const override;
+ virtual double CurValue() const override;
+ virtual double Step() const override;
+ virtual bool SetCurValue(double aValue) override;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Widgets
+
+ /**
+ * Return true if accessible is a widget, i.e. control or accessible that
+ * manages its items. Note, being a widget the accessible may be a part of
+ * composite widget.
+ */
+ virtual bool IsWidget() const;
+
+ /**
+ * Return true if the widget is active, i.e. has a focus within it.
+ */
+ virtual bool IsActiveWidget() const;
+
+ /**
+ * Return true if the widget has items and items are operable by user and
+ * can be activated.
+ */
+ virtual bool AreItemsOperable() const;
+
+ /**
+ * Return the current item of the widget, i.e. an item that has or will have
+ * keyboard focus when widget gets active.
+ */
+ virtual LocalAccessible* CurrentItem() const;
+
+ /**
+ * Set the current item of the widget.
+ */
+ virtual void SetCurrentItem(const LocalAccessible* aItem);
+
+ /**
+ * Return container widget this accessible belongs to.
+ */
+ virtual LocalAccessible* ContainerWidget() const;
+
+ bool IsActiveDescendant(LocalAccessible** aWidget = nullptr) const;
+
+ /**
+ * Return true if the accessible is defunct.
+ */
+ inline bool IsDefunct() const;
+
+ /**
+ * Return false if the accessible is no longer in the document.
+ */
+ bool IsInDocument() const { return !(mStateFlags & eIsNotInDocument); }
+
+ /**
+ * Return true if the accessible should be contained by document node map.
+ */
+ bool IsNodeMapEntry() const {
+ return HasOwnContent() && !(mStateFlags & eNotNodeMapEntry);
+ }
+
+ /**
+ * Return true if the accessible has associated DOM content.
+ */
+ bool HasOwnContent() const {
+ return mContent && !(mStateFlags & eSharedNode);
+ }
+
+ /**
+ * Return true if native markup has a numeric value.
+ */
+ inline bool NativeHasNumericValue() const;
+
+ /**
+ * Return true if ARIA specifies support for a numeric value.
+ */
+ inline bool ARIAHasNumericValue() const;
+
+ /**
+ * Return true if the accessible has a numeric value.
+ */
+ virtual bool HasNumericValue() const override;
+
+ /**
+ * Return true if the accessible state change is processed by handling proper
+ * DOM UI event, if otherwise then false. For example, CheckboxAccessible
+ * created for HTML:input@type="checkbox" will process
+ * nsIDocumentObserver::ElementStateChanged instead of 'CheckboxStateChange'
+ * event.
+ */
+ bool NeedsDOMUIEvent() const { return !(mStateFlags & eIgnoreDOMUIEvent); }
+
+ /**
+ * Get/set repositioned bit indicating that the accessible was moved in
+ * the accessible tree, i.e. the accessible tree structure differs from DOM.
+ */
+ bool IsRelocated() const { return mStateFlags & eRelocated; }
+ void SetRelocated(bool aRelocated) {
+ if (aRelocated) {
+ mStateFlags |= eRelocated;
+ } else {
+ mStateFlags &= ~eRelocated;
+ }
+ }
+
+ /**
+ * Return true if the accessible allows accessible children from subtree of
+ * a DOM element of this accessible.
+ */
+ bool KidsFromDOM() const { return !(mStateFlags & eNoKidsFromDOM); }
+
+ /**
+ * Return true if this accessible has a parent, relation or ancestor with a
+ * relation whose name depends on this accessible.
+ */
+ bool HasNameDependent() const { return mContextFlags & eHasNameDependent; }
+
+ /**
+ * Return true if this accessible has a parent, relation or ancestor with a
+ * relation whose description depends on this accessible.
+ */
+ bool HasDescriptionDependent() const {
+ return mContextFlags & eHasDescriptionDependent;
+ }
+
+ /**
+ * Return true if the element is inside an alert.
+ */
+ bool IsInsideAlert() const { return mContextFlags & eInsideAlert; }
+
+ /**
+ * Return true if there is a pending reorder event for this accessible.
+ */
+ bool ReorderEventTarget() const { return mReorderEventTarget; }
+
+ /**
+ * Return true if there is a pending show event for this accessible.
+ */
+ bool ShowEventTarget() const { return mShowEventTarget; }
+
+ /**
+ * Return true if there is a pending hide event for this accessible.
+ */
+ bool HideEventTarget() const { return mHideEventTarget; }
+
+ /**
+ * Set if there is a pending reorder event for this accessible.
+ */
+ void SetReorderEventTarget(bool aTarget) { mReorderEventTarget = aTarget; }
+
+ /**
+ * Set if this accessible is a show event target.
+ */
+ void SetShowEventTarget(bool aTarget) { mShowEventTarget = aTarget; }
+
+ /**
+ * Set if this accessible is a hide event target.
+ */
+ void SetHideEventTarget(bool aTarget) { mHideEventTarget = aTarget; }
+
+ void Announce(const nsAString& aAnnouncement, uint16_t aPriority);
+
+ virtual bool IsRemote() const override { return false; }
+
+ already_AddRefed<AccAttributes> BundleFieldsForCache(
+ uint64_t aCacheDomain, CacheUpdateType aUpdateType);
+
+ /**
+ * Push fields to cache.
+ * aCacheDomain - describes which fields to bundle and ultimately send
+ * aUpdate - describes whether this is an initial or subsequent update
+ */
+ void SendCache(uint64_t aCacheDomain, CacheUpdateType aUpdate);
+
+ void MaybeQueueCacheUpdateForStyleChanges();
+
+ virtual nsAtom* TagName() const override;
+
+ virtual already_AddRefed<nsAtom> InputType() const override;
+
+ virtual already_AddRefed<nsAtom> DisplayStyle() const override;
+
+ virtual float Opacity() const override;
+
+ virtual void DOMNodeID(nsString& aID) const override;
+
+ virtual void LiveRegionAttributes(nsAString* aLive, nsAString* aRelevant,
+ Maybe<bool>* aAtomic,
+ nsAString* aBusy) const override;
+
+ virtual Maybe<bool> ARIASelected() const override;
+
+ protected:
+ virtual ~LocalAccessible();
+
+ /**
+ * Return the accessible name provided by native markup. It doesn't take
+ * into account ARIA markup used to specify the name.
+ */
+ virtual mozilla::a11y::ENameValueFlag NativeName(nsString& aName) const;
+
+ /**
+ * Return the accessible description provided by native markup. It doesn't
+ * take into account ARIA markup used to specify the description.
+ */
+ void NativeDescription(nsString& aDescription) const;
+
+ /**
+ * Return object attributes provided by native markup. It doesn't take into
+ * account ARIA.
+ */
+ virtual already_AddRefed<AccAttributes> NativeAttributes();
+
+ /**
+ * The given attribute has the potential of changing the accessible's state.
+ * This is used to capture the state before the attribute change and compare
+ * it with the state after.
+ */
+ virtual bool AttributeChangesState(nsAtom* aAttribute);
+
+ /**
+ * Notify accessible that a DOM attribute on its associated content has
+ * changed. This allows the accessible to update its state and emit any
+ * relevant events.
+ */
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState);
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Initializing, cache and tree traverse methods
+
+ /**
+ * Destroy the object.
+ */
+ void LastRelease();
+
+ /**
+ * Set accessible parent and index in parent.
+ */
+ void BindToParent(LocalAccessible* aParent, uint32_t aIndexInParent);
+ void UnbindFromParent();
+
+ /**
+ * Return sibling accessible at the given offset.
+ */
+ virtual LocalAccessible* GetSiblingAtOffset(int32_t aOffset,
+ nsresult* aError = nullptr) const;
+
+ void ModifySubtreeContextFlags(uint32_t aContextFlags, bool aAdd);
+
+ /**
+ * Flags used to describe the state of this accessible.
+ */
+ enum StateFlags {
+ eIsDefunct = 1 << 0, // accessible is defunct
+ eIsNotInDocument = 1 << 1, // accessible is not in document
+ eSharedNode = 1 << 2, // accessible shares DOM node from another accessible
+ eNotNodeMapEntry = 1 << 3, // accessible shouldn't be in document node map
+ eGroupInfoDirty = 1 << 4, // accessible needs to update group info
+ eKidsMutating = 1 << 5, // subtree is being mutated
+ eIgnoreDOMUIEvent = 1 << 6, // don't process DOM UI events for a11y events
+ eRelocated = 1 << 7, // accessible was moved in tree
+ eNoKidsFromDOM = 1 << 8, // accessible doesn't allow children from DOM
+ eHasTextKids = 1 << 9, // accessible have a text leaf in children
+ eOldFrameHasValidTransformStyle =
+ 1 << 10, // frame prior to most recent style change both has transform
+ // styling and supports transforms
+
+ eLastStateFlag = eOldFrameHasValidTransformStyle
+ };
+
+ /**
+ * Flags used for contextual information about the accessible.
+ */
+ enum ContextFlags {
+ eHasNameDependent = 1 << 0, // See HasNameDependent().
+ eInsideAlert = 1 << 1,
+ eHasDescriptionDependent = 1 << 2, // See HasDescriptionDependent().
+
+ eLastContextFlag = eHasDescriptionDependent
+ };
+
+ protected:
+ //////////////////////////////////////////////////////////////////////////////
+ // Miscellaneous helpers
+
+ /**
+ * Return ARIA role (helper method).
+ */
+ mozilla::a11y::role ARIATransformRole(mozilla::a11y::role aRole) const;
+
+ /**
+ * Return the minimum role that should be used as a last resort if the element
+ * does not have a more specific role.
+ */
+ mozilla::a11y::role GetMinimumRole(mozilla::a11y::role aRole) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Name helpers
+
+ /**
+ * Returns the accessible name specified by ARIA.
+ */
+ void ARIAName(nsString& aName) const;
+
+ /**
+ * Returns the accessible description specified by ARIA.
+ */
+ void ARIADescription(nsString& aDescription) const;
+
+ /**
+ * Returns the accessible name specified for this control using XUL
+ * <label control="id" ...>.
+ */
+ static void NameFromAssociatedXULLabel(DocAccessible* aDocument,
+ nsIContent* aElm, nsString& aName);
+
+ /**
+ * Return the name for XUL element.
+ */
+ static void XULElmName(DocAccessible* aDocument, nsIContent* aElm,
+ nsString& aName);
+
+ // helper method to verify frames
+ static nsresult GetFullKeyName(const nsAString& aModifierName,
+ const nsAString& aKeyName,
+ nsAString& aStringOut);
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Action helpers
+
+ /**
+ * Prepares click action that will be invoked in timeout.
+ *
+ * @note DoCommand() prepares an action in timeout because when action
+ * command opens a modal dialog/window, it won't return until the
+ * dialog/window is closed. If executing action command directly in
+ * nsIAccessible::DoAction() method, it will block AT tools (e.g. GOK) that
+ * invoke action of mozilla accessibles direclty (see bug 277888 for
+ * details).
+ *
+ * @param aContent [in, optional] element to click
+ * @param aActionIndex [in, optional] index of accessible action
+ */
+ void DoCommand(nsIContent* aContent = nullptr,
+ uint32_t aActionIndex = 0) const;
+
+ /**
+ * Dispatch click event.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ virtual void DispatchClickEvent(nsIContent* aContent,
+ uint32_t aActionIndex) const;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // Helpers
+
+ /**
+ * Get the container node for an atomic region, defined by aria-atomic="true"
+ * @return the container node
+ */
+ nsIContent* GetAtomicRegion() const;
+
+ /**
+ * Return numeric value of the given ARIA attribute, NaN if not applicable.
+ *
+ * @param aARIAProperty [in] the ARIA property we're using
+ * @return a numeric value
+ */
+ double AttrNumericValue(nsAtom* aARIAAttr) const;
+
+ /**
+ * Return the action rule based on ARIA enum constants EActionRule
+ * (see ARIAMap.h). Used by ActionCount() and ActionNameAt().
+ */
+ uint32_t GetActionRule() const;
+
+ virtual AccGroupInfo* GetGroupInfo() const override;
+
+ virtual AccGroupInfo* GetOrCreateGroupInfo() override;
+
+ virtual void ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize,
+ int32_t* aPosInSet) const override;
+
+ // Data Members
+ // mContent can be null in a DocAccessible if the document has no body or
+ // root element, or if the initial tree hasn't been constructed yet.
+ nsCOMPtr<nsIContent> mContent;
+ RefPtr<DocAccessible> mDoc;
+
+ LocalAccessible* mParent;
+ nsTArray<LocalAccessible*> mChildren;
+ int32_t mIndexInParent;
+
+ // These are used to determine whether to send cache updates.
+ Maybe<nsRect> mBounds;
+ int32_t mFirstLineStart;
+
+ /**
+ * Maintain a reference to the ComputedStyle of our frame so we can
+ * send cache updates when style changes are observed.
+ *
+ * This RefPtr is initialised in BundleFieldsForCache to the ComputedStyle
+ * for our initial frame.
+ * Style changes are observed in one of two ways:
+ * 1. Style changes on the same frame are observed in
+ * nsIFrame::DidSetComputedStyle.
+ * 2. Style changes for reconstructed frames are handled in
+ * DocAccessible::PruneOrInsertSubtree.
+ * In both cases, we call into MaybeQueueCacheUpdateForStyleChanges. There, we
+ * compare a11y-relevant properties in mOldComputedStyle with the current
+ * ComputedStyle fetched from GetFrame()->Style(). Finally, we send cache
+ * updates for attributes affected by the style change and update
+ * mOldComputedStyle to the style of our current frame.
+ */
+ RefPtr<const ComputedStyle> mOldComputedStyle;
+
+ static const uint8_t kStateFlagsBits = 11;
+ static const uint8_t kContextFlagsBits = 3;
+
+ /**
+ * Keep in sync with StateFlags, ContextFlags, and AccTypes.
+ */
+ mutable uint32_t mStateFlags : kStateFlagsBits;
+ uint32_t mContextFlags : kContextFlagsBits;
+ uint32_t mReorderEventTarget : 1;
+ uint32_t mShowEventTarget : 1;
+ uint32_t mHideEventTarget : 1;
+
+ void StaticAsserts() const;
+
+#ifdef A11Y_LOG
+ friend void logging::Tree(const char* aTitle, const char* aMsgText,
+ LocalAccessible* aRoot,
+ logging::GetTreePrefix aPrefixFunc,
+ void* aGetTreePrefixData);
+ friend void logging::TreeSize(const char* aTitle, const char* aMsgText,
+ LocalAccessible* aRoot);
+#endif
+ friend class DocAccessible;
+ friend class xpcAccessible;
+ friend class TreeMutation;
+
+ UniquePtr<mozilla::a11y::EmbeddedObjCollector> mEmbeddedObjCollector;
+ int32_t mIndexOfEmbeddedChild;
+
+ friend class EmbeddedObjCollector;
+
+ mutable AccGroupInfo* mGroupInfo;
+ friend class AccGroupInfo;
+
+ private:
+ LocalAccessible() = delete;
+ LocalAccessible(const LocalAccessible&) = delete;
+ LocalAccessible& operator=(const LocalAccessible&) = delete;
+
+ /**
+ * Traverses the accessible's parent chain in search of an accessible with
+ * a frame. Returns the frame when found. Includes special handling for
+ * OOP iframe docs and tab documents.
+ */
+ nsIFrame* FindNearestAccessibleAncestorFrame();
+
+ LocalAccessible* GetPopoverTargetDetailsRelation() const;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(LocalAccessible, NS_ACCESSIBLE_IMPL_IID)
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible downcasting method
+
+inline LocalAccessible* Accessible::AsLocal() {
+ return IsLocal() ? static_cast<LocalAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/OuterDocAccessible.cpp b/accessible/generic/OuterDocAccessible.cpp
new file mode 100644
index 0000000000..67b2b9e77f
--- /dev/null
+++ b/accessible/generic/OuterDocAccessible.cpp
@@ -0,0 +1,224 @@
+/* -*- 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 "OuterDocAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "DocAccessible-inl.h"
+#include "mozilla/a11y/DocAccessibleChild.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/dom/BrowserBridgeChild.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/a11y/Role.h"
+
+#ifdef A11Y_LOG
+# include "Logging.h"
+#endif
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// OuterDocAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+OuterDocAccessible::OuterDocAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {
+ mType = eOuterDocType;
+
+ if (IPCAccessibilityActive()) {
+ auto bridge = dom::BrowserBridgeChild::GetFrom(aContent);
+ if (bridge) {
+ // This is an iframe which will be rendered in another process.
+ SendEmbedderAccessible(bridge);
+ }
+ }
+
+ // Request document accessible for the content document to make sure it's
+ // created. It will appended to outerdoc accessible children asynchronously.
+ dom::Document* outerDoc = mContent->GetUncomposedDoc();
+ if (outerDoc) {
+ dom::Document* innerDoc = outerDoc->GetSubDocumentFor(mContent);
+ if (innerDoc) GetAccService()->GetDocAccessible(innerDoc);
+ }
+}
+
+OuterDocAccessible::~OuterDocAccessible() {}
+
+void OuterDocAccessible::SendEmbedderAccessible(
+ dom::BrowserBridgeChild* aBridge) {
+ MOZ_ASSERT(mDoc);
+ DocAccessibleChild* ipcDoc = mDoc->IPCDoc();
+ if (ipcDoc) {
+ uint64_t id = reinterpret_cast<uintptr_t>(UniqueID());
+ aBridge->SetEmbedderAccessible(ipcDoc, id);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible public (DON'T add methods here)
+
+role OuterDocAccessible::NativeRole() const { return roles::INTERNAL_FRAME; }
+
+LocalAccessible* OuterDocAccessible::LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) {
+ LayoutDeviceIntRect docRect = Bounds();
+ if (!docRect.Contains(aX, aY)) return nullptr;
+
+ // Always return the inner doc as direct child accessible unless bounds
+ // outside of it.
+ LocalAccessible* child = LocalChildAt(0);
+ NS_ENSURE_TRUE(child, nullptr);
+
+ if (aWhichChild == Accessible::EWhichChildAtPoint::DeepestChild) {
+ return child->LocalChildAtPoint(
+ aX, aY, Accessible::EWhichChildAtPoint::DeepestChild);
+ }
+ return child;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible public
+
+void OuterDocAccessible::Shutdown() {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocDestroy)) logging::OuterDocDestroy(this);
+#endif
+
+ if (auto* bridge = dom::BrowserBridgeChild::GetFrom(mContent)) {
+ uint64_t id = reinterpret_cast<uintptr_t>(UniqueID());
+ if (bridge->GetEmbedderAccessibleID() == id) {
+ // We were the last embedder accessible sent via PBrowserBridge; i.e. a
+ // new embedder accessible hasn't been created yet for this iframe. Clear
+ // the embedder accessible on PBrowserBridge.
+ bridge->SetEmbedderAccessible(nullptr, 0);
+ }
+ }
+
+ LocalAccessible* child = mChildren.SafeElementAt(0, nullptr);
+ if (child) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocDestroy)) {
+ logging::DocDestroy("outerdoc's child document rebind is scheduled",
+ child->AsDoc()->DocumentNode());
+ }
+#endif
+ RemoveChild(child);
+
+ // XXX: sometimes outerdoc accessible is shutdown because of layout style
+ // change however the presshell of underlying document isn't destroyed and
+ // the document doesn't get pagehide events. Schedule a document rebind
+ // to its parent document. Otherwise a document accessible may be lost if
+ // its outerdoc has being recreated (see bug 862863 for details).
+ if (!mDoc->IsDefunct()) {
+ MOZ_ASSERT(!child->IsDefunct(),
+ "Attempt to reattach shutdown document accessible");
+ if (!child->IsDefunct()) {
+ mDoc->BindChildDocument(child->AsDoc());
+ }
+ }
+ }
+
+ AccessibleWrap::Shutdown();
+}
+
+bool OuterDocAccessible::InsertChildAt(uint32_t aIdx,
+ LocalAccessible* aAccessible) {
+ MOZ_RELEASE_ASSERT(aAccessible->IsDoc(),
+ "OuterDocAccessible can have a document child only!");
+
+ // We keep showing the old document for a bit after creating the new one,
+ // and while building the new DOM and frame tree. That's done on purpose
+ // to avoid weird flashes of default background color.
+ // The old viewer will be destroyed after the new one is created.
+ // For a11y, it should be safe to shut down the old document now.
+ if (mChildren.Length()) mChildren[0]->Shutdown();
+
+ if (!AccessibleWrap::InsertChildAt(0, aAccessible)) return false;
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocCreate)) {
+ logging::DocCreate("append document to outerdoc",
+ aAccessible->AsDoc()->DocumentNode());
+ logging::Address("outerdoc", this);
+ }
+#endif
+
+ return true;
+}
+
+bool OuterDocAccessible::RemoveChild(LocalAccessible* aAccessible) {
+ LocalAccessible* child = mChildren.SafeElementAt(0, nullptr);
+ MOZ_ASSERT(child == aAccessible, "Wrong child to remove!");
+ if (child != aAccessible) {
+ return false;
+ }
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDocDestroy)) {
+ logging::DocDestroy("remove document from outerdoc",
+ child->AsDoc()->DocumentNode(), child->AsDoc());
+ logging::Address("outerdoc", this);
+ }
+#endif
+
+ bool wasRemoved = AccessibleWrap::RemoveChild(child);
+
+ NS_ASSERTION(!mChildren.Length(),
+ "This child document of outerdoc accessible wasn't removed!");
+
+ return wasRemoved;
+}
+
+bool OuterDocAccessible::IsAcceptableChild(nsIContent* aEl) const {
+ // outer document accessible doesn't not participate in ordinal tree
+ // mutations.
+ return false;
+}
+
+// Accessible
+
+uint32_t OuterDocAccessible::ChildCount() const {
+ uint32_t result = mChildren.Length();
+ if (!result && RemoteChildDoc()) {
+ result = 1;
+ }
+ return result;
+}
+
+Accessible* OuterDocAccessible::ChildAt(uint32_t aIndex) const {
+ LocalAccessible* result = LocalChildAt(aIndex);
+ if (result || aIndex) {
+ return result;
+ }
+
+ return RemoteChildDoc();
+}
+
+Accessible* OuterDocAccessible::ChildAtPoint(int32_t aX, int32_t aY,
+ EWhichChildAtPoint aWhichChild) {
+ LayoutDeviceIntRect docRect = Bounds();
+ if (!docRect.Contains(aX, aY)) return nullptr;
+
+ // Always return the inner doc as direct child accessible unless bounds
+ // outside of it.
+ Accessible* child = ChildAt(0);
+ NS_ENSURE_TRUE(child, nullptr);
+
+ if (aWhichChild == EWhichChildAtPoint::DeepestChild) {
+ return child->ChildAtPoint(aX, aY, EWhichChildAtPoint::DeepestChild);
+ }
+ return child;
+}
+
+DocAccessibleParent* OuterDocAccessible::RemoteChildDoc() const {
+ dom::BrowserParent* tab = dom::BrowserParent::GetFrom(GetContent());
+ if (!tab) {
+ return nullptr;
+ }
+
+ return tab->GetTopLevelDocAccessible();
+}
diff --git a/accessible/generic/OuterDocAccessible.h b/accessible/generic/OuterDocAccessible.h
new file mode 100644
index 0000000000..6c16d69124
--- /dev/null
+++ b/accessible/generic/OuterDocAccessible.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_OUTERDOCACCESSIBLE_H_
+#define MOZILLA_A11Y_OUTERDOCACCESSIBLE_H_
+
+#include "AccessibleWrap.h"
+
+namespace mozilla {
+
+namespace dom {
+class BrowserBridgeChild;
+}
+
+namespace a11y {
+class DocAccessibleParent;
+
+/**
+ * Used for <browser>, <frame>, <iframe>, <page> or editor> elements.
+ *
+ * In these variable names, "outer" relates to the OuterDocAccessible as
+ * opposed to the DocAccessibleWrap which is "inner". The outer node is
+ * a something like tags listed above, whereas the inner node corresponds to
+ * the inner document root.
+ */
+
+class OuterDocAccessible final : public AccessibleWrap {
+ public:
+ OuterDocAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(OuterDocAccessible, AccessibleWrap)
+
+ DocAccessibleParent* RemoteChildDoc() const;
+
+ /**
+ * For iframes in a content process which will be rendered in another content
+ * process, tell the parent process about this OuterDocAccessible
+ * so it can link the trees together when the embedded document is added.
+ * Note that an OuterDocAccessible can be created before the
+ * BrowserBridgeChild or vice versa. Therefore, this must be conditionally
+ * called when either of these is created.
+ */
+ void SendEmbedderAccessible(dom::BrowserBridgeChild* aBridge);
+
+ Maybe<nsMargin> GetCrossDocOffset() { return mCrossDocOffset; }
+
+ void SetCrossDocOffset(nsMargin aMargin) { mCrossDocOffset = Some(aMargin); }
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual LocalAccessible* LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override;
+
+ virtual bool InsertChildAt(uint32_t aIdx, LocalAccessible* aChild) override;
+ virtual bool RemoveChild(LocalAccessible* aAccessible) override;
+ virtual bool IsAcceptableChild(nsIContent* aEl) const override;
+
+ virtual uint32_t ChildCount() const override;
+
+ // Accessible
+ virtual Accessible* ChildAt(uint32_t aIndex) const override;
+ virtual Accessible* ChildAtPoint(int32_t aX, int32_t aY,
+ EWhichChildAtPoint aWhichChild) override;
+
+ protected:
+ virtual ~OuterDocAccessible() override;
+ Maybe<nsMargin> mCrossDocOffset;
+};
+
+inline OuterDocAccessible* LocalAccessible::AsOuterDoc() {
+ return IsOuterDoc() ? static_cast<OuterDocAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/RootAccessible.cpp b/accessible/generic/RootAccessible.cpp
new file mode 100644
index 0000000000..aea5d4c84c
--- /dev/null
+++ b/accessible/generic/RootAccessible.cpp
@@ -0,0 +1,706 @@
+/* -*- 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 "RootAccessible.h"
+
+#include "mozilla/ArrayUtils.h"
+#include "nsXULPopupManager.h"
+
+#define CreateEvent CreateEventA
+
+#include "LocalAccessible-inl.h"
+#include "DocAccessible-inl.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "nsAccessibilityService.h"
+#include "nsAccUtils.h"
+#include "nsCoreUtils.h"
+#include "nsEventShell.h"
+#include "Relation.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+#include "XULTreeAccessible.h"
+
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/CustomEvent.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/BrowserHost.h"
+
+#include "nsIDocShellTreeOwner.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/EventTarget.h"
+#include "nsIDOMXULMultSelectCntrlEl.h"
+#include "mozilla/dom/Document.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIPropertyBag2.h"
+#include "nsPIDOMWindow.h"
+#include "nsIWebBrowserChrome.h"
+#include "nsFocusManager.h"
+
+#include "nsIAppWindow.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+using namespace mozilla::dom;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_IMPL_ISUPPORTS_INHERITED(RootAccessible, DocAccessible, nsIDOMEventListener)
+
+////////////////////////////////////////////////////////////////////////////////
+// Constructor/destructor
+
+RootAccessible::RootAccessible(Document* aDocument, PresShell* aPresShell)
+ : DocAccessibleWrap(aDocument, aPresShell) {
+ mType = eRootType;
+}
+
+RootAccessible::~RootAccessible() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible
+
+ENameValueFlag RootAccessible::Name(nsString& aName) const {
+ aName.Truncate();
+
+ if (ARIARoleMap()) {
+ LocalAccessible::Name(aName);
+ if (!aName.IsEmpty()) return eNameOK;
+ }
+
+ mDocumentNode->GetTitle(aName);
+ return eNameOK;
+}
+
+// RootAccessible protected member
+uint32_t RootAccessible::GetChromeFlags() const {
+ // Return the flag set for the top level window as defined
+ // by nsIWebBrowserChrome::CHROME_WINDOW_[FLAGNAME]
+ // Not simple: nsIAppWindow is not just a QI from nsIDOMWindow
+ nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mDocumentNode);
+ NS_ENSURE_TRUE(docShell, 0);
+ nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
+ docShell->GetTreeOwner(getter_AddRefs(treeOwner));
+ NS_ENSURE_TRUE(treeOwner, 0);
+ nsCOMPtr<nsIAppWindow> appWin(do_GetInterface(treeOwner));
+ if (!appWin) {
+ return 0;
+ }
+ uint32_t chromeFlags;
+ appWin->GetChromeFlags(&chromeFlags);
+ return chromeFlags;
+}
+
+uint64_t RootAccessible::NativeState() const {
+ uint64_t state = DocAccessibleWrap::NativeState();
+ if (state & states::DEFUNCT) return state;
+
+ uint32_t chromeFlags = GetChromeFlags();
+ if (chromeFlags & nsIWebBrowserChrome::CHROME_WINDOW_RESIZE) {
+ state |= states::SIZEABLE;
+ }
+ // If it has a titlebar it's movable
+ // XXX unless it's minimized or maximized, but not sure
+ // how to detect that
+ if (chromeFlags & nsIWebBrowserChrome::CHROME_TITLEBAR) {
+ state |= states::MOVEABLE;
+ }
+ if (chromeFlags & nsIWebBrowserChrome::CHROME_MODAL) state |= states::MODAL;
+
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (fm && fm->GetActiveWindow() == mDocumentNode->GetWindow()) {
+ state |= states::ACTIVE;
+ }
+
+ return state;
+}
+
+const char* const kEventTypes[] = {
+#ifdef DEBUG_DRAGDROPSTART
+ // Capture mouse over events and fire fake DRAGDROPSTART event to simplify
+ // debugging a11y objects with event viewers.
+ "mouseover",
+#endif
+ // Fired when list or tree selection changes.
+ "select",
+ // Fired when value changes immediately, wether or not focused changed.
+ "ValueChange", "AlertActive", "TreeRowCountChanged", "TreeInvalidated",
+ // add ourself as a OpenStateChange listener (custom event fired in
+ // tree.xml)
+ "OpenStateChange",
+ // add ourself as a CheckboxStateChange listener (custom event fired in
+ // HTMLInputElement.cpp)
+ "CheckboxStateChange",
+ // add ourself as a RadioStateChange Listener (custom event fired in in
+ // HTMLInputElement.cpp & radio.js)
+ "RadioStateChange", "popupshown", "popuphiding", "DOMMenuInactive",
+ "DOMMenuItemActive", "DOMMenuItemInactive", "DOMMenuBarActive",
+ "DOMMenuBarInactive", "scroll", "DOMTitleChanged"};
+
+nsresult RootAccessible::AddEventListeners() {
+ // EventTarget interface allows to register event listeners to
+ // receive untrusted events (synthetic events generated by untrusted code).
+ // For example, XBL bindings implementations for elements that are hosted in
+ // non chrome document fire untrusted events.
+ // We must use the window's parent target in order to receive events from
+ // iframes and shadow DOM; e.g. ValueChange events from a <select> in an
+ // iframe or shadow DOM. The root document itself doesn't receive these.
+ nsPIDOMWindowOuter* window = mDocumentNode->GetWindow();
+ nsCOMPtr<EventTarget> nstarget = window ? window->GetParentTarget() : nullptr;
+
+ if (nstarget) {
+ for (const char *const *e = kEventTypes, *const *e_end =
+ ArrayEnd(kEventTypes);
+ e < e_end; ++e) {
+ nsresult rv = nstarget->AddEventListener(NS_ConvertASCIItoUTF16(*e), this,
+ true, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return DocAccessible::AddEventListeners();
+}
+
+nsresult RootAccessible::RemoveEventListeners() {
+ nsPIDOMWindowOuter* window = mDocumentNode->GetWindow();
+ nsCOMPtr<EventTarget> target = window ? window->GetParentTarget() : nullptr;
+ if (target) {
+ for (const char *const *e = kEventTypes, *const *e_end =
+ ArrayEnd(kEventTypes);
+ e < e_end; ++e) {
+ target->RemoveEventListener(NS_ConvertASCIItoUTF16(*e), this, true);
+ }
+ }
+
+ // Do this before removing clearing caret accessible, so that it can use
+ // shutdown the caret accessible's selection listener
+ DocAccessible::RemoveEventListeners();
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// public
+
+void RootAccessible::DocumentActivated(DocAccessible* aDocument) {}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIDOMEventListener
+
+NS_IMETHODIMP
+RootAccessible::HandleEvent(Event* aDOMEvent) {
+ MOZ_ASSERT(aDOMEvent);
+ if (IsDefunct()) {
+ // Even though we've been shut down, RemoveEventListeners might not have
+ // removed the event handlers on the window's parent target if GetWindow
+ // returned null, so we might still get events here in this case. We should
+ // just ignore these events.
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsINode> origTargetNode =
+ do_QueryInterface(aDOMEvent->GetOriginalTarget());
+ if (!origTargetNode) return NS_OK;
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDOMEvents)) {
+ nsAutoString eventType;
+ aDOMEvent->GetType(eventType);
+ logging::DOMEvent("handled", origTargetNode, eventType);
+ }
+#endif
+
+ DocAccessible* document =
+ GetAccService()->GetDocAccessible(origTargetNode->OwnerDoc());
+
+ if (document) {
+ nsAutoString eventType;
+ aDOMEvent->GetType(eventType);
+ if (eventType.EqualsLiteral("scroll")) {
+ // We don't put this in the notification queue for 2 reasons:
+ // 1. We will flood the queue with repetitive events.
+ // 2. Since this doesn't necessarily touch layout, we are not
+ // guaranteed to have a WillRefresh tick any time soon.
+ document->HandleScroll(origTargetNode);
+ } else {
+ // Root accessible exists longer than any of its descendant documents so
+ // that we are guaranteed notification is processed before root accessible
+ // is destroyed.
+ // For shadow DOM, GetOriginalTarget on the Event returns null if we
+ // process the event async, so we must pass the target node as well.
+ document->HandleNotification<RootAccessible, Event, nsINode>(
+ this, &RootAccessible::ProcessDOMEvent, aDOMEvent, origTargetNode);
+ }
+ }
+
+ return NS_OK;
+}
+
+// RootAccessible protected
+void RootAccessible::ProcessDOMEvent(Event* aDOMEvent, nsINode* aTarget) {
+ MOZ_ASSERT(aDOMEvent);
+ MOZ_ASSERT(aTarget);
+
+ nsAutoString eventType;
+ aDOMEvent->GetType(eventType);
+
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eDOMEvents)) {
+ logging::DOMEvent("processed", aTarget, eventType);
+ }
+#endif
+
+ if (eventType.EqualsLiteral("popuphiding")) {
+ HandlePopupHidingEvent(aTarget);
+ return;
+ }
+
+ DocAccessible* targetDocument =
+ GetAccService()->GetDocAccessible(aTarget->OwnerDoc());
+ if (!targetDocument) {
+ // Document has ceased to exist.
+ return;
+ }
+
+ if (eventType.EqualsLiteral("popupshown") &&
+ aTarget->IsAnyOfXULElements(nsGkAtoms::tooltip, nsGkAtoms::panel)) {
+ targetDocument->ContentInserted(aTarget->AsContent(),
+ aTarget->GetNextSibling());
+ return;
+ }
+
+ LocalAccessible* accessible =
+ targetDocument->GetAccessibleOrContainer(aTarget);
+ if (!accessible) return;
+
+ if (accessible->IsDoc() && eventType.EqualsLiteral("DOMTitleChanged")) {
+ targetDocument->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE,
+ accessible);
+ return;
+ }
+
+ XULTreeAccessible* treeAcc = accessible->AsXULTree();
+ if (treeAcc) {
+ if (eventType.EqualsLiteral("TreeRowCountChanged")) {
+ HandleTreeRowCountChangedEvent(aDOMEvent, treeAcc);
+ return;
+ }
+
+ if (eventType.EqualsLiteral("TreeInvalidated")) {
+ HandleTreeInvalidatedEvent(aDOMEvent, treeAcc);
+ return;
+ }
+ }
+
+ if (eventType.EqualsLiteral("RadioStateChange")) {
+ uint64_t state = accessible->State();
+ bool isEnabled = (state & (states::CHECKED | states::SELECTED)) != 0;
+
+ if (accessible->NeedsDOMUIEvent()) {
+ RefPtr<AccEvent> accEvent =
+ new AccStateChangeEvent(accessible, states::CHECKED, isEnabled);
+ nsEventShell::FireEvent(accEvent);
+ }
+
+ if (isEnabled) {
+ FocusMgr()->ActiveItemChanged(accessible);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("RadioStateChange", accessible);
+ }
+#endif
+ }
+
+ return;
+ }
+
+ if (eventType.EqualsLiteral("CheckboxStateChange")) {
+ if (accessible->NeedsDOMUIEvent()) {
+ uint64_t state = accessible->State();
+ bool isEnabled = !!(state & states::CHECKED);
+
+ RefPtr<AccEvent> accEvent =
+ new AccStateChangeEvent(accessible, states::CHECKED, isEnabled);
+ nsEventShell::FireEvent(accEvent);
+ }
+ return;
+ }
+
+ LocalAccessible* treeItemAcc = nullptr;
+ // If it's a tree element, need the currently selected item.
+ if (treeAcc) {
+ treeItemAcc = accessible->CurrentItem();
+ if (treeItemAcc) accessible = treeItemAcc;
+ }
+
+ if (treeItemAcc && eventType.EqualsLiteral("OpenStateChange")) {
+ uint64_t state = accessible->State();
+ bool isEnabled = (state & states::EXPANDED) != 0;
+
+ RefPtr<AccEvent> accEvent =
+ new AccStateChangeEvent(accessible, states::EXPANDED, isEnabled);
+ nsEventShell::FireEvent(accEvent);
+ return;
+ }
+
+ nsINode* targetNode = accessible->GetNode();
+ if (treeItemAcc && eventType.EqualsLiteral("select")) {
+ // XXX: We shouldn't be based on DOM select event which doesn't provide us
+ // any context info. We should integrate into nsTreeSelection instead.
+ // If multiselect tree, we should fire selectionadd or selection removed
+ if (FocusMgr()->HasDOMFocus(targetNode)) {
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> multiSel =
+ targetNode->AsElement()->AsXULMultiSelectControl();
+ if (!multiSel) {
+ // This shouldn't be possible. All XUL trees should have
+ // nsIDOMXULMultiSelectControlElement, and the tree is focused, so it
+ // shouldn't be dying. Nevertheless, this sometimes happens in the wild
+ // (bug 1597043).
+ MOZ_ASSERT_UNREACHABLE(
+ "XUL tree doesn't have nsIDOMXULMultiSelectControlElement");
+ return;
+ }
+ nsAutoString selType;
+ multiSel->GetSelType(selType);
+ if (selType.IsEmpty() || !selType.EqualsLiteral("single")) {
+ // XXX: We need to fire EVENT_SELECTION_ADD and EVENT_SELECTION_REMOVE
+ // for each tree item. Perhaps each tree item will need to cache its
+ // selection state and fire an event after a DOM "select" event when
+ // that state changes. XULTreeAccessible::UpdateTreeSelection();
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SELECTION_WITHIN,
+ accessible);
+ return;
+ }
+
+ RefPtr<AccSelChangeEvent> selChangeEvent = new AccSelChangeEvent(
+ treeAcc, treeItemAcc, AccSelChangeEvent::eSelectionAdd);
+ nsEventShell::FireEvent(selChangeEvent);
+ return;
+ }
+ } else if (eventType.EqualsLiteral("AlertActive")) {
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_ALERT, accessible);
+ } else if (eventType.EqualsLiteral("popupshown")) {
+ HandlePopupShownEvent(accessible);
+ } else if (eventType.EqualsLiteral("DOMMenuInactive")) {
+ if (accessible->Role() == roles::MENUPOPUP) {
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_END,
+ accessible);
+ }
+ if (auto* focus = FocusMgr()->FocusedLocalAccessible()) {
+ // Intentionally use the content tree, because Linux strips menupopups
+ // from the a11y tree so accessible might be an arbitrary ancestor.
+ if (focus->GetContent() &&
+ focus->GetContent()->IsShadowIncludingInclusiveDescendantOf(
+ aTarget)) {
+ // Move the focus to the topmost menu active content if any. The
+ // menu item in the parent menu will not fire a DOMMenuItemActive
+ // event if it's already active.
+ LocalAccessible* newActiveAccessible = nullptr;
+ if (auto* pm = nsXULPopupManager::GetInstance()) {
+ if (auto* content = pm->GetTopActiveMenuItemContent()) {
+ newActiveAccessible =
+ accessible->Document()->GetAccessible(content);
+ }
+ }
+ FocusMgr()->ActiveItemChanged(newActiveAccessible);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("DOMMenuInactive",
+ newActiveAccessible);
+ }
+#endif
+ }
+ }
+ } else if (eventType.EqualsLiteral("DOMMenuItemActive")) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::ACTIVE, true);
+ nsEventShell::FireEvent(event);
+ FocusMgr()->ActiveItemChanged(accessible);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("DOMMenuItemActive", accessible);
+ }
+#endif
+ } else if (eventType.EqualsLiteral("DOMMenuItemInactive")) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(accessible, states::ACTIVE, false);
+ nsEventShell::FireEvent(event);
+
+ // Process DOMMenuItemInactive event for autocomplete only because this is
+ // unique widget that may acquire focus from autocomplete popup while popup
+ // stays open and has no active item. In case of XUL tree autocomplete
+ // popup this event is fired for tree accessible.
+ LocalAccessible* widget =
+ accessible->IsWidget() ? accessible : accessible->ContainerWidget();
+ if (widget && widget->IsAutoCompletePopup()) {
+ FocusMgr()->ActiveItemChanged(nullptr);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("DOMMenuItemInactive", accessible);
+ }
+#endif
+ }
+ } else if (eventType.EqualsLiteral(
+ "DOMMenuBarActive")) { // Always from user input
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENU_START, accessible,
+ eFromUserInput);
+
+ // Notify of active item change when menubar gets active and if it has
+ // current item. This is a case of mouseover (set current menuitem) and
+ // mouse click (activate the menubar). If menubar doesn't have current item
+ // (can be a case of menubar activation from keyboard) then ignore this
+ // notification because later we'll receive DOMMenuItemActive event after
+ // current menuitem is set.
+ LocalAccessible* activeItem = accessible->CurrentItem();
+ if (activeItem) {
+ FocusMgr()->ActiveItemChanged(activeItem);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("DOMMenuBarActive", accessible);
+ }
+#endif
+ }
+ } else if (eventType.EqualsLiteral(
+ "DOMMenuBarInactive")) { // Always from user input
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENU_END, accessible,
+ eFromUserInput);
+
+ FocusMgr()->ActiveItemChanged(nullptr);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("DOMMenuBarInactive", accessible);
+ }
+#endif
+ } else if (accessible->NeedsDOMUIEvent() &&
+ eventType.EqualsLiteral("ValueChange")) {
+ uint32_t event = accessible->HasNumericValue()
+ ? nsIAccessibleEvent::EVENT_VALUE_CHANGE
+ : nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE;
+ targetDocument->FireDelayedEvent(event, accessible);
+ }
+#ifdef DEBUG_DRAGDROPSTART
+ else if (eventType.EqualsLiteral("mouseover")) {
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_DRAGDROP_START,
+ accessible);
+ }
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible
+
+void RootAccessible::Shutdown() {
+ // Called manually or by LocalAccessible::LastRelease()
+ if (HasShutdown()) {
+ return;
+ }
+ DocAccessibleWrap::Shutdown();
+}
+
+Relation RootAccessible::RelationByType(RelationType aType) const {
+ if (!mDocumentNode || aType != RelationType::EMBEDS) {
+ return DocAccessibleWrap::RelationByType(aType);
+ }
+
+ if (RemoteAccessible* remoteDoc = GetPrimaryRemoteTopLevelContentDoc()) {
+ return Relation(remoteDoc);
+ }
+
+ if (nsIDocShell* docShell = mDocumentNode->GetDocShell()) {
+ nsCOMPtr<nsIDocShellTreeOwner> owner;
+ docShell->GetTreeOwner(getter_AddRefs(owner));
+ if (owner) {
+ nsCOMPtr<nsIDocShellTreeItem> contentShell;
+ owner->GetPrimaryContentShell(getter_AddRefs(contentShell));
+ if (contentShell) {
+ return Relation(nsAccUtils::GetDocAccessibleFor(contentShell));
+ }
+ }
+ }
+
+ return Relation();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Protected members
+
+void RootAccessible::HandlePopupShownEvent(LocalAccessible* aAccessible) {
+ roles::Role role = aAccessible->Role();
+
+ if (role == roles::MENUPOPUP) {
+ // Don't fire menupopup events for combobox and autocomplete lists.
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START,
+ aAccessible);
+ return;
+ }
+
+ if (role == roles::COMBOBOX_LIST) {
+ // Fire expanded state change event for comboboxes and autocompeletes.
+ LocalAccessible* combobox = aAccessible->LocalParent();
+ if (!combobox) return;
+
+ if (combobox->IsCombobox()) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(combobox, states::EXPANDED, true);
+ nsEventShell::FireEvent(event);
+ }
+
+ // If aria-activedescendant is present, redirect focus.
+ // This is needed for parent process <select> dropdowns, which use a
+ // menulist containing div elements instead of XUL menuitems. XUL menuitems
+ // fire DOMMenuItemActive events from layout instead.
+ MOZ_ASSERT(aAccessible->Elm());
+ if (aAccessible->Elm()->HasAttr(nsGkAtoms::aria_activedescendant)) {
+ LocalAccessible* activeDescendant = aAccessible->CurrentItem();
+ if (activeDescendant) {
+ FocusMgr()->ActiveItemChanged(activeDescendant, false);
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eFocus)) {
+ logging::ActiveItemChangeCausedBy("ARIA activedescendant on popup",
+ activeDescendant);
+ }
+#endif
+ }
+ }
+ }
+}
+
+void RootAccessible::HandlePopupHidingEvent(nsINode* aPopupNode) {
+ DocAccessible* document = nsAccUtils::GetDocAccessibleFor(aPopupNode);
+ if (!document) {
+ return;
+ }
+
+ if (aPopupNode->IsAnyOfXULElements(nsGkAtoms::tooltip, nsGkAtoms::panel)) {
+ document->ContentRemoved(aPopupNode->AsContent());
+ return;
+ }
+
+ // Get popup accessible. There are cases when popup element isn't accessible
+ // but an underlying widget is and behaves like popup, an example is
+ // autocomplete popups.
+ LocalAccessible* popup = document->GetAccessible(aPopupNode);
+ if (!popup) {
+ LocalAccessible* popupContainer =
+ document->GetContainerAccessible(aPopupNode);
+ if (!popupContainer) {
+ return;
+ }
+
+ uint32_t childCount = popupContainer->ChildCount();
+ for (uint32_t idx = 0; idx < childCount; idx++) {
+ LocalAccessible* child = popupContainer->LocalChildAt(idx);
+ if (child->IsAutoCompletePopup()) {
+ popup = child;
+ break;
+ }
+ }
+
+ // No popup no events. Focus is managed by DOM. This is a case for
+ // menupopups of menus on Linux since there are no accessible for popups.
+ if (!popup) {
+ return;
+ }
+ }
+
+ // In case of autocompletes and comboboxes fire state change event for
+ // expanded state. Note, HTML form autocomplete isn't a subject of state
+ // change event because they aren't autocompletes strictly speaking.
+
+ // HTML select is target of popuphidding event. Otherwise get container
+ // widget. No container widget means this is either tooltip or menupopup.
+ // No events in the former case.
+ LocalAccessible* widget = nullptr;
+ if (popup->IsCombobox()) {
+ widget = popup;
+ } else {
+ widget = popup->ContainerWidget();
+ if (!widget) {
+ if (!popup->IsMenuPopup()) {
+ return;
+ }
+ widget = popup;
+ }
+ }
+
+ // Fire expanded state change event.
+ if (widget->IsCombobox()) {
+ RefPtr<AccEvent> event =
+ new AccStateChangeEvent(widget, states::EXPANDED, false);
+ document->FireDelayedEvent(event);
+ }
+}
+
+static void GetPropertyBagFromEvent(Event* aEvent,
+ nsIPropertyBag2** aPropertyBag) {
+ *aPropertyBag = nullptr;
+
+ CustomEvent* customEvent = aEvent->AsCustomEvent();
+ if (!customEvent) return;
+
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(customEvent->GetParentObject())) return;
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JS::Value> detail(cx);
+ customEvent->GetDetail(cx, &detail);
+ if (!detail.isObject()) return;
+
+ JS::Rooted<JSObject*> detailObj(cx, &detail.toObject());
+
+ nsresult rv;
+ nsCOMPtr<nsIPropertyBag2> propBag;
+ rv = UnwrapArg<nsIPropertyBag2>(cx, detailObj, getter_AddRefs(propBag));
+ if (NS_FAILED(rv)) return;
+
+ propBag.forget(aPropertyBag);
+}
+
+void RootAccessible::HandleTreeRowCountChangedEvent(
+ Event* aEvent, XULTreeAccessible* aAccessible) {
+ nsCOMPtr<nsIPropertyBag2> propBag;
+ GetPropertyBagFromEvent(aEvent, getter_AddRefs(propBag));
+ if (!propBag) return;
+
+ nsresult rv;
+ int32_t index, count;
+ rv = propBag->GetPropertyAsInt32(u"index"_ns, &index);
+ if (NS_FAILED(rv)) return;
+
+ rv = propBag->GetPropertyAsInt32(u"count"_ns, &count);
+ if (NS_FAILED(rv)) return;
+
+ aAccessible->InvalidateCache(index, count);
+}
+
+void RootAccessible::HandleTreeInvalidatedEvent(
+ Event* aEvent, XULTreeAccessible* aAccessible) {
+ nsCOMPtr<nsIPropertyBag2> propBag;
+ GetPropertyBagFromEvent(aEvent, getter_AddRefs(propBag));
+ if (!propBag) return;
+
+ int32_t startRow = 0, endRow = -1, startCol = 0, endCol = -1;
+ propBag->GetPropertyAsInt32(u"startrow"_ns, &startRow);
+ propBag->GetPropertyAsInt32(u"endrow"_ns, &endRow);
+ propBag->GetPropertyAsInt32(u"startcolumn"_ns, &startCol);
+ propBag->GetPropertyAsInt32(u"endcolumn"_ns, &endCol);
+
+ aAccessible->TreeViewInvalidated(startRow, endRow, startCol, endCol);
+}
+
+RemoteAccessible* RootAccessible::GetPrimaryRemoteTopLevelContentDoc() const {
+ nsCOMPtr<nsIDocShellTreeOwner> owner;
+ mDocumentNode->GetDocShell()->GetTreeOwner(getter_AddRefs(owner));
+ NS_ENSURE_TRUE(owner, nullptr);
+
+ nsCOMPtr<nsIRemoteTab> remoteTab;
+ owner->GetPrimaryRemoteTab(getter_AddRefs(remoteTab));
+ if (!remoteTab) {
+ return nullptr;
+ }
+
+ auto tab = static_cast<dom::BrowserHost*>(remoteTab.get());
+ return tab->GetTopLevelDocAccessible();
+}
diff --git a/accessible/generic/RootAccessible.h b/accessible/generic/RootAccessible.h
new file mode 100644
index 0000000000..b1e7e42fdb
--- /dev/null
+++ b/accessible/generic/RootAccessible.h
@@ -0,0 +1,93 @@
+/* -*- 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_RootAccessible_h__
+#define mozilla_a11y_RootAccessible_h__
+
+#include "HyperTextAccessible.h"
+#include "DocAccessibleWrap.h"
+
+#include "nsIDOMEventListener.h"
+
+namespace mozilla {
+
+class PresShell;
+
+namespace a11y {
+
+/**
+ * The node at a root of the accessibility tree. This node originated in the
+ * current process. If this is the parent process, RootAccessible is the
+ * Accessible for the top-level window. If this is a content process,
+ * RootAccessible is a top-level content document in this process, which is
+ * either a tab document or an out-of-process iframe.
+ */
+class RootAccessible : public DocAccessibleWrap, public nsIDOMEventListener {
+ NS_DECL_ISUPPORTS_INHERITED
+
+ public:
+ RootAccessible(dom::Document* aDocument, PresShell* aPresShell);
+
+ // nsIDOMEventListener
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual mozilla::a11y::ENameValueFlag Name(nsString& aName) const override;
+ virtual Relation RelationByType(RelationType aType) const override;
+ virtual uint64_t NativeState() const override;
+
+ // RootAccessible
+
+ /**
+ * Notify that the sub document presshell was activated.
+ */
+ virtual void DocumentActivated(DocAccessible* aDocument);
+
+ /**
+ * Return the primary remote top level document if any.
+ */
+ RemoteAccessible* GetPrimaryRemoteTopLevelContentDoc() const;
+
+ protected:
+ virtual ~RootAccessible();
+
+ /**
+ * Add/remove DOM event listeners.
+ */
+ virtual nsresult AddEventListeners() override;
+ virtual nsresult RemoveEventListeners() override;
+
+ /**
+ * Process the DOM event.
+ */
+ void ProcessDOMEvent(dom::Event* aDOMEvent, nsINode* aTarget);
+
+ /**
+ * Process "popupshown" event. Used by HandleEvent().
+ */
+ void HandlePopupShownEvent(LocalAccessible* aAccessible);
+
+ /*
+ * Process "popuphiding" event. Used by HandleEvent().
+ */
+ void HandlePopupHidingEvent(nsINode* aNode);
+
+ void HandleTreeRowCountChangedEvent(dom::Event* aEvent,
+ XULTreeAccessible* aAccessible);
+ void HandleTreeInvalidatedEvent(dom::Event* aEvent,
+ XULTreeAccessible* aAccessible);
+
+ uint32_t GetChromeFlags() const;
+};
+
+inline RootAccessible* LocalAccessible::AsRoot() {
+ return IsRoot() ? static_cast<mozilla::a11y::RootAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/TextLeafAccessible.cpp b/accessible/generic/TextLeafAccessible.cpp
new file mode 100644
index 0000000000..42dc07f228
--- /dev/null
+++ b/accessible/generic/TextLeafAccessible.cpp
@@ -0,0 +1,42 @@
+/* -*- 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 "TextLeafAccessible.h"
+
+#include "mozilla/a11y/Role.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// TextLeafAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+TextLeafAccessible::TextLeafAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : LinkableAccessible(aContent, aDoc) {
+ mType = eTextLeafType;
+ mGenericTypes |= eText;
+ mStateFlags |= eNoKidsFromDOM;
+}
+
+TextLeafAccessible::~TextLeafAccessible() {}
+
+role TextLeafAccessible::NativeRole() const {
+ nsIFrame* frame = GetFrame();
+ if (frame && frame->IsGeneratedContentFrame()) return roles::STATICTEXT;
+
+ return roles::TEXT_LEAF;
+}
+
+void TextLeafAccessible::AppendTextTo(nsAString& aText, uint32_t aStartOffset,
+ uint32_t aLength) {
+ aText.Append(Substring(mText, aStartOffset, aLength));
+}
+
+ENameValueFlag TextLeafAccessible::Name(nsString& aName) const {
+ // Text node, ARIA can't be used.
+ aName = mText;
+ return eNameOK;
+}
diff --git a/accessible/generic/TextLeafAccessible.h b/accessible/generic/TextLeafAccessible.h
new file mode 100644
index 0000000000..65d59a2aad
--- /dev/null
+++ b/accessible/generic/TextLeafAccessible.h
@@ -0,0 +1,46 @@
+/* -*- 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_TextLeafAccessible_h__
+#define mozilla_a11y_TextLeafAccessible_h__
+
+#include "BaseAccessibles.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Generic class used for text nodes.
+ */
+class TextLeafAccessible : public LinkableAccessible {
+ public:
+ TextLeafAccessible(nsIContent* aContent, DocAccessible* aDoc);
+ virtual ~TextLeafAccessible();
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual void AppendTextTo(nsAString& aText, uint32_t aStartOffset = 0,
+ uint32_t aLength = UINT32_MAX) override;
+ virtual ENameValueFlag Name(nsString& aName) const override;
+
+ // TextLeafAccessible
+ void SetText(const nsAString& aText) { mText = aText; }
+ const nsString& Text() const { return mText; }
+
+ protected:
+ nsString mText;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible downcast method
+
+inline TextLeafAccessible* LocalAccessible::AsTextLeaf() {
+ return IsTextLeaf() ? static_cast<TextLeafAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/generic/moz.build b/accessible/generic/moz.build
new file mode 100644
index 0000000000..06e229b999
--- /dev/null
+++ b/accessible/generic/moz.build
@@ -0,0 +1,63 @@
+# -*- 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 += [
+ "DocAccessible.h",
+ "HyperTextAccessible.h",
+ "LocalAccessible.h",
+ "OuterDocAccessible.h",
+]
+
+UNIFIED_SOURCES += [
+ "ApplicationAccessible.cpp",
+ "ARIAGridAccessible.cpp",
+ "BaseAccessibles.cpp",
+ "DocAccessible.cpp",
+ "FormControlAccessible.cpp",
+ "HyperTextAccessible.cpp",
+ "ImageAccessible.cpp",
+ "LocalAccessible.cpp",
+ "OuterDocAccessible.cpp",
+ "RootAccessible.cpp",
+ "TextLeafAccessible.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/html",
+ "/accessible/xpcom",
+ "/accessible/xul",
+ "/dom/base",
+ "/dom/xul",
+ "/layout/generic",
+ "/layout/xul",
+]
+
+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",
+ ]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/accessible/html/HTMLCanvasAccessible.cpp b/accessible/html/HTMLCanvasAccessible.cpp
new file mode 100644
index 0000000000..d6b02ce61c
--- /dev/null
+++ b/accessible/html/HTMLCanvasAccessible.cpp
@@ -0,0 +1,16 @@
+/* -*- 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 "HTMLCanvasAccessible.h"
+
+#include "mozilla/a11y/Role.h"
+
+using namespace mozilla::a11y;
+
+HTMLCanvasAccessible::HTMLCanvasAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+role HTMLCanvasAccessible::NativeRole() const { return roles::CANVAS; }
diff --git a/accessible/html/HTMLCanvasAccessible.h b/accessible/html/HTMLCanvasAccessible.h
new file mode 100644
index 0000000000..49d443bce1
--- /dev/null
+++ b/accessible/html/HTMLCanvasAccessible.h
@@ -0,0 +1,35 @@
+/* -*- 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_HTMLCanvasAccessible_h__
+#define mozilla_a11y_HTMLCanvasAccessible_h__
+
+#include "HyperTextAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * HTML canvas accessible (html:canvas).
+ */
+class HTMLCanvasAccessible : public HyperTextAccessible {
+ public:
+ HTMLCanvasAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLCanvasAccessible,
+ HyperTextAccessible)
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+
+ protected:
+ virtual ~HTMLCanvasAccessible() {}
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/html/HTMLElementAccessibles.cpp b/accessible/html/HTMLElementAccessibles.cpp
new file mode 100644
index 0000000000..e01de31ff2
--- /dev/null
+++ b/accessible/html/HTMLElementAccessibles.cpp
@@ -0,0 +1,231 @@
+/* -*- 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 "HTMLElementAccessibles.h"
+
+#include "CacheConstants.h"
+#include "nsCoreUtils.h"
+#include "nsTextEquivUtils.h"
+#include "Relation.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+
+#include "mozilla/dom/HTMLLabelElement.h"
+#include "mozilla/dom/HTMLDetailsElement.h"
+#include "mozilla/dom/HTMLSummaryElement.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLHRAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role HTMLHRAccessible::NativeRole() const { return roles::SEPARATOR; }
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLBRAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role HTMLBRAccessible::NativeRole() const { return roles::WHITESPACE; }
+
+uint64_t HTMLBRAccessible::NativeState() const { return states::READONLY; }
+
+ENameValueFlag HTMLBRAccessible::NativeName(nsString& aName) const {
+ aName = static_cast<char16_t>('\n'); // Newline char
+ return eNameOK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLLabelAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+ENameValueFlag HTMLLabelAccessible::NativeName(nsString& aName) const {
+ nsTextEquivUtils::GetNameFromSubtree(this, aName);
+ return aName.IsEmpty() ? eNameOK : eNameFromSubtree;
+}
+
+Relation HTMLLabelAccessible::RelationByType(RelationType aType) const {
+ Relation rel = AccessibleWrap::RelationByType(aType);
+ if (aType == RelationType::LABEL_FOR) {
+ dom::HTMLLabelElement* label = dom::HTMLLabelElement::FromNode(mContent);
+ rel.AppendTarget(mDoc, label->GetControl());
+ }
+
+ return rel;
+}
+
+void HTMLLabelAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ HyperTextAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
+ aOldValue, aOldState);
+
+ if (aAttribute == nsGkAtoms::_for) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations | CacheDomain::Actions);
+ }
+}
+
+bool HTMLLabelAccessible::HasPrimaryAction() const {
+ return nsCoreUtils::IsLabelWithControl(mContent);
+}
+
+void HTMLLabelAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == 0) {
+ if (HasPrimaryAction()) {
+ aName.AssignLiteral("click");
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsHTMLOuputAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+Relation HTMLOutputAccessible::RelationByType(RelationType aType) const {
+ Relation rel = AccessibleWrap::RelationByType(aType);
+ if (aType == RelationType::CONTROLLED_BY) {
+ rel.AppendIter(new IDRefsIterator(mDoc, mContent, nsGkAtoms::_for));
+ }
+
+ return rel;
+}
+
+void HTMLOutputAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ HyperTextAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
+ aOldValue, aOldState);
+
+ if (aAttribute == nsGkAtoms::_for) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLSummaryAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLSummaryAccessible::HTMLSummaryAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mGenericTypes |= eButton;
+}
+
+bool HTMLSummaryAccessible::HasPrimaryAction() const { return true; }
+
+void HTMLSummaryAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex != eAction_Click) {
+ return;
+ }
+
+ dom::HTMLSummaryElement* summary =
+ dom::HTMLSummaryElement::FromNode(mContent);
+ if (!summary) {
+ return;
+ }
+
+ dom::HTMLDetailsElement* details = summary->GetDetails();
+ if (!details) {
+ return;
+ }
+
+ if (details->Open()) {
+ aName.AssignLiteral("collapse");
+ } else {
+ aName.AssignLiteral("expand");
+ }
+}
+
+uint64_t HTMLSummaryAccessible::NativeState() const {
+ uint64_t state = HyperTextAccessible::NativeState();
+
+ dom::HTMLSummaryElement* summary =
+ dom::HTMLSummaryElement::FromNode(mContent);
+ if (!summary) {
+ return state;
+ }
+
+ dom::HTMLDetailsElement* details = summary->GetDetails();
+ if (!details) {
+ return state;
+ }
+
+ if (details->Open()) {
+ state |= states::EXPANDED;
+ } else {
+ state |= states::COLLAPSED;
+ }
+
+ return state;
+}
+
+HTMLSummaryAccessible* HTMLSummaryAccessible::FromDetails(
+ LocalAccessible* details) {
+ if (!dom::HTMLDetailsElement::FromNodeOrNull(details->GetContent())) {
+ return nullptr;
+ }
+
+ HTMLSummaryAccessible* summaryAccessible = nullptr;
+ for (uint32_t i = 0; i < details->ChildCount(); i++) {
+ // Iterate through the children of our details accessible to locate main
+ // summary. This iteration includes the anonymous summary if the details
+ // element was not explicitly created with one.
+ LocalAccessible* child = details->LocalChildAt(i);
+ auto* summary =
+ mozilla::dom::HTMLSummaryElement::FromNodeOrNull(child->GetContent());
+ if (summary && summary->IsMainSummary()) {
+ summaryAccessible = static_cast<HTMLSummaryAccessible*>(child);
+ break;
+ }
+ }
+
+ return summaryAccessible;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLSummaryAccessible: Widgets
+
+bool HTMLSummaryAccessible::IsWidget() const { return true; }
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLHeaderOrFooterAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role HTMLHeaderOrFooterAccessible::NativeRole() const {
+ // Only map header and footer if they are direct descendants of the body tag.
+ // If other sectioning or sectioning root elements, they become sections.
+ nsIContent* parent = mContent->GetParent();
+ while (parent) {
+ if (parent->IsAnyOfHTMLElements(
+ nsGkAtoms::article, nsGkAtoms::aside, nsGkAtoms::nav,
+ nsGkAtoms::section, nsGkAtoms::main, nsGkAtoms::blockquote,
+ nsGkAtoms::details, nsGkAtoms::dialog, nsGkAtoms::fieldset,
+ nsGkAtoms::figure, nsGkAtoms::td)) {
+ break;
+ }
+ parent = parent->GetParent();
+ }
+
+ // No sectioning or sectioning root elements found.
+ if (!parent) {
+ return roles::LANDMARK;
+ }
+
+ return roles::SECTION;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLSectionAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role HTMLSectionAccessible::NativeRole() const {
+ nsAutoString name;
+ const_cast<HTMLSectionAccessible*>(this)->Name(name);
+ return name.IsEmpty() ? roles::SECTION : roles::REGION;
+}
diff --git a/accessible/html/HTMLElementAccessibles.h b/accessible/html/HTMLElementAccessibles.h
new file mode 100644
index 0000000000..5a3ec6cef8
--- /dev/null
+++ b/accessible/html/HTMLElementAccessibles.h
@@ -0,0 +1,159 @@
+/* -*- 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_HTMLElementAccessibles_h__
+#define mozilla_a11y_HTMLElementAccessibles_h__
+
+#include "BaseAccessibles.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Used for HTML hr element.
+ */
+class HTMLHRAccessible : public LeafAccessible {
+ public:
+ HTMLHRAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {}
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+};
+
+/**
+ * Used for HTML br element.
+ */
+class HTMLBRAccessible : public LeafAccessible {
+ public:
+ HTMLBRAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {
+ mType = eHTMLBRType;
+ mGenericTypes |= eText;
+ }
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+};
+
+/**
+ * Used for HTML label element.
+ */
+class HTMLLabelAccessible : public HyperTextAccessible {
+ public:
+ HTMLLabelAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLLabelAccessible, HyperTextAccessible)
+
+ // LocalAccessible
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ // ActionAccessible
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual bool HasPrimaryAction() const override;
+
+ protected:
+ virtual ~HTMLLabelAccessible() {}
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+};
+
+/**
+ * Used for HTML output element.
+ */
+class HTMLOutputAccessible : public HyperTextAccessible {
+ public:
+ HTMLOutputAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLOutputAccessible,
+ HyperTextAccessible)
+
+ // LocalAccessible
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+
+ protected:
+ virtual ~HTMLOutputAccessible() {}
+};
+
+/**
+ * Accessible for the HTML summary element.
+ */
+class HTMLSummaryAccessible : public HyperTextAccessible {
+ public:
+ enum { eAction_Click = 0 };
+
+ HTMLSummaryAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // Check that the given LocalAccessible belongs to a details frame.
+ // If so, find and return the accessible for the detail frame's
+ // main summary.
+ static HTMLSummaryAccessible* FromDetails(LocalAccessible* aDetails);
+
+ // LocalAccessible
+ virtual uint64_t NativeState() const override;
+
+ // ActionAccessible
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual bool HasPrimaryAction() const override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+};
+
+/**
+ * Used for HTML header and footer elements.
+ */
+class HTMLHeaderOrFooterAccessible : public HyperTextAccessible {
+ public:
+ HTMLHeaderOrFooterAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLHeaderOrFooterAccessible,
+ HyperTextAccessible)
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+
+ protected:
+ virtual ~HTMLHeaderOrFooterAccessible() {}
+};
+
+/**
+ * Used for HTML section element.
+ */
+class HTMLSectionAccessible : public HyperTextAccessible {
+ public:
+ HTMLSectionAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLSectionAccessible,
+ HyperTextAccessible)
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+
+ protected:
+ virtual ~HTMLSectionAccessible() = default;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/html/HTMLFormControlAccessible.cpp b/accessible/html/HTMLFormControlAccessible.cpp
new file mode 100644
index 0000000000..95d6fed7b3
--- /dev/null
+++ b/accessible/html/HTMLFormControlAccessible.cpp
@@ -0,0 +1,979 @@
+/* -*- 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 "HTMLFormControlAccessible.h"
+
+#include "CacheConstants.h"
+#include "DocAccessible-inl.h"
+#include "LocalAccessible-inl.h"
+#include "nsAccUtils.h"
+#include "nsEventShell.h"
+#include "nsTextEquivUtils.h"
+#include "Relation.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+#include "TextLeafAccessible.h"
+
+#include "nsContentList.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/dom/HTMLTextAreaElement.h"
+#include "mozilla/dom/HTMLFormControlsCollection.h"
+#include "nsIFormControl.h"
+
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TextEditor.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLFormAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role HTMLFormAccessible::NativeRole() const {
+ nsAutoString name;
+ const_cast<HTMLFormAccessible*>(this)->Name(name);
+ return name.IsEmpty() ? roles::FORM : roles::FORM_LANDMARK;
+}
+
+void HTMLFormAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ HyperTextAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
+ aOldValue, aOldState);
+ if (aAttribute == nsGkAtoms::autocomplete) {
+ dom::HTMLFormElement* formEl = dom::HTMLFormElement::FromNode(mContent);
+
+ HTMLFormControlsCollection* controls = formEl->Elements();
+ uint32_t length = controls->Length();
+ for (uint32_t i = 0; i < length; i++) {
+ if (LocalAccessible* acc = mDoc->GetAccessible(controls->Item(i))) {
+ if (acc->IsTextField() && !acc->IsPassword()) {
+ if (!acc->Elm()->HasAttr(nsGkAtoms::list_) &&
+ !acc->Elm()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::autocomplete, nsGkAtoms::OFF,
+ eIgnoreCase)) {
+ RefPtr<AccEvent> stateChangeEvent =
+ new AccStateChangeEvent(acc, states::SUPPORTS_AUTOCOMPLETION);
+ mDoc->FireDelayedEvent(stateChangeEvent);
+ }
+ }
+ }
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLRadioButtonAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+uint64_t HTMLRadioButtonAccessible::NativeState() const {
+ uint64_t state = AccessibleWrap::NativeState();
+
+ state |= states::CHECKABLE;
+
+ HTMLInputElement* input = HTMLInputElement::FromNode(mContent);
+ if (input && input->Checked()) state |= states::CHECKED;
+
+ return state;
+}
+
+void HTMLRadioButtonAccessible::GetPositionAndSetSize(int32_t* aPosInSet,
+ int32_t* aSetSize) {
+ Unused << ComputeGroupAttributes(aPosInSet, aSetSize);
+}
+
+void HTMLRadioButtonAccessible::DOMAttributeChanged(
+ int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue, uint64_t aOldState) {
+ if (aAttribute == nsGkAtoms::name) {
+ // If our name changed, it's possible our MEMBER_OF relation
+ // also changed. Push a cache update for Relations.
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
+ } else {
+ // Otherwise, handle this attribute change the way our parent
+ // class wants us to handle it.
+ RadioButtonAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute,
+ aModType, aOldValue, aOldState);
+ }
+}
+
+Relation HTMLRadioButtonAccessible::ComputeGroupAttributes(
+ int32_t* aPosInSet, int32_t* aSetSize) const {
+ Relation rel = Relation();
+ int32_t namespaceId = mContent->NodeInfo()->NamespaceID();
+ nsAutoString tagName;
+ mContent->NodeInfo()->GetName(tagName);
+
+ nsAutoString type;
+ mContent->AsElement()->GetAttr(nsGkAtoms::type, type);
+ nsAutoString name;
+ mContent->AsElement()->GetAttr(nsGkAtoms::name, name);
+
+ RefPtr<nsContentList> inputElms;
+
+ nsCOMPtr<nsIFormControl> formControlNode(do_QueryInterface(mContent));
+ if (dom::Element* formElm = formControlNode->GetForm()) {
+ inputElms = NS_GetContentList(formElm, namespaceId, tagName);
+ } else {
+ inputElms = NS_GetContentList(mContent->OwnerDoc(), namespaceId, tagName);
+ }
+ NS_ENSURE_TRUE(inputElms, rel);
+
+ uint32_t inputCount = inputElms->Length(false);
+
+ // Compute posinset and setsize.
+ int32_t indexOf = 0;
+ int32_t count = 0;
+
+ for (uint32_t index = 0; index < inputCount; index++) {
+ nsIContent* inputElm = inputElms->Item(index, false);
+ if (inputElm->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ type, eCaseMatters) &&
+ inputElm->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name,
+ name, eCaseMatters) &&
+ mDoc->HasAccessible(inputElm)) {
+ count++;
+ rel.AppendTarget(mDoc->GetAccessible(inputElm));
+ if (inputElm == mContent) indexOf = count;
+ }
+ }
+
+ *aPosInSet = indexOf;
+ *aSetSize = count;
+ return rel;
+}
+
+Relation HTMLRadioButtonAccessible::RelationByType(RelationType aType) const {
+ if (aType == RelationType::MEMBER_OF) {
+ int32_t unusedPos, unusedSetSize;
+ return ComputeGroupAttributes(&unusedPos, &unusedSetSize);
+ }
+
+ return LocalAccessible::RelationByType(aType);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLButtonAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLButtonAccessible::HTMLButtonAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mGenericTypes |= eButton;
+}
+
+bool HTMLButtonAccessible::HasPrimaryAction() const { return true; }
+
+void HTMLButtonAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == eAction_Click) aName.AssignLiteral("press");
+}
+
+uint64_t HTMLButtonAccessible::NativeState() const {
+ uint64_t state = HyperTextAccessible::NativeState();
+
+ dom::Element* elm = Elm();
+ if (auto* popover = elm->GetEffectivePopoverTargetElement()) {
+ LocalAccessible* popoverAcc = mDoc->GetAccessible(popover);
+ if (!popoverAcc || !popoverAcc->IsAncestorOf(this)) {
+ if (popover->IsPopoverOpen()) {
+ state |= states::EXPANDED;
+ } else {
+ state |= states::COLLAPSED;
+ }
+ }
+ }
+
+ ElementState elmState = mContent->AsElement()->State();
+ if (elmState.HasState(ElementState::DEFAULT)) state |= states::DEFAULT;
+
+ return state;
+}
+
+role HTMLButtonAccessible::NativeRole() const { return roles::PUSHBUTTON; }
+
+ENameValueFlag HTMLButtonAccessible::NativeName(nsString& aName) const {
+ // No need to check @value attribute for buttons since this attribute results
+ // in native anonymous text node and the name is calculated from subtree.
+ // The same magic works for @alt and @value attributes in case of type="image"
+ // element that has no valid @src (note if input@type="image" has an image
+ // then neither @alt nor @value attributes are used to generate a visual label
+ // and thus we need to obtain the accessible name directly from attribute
+ // value). Also the same algorithm works in case of default labels for
+ // type="submit"/"reset"/"image" elements.
+
+ ENameValueFlag nameFlag = LocalAccessible::NativeName(aName);
+ if (!aName.IsEmpty() || !mContent->IsHTMLElement(nsGkAtoms::input) ||
+ !mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::image, eCaseMatters)) {
+ return nameFlag;
+ }
+
+ if (!mContent->AsElement()->GetAttr(nsGkAtoms::alt, aName)) {
+ mContent->AsElement()->GetAttr(nsGkAtoms::value, aName);
+ }
+
+ aName.CompressWhitespace();
+ return eNameOK;
+}
+
+void HTMLButtonAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ HyperTextAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
+ aOldValue, aOldState);
+
+ if (aAttribute == nsGkAtoms::value) {
+ dom::Element* elm = Elm();
+ if (elm->IsHTMLElement(nsGkAtoms::input) ||
+ (elm->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::image,
+ eCaseMatters) &&
+ !elm->HasAttr(nsGkAtoms::alt))) {
+ if (!nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_labelledby) &&
+ !nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_label)) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ }
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLButtonAccessible: Widgets
+
+bool HTMLButtonAccessible::IsWidget() const { return true; }
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLTextFieldAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLTextFieldAccessible::HTMLTextFieldAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mType = mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::password, eIgnoreCase)
+ ? eHTMLTextPasswordFieldType
+ : eHTMLTextFieldType;
+}
+
+role HTMLTextFieldAccessible::NativeRole() const {
+ if (mType == eHTMLTextPasswordFieldType) {
+ return roles::PASSWORD_TEXT;
+ }
+ if (mContent->AsElement()->HasAttr(nsGkAtoms::list_)) {
+ return roles::EDITCOMBOBOX;
+ }
+ return roles::ENTRY;
+}
+
+already_AddRefed<AccAttributes> HTMLTextFieldAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = HyperTextAccessible::NativeAttributes();
+
+ // Expose type for text input elements as it gives some useful context,
+ // especially for mobile.
+ if (const nsAttrValue* attr =
+ mContent->AsElement()->GetParsedAttr(nsGkAtoms::type)) {
+ RefPtr<nsAtom> inputType = attr->GetAsAtom();
+ if (inputType) {
+ if (!ARIARoleMap() && inputType == nsGkAtoms::search) {
+ attributes->SetAttribute(nsGkAtoms::xmlroles, nsGkAtoms::searchbox);
+ }
+ attributes->SetAttribute(nsGkAtoms::textInputType, inputType);
+ }
+ }
+ // If this element has the placeholder attribute set,
+ // and if that is not identical to the name, expose it as an object attribute.
+ nsString placeholderText;
+ if (mContent->AsElement()->GetAttr(nsGkAtoms::placeholder, placeholderText)) {
+ nsAutoString name;
+ const_cast<HTMLTextFieldAccessible*>(this)->Name(name);
+ if (!name.Equals(placeholderText)) {
+ attributes->SetAttribute(nsGkAtoms::placeholder,
+ std::move(placeholderText));
+ }
+ }
+
+ return attributes.forget();
+}
+
+ENameValueFlag HTMLTextFieldAccessible::Name(nsString& aName) const {
+ ENameValueFlag nameFlag = LocalAccessible::Name(aName);
+ if (!aName.IsEmpty()) return nameFlag;
+
+ // text inputs and textareas might have useful placeholder text
+ mContent->AsElement()->GetAttr(nsGkAtoms::placeholder, aName);
+ return eNameOK;
+}
+
+void HTMLTextFieldAccessible::Value(nsString& aValue) const {
+ aValue.Truncate();
+ if (NativeState() & states::PROTECTED) { // Don't return password text!
+ return;
+ }
+
+ HTMLTextAreaElement* textArea = HTMLTextAreaElement::FromNode(mContent);
+ if (textArea) {
+ textArea->GetValue(aValue);
+ return;
+ }
+
+ HTMLInputElement* input = HTMLInputElement::FromNode(mContent);
+ if (input) {
+ // Pass NonSystem as the caller type, to be safe. We don't expect to have a
+ // file input here.
+ input->GetValue(aValue, CallerType::NonSystem);
+ }
+}
+
+bool HTMLTextFieldAccessible::AttributeChangesState(nsAtom* aAttribute) {
+ if (aAttribute == nsGkAtoms::readonly || aAttribute == nsGkAtoms::list_ ||
+ aAttribute == nsGkAtoms::autocomplete) {
+ return true;
+ }
+
+ return LocalAccessible::AttributeChangesState(aAttribute);
+}
+
+void HTMLTextFieldAccessible::ApplyARIAState(uint64_t* aState) const {
+ HyperTextAccessible::ApplyARIAState(aState);
+ aria::MapToState(aria::eARIAAutoComplete, mContent->AsElement(), aState);
+}
+
+uint64_t HTMLTextFieldAccessible::NativeState() const {
+ uint64_t state = HyperTextAccessible::NativeState();
+
+ // Text fields are always editable, even if they are also read only or
+ // disabled.
+ state |= states::EDITABLE;
+
+ // can be focusable, focused, protected. readonly, unavailable, selected
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::password, eIgnoreCase)) {
+ state |= states::PROTECTED;
+ }
+
+ if (mContent->AsElement()->HasAttr(nsGkAtoms::readonly)) {
+ state |= states::READONLY;
+ }
+
+ // Is it an <input> or a <textarea> ?
+ HTMLInputElement* input = HTMLInputElement::FromNode(mContent);
+ state |= input && input->IsSingleLineTextControl() ? states::SINGLE_LINE
+ : states::MULTI_LINE;
+
+ if (state & (states::PROTECTED | states::MULTI_LINE | states::READONLY |
+ states::UNAVAILABLE)) {
+ return state;
+ }
+
+ // Expose autocomplete state if it has associated autocomplete list.
+ if (mContent->AsElement()->HasAttr(nsGkAtoms::list_)) {
+ return state | states::SUPPORTS_AUTOCOMPLETION | states::HASPOPUP;
+ }
+
+ if (Preferences::GetBool("browser.formfill.enable")) {
+ // Check to see if autocompletion is allowed on this input. We don't expose
+ // it for password fields even though the entire password can be remembered
+ // for a page if the user asks it to be. However, the kind of autocomplete
+ // we're talking here is based on what the user types, where a popup of
+ // possible choices comes up.
+ nsAutoString autocomplete;
+ mContent->AsElement()->GetAttr(nsGkAtoms::autocomplete, autocomplete);
+
+ if (!autocomplete.LowerCaseEqualsLiteral("off")) {
+ Element* formElement = input->GetForm();
+ if (formElement) {
+ formElement->GetAttr(nsGkAtoms::autocomplete, autocomplete);
+ }
+
+ if (!formElement || !autocomplete.LowerCaseEqualsLiteral("off")) {
+ state |= states::SUPPORTS_AUTOCOMPLETION;
+ }
+ }
+ }
+
+ return state;
+}
+
+bool HTMLTextFieldAccessible::HasPrimaryAction() const { return true; }
+
+void HTMLTextFieldAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == eAction_Click) aName.AssignLiteral("activate");
+}
+
+bool HTMLTextFieldAccessible::DoAction(uint8_t aIndex) const {
+ if (aIndex != 0) return false;
+
+ if (FocusMgr()->IsFocused(this)) {
+ // This already has focus, so TakeFocus()will do nothing. However, the user
+ // might be activating this element because they dismissed a touch keyboard
+ // and want to bring it back.
+ DoCommand();
+ } else {
+ TakeFocus();
+ }
+ return true;
+}
+
+already_AddRefed<EditorBase> HTMLTextFieldAccessible::GetEditor() const {
+ RefPtr<TextControlElement> textControlElement =
+ TextControlElement::FromNodeOrNull(mContent);
+ if (!textControlElement) {
+ return nullptr;
+ }
+ RefPtr<TextEditor> textEditor = textControlElement->GetTextEditor();
+ return textEditor.forget();
+}
+
+void HTMLTextFieldAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ if (aAttribute == nsGkAtoms::placeholder) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::NameAndDescription);
+ return;
+ }
+ HyperTextAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
+ aOldValue, aOldState);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLTextFieldAccessible: Widgets
+
+bool HTMLTextFieldAccessible::IsWidget() const { return true; }
+
+LocalAccessible* HTMLTextFieldAccessible::ContainerWidget() const {
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLFileInputAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLFileInputAccessible::HTMLFileInputAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mType = eHTMLFileInputType;
+ mGenericTypes |= eButton;
+}
+
+role HTMLFileInputAccessible::NativeRole() const { return roles::PUSHBUTTON; }
+
+bool HTMLFileInputAccessible::IsAcceptableChild(nsIContent* aEl) const {
+ // File inputs are rendered using native anonymous children. However, we
+ // want to expose this as a button Accessible so that clients can pick up the
+ // name and description from the button they activate, rather than a
+ // container. We still expose the text leaf descendants so we can get the
+ // name of the Browse button and the file name.
+ return aEl->IsText();
+}
+
+ENameValueFlag HTMLFileInputAccessible::Name(nsString& aName) const {
+ ENameValueFlag flag = HyperTextAccessible::Name(aName);
+ if (flag == eNameFromSubtree) {
+ // The author didn't provide a name. We'll compute the name from our subtree
+ // below.
+ aName.Truncate();
+ } else {
+ // The author provided a name. We do use that, but we also append our
+ // subtree text so the user knows this is a file chooser button and what
+ // file has been chosen.
+ if (aName.IsEmpty()) {
+ // Name computation is recursing, perhaps due to a wrapping <label>. Don't
+ // append the subtree text. Return " " to prevent
+ // nsTextEquivUtils::AppendFromAccessible walking the subtree itself.
+ aName += ' ';
+ return flag;
+ }
+ }
+ // Unfortunately, GetNameFromSubtree doesn't separate the button text from the
+ // file name text. Compute the text ourselves.
+ uint32_t count = ChildCount();
+ for (uint32_t c = 0; c < count; ++c) {
+ TextLeafAccessible* leaf = LocalChildAt(c)->AsTextLeaf();
+ MOZ_ASSERT(leaf);
+ if (!aName.IsEmpty()) {
+ aName += ' ';
+ }
+ aName += leaf->Text();
+ }
+ return flag;
+}
+
+bool HTMLFileInputAccessible::HasPrimaryAction() const { return true; }
+
+void HTMLFileInputAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == 0) {
+ aName.AssignLiteral("press");
+ }
+}
+
+bool HTMLFileInputAccessible::IsWidget() const { return true; }
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLSpinnerAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role HTMLSpinnerAccessible::NativeRole() const { return roles::SPINBUTTON; }
+
+void HTMLSpinnerAccessible::Value(nsString& aValue) const {
+ HTMLTextFieldAccessible::Value(aValue);
+ if (!aValue.IsEmpty()) return;
+
+ // Pass NonSystem as the caller type, to be safe. We don't expect to have a
+ // file input here.
+ HTMLInputElement::FromNode(mContent)->GetValue(aValue, CallerType::NonSystem);
+}
+
+double HTMLSpinnerAccessible::MaxValue() const {
+ double value = HTMLTextFieldAccessible::MaxValue();
+ if (!std::isnan(value)) return value;
+
+ return HTMLInputElement::FromNode(mContent)->GetMaximum().toDouble();
+}
+
+double HTMLSpinnerAccessible::MinValue() const {
+ double value = HTMLTextFieldAccessible::MinValue();
+ if (!std::isnan(value)) return value;
+
+ return HTMLInputElement::FromNode(mContent)->GetMinimum().toDouble();
+}
+
+double HTMLSpinnerAccessible::Step() const {
+ double value = HTMLTextFieldAccessible::Step();
+ if (!std::isnan(value)) return value;
+
+ return HTMLInputElement::FromNode(mContent)->GetStep().toDouble();
+}
+
+double HTMLSpinnerAccessible::CurValue() const {
+ double value = HTMLTextFieldAccessible::CurValue();
+ if (!std::isnan(value)) return value;
+
+ return HTMLInputElement::FromNode(mContent)->GetValueAsDecimal().toDouble();
+}
+
+bool HTMLSpinnerAccessible::SetCurValue(double aValue) {
+ ErrorResult er;
+ HTMLInputElement::FromNode(mContent)->SetValueAsNumber(aValue, er);
+ return !er.Failed();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLRangeAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role HTMLRangeAccessible::NativeRole() const { return roles::SLIDER; }
+
+bool HTMLRangeAccessible::IsWidget() const { return true; }
+
+void HTMLRangeAccessible::Value(nsString& aValue) const {
+ LeafAccessible::Value(aValue);
+ if (!aValue.IsEmpty()) return;
+
+ // Pass NonSystem as the caller type, to be safe. We don't expect to have a
+ // file input here.
+ HTMLInputElement::FromNode(mContent)->GetValue(aValue, CallerType::NonSystem);
+}
+
+double HTMLRangeAccessible::MaxValue() const {
+ double value = LeafAccessible::MaxValue();
+ if (!std::isnan(value)) return value;
+
+ return HTMLInputElement::FromNode(mContent)->GetMaximum().toDouble();
+}
+
+double HTMLRangeAccessible::MinValue() const {
+ double value = LeafAccessible::MinValue();
+ if (!std::isnan(value)) return value;
+
+ return HTMLInputElement::FromNode(mContent)->GetMinimum().toDouble();
+}
+
+double HTMLRangeAccessible::Step() const {
+ double value = LeafAccessible::Step();
+ if (!std::isnan(value)) return value;
+
+ return HTMLInputElement::FromNode(mContent)->GetStep().toDouble();
+}
+
+double HTMLRangeAccessible::CurValue() const {
+ double value = LeafAccessible::CurValue();
+ if (!std::isnan(value)) return value;
+
+ return HTMLInputElement::FromNode(mContent)->GetValueAsDecimal().toDouble();
+}
+
+bool HTMLRangeAccessible::SetCurValue(double aValue) {
+ nsAutoString strValue;
+ strValue.AppendFloat(aValue);
+ HTMLInputElement::FromNode(mContent)->SetUserInput(
+ strValue, *nsContentUtils::GetSystemPrincipal());
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLGroupboxAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLGroupboxAccessible::HTMLGroupboxAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+role HTMLGroupboxAccessible::NativeRole() const { return roles::GROUPING; }
+
+nsIContent* HTMLGroupboxAccessible::GetLegend() const {
+ for (nsIContent* legendContent = mContent->GetFirstChild(); legendContent;
+ legendContent = legendContent->GetNextSibling()) {
+ if (legendContent->NodeInfo()->Equals(nsGkAtoms::legend,
+ mContent->GetNameSpaceID())) {
+ // Either XHTML namespace or no namespace
+ return legendContent;
+ }
+ }
+
+ return nullptr;
+}
+
+ENameValueFlag HTMLGroupboxAccessible::NativeName(nsString& aName) const {
+ ENameValueFlag nameFlag = LocalAccessible::NativeName(aName);
+ if (!aName.IsEmpty()) return nameFlag;
+
+ nsIContent* legendContent = GetLegend();
+ if (legendContent) {
+ nsTextEquivUtils::AppendTextEquivFromContent(this, legendContent, &aName);
+ }
+
+ aName.CompressWhitespace();
+ return eNameOK;
+}
+
+Relation HTMLGroupboxAccessible::RelationByType(RelationType aType) const {
+ Relation rel = HyperTextAccessible::RelationByType(aType);
+ // No override for label, so use <legend> for this <fieldset>
+ if (aType == RelationType::LABELLED_BY) rel.AppendTarget(mDoc, GetLegend());
+
+ return rel;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLLegendAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLLegendAccessible::HTMLLegendAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+Relation HTMLLegendAccessible::RelationByType(RelationType aType) const {
+ Relation rel = HyperTextAccessible::RelationByType(aType);
+ if (aType != RelationType::LABEL_FOR) return rel;
+
+ LocalAccessible* groupbox = LocalParent();
+ if (groupbox && groupbox->Role() == roles::GROUPING) {
+ rel.AppendTarget(groupbox);
+ }
+
+ return rel;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLFigureAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLFigureAccessible::HTMLFigureAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+ENameValueFlag HTMLFigureAccessible::NativeName(nsString& aName) const {
+ ENameValueFlag nameFlag = HyperTextAccessible::NativeName(aName);
+ if (!aName.IsEmpty()) return nameFlag;
+
+ nsIContent* captionContent = Caption();
+ if (captionContent) {
+ nsTextEquivUtils::AppendTextEquivFromContent(this, captionContent, &aName);
+ }
+
+ aName.CompressWhitespace();
+ return eNameOK;
+}
+
+Relation HTMLFigureAccessible::RelationByType(RelationType aType) const {
+ Relation rel = HyperTextAccessible::RelationByType(aType);
+ if (aType == RelationType::LABELLED_BY) rel.AppendTarget(mDoc, Caption());
+
+ return rel;
+}
+
+nsIContent* HTMLFigureAccessible::Caption() const {
+ for (nsIContent* childContent = mContent->GetFirstChild(); childContent;
+ childContent = childContent->GetNextSibling()) {
+ if (childContent->NodeInfo()->Equals(nsGkAtoms::figcaption,
+ mContent->GetNameSpaceID())) {
+ return childContent;
+ }
+ }
+
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLFigcaptionAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLFigcaptionAccessible::HTMLFigcaptionAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+Relation HTMLFigcaptionAccessible::RelationByType(RelationType aType) const {
+ Relation rel = HyperTextAccessible::RelationByType(aType);
+ if (aType != RelationType::LABEL_FOR) return rel;
+
+ LocalAccessible* figure = LocalParent();
+ if (figure && figure->GetContent()->NodeInfo()->Equals(
+ nsGkAtoms::figure, mContent->GetNameSpaceID())) {
+ rel.AppendTarget(figure);
+ }
+
+ return rel;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLProgressAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role HTMLProgressAccessible::NativeRole() const { return roles::PROGRESSBAR; }
+
+uint64_t HTMLProgressAccessible::NativeState() const {
+ uint64_t state = LeafAccessible::NativeState();
+
+ // An undetermined progressbar (i.e. without a value) has a mixed state.
+ nsAutoString attrValue;
+ mContent->AsElement()->GetAttr(nsGkAtoms::value, attrValue);
+ if (attrValue.IsEmpty()) {
+ state |= states::MIXED;
+ }
+
+ return state;
+}
+
+bool HTMLProgressAccessible::IsWidget() const { return true; }
+
+void HTMLProgressAccessible::Value(nsString& aValue) const {
+ LeafAccessible::Value(aValue);
+ if (!aValue.IsEmpty()) {
+ return;
+ }
+
+ double maxValue = MaxValue();
+ if (std::isnan(maxValue) || maxValue == 0) {
+ return;
+ }
+
+ double curValue = CurValue();
+ if (std::isnan(curValue)) {
+ return;
+ }
+
+ // Treat the current value bigger than maximum as 100%.
+ double percentValue =
+ (curValue < maxValue) ? (curValue / maxValue) * 100 : 100;
+
+ aValue.AppendFloat(percentValue);
+ aValue.Append('%');
+}
+
+double HTMLProgressAccessible::MaxValue() const {
+ double value = LeafAccessible::MaxValue();
+ if (!std::isnan(value)) {
+ return value;
+ }
+
+ nsAutoString strValue;
+ if (mContent->AsElement()->GetAttr(nsGkAtoms::max, strValue)) {
+ nsresult result = NS_OK;
+ value = strValue.ToDouble(&result);
+ if (NS_SUCCEEDED(result)) {
+ return value;
+ }
+ }
+
+ return 1;
+}
+
+double HTMLProgressAccessible::MinValue() const {
+ double value = LeafAccessible::MinValue();
+ return std::isnan(value) ? 0 : value;
+}
+
+double HTMLProgressAccessible::Step() const {
+ double value = LeafAccessible::Step();
+ return std::isnan(value) ? 0 : value;
+}
+
+double HTMLProgressAccessible::CurValue() const {
+ double value = LeafAccessible::CurValue();
+ if (!std::isnan(value)) {
+ return value;
+ }
+
+ nsAutoString attrValue;
+ if (!mContent->AsElement()->GetAttr(nsGkAtoms::value, attrValue)) {
+ return UnspecifiedNaN<double>();
+ }
+
+ nsresult error = NS_OK;
+ value = attrValue.ToDouble(&error);
+ return NS_FAILED(error) ? UnspecifiedNaN<double>() : value;
+}
+
+bool HTMLProgressAccessible::SetCurValue(double aValue) {
+ return false; // progress meters are readonly.
+}
+
+void HTMLProgressAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ LeafAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
+ aOldValue, aOldState);
+
+ if (aAttribute == nsGkAtoms::value) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, this);
+
+ uint64_t currState = NativeState();
+ if ((aOldState ^ currState) & states::MIXED) {
+ RefPtr<AccEvent> stateChangeEvent = new AccStateChangeEvent(
+ this, states::MIXED, (currState & states::MIXED));
+ mDoc->FireDelayedEvent(stateChangeEvent);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLMeterAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role HTMLMeterAccessible::NativeRole() const { return roles::METER; }
+
+bool HTMLMeterAccessible::IsWidget() const { return true; }
+
+void HTMLMeterAccessible::Value(nsString& aValue) const {
+ LeafAccessible::Value(aValue);
+ if (!aValue.IsEmpty()) {
+ return;
+ }
+
+ // If we did not get a value from the above LeafAccessible call,
+ // we should check to see if the meter has inner text.
+ // If it does, we'll use that as our value.
+ nsTextEquivUtils::AppendFromDOMChildren(mContent, &aValue);
+ aValue.CompressWhitespace();
+ if (!aValue.IsEmpty()) {
+ return;
+ }
+
+ // If no inner text is found, use curValue
+ double curValue = CurValue();
+ if (std::isnan(curValue)) {
+ return;
+ }
+
+ aValue.AppendFloat(curValue);
+}
+
+double HTMLMeterAccessible::MaxValue() const {
+ double max = LeafAccessible::MaxValue();
+ double min = MinValue();
+
+ if (!std::isnan(max)) {
+ return max > min ? max : min;
+ }
+
+ // If we didn't find a max value, check for the max attribute
+ nsAutoString strValue;
+ if (mContent->AsElement()->GetAttr(nsGkAtoms::max, strValue)) {
+ nsresult result = NS_OK;
+ max = strValue.ToDouble(&result);
+ if (NS_SUCCEEDED(result)) {
+ return max > min ? max : min;
+ }
+ }
+
+ return 1 > min ? 1 : min;
+}
+
+double HTMLMeterAccessible::MinValue() const {
+ double min = LeafAccessible::MinValue();
+ if (!std::isnan(min)) {
+ return min;
+ }
+
+ nsAutoString strValue;
+ if (mContent->AsElement()->GetAttr(nsGkAtoms::min, strValue)) {
+ nsresult result = NS_OK;
+ min = strValue.ToDouble(&result);
+ if (NS_SUCCEEDED(result)) {
+ return min;
+ }
+ }
+
+ return 0;
+}
+
+double HTMLMeterAccessible::CurValue() const {
+ double value = LeafAccessible::CurValue();
+ double minValue = MinValue();
+
+ if (std::isnan(value)) {
+ /* If we didn't find a value from the LeafAccessible call above, check
+ * for a value attribute */
+ nsAutoString attrValue;
+ if (!mContent->AsElement()->GetAttr(nsGkAtoms::value, attrValue)) {
+ return minValue;
+ }
+
+ // If we find a value attribute, attempt to convert it to a double
+ nsresult error = NS_OK;
+ value = attrValue.ToDouble(&error);
+ if (NS_FAILED(error)) {
+ return minValue;
+ }
+ }
+
+ /* If we end up with a defined value, verify it falls between
+ * our established min/max. Otherwise, snap it to the nearest boundary. */
+ double maxValue = MaxValue();
+ if (value > maxValue) {
+ value = maxValue;
+ } else if (value < minValue) {
+ value = minValue;
+ }
+
+ return value;
+}
+
+bool HTMLMeterAccessible::SetCurValue(double aValue) {
+ return false; // meters are readonly.
+}
+
+void HTMLMeterAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ LeafAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
+ aOldValue, aOldState);
+
+ if (aAttribute == nsGkAtoms::value) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, this);
+ }
+}
diff --git a/accessible/html/HTMLFormControlAccessible.h b/accessible/html/HTMLFormControlAccessible.h
new file mode 100644
index 0000000000..1e00bd4e40
--- /dev/null
+++ b/accessible/html/HTMLFormControlAccessible.h
@@ -0,0 +1,387 @@
+/* -*- 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_HTMLFormControlAccessible_H_
+#define MOZILLA_A11Y_HTMLFormControlAccessible_H_
+
+#include "FormControlAccessible.h"
+#include "HyperTextAccessible.h"
+#include "mozilla/a11y/AccTypes.h"
+#include "mozilla/dom/Element.h"
+#include "AccAttributes.h"
+#include "nsAccUtils.h"
+#include "Relation.h"
+
+namespace mozilla {
+class EditorBase;
+namespace a11y {
+
+/**
+ * Accessible for HTML input@type="radio" element.
+ */
+class HTMLRadioButtonAccessible : public RadioButtonAccessible {
+ public:
+ HTMLRadioButtonAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : RadioButtonAccessible(aContent, aDoc) {
+ // Ignore "RadioStateChange" DOM event in lieu of document observer
+ // state change notification.
+ mStateFlags |= eIgnoreDOMUIEvent;
+ mType = eHTMLRadioButtonType;
+ }
+
+ // LocalAccessible
+ virtual uint64_t NativeState() const override;
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ protected:
+ virtual void GetPositionAndSetSize(int32_t* aPosInSet,
+ int32_t* aSetSize) override;
+
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+
+ private:
+ Relation ComputeGroupAttributes(int32_t* aPosInSet, int32_t* aSetSize) const;
+};
+
+/**
+ * Accessible for HTML input@type="button", @type="submit", @type="image"
+ * and HTML button elements.
+ */
+class HTMLButtonAccessible : public HyperTextAccessible {
+ public:
+ enum { eAction_Click = 0 };
+
+ HTMLButtonAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+};
+
+/**
+ * Accessible for HTML input@type="text", input@type="password", textarea
+ * and other HTML text controls.
+ */
+class HTMLTextFieldAccessible : public HyperTextAccessible {
+ public:
+ enum { eAction_Click = 0 };
+
+ HTMLTextFieldAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLTextFieldAccessible,
+ HyperTextAccessible)
+
+ // HyperTextAccessible
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual already_AddRefed<EditorBase> GetEditor()
+ const override;
+
+ // LocalAccessible
+ virtual void Value(nsString& aValue) const override;
+ virtual void ApplyARIAState(uint64_t* aState) const override;
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+ virtual bool AttributeChangesState(nsAtom* aAttribute) override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual bool DoAction(uint8_t aIndex) const override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+ virtual LocalAccessible* ContainerWidget() const override;
+
+ protected:
+ virtual ~HTMLTextFieldAccessible() {}
+
+ // LocalAccessible
+ virtual ENameValueFlag Name(nsString& aName) const override;
+
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+};
+
+/**
+ * Accessible for input@type="file" element.
+ */
+class HTMLFileInputAccessible : public HyperTextAccessible {
+ public:
+ HTMLFileInputAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual bool IsAcceptableChild(nsIContent* aEl) const override;
+ virtual ENameValueFlag Name(nsString& aName) const override;
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual bool IsWidget() const override;
+};
+
+/**
+ * Used for HTML input@type="number".
+ */
+class HTMLSpinnerAccessible final : public HTMLTextFieldAccessible {
+ public:
+ HTMLSpinnerAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HTMLTextFieldAccessible(aContent, aDoc) {
+ mGenericTypes |= eNumericValue;
+ }
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual void Value(nsString& aValue) const override;
+
+ virtual double MaxValue() const override;
+ virtual double MinValue() const override;
+ virtual double CurValue() const override;
+ virtual double Step() const override;
+ virtual bool SetCurValue(double aValue) override;
+};
+
+/**
+ * Used for input@type="range" element.
+ */
+class HTMLRangeAccessible : public LeafAccessible {
+ public:
+ HTMLRangeAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {
+ mGenericTypes |= eNumericValue;
+ }
+
+ // LocalAccessible
+ virtual void Value(nsString& aValue) const override;
+ virtual mozilla::a11y::role NativeRole() const override;
+
+ // Value
+ virtual double MaxValue() const override;
+ virtual double MinValue() const override;
+ virtual double CurValue() const override;
+ virtual double Step() const override;
+ virtual bool SetCurValue(double aValue) override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+};
+
+/**
+ * Accessible for HTML fieldset element.
+ */
+class HTMLGroupboxAccessible : public HyperTextAccessible {
+ public:
+ HTMLGroupboxAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+
+ // HTMLGroupboxAccessible
+ nsIContent* GetLegend() const;
+};
+
+/**
+ * Accessible for HTML legend element.
+ */
+class HTMLLegendAccessible : public HyperTextAccessible {
+ public:
+ HTMLLegendAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual Relation RelationByType(RelationType aType) const override;
+};
+
+/**
+ * Accessible for HTML5 figure element.
+ */
+class HTMLFigureAccessible : public HyperTextAccessible {
+ public:
+ HTMLFigureAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+
+ // HTMLLegendAccessible
+ nsIContent* Caption() const;
+};
+
+/**
+ * Accessible for HTML5 figcaption element.
+ */
+class HTMLFigcaptionAccessible : public HyperTextAccessible {
+ public:
+ HTMLFigcaptionAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual Relation RelationByType(RelationType aType) const override;
+};
+
+/**
+ * Used for HTML form element.
+ */
+class HTMLFormAccessible : public HyperTextAccessible {
+ public:
+ HTMLFormAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLFormAccessible, HyperTextAccessible)
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+
+ protected:
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+
+ virtual ~HTMLFormAccessible() = default;
+};
+
+/**
+ * Accessible for HTML progress element.
+ */
+
+class HTMLProgressAccessible : public LeafAccessible {
+ public:
+ HTMLProgressAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {
+ // Ignore 'ValueChange' DOM event in lieu of @value attribute change
+ // notifications.
+ mStateFlags |= eIgnoreDOMUIEvent;
+ mGenericTypes |= eNumericValue;
+ mType = eProgressType;
+ }
+
+ // LocalAccessible
+ virtual void Value(nsString& aValue) const override;
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ // Value
+ virtual double MaxValue() const override;
+ virtual double MinValue() const override;
+ virtual double CurValue() const override;
+ virtual double Step() const override;
+ virtual bool SetCurValue(double aValue) override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+
+ protected:
+ virtual ~HTMLProgressAccessible() {}
+
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+};
+
+/**
+ * Accessible for HTML meter element.
+ */
+
+class HTMLMeterAccessible : public LeafAccessible {
+ public:
+ HTMLMeterAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {
+ // Ignore 'ValueChange' DOM event in lieu of @value attribute change
+ // notifications.
+ mStateFlags |= eIgnoreDOMUIEvent;
+ mGenericTypes |= eNumericValue;
+ mType = eProgressType;
+ }
+
+ // LocalAccessible
+ virtual void Value(nsString& aValue) const override;
+ virtual mozilla::a11y::role NativeRole() const override;
+
+ // Value
+ virtual double MaxValue() const override;
+ virtual double MinValue() const override;
+ virtual double CurValue() const override;
+ virtual bool SetCurValue(double aValue) override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+
+ protected:
+ virtual ~HTMLMeterAccessible() {}
+
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+};
+
+/**
+ * Accessible for HTML date/time inputs.
+ */
+template <a11y::role R>
+class HTMLDateTimeAccessible : public HyperTextAccessible {
+ public:
+ HTMLDateTimeAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mType = eHTMLDateTimeFieldType;
+ }
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLDateTimeAccessible,
+ HyperTextAccessible)
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override { return R; }
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override {
+ RefPtr<AccAttributes> attributes = HyperTextAccessible::NativeAttributes();
+ // Unfortunately, an nsStaticAtom can't be passed as a
+ // template argument, so fetch the type from the DOM.
+ if (const nsAttrValue* attr =
+ mContent->AsElement()->GetParsedAttr(nsGkAtoms::type)) {
+ RefPtr<nsAtom> inputType = attr->GetAsAtom();
+ if (inputType) {
+ attributes->SetAttribute(nsGkAtoms::textInputType, inputType);
+ }
+ }
+ return attributes.forget();
+ }
+
+ // Widgets
+ virtual bool IsWidget() const override { return true; }
+
+ protected:
+ virtual ~HTMLDateTimeAccessible() {}
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/html/HTMLImageMapAccessible.cpp b/accessible/html/HTMLImageMapAccessible.cpp
new file mode 100644
index 0000000000..14dd485875
--- /dev/null
+++ b/accessible/html/HTMLImageMapAccessible.cpp
@@ -0,0 +1,200 @@
+/* -*- 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 "HTMLImageMapAccessible.h"
+
+#include "ARIAMap.h"
+#include "EventTree.h"
+#include "mozilla/a11y/Role.h"
+
+#include "nsCoreUtils.h"
+#include "nsIFrame.h"
+#include "nsImageFrame.h"
+#include "nsImageMap.h"
+#include "nsLayoutUtils.h"
+#include "mozilla/dom/HTMLAreaElement.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLImageMapAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLImageMapAccessible::HTMLImageMapAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : ImageAccessible(aContent, aDoc) {
+ mType = eImageMapType;
+
+ UpdateChildAreas(false);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLImageMapAccessible: LocalAccessible public
+
+role HTMLImageMapAccessible::NativeRole() const { return roles::IMAGE_MAP; }
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLImageMapAccessible: public
+
+void HTMLImageMapAccessible::UpdateChildAreas(bool aDoFireEvents) {
+ if (!mContent || !mContent->GetPrimaryFrame()) {
+ return;
+ }
+ nsImageFrame* imageFrame = do_QueryFrame(mContent->GetPrimaryFrame());
+
+ // If image map is not initialized yet then we trigger one time more later.
+ nsImageMap* imageMapObj = imageFrame->GetExistingImageMap();
+ if (!imageMapObj) return;
+
+ TreeMutation mt(this, TreeMutation::kNoEvents & !aDoFireEvents);
+
+ // Remove areas that are not a valid part of the image map anymore.
+ for (int32_t childIdx = mChildren.Length() - 1; childIdx >= 0; childIdx--) {
+ LocalAccessible* area = mChildren.ElementAt(childIdx);
+ if (area->GetContent()->GetPrimaryFrame()) continue;
+
+ mt.BeforeRemoval(area);
+ RemoveChild(area);
+ }
+
+ // Insert new areas into the tree.
+ uint32_t areaElmCount = imageMapObj->AreaCount();
+ for (uint32_t idx = 0; idx < areaElmCount; idx++) {
+ nsIContent* areaContent = imageMapObj->GetAreaAt(idx);
+ LocalAccessible* area = mChildren.SafeElementAt(idx);
+ if (!area || area->GetContent() != areaContent) {
+ RefPtr<LocalAccessible> area = new HTMLAreaAccessible(areaContent, mDoc);
+ mDoc->BindToDocument(area, aria::GetRoleMap(areaContent->AsElement()));
+
+ if (!InsertChildAt(idx, area)) {
+ mDoc->UnbindFromDocument(area);
+ break;
+ }
+
+ mt.AfterInsertion(area);
+ }
+ }
+
+ mt.Done();
+}
+
+LocalAccessible* HTMLImageMapAccessible::GetChildAccessibleFor(
+ const nsINode* aNode) const {
+ uint32_t length = mChildren.Length();
+ for (uint32_t i = 0; i < length; i++) {
+ LocalAccessible* area = mChildren[i];
+ if (area->GetContent() == aNode) return area;
+ }
+
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLAreaAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLAreaAccessible::HTMLAreaAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HTMLLinkAccessible(aContent, aDoc) {
+ // Make HTML area DOM element not accessible. HTML image map accessible
+ // manages its tree itself.
+ mStateFlags |= eNotNodeMapEntry;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLAreaAccessible: LocalAccessible
+
+role HTMLAreaAccessible::NativeRole() const {
+ // A link element without an href attribute and without a click listener
+ // should be reported as a generic.
+ if (mContent->IsElement()) {
+ dom::Element* element = mContent->AsElement();
+ if (!element->HasAttr(nsGkAtoms::href) &&
+ !nsCoreUtils::HasClickListener(element)) {
+ return roles::TEXT;
+ }
+ }
+ return HTMLLinkAccessible::NativeRole();
+}
+
+ENameValueFlag HTMLAreaAccessible::NativeName(nsString& aName) const {
+ ENameValueFlag nameFlag = LocalAccessible::NativeName(aName);
+ if (!aName.IsEmpty()) return nameFlag;
+
+ if (!mContent->AsElement()->GetAttr(nsGkAtoms::alt, aName)) {
+ Value(aName);
+ }
+
+ return eNameOK;
+}
+
+void HTMLAreaAccessible::Description(nsString& aDescription) const {
+ aDescription.Truncate();
+
+ // Still to do - follow IE's standard here
+ RefPtr<dom::HTMLAreaElement> area =
+ dom::HTMLAreaElement::FromNodeOrNull(mContent);
+ if (area) area->GetShape(aDescription);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLAreaAccessible: LocalAccessible public
+
+LocalAccessible* HTMLAreaAccessible::LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) {
+ // Don't walk into area accessibles.
+ return this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLImageMapAccessible: HyperLinkAccessible
+
+uint32_t HTMLAreaAccessible::StartOffset() {
+ // Image map accessible is not hypertext accessible therefore
+ // StartOffset/EndOffset implementations of LocalAccessible doesn't work here.
+ // We return index in parent because image map contains area links only which
+ // are embedded objects.
+ // XXX: image map should be a hypertext accessible.
+ return IndexInParent();
+}
+
+uint32_t HTMLAreaAccessible::EndOffset() { return IndexInParent() + 1; }
+
+nsRect HTMLAreaAccessible::RelativeBounds(nsIFrame** aBoundingFrame) const {
+ nsIFrame* frame = GetFrame();
+ if (!frame) return nsRect();
+
+ nsImageFrame* imageFrame = do_QueryFrame(frame);
+ nsImageMap* map = imageFrame->GetImageMap();
+
+ nsRect bounds;
+ nsresult rv = map->GetBoundsForAreaContent(mContent, bounds);
+
+ if (NS_FAILED(rv)) return nsRect();
+
+ // XXX Areas are screwy; they return their rects as a pair of points, one pair
+ // stored into the width and height.
+ *aBoundingFrame = frame;
+ bounds.SizeTo(bounds.Width() - bounds.X(), bounds.Height() - bounds.Y());
+ return bounds;
+}
+
+nsRect HTMLAreaAccessible::ParentRelativeBounds() {
+ nsIFrame* boundingFrame = nullptr;
+ nsRect relativeBoundsRect = RelativeBounds(&boundingFrame);
+ if (MOZ_UNLIKELY(!boundingFrame)) {
+ // Area is not attached to an image map?
+ return nsRect();
+ }
+
+ // The relative bounds returned above are relative to this area's
+ // image map, which is technically already "parent relative".
+ // Because area elements are `display:none` to layout, they can't
+ // have transforms or other styling applied directly, and so we
+ // don't apply any additional transforms here. Any transform
+ // at the image map layer will be taken care of when computing bounds
+ // in the parent process.
+ return relativeBoundsRect;
+}
diff --git a/accessible/html/HTMLImageMapAccessible.h b/accessible/html/HTMLImageMapAccessible.h
new file mode 100644
index 0000000000..15c56b44de
--- /dev/null
+++ b/accessible/html/HTMLImageMapAccessible.h
@@ -0,0 +1,82 @@
+/* -*- 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_HTMLImageMapAccessible_h__
+#define mozilla_a11y_HTMLImageMapAccessible_h__
+
+#include "HTMLLinkAccessible.h"
+#include "ImageAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Used for HTML image maps.
+ */
+class HTMLImageMapAccessible final : public ImageAccessible {
+ public:
+ HTMLImageMapAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // nsISupports and cycle collector
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLImageMapAccessible, ImageAccessible)
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+
+ /**
+ * Update area children of the image map.
+ */
+ void UpdateChildAreas(bool aDoFireEvents = true);
+
+ /**
+ * Return accessible of child node.
+ */
+ LocalAccessible* GetChildAccessibleFor(const nsINode* aNode) const;
+
+ protected:
+ virtual ~HTMLImageMapAccessible() {}
+};
+
+/**
+ * Accessible for image map areas - must be child of image.
+ */
+class HTMLAreaAccessible final : public HTMLLinkAccessible {
+ public:
+ HTMLAreaAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual void Description(nsString& aDescription) const override;
+ virtual LocalAccessible* LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override;
+ virtual nsRect RelativeBounds(nsIFrame** aBoundingFrame) const override;
+ virtual nsRect ParentRelativeBounds() override;
+
+ // HyperLinkAccessible
+ virtual uint32_t StartOffset() override;
+ virtual uint32_t EndOffset() override;
+
+ virtual bool IsAcceptableChild(nsIContent* aEl) const override {
+ return false;
+ }
+
+ // LocalAccessible
+ virtual role NativeRole() const override;
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible downcasting method
+
+inline HTMLImageMapAccessible* LocalAccessible::AsImageMap() {
+ return IsImageMap() ? static_cast<HTMLImageMapAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/html/HTMLLinkAccessible.cpp b/accessible/html/HTMLLinkAccessible.cpp
new file mode 100644
index 0000000000..b3549996fc
--- /dev/null
+++ b/accessible/html/HTMLLinkAccessible.cpp
@@ -0,0 +1,129 @@
+/* -*- 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 "HTMLLinkAccessible.h"
+
+#include "CacheConstants.h"
+#include "nsCoreUtils.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+
+#include "nsContentUtils.h"
+#include "mozilla/a11y/DocAccessible.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/MutationEventBinding.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLLinkAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLLinkAccessible::HTMLLinkAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mType = eHTMLLinkType;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessible
+
+role HTMLLinkAccessible::NativeRole() const { return roles::LINK; }
+
+uint64_t HTMLLinkAccessible::NativeState() const {
+ return HyperTextAccessible::NativeState() & ~states::READONLY;
+}
+
+uint64_t HTMLLinkAccessible::NativeLinkState() const {
+ dom::ElementState state = mContent->AsElement()->State();
+ if (state.HasState(dom::ElementState::UNVISITED)) {
+ return states::LINKED;
+ }
+
+ if (state.HasState(dom::ElementState::VISITED)) {
+ return states::LINKED | states::TRAVERSED;
+ }
+
+ // This is a either named anchor (a link with also a name attribute) or
+ // it doesn't have any attributes. Check if 'click' event handler is
+ // registered, otherwise bail out.
+ return nsCoreUtils::HasClickListener(mContent) ? states::LINKED : 0;
+}
+
+uint64_t HTMLLinkAccessible::NativeInteractiveState() const {
+ uint64_t state = HyperTextAccessible::NativeInteractiveState();
+
+ // This is how we indicate it is a named anchor. In other words, this anchor
+ // can be selected as a location :) There is no other better state to use to
+ // indicate this.
+ if (mContent->AsElement()->HasAttr(nsGkAtoms::name)) {
+ state |= states::SELECTABLE;
+ }
+
+ return state;
+}
+
+void HTMLLinkAccessible::Value(nsString& aValue) const {
+ aValue.Truncate();
+
+ HyperTextAccessible::Value(aValue);
+ if (aValue.IsEmpty()) {
+ nsContentUtils::GetLinkLocation(mContent->AsElement(), aValue);
+ }
+}
+
+bool HTMLLinkAccessible::HasPrimaryAction() const {
+ return IsLinked() || HyperTextAccessible::HasPrimaryAction();
+ ;
+}
+
+void HTMLLinkAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ aName.Truncate();
+
+ if (!IsLinked()) {
+ HyperTextAccessible::ActionNameAt(aIndex, aName);
+ return;
+ }
+
+ // Action 0 (default action): Jump to link
+ if (aIndex == eAction_Jump) aName.AssignLiteral("jump");
+}
+
+bool HTMLLinkAccessible::AttributeChangesState(nsAtom* aAttribute) {
+ return aAttribute == nsGkAtoms::href ||
+ HyperTextAccessible::AttributeChangesState(aAttribute);
+}
+
+void HTMLLinkAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ HyperTextAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
+ aOldValue, aOldState);
+
+ if (aAttribute == nsGkAtoms::href &&
+ (aModType == dom::MutationEvent_Binding::ADDITION ||
+ aModType == dom::MutationEvent_Binding::REMOVAL)) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Actions);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HyperLinkAccessible
+
+bool HTMLLinkAccessible::IsLink() const {
+ // Expose HyperLinkAccessible unconditionally.
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLLinkAccessible
+
+bool HTMLLinkAccessible::IsLinked() const {
+ dom::ElementState state = mContent->AsElement()->State();
+ return state.HasAtLeastOneOfStates(dom::ElementState::VISITED_OR_UNVISITED);
+}
diff --git a/accessible/html/HTMLLinkAccessible.h b/accessible/html/HTMLLinkAccessible.h
new file mode 100644
index 0000000000..de5f903a3d
--- /dev/null
+++ b/accessible/html/HTMLLinkAccessible.h
@@ -0,0 +1,59 @@
+/* -*- 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_HTMLLinkAccessible_h__
+#define mozilla_a11y_HTMLLinkAccessible_h__
+
+#include "HyperTextAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+class HTMLLinkAccessible : public HyperTextAccessible {
+ public:
+ HTMLLinkAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLLinkAccessible, HyperTextAccessible)
+
+ // LocalAccessible
+ virtual void Value(nsString& aValue) const override;
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual uint64_t NativeLinkState() const override;
+ virtual uint64_t NativeInteractiveState() const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+
+ // HyperLinkAccessible
+ virtual bool IsLink() const override;
+
+ /**
+ * Returns true if the link has href attribute.
+ */
+ bool IsLinked() const;
+
+ protected:
+ virtual ~HTMLLinkAccessible() {}
+
+ virtual bool AttributeChangesState(nsAtom* aAttribute) override;
+
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+
+ enum { eAction_Jump = 0 };
+};
+
+inline HTMLLinkAccessible* LocalAccessible::AsHTMLLink() {
+ return IsHTMLLink() ? static_cast<HTMLLinkAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/html/HTMLListAccessible.cpp b/accessible/html/HTMLListAccessible.cpp
new file mode 100644
index 0000000000..d7a2fc23ae
--- /dev/null
+++ b/accessible/html/HTMLListAccessible.cpp
@@ -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/. */
+
+#include "HTMLListAccessible.h"
+
+#include "AccAttributes.h"
+#include "nsAccessibilityService.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+
+#include "nsLayoutUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLListAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role HTMLListAccessible::NativeRole() const {
+ a11y::role r = GetAccService()->MarkupRole(mContent);
+ return r != roles::NOTHING ? r : roles::LIST;
+}
+
+uint64_t HTMLListAccessible::NativeState() const {
+ return HyperTextAccessible::NativeState() | states::READONLY;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLLIAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLLIAccessible::HTMLLIAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mType = eHTMLLiType;
+}
+
+role HTMLLIAccessible::NativeRole() const {
+ a11y::role r = GetAccService()->MarkupRole(mContent);
+ return r != roles::NOTHING ? r : roles::LISTITEM;
+}
+
+uint64_t HTMLLIAccessible::NativeState() const {
+ return HyperTextAccessible::NativeState() | states::READONLY;
+}
+
+nsRect HTMLLIAccessible::BoundsInAppUnits() const {
+ nsRect rect = AccessibleWrap::BoundsInAppUnits();
+
+ LocalAccessible* bullet = Bullet();
+ nsIFrame* frame = GetFrame();
+ MOZ_ASSERT(!(bullet && !frame), "Cannot have a bullet if there is no frame");
+
+ if (bullet && frame &&
+ frame->StyleList()->mListStylePosition !=
+ StyleListStylePosition::Inside) {
+ nsRect bulletRect = bullet->BoundsInAppUnits();
+ return rect.Union(bulletRect);
+ }
+
+ return rect;
+}
+
+LocalAccessible* HTMLLIAccessible::Bullet() const {
+ LocalAccessible* firstChild = LocalFirstChild();
+ if (firstChild && firstChild->NativeRole() == roles::LISTITEM_MARKER) {
+ return firstChild;
+ }
+
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLListBulletAccessible
+////////////////////////////////////////////////////////////////////////////////
+HTMLListBulletAccessible::HTMLListBulletAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {
+ mGenericTypes |= eText;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLListBulletAccessible: LocalAccessible
+
+ENameValueFlag HTMLListBulletAccessible::Name(nsString& aName) const {
+ nsLayoutUtils::GetMarkerSpokenText(mContent, aName);
+ return eNameOK;
+}
+
+role HTMLListBulletAccessible::NativeRole() const {
+ return roles::LISTITEM_MARKER;
+}
+
+uint64_t HTMLListBulletAccessible::NativeState() const {
+ return LeafAccessible::NativeState() | states::READONLY;
+}
+
+already_AddRefed<AccAttributes> HTMLListBulletAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+ return attributes.forget();
+}
+
+void HTMLListBulletAccessible::AppendTextTo(nsAString& aText,
+ uint32_t aStartOffset,
+ uint32_t aLength) {
+ nsAutoString bulletText;
+ Name(bulletText);
+ aText.Append(Substring(bulletText, aStartOffset, aLength));
+}
diff --git a/accessible/html/HTMLListAccessible.h b/accessible/html/HTMLListAccessible.h
new file mode 100644
index 0000000000..ac48ad2380
--- /dev/null
+++ b/accessible/html/HTMLListAccessible.h
@@ -0,0 +1,85 @@
+/* -*- 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_HTMLListAccessible_h__
+#define mozilla_a11y_HTMLListAccessible_h__
+
+#include "BaseAccessibles.h"
+#include "HyperTextAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+class HTMLListBulletAccessible;
+
+/**
+ * Used for HTML list (like HTML ul).
+ */
+class HTMLListAccessible : public HyperTextAccessible {
+ public:
+ HTMLListAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mGenericTypes |= eList;
+ }
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLListAccessible, HyperTextAccessible)
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ protected:
+ virtual ~HTMLListAccessible() {}
+};
+
+/**
+ * Used for HTML list item (e.g. HTML li).
+ */
+class HTMLLIAccessible : public HyperTextAccessible {
+ public:
+ HTMLLIAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLLIAccessible, HyperTextAccessible)
+
+ // LocalAccessible
+ virtual nsRect BoundsInAppUnits() const override;
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ // HTMLLIAccessible
+ LocalAccessible* Bullet() const;
+
+ protected:
+ virtual ~HTMLLIAccessible() {}
+};
+
+/**
+ * Used for bullet of HTML list item element (for example, HTML li).
+ */
+class HTMLListBulletAccessible : public LeafAccessible {
+ public:
+ HTMLListBulletAccessible(nsIContent* aContent, DocAccessible* aDoc);
+ virtual ~HTMLListBulletAccessible() {}
+
+ // LocalAccessible
+ virtual ENameValueFlag Name(nsString& aName) const override;
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+ virtual void AppendTextTo(nsAString& aText, uint32_t aStartOffset = 0,
+ uint32_t aLength = UINT32_MAX) override;
+};
+
+inline HTMLLIAccessible* LocalAccessible::AsHTMLListItem() {
+ return IsHTMLListItem() ? static_cast<HTMLLIAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/html/HTMLSelectAccessible.cpp b/accessible/html/HTMLSelectAccessible.cpp
new file mode 100644
index 0000000000..f8fb4180c7
--- /dev/null
+++ b/accessible/html/HTMLSelectAccessible.cpp
@@ -0,0 +1,472 @@
+/* -*- Mode: C++; tab-width: 4; 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 "HTMLSelectAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "nsAccessibilityService.h"
+#include "nsAccUtils.h"
+#include "DocAccessible.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+
+#include "nsCOMPtr.h"
+#include "mozilla/dom/HTMLOptionElement.h"
+#include "mozilla/dom/HTMLOptGroupElement.h"
+#include "mozilla/dom/HTMLSelectElement.h"
+#include "nsComboboxControlFrame.h"
+#include "nsContainerFrame.h"
+#include "nsListControlFrame.h"
+
+using namespace mozilla::a11y;
+using namespace mozilla::dom;
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLSelectListAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLSelectListAccessible::HTMLSelectListAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {
+ mGenericTypes |= eListControl | eSelect;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLSelectListAccessible: LocalAccessible public
+
+uint64_t HTMLSelectListAccessible::NativeState() const {
+ uint64_t state = AccessibleWrap::NativeState();
+ if (mContent->AsElement()->HasAttr(nsGkAtoms::multiple)) {
+ state |= states::MULTISELECTABLE | states::EXTSELECTABLE;
+ }
+
+ return state;
+}
+
+role HTMLSelectListAccessible::NativeRole() const { return roles::LISTBOX; }
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLSelectListAccessible: SelectAccessible
+
+bool HTMLSelectListAccessible::SelectAll() {
+ return mContent->AsElement()->HasAttr(nsGkAtoms::multiple)
+ ? AccessibleWrap::SelectAll()
+ : false;
+}
+
+bool HTMLSelectListAccessible::UnselectAll() {
+ return mContent->AsElement()->HasAttr(nsGkAtoms::multiple)
+ ? AccessibleWrap::UnselectAll()
+ : false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLSelectListAccessible: Widgets
+
+bool HTMLSelectListAccessible::IsWidget() const { return true; }
+
+bool HTMLSelectListAccessible::IsActiveWidget() const {
+ return FocusMgr()->HasDOMFocus(mContent);
+}
+
+bool HTMLSelectListAccessible::AreItemsOperable() const { return true; }
+
+LocalAccessible* HTMLSelectListAccessible::CurrentItem() const {
+ nsListControlFrame* listControlFrame = do_QueryFrame(GetFrame());
+ if (listControlFrame) {
+ nsCOMPtr<nsIContent> activeOptionNode =
+ listControlFrame->GetCurrentOption();
+ if (activeOptionNode) {
+ DocAccessible* document = Document();
+ if (document) return document->GetAccessible(activeOptionNode);
+ }
+ }
+ return nullptr;
+}
+
+void HTMLSelectListAccessible::SetCurrentItem(const LocalAccessible* aItem) {
+ if (!aItem->GetContent()->IsElement()) return;
+
+ aItem->GetContent()->AsElement()->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::selected, u"true"_ns, true);
+}
+
+bool HTMLSelectListAccessible::IsAcceptableChild(nsIContent* aEl) const {
+ return aEl->IsAnyOfHTMLElements(nsGkAtoms::option, nsGkAtoms::optgroup);
+}
+
+bool HTMLSelectListAccessible::AttributeChangesState(nsAtom* aAttribute) {
+ return aAttribute == nsGkAtoms::multiple ||
+ LocalAccessible::AttributeChangesState(aAttribute);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLSelectOptionAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLSelectOptionAccessible::HTMLSelectOptionAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLSelectOptionAccessible: LocalAccessible public
+
+role HTMLSelectOptionAccessible::NativeRole() const {
+ if (GetCombobox()) return roles::COMBOBOX_OPTION;
+
+ return roles::OPTION;
+}
+
+ENameValueFlag HTMLSelectOptionAccessible::NativeName(nsString& aName) const {
+ if (auto* option = dom::HTMLOptionElement::FromNode(mContent)) {
+ option->GetAttr(nsGkAtoms::label, aName);
+ if (!aName.IsEmpty()) {
+ return eNameOK;
+ }
+ option->GetText(aName);
+ return eNameFromSubtree;
+ }
+ if (auto* group = dom::HTMLOptGroupElement::FromNode(mContent)) {
+ group->GetLabel(aName);
+ return aName.IsEmpty() ? eNameOK : eNameFromSubtree;
+ }
+ MOZ_ASSERT_UNREACHABLE("What content do we have?");
+ return eNameFromSubtree;
+}
+
+void HTMLSelectOptionAccessible::DOMAttributeChanged(
+ int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue, uint64_t aOldState) {
+ HyperTextAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
+ aOldValue, aOldState);
+
+ if (aAttribute == nsGkAtoms::label) {
+ dom::Element* elm = Elm();
+ if (!nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_labelledby) &&
+ !nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_label)) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ }
+ }
+}
+
+uint64_t HTMLSelectOptionAccessible::NativeState() const {
+ // As a HTMLSelectOptionAccessible we can have the following states:
+ // SELECTABLE, SELECTED, FOCUSED, FOCUSABLE, OFFSCREEN
+ // Upcall to LocalAccessible, but skip HyperTextAccessible impl
+ // because we don't want EDITABLE or SELECTABLE_TEXT
+ uint64_t state = LocalAccessible::NativeState();
+
+ LocalAccessible* select = GetSelect();
+ if (!select) return state;
+
+ uint64_t selectState = select->State();
+ if (selectState & states::INVISIBLE) return state;
+
+ // Are we selected?
+ HTMLOptionElement* option = HTMLOptionElement::FromNode(mContent);
+ bool selected = option && option->Selected();
+ if (selected) state |= states::SELECTED;
+
+ if (selectState & states::OFFSCREEN) {
+ state |= states::OFFSCREEN;
+ } else if (selectState & states::COLLAPSED) {
+ // <select> is COLLAPSED: add OFFSCREEN, if not the currently
+ // visible option
+ if (!selected) {
+ state |= states::OFFSCREEN;
+ // Ensure the invisible state is removed. Otherwise, group info will skip
+ // this option. Furthermore, this gets cached and this doesn't get
+ // invalidated even once the select is expanded.
+ state &= ~states::INVISIBLE;
+ } else {
+ // Clear offscreen and invisible for currently showing option
+ state &= ~(states::OFFSCREEN | states::INVISIBLE);
+ state |= selectState & states::OPAQUE1;
+ }
+ } else {
+ // XXX list frames are weird, don't rely on LocalAccessible's general
+ // visibility implementation unless they get reimplemented in layout
+ state &= ~states::OFFSCREEN;
+ // <select> is not collapsed: compare bounds to calculate OFFSCREEN
+ LocalAccessible* listAcc = LocalParent();
+ if (listAcc) {
+ LayoutDeviceIntRect optionRect = Bounds();
+ LayoutDeviceIntRect listRect = listAcc->Bounds();
+ if (optionRect.Y() < listRect.Y() ||
+ optionRect.YMost() > listRect.YMost()) {
+ state |= states::OFFSCREEN;
+ }
+ }
+ }
+
+ return state;
+}
+
+uint64_t HTMLSelectOptionAccessible::NativeInteractiveState() const {
+ return NativelyUnavailable() ? states::UNAVAILABLE
+ : states::FOCUSABLE | states::SELECTABLE;
+}
+
+nsRect HTMLSelectOptionAccessible::RelativeBounds(
+ nsIFrame** aBoundingFrame) const {
+ LocalAccessible* combobox = GetCombobox();
+ if (combobox && (combobox->State() & states::COLLAPSED)) {
+ return combobox->RelativeBounds(aBoundingFrame);
+ }
+
+ return HyperTextAccessible::RelativeBounds(aBoundingFrame);
+}
+
+void HTMLSelectOptionAccessible::ActionNameAt(uint8_t aIndex,
+ nsAString& aName) {
+ if (aIndex == eAction_Select) aName.AssignLiteral("select");
+}
+
+bool HTMLSelectOptionAccessible::HasPrimaryAction() const { return true; }
+
+void HTMLSelectOptionAccessible::SetSelected(bool aSelect) {
+ HTMLOptionElement* option = HTMLOptionElement::FromNode(mContent);
+ if (option) option->SetSelected(aSelect);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLSelectOptionAccessible: Widgets
+
+LocalAccessible* HTMLSelectOptionAccessible::ContainerWidget() const {
+ LocalAccessible* parent = LocalParent();
+ if (parent && parent->IsHTMLOptGroup()) {
+ parent = parent->LocalParent();
+ }
+
+ return parent && parent->IsListControl() ? parent : nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLSelectOptGroupAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role HTMLSelectOptGroupAccessible::NativeRole() const {
+ return roles::GROUPING;
+}
+
+uint64_t HTMLSelectOptGroupAccessible::NativeInteractiveState() const {
+ return NativelyUnavailable() ? states::UNAVAILABLE : 0;
+}
+
+bool HTMLSelectOptGroupAccessible::IsAcceptableChild(nsIContent* aEl) const {
+ return aEl->IsCharacterData() || aEl->IsHTMLElement(nsGkAtoms::option);
+}
+
+bool HTMLSelectOptGroupAccessible::HasPrimaryAction() const { return false; }
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLComboboxAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLComboboxAccessible::HTMLComboboxAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {
+ mType = eHTMLComboboxType;
+ mGenericTypes |= eCombobox;
+ mStateFlags |= eNoKidsFromDOM;
+
+ if ((nsComboboxControlFrame*)do_QueryFrame(GetFrame())) {
+ mListAccessible = new HTMLComboboxListAccessible(mParent, mContent, mDoc);
+ Document()->BindToDocument(mListAccessible, nullptr);
+ AppendChild(mListAccessible);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLComboboxAccessible: LocalAccessible
+
+role HTMLComboboxAccessible::NativeRole() const { return roles::COMBOBOX; }
+
+bool HTMLComboboxAccessible::RemoveChild(LocalAccessible* aChild) {
+ MOZ_ASSERT(aChild == mListAccessible);
+ if (AccessibleWrap::RemoveChild(aChild)) {
+ mListAccessible = nullptr;
+ return true;
+ }
+ return false;
+}
+
+void HTMLComboboxAccessible::Shutdown() {
+ MOZ_ASSERT(!mDoc || mDoc->IsDefunct() || !mListAccessible);
+ if (mListAccessible) {
+ mListAccessible->Shutdown();
+ mListAccessible = nullptr;
+ }
+
+ AccessibleWrap::Shutdown();
+}
+
+uint64_t HTMLComboboxAccessible::NativeState() const {
+ // As a HTMLComboboxAccessible we can have the following states:
+ // FOCUSED, FOCUSABLE, HASPOPUP, EXPANDED, COLLAPSED
+ // Get focus status from base class
+ uint64_t state = LocalAccessible::NativeState();
+
+ nsComboboxControlFrame* comboFrame = do_QueryFrame(GetFrame());
+ if (comboFrame && comboFrame->IsDroppedDown()) {
+ state |= states::EXPANDED;
+ } else {
+ state |= states::COLLAPSED;
+ }
+
+ state |= states::HASPOPUP;
+ return state;
+}
+
+void HTMLComboboxAccessible::Description(nsString& aDescription) const {
+ aDescription.Truncate();
+ // First check to see if combo box itself has a description, perhaps through
+ // tooltip (title attribute) or via aria-describedby
+ LocalAccessible::Description(aDescription);
+ if (!aDescription.IsEmpty()) return;
+
+ // Otherwise use description of selected option.
+ LocalAccessible* option = SelectedOption();
+ if (option) option->Description(aDescription);
+}
+
+void HTMLComboboxAccessible::Value(nsString& aValue) const {
+ // Use accessible name of selected option.
+ LocalAccessible* option = SelectedOption();
+ if (option) option->Name(aValue);
+}
+
+bool HTMLComboboxAccessible::HasPrimaryAction() const { return true; }
+
+void HTMLComboboxAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex != HTMLComboboxAccessible::eAction_Click) return;
+
+ nsComboboxControlFrame* comboFrame = do_QueryFrame(GetFrame());
+ if (!comboFrame) return;
+
+ if (comboFrame->IsDroppedDown()) {
+ aName.AssignLiteral("close");
+ } else {
+ aName.AssignLiteral("open");
+ }
+}
+
+bool HTMLComboboxAccessible::IsAcceptableChild(nsIContent* aEl) const {
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLComboboxAccessible: Widgets
+
+bool HTMLComboboxAccessible::IsWidget() const { return true; }
+
+bool HTMLComboboxAccessible::IsActiveWidget() const {
+ return FocusMgr()->HasDOMFocus(mContent);
+}
+
+bool HTMLComboboxAccessible::AreItemsOperable() const {
+ nsComboboxControlFrame* comboboxFrame = do_QueryFrame(GetFrame());
+ return comboboxFrame && comboboxFrame->IsDroppedDown();
+}
+
+LocalAccessible* HTMLComboboxAccessible::CurrentItem() const {
+ return AreItemsOperable() ? mListAccessible->CurrentItem() : nullptr;
+}
+
+void HTMLComboboxAccessible::SetCurrentItem(const LocalAccessible* aItem) {
+ if (AreItemsOperable()) mListAccessible->SetCurrentItem(aItem);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLComboboxAccessible: protected
+
+LocalAccessible* HTMLComboboxAccessible::SelectedOption() const {
+ HTMLSelectElement* select = HTMLSelectElement::FromNode(mContent);
+ int32_t selectedIndex = select->SelectedIndex();
+
+ if (selectedIndex >= 0) {
+ HTMLOptionElement* option = select->Item(selectedIndex);
+ if (option) {
+ DocAccessible* document = Document();
+ if (document) return document->GetAccessible(option);
+ }
+ }
+
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLComboboxListAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLComboboxListAccessible::HTMLComboboxListAccessible(LocalAccessible* aParent,
+ nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HTMLSelectListAccessible(aContent, aDoc) {
+ mStateFlags |= eSharedNode;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLComboboxAccessible: LocalAccessible
+
+role HTMLComboboxListAccessible::NativeRole() const {
+ return roles::COMBOBOX_LIST;
+}
+
+uint64_t HTMLComboboxListAccessible::NativeState() const {
+ // As a HTMLComboboxListAccessible we can have the following states:
+ // FOCUSED, FOCUSABLE, FLOATING, INVISIBLE
+ // Get focus status from base class
+ uint64_t state = LocalAccessible::NativeState();
+
+ nsComboboxControlFrame* comboFrame = do_QueryFrame(mParent->GetFrame());
+ if (comboFrame && comboFrame->IsDroppedDown()) {
+ state |= states::FLOATING;
+ } else {
+ state |= states::INVISIBLE;
+ }
+
+ return state;
+}
+
+nsRect HTMLComboboxListAccessible::RelativeBounds(
+ nsIFrame** aBoundingFrame) const {
+ *aBoundingFrame = nullptr;
+
+ LocalAccessible* comboAcc = LocalParent();
+ if (!comboAcc) return nsRect();
+
+ if (0 == (comboAcc->State() & states::COLLAPSED)) {
+ return HTMLSelectListAccessible::RelativeBounds(aBoundingFrame);
+ }
+
+ // Get the first option.
+ nsIContent* content = mContent->GetFirstChild();
+ if (!content) return nsRect();
+
+ nsIFrame* frame = content->GetPrimaryFrame();
+ if (!frame) {
+ *aBoundingFrame = nullptr;
+ return nsRect();
+ }
+
+ *aBoundingFrame = frame->GetParent();
+ return (*aBoundingFrame)->GetRect();
+}
+
+bool HTMLComboboxListAccessible::IsAcceptableChild(nsIContent* aEl) const {
+ return aEl->IsAnyOfHTMLElements(nsGkAtoms::option, nsGkAtoms::optgroup);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLComboboxListAccessible: Widgets
+
+bool HTMLComboboxListAccessible::IsActiveWidget() const {
+ return mParent && mParent->IsActiveWidget();
+}
+
+bool HTMLComboboxListAccessible::AreItemsOperable() const {
+ return mParent && mParent->AreItemsOperable();
+}
diff --git a/accessible/html/HTMLSelectAccessible.h b/accessible/html/HTMLSelectAccessible.h
new file mode 100644
index 0000000000..c496490e06
--- /dev/null
+++ b/accessible/html/HTMLSelectAccessible.h
@@ -0,0 +1,216 @@
+/* -*- Mode: C++; tab-width: 4; 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_HTMLSelectAccessible_h__
+#define mozilla_a11y_HTMLSelectAccessible_h__
+
+#include "HTMLFormControlAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Selects, Listboxes and Comboboxes, are made up of a number of different
+ * widgets, some of which are shared between the two. This file contains
+ * all of the widgets for both of the Selects, for HTML only.
+ *
+ * Listbox:
+ * - HTMLSelectListAccessible
+ * - HTMLSelectOptionAccessible
+ *
+ * Comboboxes:
+ * - HTMLComboboxAccessible
+ * - HTMLComboboxListAccessible [ inserted in accessible tree ]
+ * - HTMLSelectOptionAccessible(s)
+ */
+
+/*
+ * The list that contains all the options in the select.
+ */
+class HTMLSelectListAccessible : public AccessibleWrap {
+ public:
+ HTMLSelectListAccessible(nsIContent* aContent, DocAccessible* aDoc);
+ virtual ~HTMLSelectListAccessible() {}
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual bool IsAcceptableChild(nsIContent* aEl) const override;
+ virtual bool AttributeChangesState(nsAtom* aAttribute) override;
+
+ // SelectAccessible
+ virtual bool SelectAll() override;
+ virtual bool UnselectAll() override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+ virtual bool IsActiveWidget() const override;
+ virtual bool AreItemsOperable() const override;
+ virtual LocalAccessible* CurrentItem() const override;
+ virtual void SetCurrentItem(const LocalAccessible* aItem) override;
+};
+
+/*
+ * Options inside the select, contained within the list
+ */
+class HTMLSelectOptionAccessible : public HyperTextAccessible {
+ public:
+ enum { eAction_Select = 0 };
+
+ HTMLSelectOptionAccessible(nsIContent* aContent, DocAccessible* aDoc);
+ virtual ~HTMLSelectOptionAccessible() {}
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual uint64_t NativeInteractiveState() const override;
+
+ virtual nsRect RelativeBounds(nsIFrame** aBoundingFrame) const override;
+ virtual void SetSelected(bool aSelect) override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+
+ // Widgets
+ virtual LocalAccessible* ContainerWidget() const override;
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+
+ private:
+ /**
+ * Return a select accessible the option belongs to if any.
+ */
+ LocalAccessible* GetSelect() const {
+ LocalAccessible* parent = mParent;
+ if (parent && parent->IsHTMLOptGroup()) {
+ parent = parent->LocalParent();
+ }
+
+ if (parent && parent->IsListControl()) {
+ LocalAccessible* combobox = parent->LocalParent();
+ return combobox && combobox->IsCombobox() ? combobox : mParent;
+ }
+
+ return nullptr;
+ }
+
+ /**
+ * Return a combobox accessible the option belongs to if any.
+ */
+ LocalAccessible* GetCombobox() const {
+ LocalAccessible* parent = mParent;
+ if (parent && parent->IsHTMLOptGroup()) {
+ parent = parent->LocalParent();
+ }
+
+ if (parent && parent->IsListControl()) {
+ LocalAccessible* combobox = parent->LocalParent();
+ return combobox && combobox->IsCombobox() ? combobox : nullptr;
+ }
+
+ return nullptr;
+ }
+};
+
+/*
+ * Opt Groups inside the select, contained within the list
+ */
+class HTMLSelectOptGroupAccessible : public HTMLSelectOptionAccessible {
+ public:
+ HTMLSelectOptGroupAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HTMLSelectOptionAccessible(aContent, aDoc) {
+ mType = eHTMLOptGroupType;
+ }
+ virtual ~HTMLSelectOptGroupAccessible() {}
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeInteractiveState() const override;
+ virtual bool IsAcceptableChild(nsIContent* aEl) const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+};
+
+/** ------------------------------------------------------ */
+/** Finally, the Combobox widgets */
+/** ------------------------------------------------------ */
+
+class HTMLComboboxListAccessible;
+
+/*
+ * A class the represents the HTML Combobox widget.
+ */
+class HTMLComboboxAccessible final : public AccessibleWrap {
+ public:
+ enum { eAction_Click = 0 };
+
+ HTMLComboboxAccessible(nsIContent* aContent, DocAccessible* aDoc);
+ virtual ~HTMLComboboxAccessible() {}
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual void Description(nsString& aDescription) const override;
+ virtual void Value(nsString& aValue) const override;
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual bool RemoveChild(LocalAccessible* aChild) override;
+ virtual bool IsAcceptableChild(nsIContent* aEl) const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+ virtual bool IsActiveWidget() const override;
+ virtual bool AreItemsOperable() const override;
+ virtual LocalAccessible* CurrentItem() const override;
+ virtual void SetCurrentItem(const LocalAccessible* aItem) override;
+
+ HTMLComboboxListAccessible* List() const { return mListAccessible; }
+
+ /**
+ * Return selected option.
+ */
+ LocalAccessible* SelectedOption() const;
+
+ private:
+ RefPtr<HTMLComboboxListAccessible> mListAccessible;
+};
+
+/*
+ * A class that represents the window that lives to the right
+ * of the drop down button inside the Select. This is the window
+ * that is made visible when the button is pressed.
+ */
+class HTMLComboboxListAccessible : public HTMLSelectListAccessible {
+ public:
+ HTMLComboboxListAccessible(LocalAccessible* aParent, nsIContent* aContent,
+ DocAccessible* aDoc);
+ virtual ~HTMLComboboxListAccessible() {}
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual nsRect RelativeBounds(nsIFrame** aBoundingFrame) const override;
+ virtual bool IsAcceptableChild(nsIContent* aEl) const override;
+
+ // Widgets
+ virtual bool IsActiveWidget() const override;
+ virtual bool AreItemsOperable() const override;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/html/HTMLTableAccessible.cpp b/accessible/html/HTMLTableAccessible.cpp
new file mode 100644
index 0000000000..2c3dc6b82d
--- /dev/null
+++ b/accessible/html/HTMLTableAccessible.cpp
@@ -0,0 +1,712 @@
+/* -*- 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 "HTMLTableAccessible.h"
+
+#include <stdint.h>
+
+#include "nsAccessibilityService.h"
+#include "AccAttributes.h"
+#include "ARIAMap.h"
+#include "CacheConstants.h"
+#include "LocalAccessible-inl.h"
+#include "nsTextEquivUtils.h"
+#include "Relation.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+
+#include "mozilla/a11y/TableAccessible.h"
+#include "mozilla/a11y/TableCellAccessible.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/NameSpaceConstants.h"
+#include "nsCaseTreatment.h"
+#include "nsColor.h"
+#include "nsCOMPtr.h"
+#include "nsCoreUtils.h"
+#include "nsDebug.h"
+#include "nsIHTMLCollection.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsLiteralString.h"
+#include "nsMargin.h"
+#include "nsQueryFrame.h"
+#include "nsSize.h"
+#include "nsStringFwd.h"
+#include "nsTableCellFrame.h"
+#include "nsTableWrapperFrame.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLTableCellAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLTableCellAccessible::HTMLTableCellAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mType = eHTMLTableCellType;
+ mGenericTypes |= eTableCell;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLTableCellAccessible: LocalAccessible implementation
+
+role HTMLTableCellAccessible::NativeRole() const {
+ // We implement this rather than using the markup maps because we only want
+ // this role to be returned if this is a valid cell. An invalid cell (e.g. if
+ // the table has role="none") won't use this class, so it will get a generic
+ // role, since the markup map doesn't specify a role.
+ if (mContent->IsMathMLElement(nsGkAtoms::mtd_)) {
+ return roles::MATHML_CELL;
+ }
+ return roles::CELL;
+}
+
+uint64_t HTMLTableCellAccessible::NativeState() const {
+ uint64_t state = HyperTextAccessible::NativeState();
+
+ nsIFrame* frame = mContent->GetPrimaryFrame();
+ NS_ASSERTION(frame, "No frame for valid cell accessible!");
+
+ if (frame && frame->IsSelected()) {
+ state |= states::SELECTED;
+ }
+
+ return state;
+}
+
+uint64_t HTMLTableCellAccessible::NativeInteractiveState() const {
+ return HyperTextAccessible::NativeInteractiveState() | states::SELECTABLE;
+}
+
+already_AddRefed<AccAttributes> HTMLTableCellAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = HyperTextAccessible::NativeAttributes();
+
+ // We only need to expose table-cell-index to clients. If we're in the content
+ // process, we don't need this, so building a CachedTableAccessible is very
+ // wasteful. This will be exposed by RemoteAccessible in the parent process
+ // instead.
+ if (!IPCAccessibilityActive()) {
+ if (const TableCellAccessible* cell = AsTableCell()) {
+ TableAccessible* table = cell->Table();
+ const uint32_t row = cell->RowIdx();
+ const uint32_t col = cell->ColIdx();
+ const int32_t cellIdx = table->CellIndexAt(row, col);
+ if (cellIdx != -1) {
+ attributes->SetAttribute(nsGkAtoms::tableCellIndex, cellIdx);
+ }
+ }
+ }
+
+ // abbr attribute
+
+ // Pick up object attribute from abbr DOM element (a child of the cell) or
+ // from abbr DOM attribute.
+ nsString abbrText;
+ if (ChildCount() == 1) {
+ LocalAccessible* abbr = LocalFirstChild();
+ if (abbr->IsAbbreviation()) {
+ nsIContent* firstChildNode = abbr->GetContent()->GetFirstChild();
+ if (firstChildNode) {
+ nsTextEquivUtils::AppendTextEquivFromTextContent(firstChildNode,
+ &abbrText);
+ }
+ }
+ }
+ if (abbrText.IsEmpty()) {
+ mContent->AsElement()->GetAttr(nsGkAtoms::abbr, abbrText);
+ }
+
+ if (!abbrText.IsEmpty()) {
+ attributes->SetAttribute(nsGkAtoms::abbr, std::move(abbrText));
+ }
+
+ // axis attribute
+ nsString axisText;
+ mContent->AsElement()->GetAttr(nsGkAtoms::axis, axisText);
+ if (!axisText.IsEmpty()) {
+ attributes->SetAttribute(nsGkAtoms::axis, std::move(axisText));
+ }
+
+ return attributes.forget();
+}
+
+void HTMLTableCellAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ HyperTextAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
+ aOldValue, aOldState);
+
+ if (aAttribute == nsGkAtoms::headers || aAttribute == nsGkAtoms::abbr ||
+ aAttribute == nsGkAtoms::scope) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ this);
+ if (HTMLTableAccessible* table = Table()) {
+ // Modifying these attributes can also modify our table's classification
+ // as either a layout or data table. Queue an update on the table itself
+ // to re-compute our "layout guess"
+ mDoc->QueueCacheUpdate(table, CacheDomain::Table);
+ }
+ mDoc->QueueCacheUpdate(this, CacheDomain::Table);
+ } else if (aAttribute == nsGkAtoms::rowspan ||
+ aAttribute == nsGkAtoms::colspan) {
+ if (HTMLTableAccessible* table = Table()) {
+ // Modifying these attributes can also modify our table's classification
+ // as either a layout or data table. Queue an update on the table itself
+ // to re-compute our "layout guess"
+ mDoc->QueueCacheUpdate(table, CacheDomain::Table);
+ }
+ mDoc->QueueCacheUpdate(this, CacheDomain::Table);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLTableCellAccessible implementation
+
+HTMLTableAccessible* HTMLTableCellAccessible::Table() const {
+ LocalAccessible* parent = const_cast<HTMLTableCellAccessible*>(this);
+ while ((parent = parent->LocalParent())) {
+ if (parent->IsHTMLTable()) {
+ return HTMLTableAccessible::GetFrom(parent);
+ }
+ }
+
+ return nullptr;
+}
+
+uint32_t HTMLTableCellAccessible::ColExtent() const {
+ nsTableCellFrame* cell = do_QueryFrame(GetFrame());
+ if (!cell) {
+ // This probably isn't a table according to the layout engine; e.g. it has
+ // display: block.
+ return 1;
+ }
+ nsTableFrame* table = cell->GetTableFrame();
+ MOZ_ASSERT(table);
+ return table->GetEffectiveColSpan(*cell);
+}
+
+uint32_t HTMLTableCellAccessible::RowExtent() const {
+ nsTableCellFrame* cell = do_QueryFrame(GetFrame());
+ if (!cell) {
+ // This probably isn't a table according to the layout engine; e.g. it has
+ // display: block.
+ return 1;
+ }
+ nsTableFrame* table = cell->GetTableFrame();
+ MOZ_ASSERT(table);
+ return table->GetEffectiveRowSpan(*cell);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLTableHeaderCellAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+HTMLTableHeaderCellAccessible::HTMLTableHeaderCellAccessible(
+ nsIContent* aContent, DocAccessible* aDoc)
+ : HTMLTableCellAccessible(aContent, aDoc) {}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLTableHeaderCellAccessible: LocalAccessible implementation
+
+role HTMLTableHeaderCellAccessible::NativeRole() const {
+ dom::Element* el = Elm();
+ if (!el) {
+ return roles::NOTHING;
+ }
+
+ // Check value of @scope attribute.
+ static mozilla::dom::Element::AttrValuesArray scopeValues[] = {
+ nsGkAtoms::col, nsGkAtoms::colgroup, nsGkAtoms::row, nsGkAtoms::rowgroup,
+ nullptr};
+ int32_t valueIdx = el->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::scope,
+ scopeValues, eCaseMatters);
+
+ switch (valueIdx) {
+ case 0:
+ case 1:
+ return roles::COLUMNHEADER;
+ case 2:
+ case 3:
+ return roles::ROWHEADER;
+ }
+
+ dom::Element* nextEl = el->GetNextElementSibling();
+ dom::Element* prevEl = el->GetPreviousElementSibling();
+ // If this is the only cell in its row, it's a column header.
+ if (!nextEl && !prevEl) {
+ return roles::COLUMNHEADER;
+ }
+ const bool nextIsHeader = nextEl && nsCoreUtils::IsHTMLTableHeader(nextEl);
+ const bool prevIsHeader = prevEl && nsCoreUtils::IsHTMLTableHeader(prevEl);
+ // If this has a header on both sides, it is a column header.
+ if (prevIsHeader && nextIsHeader) {
+ return roles::COLUMNHEADER;
+ }
+ // If this has a header on one side and only a single normal cell on the
+ // other, it's a column header.
+ if (nextIsHeader && prevEl && !prevEl->GetPreviousElementSibling()) {
+ return roles::COLUMNHEADER;
+ }
+ if (prevIsHeader && nextEl && !nextEl->GetNextElementSibling()) {
+ return roles::COLUMNHEADER;
+ }
+ // If this has a normal cell next to it, it 's a row header.
+ if ((nextEl && !nextIsHeader) || (prevEl && !prevIsHeader)) {
+ return roles::ROWHEADER;
+ }
+ // If this has a row span, it could be a row header.
+ if (RowExtent() > 1) {
+ // It isn't a row header if it has 1 or more consecutive headers next to it.
+ if (prevIsHeader &&
+ (!prevEl->GetPreviousElementSibling() ||
+ nsCoreUtils::IsHTMLTableHeader(prevEl->GetPreviousElementSibling()))) {
+ return roles::COLUMNHEADER;
+ }
+ if (nextIsHeader &&
+ (!nextEl->GetNextElementSibling() ||
+ nsCoreUtils::IsHTMLTableHeader(nextEl->GetNextElementSibling()))) {
+ return roles::COLUMNHEADER;
+ }
+ return roles::ROWHEADER;
+ }
+ // Otherwise, assume it's a column header.
+ return roles::COLUMNHEADER;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLTableRowAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+// LocalAccessible protected
+ENameValueFlag HTMLTableRowAccessible::NativeName(nsString& aName) const {
+ // For table row accessibles, we only want to calculate the name from the
+ // sub tree if an ARIA role is present.
+ if (HasStrongARIARole()) {
+ return AccessibleWrap::NativeName(aName);
+ }
+
+ return eNameOK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLTableAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLTableAccessible: LocalAccessible
+
+bool HTMLTableAccessible::InsertChildAt(uint32_t aIndex,
+ LocalAccessible* aChild) {
+ // Move caption accessible so that it's the first child. Check for the first
+ // caption only, because nsAccessibilityService ensures we don't create
+ // accessibles for the other captions, since only the first is actually
+ // visible.
+ return HyperTextAccessible::InsertChildAt(
+ aChild->IsHTMLCaption() ? 0 : aIndex, aChild);
+}
+
+uint64_t HTMLTableAccessible::NativeState() const {
+ return LocalAccessible::NativeState() | states::READONLY;
+}
+
+ENameValueFlag HTMLTableAccessible::NativeName(nsString& aName) const {
+ ENameValueFlag nameFlag = LocalAccessible::NativeName(aName);
+ if (!aName.IsEmpty()) {
+ return nameFlag;
+ }
+
+ // Use table caption as a name.
+ LocalAccessible* caption = Caption();
+ if (caption) {
+ nsIContent* captionContent = caption->GetContent();
+ if (captionContent) {
+ nsTextEquivUtils::AppendTextEquivFromContent(this, captionContent,
+ &aName);
+ if (!aName.IsEmpty()) {
+ return eNameOK;
+ }
+ }
+ }
+
+ // If no caption then use summary as a name.
+ mContent->AsElement()->GetAttr(nsGkAtoms::summary, aName);
+ return eNameOK;
+}
+
+void HTMLTableAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) {
+ HyperTextAccessible::DOMAttributeChanged(aNameSpaceID, aAttribute, aModType,
+ aOldValue, aOldState);
+
+ if (aAttribute == nsGkAtoms::summary) {
+ nsAutoString name;
+ ARIAName(name);
+ if (name.IsEmpty()) {
+ if (!Caption()) {
+ // XXX: Should really be checking if caption provides a name.
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ }
+ }
+
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ this);
+ mDoc->QueueCacheUpdate(this, CacheDomain::Table);
+ }
+}
+
+already_AddRefed<AccAttributes> HTMLTableAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = AccessibleWrap::NativeAttributes();
+
+ if (mContent->IsMathMLElement(nsGkAtoms::mtable_)) {
+ GetAccService()->MarkupAttributes(this, attributes);
+ }
+
+ if (IsProbablyLayoutTable()) {
+ attributes->SetAttribute(nsGkAtoms::layout_guess, true);
+ }
+
+ return attributes.forget();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLTableAccessible: LocalAccessible
+
+Relation HTMLTableAccessible::RelationByType(RelationType aType) const {
+ Relation rel = AccessibleWrap::RelationByType(aType);
+ if (aType == RelationType::LABELLED_BY) {
+ rel.AppendTarget(Caption());
+ }
+
+ return rel;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLTableAccessible: Table
+
+LocalAccessible* HTMLTableAccessible::Caption() const {
+ LocalAccessible* child = mChildren.SafeElementAt(0, nullptr);
+ // Since this is an HTML table the caption needs to be a caption
+ // element with no ARIA role (except for a reduntant role='caption').
+ // If we did a full Role() calculation here we risk getting into an infinite
+ // loop where the parent role would depend on its name which would need to be
+ // calculated by retrieving the caption (bug 1420773.)
+ return child && child->NativeRole() == roles::CAPTION &&
+ (!child->HasStrongARIARole() ||
+ child->IsARIARole(nsGkAtoms::caption))
+ ? child
+ : nullptr;
+}
+
+uint32_t HTMLTableAccessible::ColCount() const {
+ nsTableWrapperFrame* tableFrame = GetTableWrapperFrame();
+ return tableFrame ? tableFrame->GetColCount() : 0;
+}
+
+uint32_t HTMLTableAccessible::RowCount() {
+ nsTableWrapperFrame* tableFrame = GetTableWrapperFrame();
+ return tableFrame ? tableFrame->GetRowCount() : 0;
+}
+
+bool HTMLTableAccessible::IsProbablyLayoutTable() {
+ // Implement a heuristic to determine if table is most likely used for layout.
+
+ // XXX do we want to look for rowspan or colspan, especialy that span all but
+ // a couple cells at the beginning or end of a row/col, and especially when
+ // they occur at the edge of a table?
+
+ // XXX For now debugging descriptions are always on via SHOW_LAYOUT_HEURISTIC
+ // This will allow release trunk builds to be used by testers to refine
+ // the algorithm. Integrate it into Logging.
+ // Change to |#define SHOW_LAYOUT_HEURISTIC DEBUG| before final release
+#ifdef SHOW_LAYOUT_HEURISTIC
+# define RETURN_LAYOUT_ANSWER(isLayout, heuristic) \
+ { \
+ mLayoutHeuristic = isLayout \
+ ? nsLiteralString(u"layout table: " heuristic) \
+ : nsLiteralString(u"data table: " heuristic); \
+ return isLayout; \
+ }
+#else
+# define RETURN_LAYOUT_ANSWER(isLayout, heuristic) \
+ { return isLayout; }
+#endif
+
+ MOZ_ASSERT(!IsDefunct(), "Table accessible should not be defunct");
+
+ // Need to see all elements while document is being edited.
+ if (Document()->State() & states::EDITABLE) {
+ RETURN_LAYOUT_ANSWER(false, "In editable document");
+ }
+
+ // Check to see if an ARIA role overrides the role from native markup,
+ // but for which we still expose table semantics (treegrid, for example).
+ if (HasARIARole()) {
+ RETURN_LAYOUT_ANSWER(false, "Has role attribute");
+ }
+
+ dom::Element* el = Elm();
+ if (el->IsMathMLElement(nsGkAtoms::mtable_)) {
+ RETURN_LAYOUT_ANSWER(false, "MathML matrix");
+ }
+
+ MOZ_ASSERT(el->IsHTMLElement(nsGkAtoms::table),
+ "Table should not be built by CSS display:table style");
+
+ // Check if datatable attribute has "0" value.
+ if (el->AttrValueIs(kNameSpaceID_None, nsGkAtoms::datatable, u"0"_ns,
+ eCaseMatters)) {
+ RETURN_LAYOUT_ANSWER(true, "Has datatable = 0 attribute, it's for layout");
+ }
+
+ // Check for legitimate data table attributes.
+ if (el->Element::HasNonEmptyAttr(nsGkAtoms::summary)) {
+ RETURN_LAYOUT_ANSWER(false, "Has summary -- legitimate table structures");
+ }
+
+ // Check for legitimate data table elements.
+ LocalAccessible* caption = LocalFirstChild();
+ if (caption && caption->IsHTMLCaption() && caption->HasChildren()) {
+ RETURN_LAYOUT_ANSWER(false,
+ "Not empty caption -- legitimate table structures");
+ }
+
+ for (nsIContent* childElm = el->GetFirstChild(); childElm;
+ childElm = childElm->GetNextSibling()) {
+ if (!childElm->IsHTMLElement()) continue;
+
+ if (childElm->IsAnyOfHTMLElements(nsGkAtoms::col, nsGkAtoms::colgroup,
+ nsGkAtoms::tfoot, nsGkAtoms::thead)) {
+ RETURN_LAYOUT_ANSWER(
+ false,
+ "Has col, colgroup, tfoot or thead -- legitimate table structures");
+ }
+
+ if (childElm->IsHTMLElement(nsGkAtoms::tbody)) {
+ for (nsIContent* rowElm = childElm->GetFirstChild(); rowElm;
+ rowElm = rowElm->GetNextSibling()) {
+ if (rowElm->IsHTMLElement(nsGkAtoms::tr)) {
+ if (LocalAccessible* row = Document()->GetAccessible(rowElm)) {
+ if (const nsRoleMapEntry* roleMapEntry = row->ARIARoleMap()) {
+ if (roleMapEntry->role != roles::ROW) {
+ RETURN_LAYOUT_ANSWER(true, "Repurposed tr with different role");
+ }
+ }
+ }
+
+ for (nsIContent* cellElm = rowElm->GetFirstChild(); cellElm;
+ cellElm = cellElm->GetNextSibling()) {
+ if (cellElm->IsHTMLElement()) {
+ if (cellElm->NodeInfo()->Equals(nsGkAtoms::th)) {
+ RETURN_LAYOUT_ANSWER(false,
+ "Has th -- legitimate table structures");
+ }
+
+ if (cellElm->AsElement()->HasAttr(nsGkAtoms::headers) ||
+ cellElm->AsElement()->HasAttr(nsGkAtoms::scope) ||
+ cellElm->AsElement()->HasAttr(nsGkAtoms::abbr)) {
+ RETURN_LAYOUT_ANSWER(false,
+ "Has headers, scope, or abbr attribute -- "
+ "legitimate table structures");
+ }
+
+ if (LocalAccessible* cell = Document()->GetAccessible(cellElm)) {
+ if (const nsRoleMapEntry* roleMapEntry = cell->ARIARoleMap()) {
+ if (roleMapEntry->role != roles::CELL &&
+ roleMapEntry->role != roles::COLUMNHEADER &&
+ roleMapEntry->role != roles::ROWHEADER &&
+ roleMapEntry->role != roles::GRID_CELL) {
+ RETURN_LAYOUT_ANSWER(true,
+ "Repurposed cell with different role");
+ }
+ }
+ if (cell->ChildCount() == 1 &&
+ cell->LocalFirstChild()->IsAbbreviation()) {
+ RETURN_LAYOUT_ANSWER(
+ false, "has abbr -- legitimate table structures");
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // If only 1 column or only 1 row, it's for layout.
+ auto colCount = ColCount();
+ if (colCount <= 1) {
+ RETURN_LAYOUT_ANSWER(true, "Has only 1 column");
+ }
+ auto rowCount = RowCount();
+ if (rowCount <= 1) {
+ RETURN_LAYOUT_ANSWER(true, "Has only 1 row");
+ }
+
+ // Check for many columns.
+ if (colCount >= 5) {
+ RETURN_LAYOUT_ANSWER(false, ">=5 columns");
+ }
+
+ // Now we know there are 2-4 columns and 2 or more rows. Check to see if
+ // there are visible borders on the cells.
+ // XXX currently, we just check the first cell -- do we really need to do
+ // more?
+ nsTableWrapperFrame* tableFrame = do_QueryFrame(el->GetPrimaryFrame());
+ if (!tableFrame) {
+ RETURN_LAYOUT_ANSWER(false, "table with no frame!");
+ }
+
+ nsIFrame* cellFrame = tableFrame->GetCellFrameAt(0, 0);
+ if (!cellFrame) {
+ RETURN_LAYOUT_ANSWER(false, "table's first cell has no frame!");
+ }
+
+ nsMargin border = cellFrame->StyleBorder()->GetComputedBorder();
+ if (border.top || border.bottom || border.left || border.right) {
+ RETURN_LAYOUT_ANSWER(false, "Has nonzero border-width on table cell");
+ }
+
+ // Check for nested tables.
+ nsCOMPtr<nsIHTMLCollection> nestedTables =
+ el->GetElementsByTagName(u"table"_ns);
+ if (nestedTables->Length() > 0) {
+ RETURN_LAYOUT_ANSWER(true, "Has a nested table within it");
+ }
+
+ // Rules for non-bordered tables with 2-4 columns and 2+ rows from here on
+ // forward.
+
+ // Check for styled background color across rows (alternating background
+ // color is a common feature for data tables).
+ auto childCount = ChildCount();
+ nscolor rowColor = 0;
+ nscolor prevRowColor;
+ for (auto childIdx = 0U; childIdx < childCount; childIdx++) {
+ LocalAccessible* child = LocalChildAt(childIdx);
+ if (child->IsHTMLTableRow()) {
+ prevRowColor = rowColor;
+ nsIFrame* rowFrame = child->GetFrame();
+ MOZ_ASSERT(rowFrame, "Table hierarchy got screwed up");
+ if (!rowFrame) {
+ RETURN_LAYOUT_ANSWER(false, "Unexpected table hierarchy");
+ }
+
+ rowColor = rowFrame->StyleBackground()->BackgroundColor(rowFrame);
+
+ if (childIdx > 0 && prevRowColor != rowColor) {
+ RETURN_LAYOUT_ANSWER(false,
+ "2 styles of row background color, non-bordered");
+ }
+ }
+ }
+
+ // Check for many rows.
+ const uint32_t kMaxLayoutRows = 20;
+ if (rowCount > kMaxLayoutRows) { // A ton of rows, this is probably for data
+ RETURN_LAYOUT_ANSWER(false, ">= kMaxLayoutRows (20) and non-bordered");
+ }
+
+ // Check for very wide table.
+ nsIFrame* documentFrame = Document()->GetFrame();
+ nsSize documentSize = documentFrame->GetSize();
+ if (documentSize.width > 0) {
+ nsSize tableSize = GetFrame()->GetSize();
+ int32_t percentageOfDocWidth = (100 * tableSize.width) / documentSize.width;
+ if (percentageOfDocWidth > 95) {
+ // 3-4 columns, no borders, not a lot of rows, and 95% of the doc's width
+ // Probably for layout
+ RETURN_LAYOUT_ANSWER(
+ true, "<= 4 columns, table width is 95% of document width");
+ }
+ }
+
+ // Two column rules.
+ if (rowCount * colCount <= 10) {
+ RETURN_LAYOUT_ANSWER(true, "2-4 columns, 10 cells or less, non-bordered");
+ }
+
+ static const nsLiteralString tags[] = {u"embed"_ns, u"object"_ns,
+ u"iframe"_ns};
+ for (const auto& tag : tags) {
+ nsCOMPtr<nsIHTMLCollection> descendants = el->GetElementsByTagName(tag);
+ if (descendants->Length() > 0) {
+ RETURN_LAYOUT_ANSWER(true,
+ "Has no borders, and has iframe, object or embed, "
+ "typical of advertisements");
+ }
+ }
+
+ RETURN_LAYOUT_ANSWER(false,
+ "No layout factor strong enough, so will guess data");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLTableAccessible: protected implementation
+
+void HTMLTableAccessible::Description(nsString& aDescription) const {
+ // Helpful for debugging layout vs. data tables
+ aDescription.Truncate();
+ LocalAccessible::Description(aDescription);
+ if (!aDescription.IsEmpty()) {
+ return;
+ }
+
+ // Use summary as description if it weren't used as a name.
+ // XXX: get rid code duplication with NameInternal().
+ LocalAccessible* caption = Caption();
+ if (caption) {
+ nsIContent* captionContent = caption->GetContent();
+ if (captionContent) {
+ nsAutoString captionText;
+ nsTextEquivUtils::AppendTextEquivFromContent(this, captionContent,
+ &captionText);
+
+ if (!captionText.IsEmpty()) { // summary isn't used as a name.
+ mContent->AsElement()->GetAttr(nsGkAtoms::summary, aDescription);
+ }
+ }
+ }
+
+#ifdef SHOW_LAYOUT_HEURISTIC
+ if (aDescription.IsEmpty()) {
+ bool isProbablyForLayout = IsProbablyLayoutTable();
+ aDescription = mLayoutHeuristic;
+ }
+ printf("\nTABLE: %s\n", NS_ConvertUTF16toUTF8(mLayoutHeuristic).get());
+#endif
+}
+
+nsTableWrapperFrame* HTMLTableAccessible::GetTableWrapperFrame() const {
+ nsTableWrapperFrame* tableFrame = do_QueryFrame(mContent->GetPrimaryFrame());
+ if (tableFrame && tableFrame->PrincipalChildList().FirstChild()) {
+ return tableFrame;
+ }
+
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// HTMLCaptionAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+Relation HTMLCaptionAccessible::RelationByType(RelationType aType) const {
+ Relation rel = HyperTextAccessible::RelationByType(aType);
+ if (aType == RelationType::LABEL_FOR) {
+ rel.AppendTarget(LocalParent());
+ }
+
+ return rel;
+}
+
+role HTMLCaptionAccessible::NativeRole() const { return roles::CAPTION; }
diff --git a/accessible/html/HTMLTableAccessible.h b/accessible/html/HTMLTableAccessible.h
new file mode 100644
index 0000000000..7dbad68cba
--- /dev/null
+++ b/accessible/html/HTMLTableAccessible.h
@@ -0,0 +1,177 @@
+/* -*- 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_HTMLTableAccessible_h__
+#define mozilla_a11y_HTMLTableAccessible_h__
+
+#include "HyperTextAccessible.h"
+
+class nsITableCellLayout;
+class nsTableCellFrame;
+class nsTableWrapperFrame;
+
+namespace mozilla {
+
+namespace a11y {
+
+class HTMLTableAccessible;
+
+/**
+ * HTML table cell accessible (html:td).
+ */
+class HTMLTableCellAccessible : public HyperTextAccessible {
+ public:
+ HTMLTableCellAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLTableCellAccessible,
+ HyperTextAccessible)
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual uint64_t NativeInteractiveState() const override;
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+
+ protected:
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+ // HTMLTableCellAccessible
+ public:
+ HTMLTableAccessible* Table() const;
+ uint32_t ColExtent() const;
+ uint32_t RowExtent() const;
+
+ static HTMLTableCellAccessible* GetFrom(LocalAccessible* aAcc) {
+ if (aAcc->IsHTMLTableCell()) {
+ return static_cast<HTMLTableCellAccessible*>(aAcc);
+ }
+ return nullptr;
+ }
+
+ protected:
+ virtual ~HTMLTableCellAccessible() {}
+};
+
+/**
+ * HTML table row/column header accessible (html:th or html:td@scope).
+ */
+class HTMLTableHeaderCellAccessible : public HTMLTableCellAccessible {
+ public:
+ HTMLTableHeaderCellAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+};
+
+/**
+ * HTML table row accessible (html:tr).
+ */
+class HTMLTableRowAccessible : public HyperTextAccessible {
+ public:
+ HTMLTableRowAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mType = eHTMLTableRowType;
+ mGenericTypes |= eTableRow;
+ }
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLTableRowAccessible,
+ HyperTextAccessible)
+
+ protected:
+ virtual ~HTMLTableRowAccessible() {}
+
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+};
+
+/**
+ * HTML table accessible (html:table).
+ */
+
+// To turn on table debugging descriptions define SHOW_LAYOUT_HEURISTIC
+// This allow release trunk builds to be used by testers to refine the
+// data vs. layout heuristic
+// #define SHOW_LAYOUT_HEURISTIC
+
+class HTMLTableAccessible : public HyperTextAccessible {
+ public:
+ HTMLTableAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mType = eHTMLTableType;
+ mGenericTypes |= eTable;
+ }
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLTableAccessible, HyperTextAccessible)
+
+ // HTMLTableAccessible
+ LocalAccessible* Caption() const;
+ uint32_t ColCount() const;
+ uint32_t RowCount();
+ bool IsProbablyLayoutTable();
+
+ static HTMLTableAccessible* GetFrom(LocalAccessible* aAcc) {
+ if (aAcc->IsHTMLTable()) {
+ return static_cast<HTMLTableAccessible*>(aAcc);
+ }
+ return nullptr;
+ }
+
+ // LocalAccessible
+ virtual void Description(nsString& aDescription) const override;
+ virtual uint64_t NativeState() const override;
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+ virtual Relation RelationByType(RelationType aRelationType) const override;
+
+ virtual bool InsertChildAt(uint32_t aIndex, LocalAccessible* aChild) override;
+
+ protected:
+ virtual ~HTMLTableAccessible() {}
+
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+
+ virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType,
+ const nsAttrValue* aOldValue,
+ uint64_t aOldState) override;
+
+ // HTMLTableAccessible
+
+#ifdef SHOW_LAYOUT_HEURISTIC
+ nsString mLayoutHeuristic;
+#endif
+
+ private:
+ /**
+ * Get table wrapper frame, or return null if there is no inner table.
+ */
+ nsTableWrapperFrame* GetTableWrapperFrame() const;
+};
+
+/**
+ * HTML caption accessible (html:caption).
+ */
+class HTMLCaptionAccessible : public HyperTextAccessible {
+ public:
+ HTMLCaptionAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mType = eHTMLCaptionType;
+ }
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual Relation RelationByType(RelationType aRelationType) const override;
+
+ protected:
+ virtual ~HTMLCaptionAccessible() {}
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/html/moz.build b/accessible/html/moz.build
new file mode 100644
index 0000000000..3a246373da
--- /dev/null
+++ b/accessible/html/moz.build
@@ -0,0 +1,52 @@
+# -*- 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/.
+
+UNIFIED_SOURCES += [
+ "HTMLCanvasAccessible.cpp",
+ "HTMLElementAccessibles.cpp",
+ "HTMLFormControlAccessible.cpp",
+ "HTMLImageMapAccessible.cpp",
+ "HTMLLinkAccessible.cpp",
+ "HTMLListAccessible.cpp",
+ "HTMLSelectAccessible.cpp",
+ "HTMLTableAccessible.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/generic",
+ "/accessible/xpcom",
+ "/layout/forms",
+ "/layout/generic",
+ "/layout/tables",
+ "/layout/xul",
+]
+
+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/interfaces/ia2/IA2Typelib.idl b/accessible/interfaces/ia2/IA2Typelib.idl
new file mode 100644
index 0000000000..71e759d4a2
--- /dev/null
+++ b/accessible/interfaces/ia2/IA2Typelib.idl
@@ -0,0 +1,33 @@
+/* -*- 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/. */
+
+import "Accessible2.idl";
+import "Accessible2_2.idl";
+import "AccessibleAction.idl";
+import "AccessibleApplication.idl";
+import "AccessibleComponent.idl";
+import "AccessibleDocument.idl";
+import "AccessibleEditableText.idl";
+import "AccessibleEventId.idl";
+import "AccessibleHyperlink.idl";
+import "AccessibleHypertext.idl";
+import "AccessibleHypertext2.idl";
+import "AccessibleImage.idl";
+import "AccessibleRelation.idl";
+import "AccessibleRole.idl";
+import "AccessibleStates.idl";
+import "AccessibleTable.idl";
+import "AccessibleTable2.idl";
+import "AccessibleTableCell.idl";
+import "AccessibleText.idl";
+import "AccessibleText2.idl";
+import "AccessibleTextSelectionContainer.idl";
+import "AccessibleValue.idl";
+import "IA2CommonTypes.idl";
+
+// We are explicitly using #include instead of import so that the imported
+// IDL is treated as part of this IDL file.
+#include "IA2TypeLibrary.idl"
diff --git a/accessible/interfaces/ia2/moz.build b/accessible/interfaces/ia2/moz.build
new file mode 100644
index 0000000000..9d39b2c1db
--- /dev/null
+++ b/accessible/interfaces/ia2/moz.build
@@ -0,0 +1,92 @@
+# -*- 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/.
+
+midl_enums = [
+ "AccessibleEventId",
+ "AccessibleRole",
+ "AccessibleStates",
+ "IA2CommonTypes",
+]
+
+midl_interfaces = [
+ "Accessible2",
+ "Accessible2_2",
+ "AccessibleAction",
+ "AccessibleApplication",
+ "AccessibleComponent",
+ "AccessibleDocument",
+ "AccessibleEditableText",
+ "AccessibleHyperlink",
+ "AccessibleHypertext",
+ "AccessibleHypertext2",
+ "AccessibleImage",
+ "AccessibleRelation",
+ "AccessibleTable",
+ "AccessibleTable2",
+ "AccessibleTableCell",
+ "AccessibleText",
+ "AccessibleText2",
+ "AccessibleTextSelectionContainer",
+ "AccessibleValue",
+]
+
+for enum in midl_enums:
+ GeneratedFile(
+ enum + ".h",
+ inputs=["/other-licenses/ia2/" + enum + ".idl"],
+ script="/build/midl.py",
+ entry_point="midl",
+ flags=["-app_config", "-I", TOPSRCDIR + "/other-licenses/ia2"],
+ )
+
+ EXPORTS += ["!" + enum + ".h"]
+
+for iface in midl_interfaces:
+ GeneratedFile(
+ iface + ".h",
+ iface + "_p.c",
+ iface + "_i.c",
+ iface + "_dlldata.c",
+ inputs=["/other-licenses/ia2/" + iface + ".idl"],
+ script="/build/midl.py",
+ entry_point="midl",
+ flags=[
+ "-app_config",
+ "-I",
+ TOPSRCDIR + "/other-licenses/ia2",
+ "-dlldata",
+ OBJDIR + "/" + iface + "_dlldata.c",
+ ],
+ )
+
+ EXPORTS += ["!" + iface + ".h", "!" + iface + "_i.c"]
+
+ for p in [iface + "_p.c", iface + "_i.c"]:
+ SOURCES += ["!%s" % p]
+
+ # Give some symbols a unique name in each translation unit, to avoid
+ # collisions caused by https://llvm.org/pr41817.
+ if CONFIG["CC_TYPE"] == "clang-cl":
+ SOURCES["!%s" % p].flags += [
+ "-DObject_StubDesc=Object_StubDesc__%s" % p[:-2]
+ ]
+ SOURCES["!%s" % p].flags += [
+ "-DUserMarshalRoutines=UserMarshalRoutines__%s" % p[:-2]
+ ]
+
+GeneratedFile(
+ "IA2Typelib.h",
+ "IA2Typelib_i.c",
+ "IA2Typelib.tlb",
+ inputs=["IA2Typelib.idl"],
+ script="/build/midl.py",
+ entry_point="midl",
+ flags=[
+ "-app_config",
+ "-I",
+ TOPSRCDIR + "/other-licenses/ia2",
+ ],
+)
diff --git a/accessible/interfaces/moz.build b/accessible/interfaces/moz.build
new file mode 100644
index 0000000000..cf568f1d18
--- /dev/null
+++ b/accessible/interfaces/moz.build
@@ -0,0 +1,45 @@
+# -*- 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/.
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows" and CONFIG["COMPILE_ENVIRONMENT"]:
+ DIRS += ["msaa", "ia2"]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ XPIDL_SOURCES += ["nsIAccessibleMacInterface.idl"]
+
+XPIDL_SOURCES += [
+ "nsIAccessibilityService.idl",
+ "nsIAccessible.idl",
+ "nsIAccessibleAnnouncementEvent.idl",
+ "nsIAccessibleApplication.idl",
+ "nsIAccessibleCaretMoveEvent.idl",
+ "nsIAccessibleDocument.idl",
+ "nsIAccessibleEditableText.idl",
+ "nsIAccessibleEvent.idl",
+ "nsIAccessibleHideEvent.idl",
+ "nsIAccessibleHyperLink.idl",
+ "nsIAccessibleHyperText.idl",
+ "nsIAccessibleImage.idl",
+ "nsIAccessibleObjectAttributeChangedEvent.idl",
+ "nsIAccessiblePivot.idl",
+ "nsIAccessibleRelation.idl",
+ "nsIAccessibleRole.idl",
+ "nsIAccessibleScrollingEvent.idl",
+ "nsIAccessibleSelectable.idl",
+ "nsIAccessibleStateChangeEvent.idl",
+ "nsIAccessibleStates.idl",
+ "nsIAccessibleTable.idl",
+ "nsIAccessibleTableChangeEvent.idl",
+ "nsIAccessibleText.idl",
+ "nsIAccessibleTextChangeEvent.idl",
+ "nsIAccessibleTextLeafRange.idl",
+ "nsIAccessibleTextRange.idl",
+ "nsIAccessibleTextSelectionChangeEvent.idl",
+ "nsIAccessibleTypes.idl",
+ "nsIAccessibleValue.idl",
+]
+
+XPIDL_MODULE = "accessibility"
diff --git a/accessible/interfaces/msaa/AccessibleMarshal.def b/accessible/interfaces/msaa/AccessibleMarshal.def
new file mode 100644
index 0000000000..a4992db1d8
--- /dev/null
+++ b/accessible/interfaces/msaa/AccessibleMarshal.def
@@ -0,0 +1,11 @@
+;+# This Source Code Form is subject to the terms of the Mozilla Public
+;+# License, v. 2.0. If a copy of the MPL was not distributed with this
+;+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+LIBRARY AccessibleMarshal.dll
+
+EXPORTS DllGetClassObject PRIVATE
+ DllCanUnloadNow PRIVATE
+ DllRegisterServer PRIVATE
+ DllUnregisterServer PRIVATE
+
diff --git a/accessible/interfaces/msaa/AccessibleMarshal.rc b/accessible/interfaces/msaa/AccessibleMarshal.rc
new file mode 100644
index 0000000000..13db3a833f
--- /dev/null
+++ b/accessible/interfaces/msaa/AccessibleMarshal.rc
@@ -0,0 +1,5 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+1 typelib ISimpleDOM.tlb
diff --git a/accessible/interfaces/msaa/AccessibleMarshalThunk.c b/accessible/interfaces/msaa/AccessibleMarshalThunk.c
new file mode 100644
index 0000000000..481223ed13
--- /dev/null
+++ b/accessible/interfaces/msaa/AccessibleMarshalThunk.c
@@ -0,0 +1,17 @@
+/* -*- 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/. */
+
+#include "ISimpleDOM.h"
+
+HRESULT STDMETHODCALLTYPE ISimpleDOMNode_get_localInterface_Proxy(
+ ISimpleDOMNode* This, void** localInterface) {
+ return E_NOTIMPL;
+}
+
+HRESULT STDMETHODCALLTYPE ISimpleDOMNode_get_localInterface_Stub(
+ ISimpleDOMNode* This, IUnknown** localInterface) {
+ return E_NOTIMPL;
+}
diff --git a/accessible/interfaces/msaa/ISimpleDOM.idl b/accessible/interfaces/msaa/ISimpleDOM.idl
new file mode 100644
index 0000000000..4bd51b7f50
--- /dev/null
+++ b/accessible/interfaces/msaa/ISimpleDOM.idl
@@ -0,0 +1,22 @@
+/* -*- 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/. */
+
+// We use #include instead of import here so that MIDL treats these files as
+// part of the current file, thus forcing MIDL to generate proxy info for them.
+#include "ISimpleDOMNode.idl"
+#include "ISimpleDOMDocument.idl"
+#include "ISimpleDOMText.idl"
+
+[
+ uuid(a6245497-9c0b-4449-85a5-bd6ad07df8ea),
+ helpstring("ISimpleDOM Type Library")
+]
+library ISimpleDOM
+{
+ interface ISimpleDOMNode;
+ interface ISimpleDOMText;
+ interface ISimpleDOMDocument;
+};
diff --git a/accessible/interfaces/msaa/ISimpleDOMDocument.idl b/accessible/interfaces/msaa/ISimpleDOMDocument.idl
new file mode 100644
index 0000000000..3e9e007e73
--- /dev/null
+++ b/accessible/interfaces/msaa/ISimpleDOMDocument.idl
@@ -0,0 +1,83 @@
+/* -*- 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/. */
+
+cpp_quote("///////////////////////////////////////////////////////////////////////////////////////////////////////")
+cpp_quote("//")
+cpp_quote("// ISimpleDOMDocument")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("//")
+cpp_quote("// get_URL(out] BSTR *url)")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Get the internet URL associated with this document.")
+cpp_quote("//")
+cpp_quote("// get_title([out BSTR *title")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Get the document's title from the <TITLE> element")
+cpp_quote("//")
+cpp_quote("// get_mimeType([out BSTR *mimeType")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Get the registered mime type, such as text/html")
+cpp_quote("//")
+cpp_quote("// get_docType([out] BSTR *docType")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Get doctype associated with the <!DOCTYPE ..> element")
+cpp_quote("//")
+cpp_quote("// get_nameSpaceURIForID([in] short nameSpaceID, [out] BSTR *nameSpaceURI)")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Some of the methods for ISimpleDOMNode return a nameSpaceID (-1,0,1,2,3,....)")
+cpp_quote("// This method returns the associated namespace URI for each ID.")
+cpp_quote("//")
+cpp_quote("// set_alternateViewMediaTypes([in] BSTR *commaSeparatedMediaType)")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// For style property retrieval on nsISimpleDOMNode elements, ")
+cpp_quote("// set the additional alternate media types that properties are available for.")
+cpp_quote("// [in] BSTR *commaSeparatedMediaTypes is a comma separate list, for example \"aural, braille\".")
+cpp_quote("// The alternate media properties are requested with nsISimpleDOMNode::get_computedStyle.")
+cpp_quote("// Note: setting this value on a document will increase memory overhead, and may create a small delay.")
+cpp_quote("//")
+cpp_quote("// W3C media Types:")
+cpp_quote("// * all: Suitable for all devices. ")
+cpp_quote("// * aural: Intended for speech synthesizers. See the section on aural style sheets for details. ")
+cpp_quote("// * braille: Intended for braille tactile feedback devices. ")
+cpp_quote("// * embossed: Intended for paged braille printers. ")
+cpp_quote("// * handheld: Intended for handheld devices - typically small screen, monochrome, limited bandwidth. ")
+cpp_quote("// * print: Intended for paged, opaque material and for documents viewed on screen in print preview mode. Please consult the section on paged media for information about formatting issues that are specific to paged media. ")
+cpp_quote("// * projection: Intended for projected presentations, for example projectors or print to transparencies. Please consult the section on paged media for information about formatting issues that are specific to paged media. ")
+cpp_quote("// * screen: Intended primarily for color computer screens. ")
+cpp_quote("// * tty: intended for media using a fixed-pitch character grid, such as teletypes, terminals, or portable devices with limited display capabilities. Authors should not use pixel units with the tty media type. ")
+cpp_quote("// * tv: Intended for television-type devices - low resolution, color, limited-scrollability screens, sound")
+cpp_quote("// * See latest W3C CSS specs for complete list of media types")
+cpp_quote("//")
+cpp_quote("//")
+cpp_quote("///////////////////////////////////////////////////////////////////////////////////////////////////////")
+cpp_quote("")
+cpp_quote("")
+
+import "objidl.idl";
+import "oaidl.idl";
+
+[object, uuid(0D68D6D0-D93D-4d08-A30D-F00DD1F45B24)]
+interface ISimpleDOMDocument : IUnknown
+{
+ [propget] HRESULT URL(
+ [out, retval] BSTR * url
+ );
+ [propget] HRESULT title(
+ [out, retval] BSTR * title
+ );
+ [propget] HRESULT mimeType(
+ [out, retval] BSTR * mimeType
+ );
+ [propget] HRESULT docType(
+ [out, retval] BSTR * docType
+ );
+ [propget] HRESULT nameSpaceURIForID(
+ [in] short nameSpaceID,
+ [out, retval] BSTR * nameSpaceURI
+ );
+ [propput] HRESULT alternateViewMediaTypes(
+ [in] BSTR * commaSeparatedMediaTypes
+ );
+}
diff --git a/accessible/interfaces/msaa/ISimpleDOMNode.idl b/accessible/interfaces/msaa/ISimpleDOMNode.idl
new file mode 100644
index 0000000000..860111e1ea
--- /dev/null
+++ b/accessible/interfaces/msaa/ISimpleDOMNode.idl
@@ -0,0 +1,175 @@
+/* -*- 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/. */
+
+cpp_quote("///////////////////////////////////////////////////////////////////////////////////////////////////////")
+cpp_quote("//")
+cpp_quote("// ISimpleDOMNode")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// An interface that extends MSAA's IAccessible to provide readonly DOM node information via cross-process COM.")
+cpp_quote("//")
+cpp_quote("// get_nodeInfo(")
+cpp_quote("// /* [out] */ BSTR *nodeName, // For elements, this is the tag name")
+cpp_quote("// /* [out] */ short *nameSpaceID,")
+cpp_quote("// /* [out] */ BSTR *nodeValue, ")
+cpp_quote("// /* [out] */ unsigned int *numChildren); ")
+cpp_quote("// /* [out] */ unsigned int *uniqueID; // In Win32 accessible events we generate, the target's childID matches to this")
+cpp_quote("// /* [out] */ unsigned short *nodeType,")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Get the basic information about a node.")
+cpp_quote("// The namespace ID can be mapped to an URI using nsISimpleDOMDocument::get_nameSpaceURIForID()")
+cpp_quote("//")
+cpp_quote("// get_attributes(")
+cpp_quote("// /* [in] */ unsigned short maxAttribs,")
+cpp_quote("// /* [out] */ unsigned short *numAttribs,")
+cpp_quote("// /* [out] */ BSTR *attribNames,")
+cpp_quote("// /* [out] */ short *nameSpaceID,")
+cpp_quote("// /* [out] */ BSTR *attribValues);")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Returns 3 arrays - the attribute names and values, and a namespace ID for each")
+cpp_quote("// If the namespace ID is 0, it's the same namespace as the node's namespace")
+cpp_quote("//")
+cpp_quote("// get_attributesForNames(")
+cpp_quote("// /* [in] */ unsigned short numAttribs,")
+cpp_quote("// /* [in] */ BSTR *attribNames,")
+cpp_quote("// /* [in] */ short *nameSpaceID,")
+cpp_quote("// /* [out] */ BSTR *attribValues);")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Takes 2 arrays - the attribute names and namespace IDs, and returns an array of corresponding values")
+cpp_quote("// If the namespace ID is 0, it's the same namespace as the node's namespace")
+cpp_quote("//")
+cpp_quote("// computedStyle( ")
+cpp_quote("// /* [in] */ unsigned short maxStyleProperties,")
+cpp_quote("// /* [out] */ unsigned short *numStyleProperties, ")
+cpp_quote("// /* [in] */ boolean useAlternateView, // If TRUE, returns properites for media as set in Document's set_alternateViewMediaTypes")
+cpp_quote("// /* [out] */ BSTR *styleProperties, ")
+cpp_quote("// /* [out] */ BSTR *styleValues);")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Returns 2 arrays -- the style properties and their values")
+cpp_quote("// useAlternateView=FALSE: gets properties for the default media type (usually screen)")
+cpp_quote("// useAlternateView=TRUE: properties for media types set w/ nsIDOMSimpleDocument::set_alternateViewMediaTypes()")
+cpp_quote("//")
+cpp_quote("// computedStyleForProperties( ")
+cpp_quote("// /* [in] */ unsigned short numStyleProperties, ")
+cpp_quote("// /* [in] */ boolean useAlternateView, // If TRUE, returns properites for media as set in Document's set_alternateViewMediaTypes")
+cpp_quote("// /* [in] */ BSTR *styleProperties, ")
+cpp_quote("// /* [out] */ BSTR *styleValues);")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Scroll the current view so that this dom node is visible.")
+cpp_quote("// placeTopLeft=TRUE: scroll until the top left corner of the dom node is at the top left corner of the view.")
+cpp_quote("// placeTopLeft=FALSE: scroll minimally to make the dom node visible. Don't scroll at all if already visible.")
+cpp_quote("//")
+cpp_quote("// scrollTo( ")
+cpp_quote("// /* [in] */ boolean placeTopLeft); ")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Returns style property values for those properties in the styleProperties [in] array")
+cpp_quote("// Returns 2 arrays -- the style properties and their values")
+cpp_quote("// useAlternateView=FALSE: gets properties for the default media type (usually screen)")
+cpp_quote("// useAlternateView=TRUE: properties for media types set w/ nsIDOMSimpleDocument::set_alternateViewMediaTypes()")
+cpp_quote("//")
+cpp_quote("// get_parentNode (/* [out] */ ISimpleDOMNode **newNodePtr);")
+cpp_quote("// get_firstChild (/* [out] */ ISimpleDOMNode **newNodePtr);")
+cpp_quote("// get_lastChild (/* [out] */ ISimpleDOMNode **newNodePtr);")
+cpp_quote("// get_previousSibling(/* [out] */ ISimpleDOMNode **newNodePtr);")
+cpp_quote("// get_nextSibling (/* [out] */ ISimpleDOMNode **newNodePtr);")
+cpp_quote("// get_childAt (/* [in] */ unsigned childIndex, /* [out] */ ISimpleDOMNode **newNodePtr);")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// DOM navigation - get a different node.")
+cpp_quote("//")
+cpp_quote("// get_innerHTML(/* [out] */ BSTR *htmlText);")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Returns HTML of this DOM node's subtree. Does not include the start and end tag for this node/element.")
+cpp_quote("//")
+cpp_quote("//")
+cpp_quote("// get_localInterface(/* [out] */ void **localInterface);")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Only available in Gecko's process")
+cpp_quote("//")
+cpp_quote("//")
+cpp_quote("// get_language(/* [out] */ BSTR *htmlText);")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Returns the computed language for this node, or empty string if unknown.")
+cpp_quote("//")
+cpp_quote("//")
+cpp_quote("///////////////////////////////////////////////////////////////////////////////////////////////////////")
+cpp_quote("")
+cpp_quote("")
+
+import "objidl.idl";
+import "oaidl.idl";
+
+[object, uuid(1814ceeb-49e2-407f-af99-fa755a7d2607)]
+interface ISimpleDOMNode : IUnknown
+{
+ const unsigned short NODETYPE_ELEMENT = 1;
+ const unsigned short NODETYPE_ATTRIBUTE = 2;
+ const unsigned short NODETYPE_TEXT = 3;
+ const unsigned short NODETYPE_CDATA_SECTION = 4;
+ const unsigned short NODETYPE_ENTITY_REFERENCE = 5;
+ const unsigned short NODETYPE_ENTITY = 6;
+ const unsigned short NODETYPE_PROCESSING_INSTRUCTION = 7;
+ const unsigned short NODETYPE_COMMENT = 8;
+ const unsigned short NODETYPE_DOCUMENT = 9;
+ const unsigned short NODETYPE_DOCUMENT_TYPE = 10;
+ const unsigned short NODETYPE_DOCUMENT_FRAGMENT = 11;
+ const unsigned short NODETYPE_NOTATION = 12;
+
+ [propget] HRESULT nodeInfo(
+ [out] BSTR *nodeName, // for performance returns NULL for text nodes (true nodeName would be "#text")
+ [out] short *nameSpaceID,
+ [out] BSTR *nodeValue,
+ [out] unsigned int *numChildren,
+ [out] unsigned int *uniqueID, // In Win32 accessible events we generate, the target's childID matches to this
+ [out, retval] unsigned short *nodeType
+ );
+
+ [propget] HRESULT attributes(
+ [in] unsigned short maxAttribs,
+ [out, size_is(maxAttribs), length_is(*numAttribs)] BSTR *attribNames,
+ [out, size_is(maxAttribs), length_is(*numAttribs)] short *nameSpaceID,
+ [out, size_is(maxAttribs), length_is(*numAttribs)] BSTR *attribValues,
+ [out, retval] unsigned short *numAttribs
+ );
+
+ [propget] HRESULT attributesForNames(
+ [in] unsigned short numAttribs,
+ [in, size_is(numAttribs), length_is(numAttribs)] BSTR *attribNames,
+ [in, size_is(numAttribs), length_is(numAttribs)] short *nameSpaceID,
+ [out, retval, size_is(numAttribs), length_is(numAttribs)] BSTR *attribValues
+ );
+
+ [propget] HRESULT computedStyle(
+ [in] unsigned short maxStyleProperties,
+ [in] boolean useAlternateView, // If TRUE, returns properites for media as set in Document's set_alternateViewMediaTypes
+ [out, size_is(maxStyleProperties), length_is(*numStyleProperties)] BSTR *styleProperties,
+ [out, size_is(maxStyleProperties), length_is(*numStyleProperties)] BSTR *styleValues,
+ [out, retval] unsigned short *numStyleProperties
+ );
+
+ [propget] HRESULT computedStyleForProperties(
+ [in] unsigned short numStyleProperties,
+ [in] boolean useAlternateView, // If TRUE, returns properites for media as set in Document's set_alternateViewMediaTypes
+ [in, size_is(numStyleProperties), length_is(numStyleProperties)] BSTR *styleProperties,
+ [out, retval, size_is(numStyleProperties), length_is(numStyleProperties)] BSTR *styleValues
+ );
+
+ HRESULT scrollTo([in] boolean placeTopLeft);
+
+ [propget] HRESULT parentNode([out, retval] ISimpleDOMNode **node);
+ [propget] HRESULT firstChild([out, retval] ISimpleDOMNode **node);
+ [propget] HRESULT lastChild([out, retval] ISimpleDOMNode **node);
+ [propget] HRESULT previousSibling([out, retval] ISimpleDOMNode **node);
+ [propget] HRESULT nextSibling([out, retval] ISimpleDOMNode **node);
+ [propget] HRESULT childAt([in] unsigned childIndex,
+ [out, retval] ISimpleDOMNode **node);
+
+ [propget] HRESULT innerHTML([out, retval] BSTR *innerHTML);
+
+ [propget, local] HRESULT localInterface([out][retval] void **localInterface);
+
+ [propget, call_as(get_localInterface)]
+ HRESULT remoteLocalInterface([out][retval] IUnknown **localInterface);
+
+ [propget] HRESULT language([out, retval] BSTR *language);
+}
diff --git a/accessible/interfaces/msaa/ISimpleDOMText.idl b/accessible/interfaces/msaa/ISimpleDOMText.idl
new file mode 100644
index 0000000000..c203cf98c1
--- /dev/null
+++ b/accessible/interfaces/msaa/ISimpleDOMText.idl
@@ -0,0 +1,78 @@
+/* -*- 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/. */
+
+import "objidl.idl";
+import "oaidl.idl";
+
+cpp_quote("///////////////////////////////////////////////////////////////////////////////////////////////////////")
+cpp_quote("//")
+cpp_quote("// ISimpleDOMText")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// An interface that extends MSAA's IAccessible to provide important additional capabilities on text nodes")
+cpp_quote("//")
+cpp_quote("// [propget] domText(/* out,retval */ BSTR *domText")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Similar to IAccessible::get_accName, but does not strip out whitespace characters.")
+cpp_quote("// Important for retrieving the correct start/end substring indices to use with other")
+cpp_quote("// methods in ISimpleDOMText.")
+cpp_quote("//")
+cpp_quote("//")
+cpp_quote("// get_[un]clippedSubstringBounds(")
+cpp_quote("// /* [in] */ unsigned int startIndex,")
+cpp_quote("// /* [in] */ unsigned int endIndex,")
+cpp_quote("// /* [out] */ int *x,")
+cpp_quote("// /* [out] */ int *y,")
+cpp_quote("// /* [out] */ int *width,")
+cpp_quote("// /* [out] */ int *height);")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Both methods get_clippedSubstringBounds and get_unclippedSubstringBounds return the screen pixel")
+cpp_quote("// coordinates of the given text substring. The in parameters for start and end indices refer")
+cpp_quote("// to the string returned by ISimpleDOMText::get_domText().")
+cpp_quote("//")
+cpp_quote("//")
+cpp_quote("// scrollToSubstring(")
+cpp_quote("// /* [in] */ unsigned int startIndex,")
+cpp_quote("// /* [in] */ unsigned int endIndex);")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// In scrollable views, scrolls to ensure that the specified substring is visible onscreen.")
+cpp_quote("// The in parameters for start and end indices refer to the string returned")
+cpp_quote("// by ISimpleDOMText::get_domText().")
+cpp_quote("//")
+cpp_quote("//")
+cpp_quote("// [propget] fontFamily(/* out,retval */ BSTR *fontFamily);")
+cpp_quote("// ---------------------------------------------------------------------------------------------------=")
+cpp_quote("// Return a single computed font family name, which is better than the comma delineated list")
+cpp_quote("// that is returned by the ISimpleDOMNode computed style methods for font-family.")
+cpp_quote("// In other words, return something like 'Arial' instead of 'Arial, Helvetica, Sans-serif'.")
+cpp_quote("///////////////////////////////////////////////////////////////////////////////////////////////////////")
+cpp_quote("")
+cpp_quote("")
+
+[object, uuid(4e747be5-2052-4265-8af0-8ecad7aad1c0)]
+interface ISimpleDOMText: IUnknown
+{
+ // Includes whitespace in DOM
+ [propget] HRESULT domText([out, retval] BSTR *domText);
+
+ HRESULT get_clippedSubstringBounds([in] unsigned int startIndex,
+ [in] unsigned int endIndex,
+ [out] int *x,
+ [out] int *y,
+ [out] int *width,
+ [out] int *height);
+
+ HRESULT get_unclippedSubstringBounds([in] unsigned int startIndex,
+ [in] unsigned int endIndex,
+ [out] int *x,
+ [out] int *y,
+ [out] int *width,
+ [out] int *height);
+
+ HRESULT scrollToSubstring([in] unsigned int startIndex,
+ [in] unsigned int endIndex);
+
+ [propget] HRESULT fontFamily([out, retval] BSTR *fontFamily);
+};
diff --git a/accessible/interfaces/msaa/moz.build b/accessible/interfaces/msaa/moz.build
new file mode 100644
index 0000000000..7a1053317a
--- /dev/null
+++ b/accessible/interfaces/msaa/moz.build
@@ -0,0 +1,57 @@
+# -*- 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/.
+
+GeckoSharedLibrary("AccessibleMarshal", linkage=None)
+
+# Missing here, is the notion that changes to the idl files included by
+# ISimpleDOM.idl (e.g. ISimpleDOMNode.idl) should rebuild the outputs.
+GeneratedFile(
+ "ISimpleDOM.h",
+ "ISimpleDOM_p.c",
+ "ISimpleDOM_i.c",
+ "ISimpleDOM_dlldata.c",
+ "ISimpleDOM.tlb",
+ inputs=["ISimpleDOM.idl"],
+ script="/build/midl.py",
+ entry_point="midl",
+ flags=["-I", SRCDIR, "-robust", "-dlldata", OBJDIR + "/ISimpleDOM_dlldata.c"],
+)
+
+SOURCES += [
+ "!ISimpleDOM_dlldata.c",
+ "!ISimpleDOM_i.c",
+ "!ISimpleDOM_p.c",
+ "AccessibleMarshalThunk.c",
+]
+
+EXPORTS += [
+ "!ISimpleDOM.h",
+ "!ISimpleDOM_i.c",
+]
+
+DEFINES["REGISTER_PROXY_DLL"] = True
+# The following line is required to preserve compatibility with older versions
+# of AccessibleMarshal.dll.
+DEFINES["PROXY_CLSID"] = "IID_ISimpleDOMNode"
+
+DEFFILE = "AccessibleMarshal.def"
+
+OS_LIBS += [
+ "kernel32",
+ "rpcrt4",
+ "oleaut32",
+]
+
+RCINCLUDE = "AccessibleMarshal.rc"
+
+# Suppress warnings from the MIDL generated code.
+if CONFIG["CC_TYPE"] == "clang-cl":
+ CFLAGS += [
+ "-Wno-extern-initializer",
+ "-Wno-incompatible-pointer-types",
+ "-Wno-missing-braces",
+ "-Wno-unused-const-variable",
+ ]
diff --git a/accessible/interfaces/nsIAccessibilityService.idl b/accessible/interfaces/nsIAccessibilityService.idl
new file mode 100644
index 0000000000..ff145c877e
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibilityService.idl
@@ -0,0 +1,119 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIAccessible;
+interface nsIWeakReference;
+interface nsIAccessiblePivot;
+interface nsIAccessibleTextLeafPoint;
+
+webidl Node;
+
+/**
+ * An interface for in-process accessibility clients wishing to get an
+ * nsIAccessible for a given DOM node. More documentation at:
+ * http://www.mozilla.org/projects/ui/accessibility
+ */
+[scriptable, builtinclass, uuid(2188e3a0-c88e-11e7-8f1a-0800200c9a66)]
+interface nsIAccessibilityService : nsISupports
+{
+ /**
+ * Return application accessible.
+ */
+ nsIAccessible getApplicationAccessible();
+
+ /**
+ * Return an nsIAccessible for a DOM node in pres shell 0.
+ * Create a new accessible of the appropriate type if necessary,
+ * or use one from the accessibility cache if it already exists.
+ * @param aNode The DOM node to get an accessible for.
+ * @return The nsIAccessible for the given DOM node.
+ */
+ nsIAccessible getAccessibleFor(in Node aNode);
+
+ nsIAccessible getAccessibleDescendantFor(in Node aNode);
+
+ /**
+ * Returns accessible role as a string.
+ *
+ * @param aRole - the accessible role constants.
+ */
+ AString getStringRole(in unsigned long aRole);
+
+ /**
+ * Returns list which contains accessible states as a strings.
+ *
+ * @param aStates - accessible states.
+ * @param aExtraStates - accessible extra states.
+ */
+ nsISupports getStringStates(in unsigned long aStates,
+ in unsigned long aExtraStates);
+
+ /**
+ * Get the type of accessible event as a string.
+ *
+ * @param aEventType - the accessible event type constant
+ * @return - accessible event type presented as human readable string
+ */
+ AString getStringEventType(in unsigned long aEventType);
+
+ /**
+ * Get the type of accessible relation as a string.
+ *
+ * @param aRelationType - the accessible relation type constant
+ * @return - accessible relation type presented as human readable string
+ */
+ AString getStringRelationType(in unsigned long aRelationType);
+
+ /**
+ * Return an accessible for the given DOM node from the cache.
+ * @note the method is intended for testing purposes
+ *
+ * @param aNode [in] the DOM node to get an accessible for
+ *
+ * @return cached accessible for the given DOM node if any
+ */
+ nsIAccessible getAccessibleFromCache(in Node aNode);
+
+ /**
+ * Create a new pivot for tracking a position and traversing a subtree.
+ *
+ * @param aRoot [in] the accessible root for the pivot
+ * @return a new pivot
+ */
+ nsIAccessiblePivot createAccessiblePivot(in nsIAccessible aRoot);
+
+ /**
+ * Create a new nsIAccessibleTextLeafPoint.
+ *
+ * @param aAccessible [in] the accessible for the point
+ * @param aOffset [in] the offset of the point
+ * @return a new point
+ */
+ nsIAccessibleTextLeafPoint createTextLeafPoint(in nsIAccessible aAccessible,
+ in long aOffset);
+
+ /**
+ * Enable logging for the given modules, all other modules aren't logged.
+ *
+ * @param aModules [in] list of modules, format is comma separated list
+ * like 'docload,doccreate'.
+ * @note Works on debug build only.
+ * @see Logging.cpp for list of possible values.
+ */
+ void setLogging(in ACString aModules);
+
+ /**
+ * Return true if the given module is logged.
+ */
+ boolean isLogged(in AString aModule);
+
+ /**
+ * Get the current accessibility service consumers.
+ * @returns a JSON string representing the accessibility service consumers.
+ */
+ AString getConsumers();
+};
diff --git a/accessible/interfaces/nsIAccessible.idl b/accessible/interfaces/nsIAccessible.idl
new file mode 100644
index 0000000000..e7bdb7d434
--- /dev/null
+++ b/accessible/interfaces/nsIAccessible.idl
@@ -0,0 +1,351 @@
+/* -*- 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 "nsISupports.idl"
+#include "nsIArray.idl"
+
+interface nsIPersistentProperties;
+interface nsIAccessibleDocument;
+interface nsIAccessibleRelation;
+
+webidl Node;
+
+%{C++
+#define NS_ACCESSIBLE_CACHE_TOPIC "accessible-cache"
+namespace mozilla {
+namespace a11y {
+class Accessible;
+class LocalAccessible;
+}
+}
+%}
+
+[ptr] native InternalAccessible(mozilla::a11y::Accessible);
+[ptr] native InternalLocalAccessible(mozilla::a11y::LocalAccessible);
+
+/**
+ * A cross-platform interface that supports platform-specific
+ * accessibility APIs like MSAA and ATK. Contains the sum of what's needed
+ * to support IAccessible as well as ATK's generic accessibility objects.
+ * Can also be used by in-process accessibility clients to get information
+ * about objects in the accessible tree. The accessible tree is a subset of
+ * nodes in the DOM tree -- such as documents, focusable elements and text.
+ * Mozilla creates the implementations of nsIAccessible on demand.
+ * See http://www.mozilla.org/projects/ui/accessibility for more information.
+ */
+[scriptable, builtinclass, uuid(de2869d9-563c-4943-996b-31a4daa4d097)]
+interface nsIAccessible : nsISupports
+{
+ /**
+ * Parent node in accessible tree.
+ */
+ readonly attribute nsIAccessible parent;
+
+ /**
+ * Next sibling in accessible tree
+ */
+ readonly attribute nsIAccessible nextSibling;
+
+ /**
+ * Previous sibling in accessible tree
+ */
+ readonly attribute nsIAccessible previousSibling;
+
+ /**
+ * First child in accessible tree
+ */
+ readonly attribute nsIAccessible firstChild;
+
+ /**
+ * Last child in accessible tree
+ */
+ readonly attribute nsIAccessible lastChild;
+
+ /**
+ * Array of all this element's children.
+ */
+ readonly attribute nsIArray children;
+
+ /**
+ * Number of accessible children
+ */
+ readonly attribute long childCount;
+
+ /**
+ * The 0-based index of this accessible in its parent's list of children,
+ * or -1 if this accessible does not have a parent.
+ */
+ readonly attribute long indexInParent;
+
+ /**
+ * The unique identifier of the accessible. ID is only guaranteed to be unique
+ * per document (Windows IDs are unique even across documents, but that is
+ * Windows specific and not exposed to core).
+ */
+ readonly attribute long long uniqueID;
+
+ /**
+ * The DOM node this nsIAccessible is associated with.
+ */
+ readonly attribute Node DOMNode;
+
+ /**
+ * For remote accessibles the id of the related DOM node.
+ */
+ readonly attribute AString id;
+
+ /**
+ * The document accessible that this access node resides in.
+ */
+ readonly attribute nsIAccessibleDocument document;
+
+ /**
+ * The root document accessible that this access node resides in.
+ */
+ readonly attribute nsIAccessibleDocument rootDocument;
+
+ /**
+ * The language for the current DOM node, e.g. en, de, etc.
+ */
+ readonly attribute AString language;
+
+ /**
+ * Accessible name -- the main text equivalent for this node. The name is
+ * specified by ARIA or by native markup. Example of ARIA markup is
+ * aria-labelledby attribute placed on element of this accessible. Example
+ * of native markup is HTML label linked with HTML element of this accessible.
+ *
+ * Value can be string or null. A null value indicates that AT may attempt to
+ * compute the name. Any string value, including the empty string, should be
+ * considered author-intentional, and respected.
+ */
+ readonly attribute AString name;
+
+ /**
+ * Accessible value -- a number or a secondary text equivalent for this node
+ * Widgets that use role attribute can force a value using the valuenow attribute
+ */
+ readonly attribute AString value;
+
+ /**
+ * Accessible description -- long text associated with this node
+ */
+ readonly attribute AString description;
+
+ /**
+ * Provides localized string of accesskey name, such as Alt+D.
+ * The modifier may be affected by user and platform preferences.
+ * Usually alt+letter, or just the letter alone for menu items.
+ */
+ readonly attribute AString accessKey;
+
+ /**
+ * Provides localized string of global keyboard accelerator for default
+ * action, such as Ctrl+O for Open file
+ */
+ readonly attribute AString keyboardShortcut;
+
+ /**
+ * Enumerated accessible role (see the constants defined in nsIAccessibleRole).
+ *
+ * @note The values might depend on platform because of variations. Widgets
+ * can use ARIA role attribute to force the final role.
+ */
+ readonly attribute unsigned long role;
+
+ /**
+ * Accessible states -- bit fields which describe boolean properties of node.
+ * Many states are only valid given a certain role attribute that supports
+ * them.
+ *
+ * @param aState - the first bit field (see nsIAccessibleStates::STATE_*
+ * constants)
+ * @param aExtraState - the second bit field
+ * (see nsIAccessibleStates::EXT_STATE_* constants)
+ */
+ void getState(out unsigned long aState, out unsigned long aExtraState);
+
+ /**
+ * Focused accessible child of node
+ */
+ readonly attribute nsIAccessible focusedChild;
+
+ /**
+ * Attributes of accessible
+ */
+ readonly attribute nsIPersistentProperties attributes;
+
+ /**
+ * Cached fields from a remote accessible
+ */
+ readonly attribute nsIPersistentProperties cache;
+
+ /**
+ * Platform specific interface for accessible
+ */
+ readonly attribute nsISupports nativeInterface;
+
+ /**
+ * Returns grouping information. Used for tree items, list items, tab panel
+ * labels, radio buttons, etc. Also used for collectons of non-text objects.
+ *
+ * @param groupLevel - 1-based, similar to ARIA 'level' property
+ * @param similarItemsInGroup - 1-based, similar to ARIA 'setsize' property,
+ * inclusive of the current item
+ * @param positionInGroup - 1-based, similar to ARIA 'posinset' property
+ */
+ void groupPosition(out long aGroupLevel, out long aSimilarItemsInGroup,
+ out long aPositionInGroup);
+
+ /**
+ * Accessible child which contains the coordinate at (x, y) in screen pixels.
+ * If the point is in the current accessible but not in a child, the
+ * current accessible will be returned.
+ * If the point is in neither the current accessible or a child, then
+ * null will be returned.
+ *
+ * @param x screen's x coordinate
+ * @param y screen's y coordinate
+ * @return the direct accessible child containing the given point
+ */
+ nsIAccessible getChildAtPoint(in long x, in long y);
+
+ /**
+ * Deepest accessible child which contains the coordinate at (x, y) in screen
+ * pixels. If the point is in the current accessible but not in a child, the
+ * current accessible will be returned. If the point is in neither the current
+ * accessible or a child, then null will be returned.
+ *
+ * @param x screen's x coordinate
+ * @param y screen's y coordinate
+ * @return the deepest accessible child containing the given point
+ */
+ nsIAccessible getDeepestChildAtPoint(in long x, in long y);
+
+/**
+ * Like GetDeepestChildAtPoint, but restricted to the current process.
+ * If the point is within a remote document, the accessible for the browser
+ * element containing that document will be returned; i.e. this will not
+ * descend into the document. If called on an accessible inside a remote
+ * document, this will fail.
+ *
+ * @param x screen's x coordinate
+ * @param y screen's y coordinate
+ * @return the deepest accessible child in this process containing the given
+ * point
+ */
+ nsIAccessible getDeepestChildAtPointInProcess(in long x, in long y);
+
+ /**
+ * Nth accessible child using zero-based index or last child if index less than zero
+ */
+ nsIAccessible getChildAt(in long aChildIndex);
+
+ /**
+ * Return accessible relation by the given relation type (see.
+ * constants defined in nsIAccessibleRelation).
+ */
+ nsIAccessibleRelation getRelationByType(in unsigned long aRelationType);
+
+ /**
+ * Returns multiple accessible relations for this object.
+ */
+ nsIArray getRelations();
+
+ /**
+ * Return accessible's x and y coordinates relative to the screen and
+ * accessible's width and height in Dev pixels.
+ */
+ void getBounds(out long x, out long y, out long width, out long height);
+
+ /**
+ * Return accessible's x and y coordinates relative to the screen and
+ * accessible's width and height in CSS pixels.
+ */
+ void getBoundsInCSSPixels(out long aX, out long aY, out long aWidth, out long aHeight);
+
+ /**
+ * Add or remove this accessible to the current selection
+ */
+ void setSelected(in boolean isSelected);
+
+ /**
+ * Select this accessible node only
+ */
+ void takeSelection();
+
+ /**
+ * Focus this accessible node,
+ * The state STATE_FOCUSABLE indicates whether this node is normally focusable.
+ * It is the callers responsibility to determine whether this node is focusable.
+ * accTakeFocus on a node that is not normally focusable (such as a table),
+ * will still set focus on that node, although normally that will not be visually
+ * indicated in most style sheets.
+ */
+ void takeFocus();
+
+ /**
+ * The number of accessible actions associated with this accessible
+ */
+ readonly attribute uint8_t actionCount;
+
+ /**
+ * The name of the accessible action at the given zero-based index
+ */
+ AString getActionName(in uint8_t index);
+
+ /**
+ * The description of the accessible action at the given zero-based index
+ */
+ AString getActionDescription(in uint8_t aIndex);
+
+ /**
+ * Perform the accessible action at the given zero-based index
+ * Action number 0 is the default action
+ */
+ void doAction(in uint8_t index);
+
+ /**
+ * Makes an object visible on screen.
+ *
+ * @param scrollType - defines where the object should be placed on
+ * the screen (see nsIAccessibleScrollType for
+ * available constants).
+ */
+ [can_run_script]
+ void scrollTo(in unsigned long aScrollType);
+
+ /**
+ * Moves the top left of an object to a specified location.
+ *
+ * @param coordinateType [in] - specifies whether the coordinates are relative to
+ * the screen or the parent object (for available
+ * constants refer to nsIAccessibleCoordinateType)
+ * @param x [in] - defines the x coordinate
+ * @param y [in] - defines the y coordinate
+ */
+ void scrollToPoint(in unsigned long coordinateType, in long x, in long y);
+
+ /**
+ * Dispatches an ANNOUNCEMENT event with this accessible as target.
+ *
+ * @param announcement [in] - string to use in announcement.
+ * @param priority [in] - priority for announcement, could be
+ * nsIAccessibleAnnouncementEvent.POLITE or
+ * nsIAccessibleAnnouncementEvent.ASSERTIVE.
+ */
+ void announce(in AString announcement, in unsigned short priority);
+
+ /**
+ * Get the role of this Accessible as an ARIA role token. This might have been
+ * set explicitly (e.g. role="button") or it might be implicit in native
+ * markup (e.g. <button> returns "button").
+ */
+ readonly attribute AString computedARIARole;
+
+ [notxpcom, nostdcall] InternalLocalAccessible toInternalAccessible();
+ [notxpcom, nostdcall] InternalAccessible toInternalGeneric();
+
+};
diff --git a/accessible/interfaces/nsIAccessibleAnnouncementEvent.idl b/accessible/interfaces/nsIAccessibleAnnouncementEvent.idl
new file mode 100644
index 0000000000..c64e95d583
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleAnnouncementEvent.idl
@@ -0,0 +1,23 @@
+/* -*- 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 "nsIAccessibleEvent.idl"
+
+/**
+ * Fired when announce() is called on the target accessible.
+ */
+[scriptable, builtinclass, uuid(8818e49c-1286-4fe6-ae82-4d1b795ec88d)]
+interface nsIAccessibleAnnouncementEvent : nsIAccessibleEvent
+{
+ const unsigned short POLITE = 0;
+ const unsigned short ASSERTIVE = 1;
+
+ // String of actual announcement
+ readonly attribute AString announcement;
+
+ // Priority for announcement, could be POLITE or ASSERTIVE, ATs
+ // will decide how to appropriately present it.
+ readonly attribute unsigned short priority;
+};
diff --git a/accessible/interfaces/nsIAccessibleApplication.idl b/accessible/interfaces/nsIAccessibleApplication.idl
new file mode 100644
index 0000000000..6facf13375
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleApplication.idl
@@ -0,0 +1,34 @@
+/* -*- 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 "nsISupports.idl"
+
+/**
+ * This interface is implemented by top level accessible object in hierarchy and
+ * provides information about application.
+ */
+[scriptable, builtinclass, uuid(79251626-387c-4531-89f3-680d31d6cf05)]
+interface nsIAccessibleApplication : nsISupports
+{
+ /**
+ * Returns the application name.
+ */
+ readonly attribute AString appName;
+
+ /**
+ * Returns the application version.
+ */
+ readonly attribute AString appVersion;
+
+ /**
+ * Returns the platform name.
+ */
+ readonly attribute AString platformName;
+
+ /**
+ * Returns the platform version.
+ */
+ readonly attribute AString platformVersion;
+};
diff --git a/accessible/interfaces/nsIAccessibleCaretMoveEvent.idl b/accessible/interfaces/nsIAccessibleCaretMoveEvent.idl
new file mode 100644
index 0000000000..4cdece16cc
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleCaretMoveEvent.idl
@@ -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/. */
+
+#include "nsIAccessibleEvent.idl"
+
+/**
+ * Fired when the caret changes position in text.
+ */
+[scriptable, builtinclass, uuid(ed1982e4-57d7-41a8-8cd8-9023f809383e)]
+interface nsIAccessibleCaretMoveEvent: nsIAccessibleEvent
+{
+ /**
+ * Return caret offset.
+ */
+ readonly attribute long caretOffset;
+
+ /**
+ * Return true if there is no selection.
+ */
+ readonly attribute bool isSelectionCollapsed;
+
+ /**
+ * Return true if the caret is at the end of a line.
+ */
+ readonly attribute bool isAtEndOfLine;
+
+ /**
+ * Return caret move granularity.
+ */
+ readonly attribute long granularity;
+};
diff --git a/accessible/interfaces/nsIAccessibleDocument.idl b/accessible/interfaces/nsIAccessibleDocument.idl
new file mode 100644
index 0000000000..1886621c37
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleDocument.idl
@@ -0,0 +1,70 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIAccessiblePivot;
+interface mozIDOMWindowProxy;
+
+webidl Document;
+
+/**
+ * An interface for in-process accessibility clients
+ * that wish to retrieve information about a document.
+ * When accessibility is turned on in Gecko,
+ * there is an nsIAccessibleDocument for each document
+ * whether it is XUL, HTML or whatever.
+ * You can QueryInterface to nsIAccessibleDocument from the nsIAccessible for
+ * the root node of a document or you can get one from
+ * nsIAccessible::GetDocument().
+ */
+[scriptable, builtinclass, uuid(5cad5f91-fcce-40e7-913e-4671701d19b4)]
+interface nsIAccessibleDocument : nsISupports
+{
+ /**
+ * The URL of the document
+ */
+ readonly attribute AString URL;
+
+ /**
+ * The title of the document, as specified in the document.
+ */
+ readonly attribute AString title;
+
+ /**
+ * The mime type of the document
+ */
+ readonly attribute AString mimeType;
+
+ /**
+ * The doc type of the document, as specified in the document.
+ */
+ readonly attribute AString docType;
+
+ /**
+ * The Document interface associated with this document.
+ */
+ readonly attribute Document DOMDocument;
+
+ /**
+ * The nsIDOMWindow that the document resides in.
+ */
+ readonly attribute mozIDOMWindowProxy window;
+
+ /**
+ * Return the parent document accessible.
+ */
+ readonly attribute nsIAccessibleDocument parentDocument;
+
+ /**
+ * Return the count of child document accessibles.
+ */
+ readonly attribute unsigned long childDocumentCount;
+
+ /**
+ * Return the child document accessible at the given index.
+ */
+ nsIAccessibleDocument getChildDocumentAt(in unsigned long index);
+};
diff --git a/accessible/interfaces/nsIAccessibleEditableText.idl b/accessible/interfaces/nsIAccessibleEditableText.idl
new file mode 100644
index 0000000000..5fce44032c
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleEditableText.idl
@@ -0,0 +1,57 @@
+/* -*- 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 "nsISupports.idl"
+
+[scriptable, builtinclass, uuid(28915cca-3366-4034-ba1d-b7afb9b37639)]
+interface nsIAccessibleEditableText : nsISupports
+{
+ /**
+ * Replaces the text represented by this object by the given text.
+ */
+ void setTextContents (in AString text);
+
+ /**
+ * Inserts text at the specified position.
+ *
+ * @param text - text that is inserted.
+ * @param position - index at which to insert the text.
+ */
+ void insertText(in AString text, in long position);
+
+ /**
+ * Copies the text range into the clipboard.
+ *
+ * @param startPos - start index of the text to moved into the clipboard.
+ * @param endPos - end index of the text to moved into the clipboard.
+ */
+ void copyText(in long startPos, in long endPos);
+
+ /**
+ * Deletes a range of text and copies it to the clipboard.
+ *
+ * @param startPos - start index of the text to be deleted.
+ * @param endOffset - end index of the text to be deleted.
+ */
+ void cutText(in long startPos, in long endPos);
+
+ /**
+ * Deletes a range of text.
+ *
+ * @param startPos - start index of the text to be deleted.
+ * @param endPos - end index of the text to be deleted.
+ */
+ void deleteText(in long startPos, in long endPos);
+
+ /**
+ * Pastes text from the clipboard.
+ *
+ * @param position - index at which to insert the text from the system
+ * clipboard into the text represented by this object.
+ */
+ [can_run_script]
+ void pasteText(in long position);
+};
diff --git a/accessible/interfaces/nsIAccessibleEvent.idl b/accessible/interfaces/nsIAccessibleEvent.idl
new file mode 100644
index 0000000000..233f516cb8
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleEvent.idl
@@ -0,0 +1,251 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIAccessible;
+interface nsIAccessibleDocument;
+
+webidl Node;
+
+%{C++
+#define NS_ACCESSIBLE_EVENT_TOPIC "accessible-event"
+%}
+
+/**
+ * An interface for accessibility events listened to
+ * by in-process accessibility clients, which can be used
+ * to find out how to get accessibility and DOM interfaces for
+ * the event and its target. To listen to in-process accessibility invents,
+ * make your object an nsIObserver, and listen for accessible-event by
+ * using code something like this:
+ * nsCOMPtr<nsIObserverService> observerService =
+ * do_GetService("@mozilla.org/observer-service;1", &rv);
+ * if (NS_SUCCEEDED(rv))
+ * rv = observerService->AddObserver(this, "accessible-event", PR_TRUE);
+ */
+[scriptable, builtinclass, uuid(20c69a40-6c2c-42a3-a578-6f4473aab9dd)]
+interface nsIAccessibleEvent : nsISupports
+{
+ /**
+ * An object has been created.
+ */
+ const unsigned long EVENT_SHOW = 0x0001;
+
+ /**
+ * An object has been destroyed.
+ */
+ const unsigned long EVENT_HIDE = 0x0002;
+
+ /**
+ * An object's children have changed
+ */
+ const unsigned long EVENT_REORDER = 0x0003;
+
+ /**
+ * An object has received the keyboard focus.
+ */
+ const unsigned long EVENT_FOCUS = 0x0004;
+
+ /**
+ * An object's state has changed.
+ */
+ const unsigned long EVENT_STATE_CHANGE = 0x0005;
+
+ /**
+ * An object's Name property has changed.
+ */
+ const unsigned long EVENT_NAME_CHANGE = 0x0006;
+
+ /**
+ * An object's Description property has changed.
+ */
+ const unsigned long EVENT_DESCRIPTION_CHANGE = 0x0007;
+
+ /**
+ * An object's numeric Value has changed.
+ */
+ const unsigned long EVENT_VALUE_CHANGE = 0x0008;
+
+ /**
+ * The selection within a container object has changed.
+ */
+ const unsigned long EVENT_SELECTION = 0x0009;
+
+ /**
+ * An item within a container object has been added to the selection.
+ */
+ const unsigned long EVENT_SELECTION_ADD = 0x000A;
+
+ /**
+ * An item within a container object has been removed from the selection.
+ */
+ const unsigned long EVENT_SELECTION_REMOVE = 0x000B;
+
+ /**
+ * Numerous selection changes have occurred within a container object.
+ */
+ const unsigned long EVENT_SELECTION_WITHIN = 0x000C;
+
+ /**
+ * An alert has been generated. Server applications send this event when a
+ * user needs to know that a user interface element has changed.
+ */
+ const unsigned long EVENT_ALERT = 0x000D;
+
+ /**
+ * A menu item on the menu bar has been selected.
+ */
+ const unsigned long EVENT_MENU_START = 0x000E;
+
+ /**
+ * A menu from the menu bar has been closed.
+ */
+ const unsigned long EVENT_MENU_END = 0x000F;
+
+ /**
+ * A pop-up menu has been displayed.
+ */
+ const unsigned long EVENT_MENUPOPUP_START = 0x0010;
+
+ /**
+ * A pop-up menu has been closed.
+ */
+ const unsigned long EVENT_MENUPOPUP_END = 0x0011;
+
+ /**
+ * An application is about to enter drag-and-drop mode
+ */
+ const unsigned long EVENT_DRAGDROP_START = 0x0012;
+
+ /**
+ * Scrolling has started on a scroll bar
+ */
+ const unsigned long EVENT_SCROLLING_START = 0x0013;
+
+ /**
+ * Scrolling has ended on a scroll bar
+ */
+ const unsigned long EVENT_SCROLLING_END = 0x0014;
+
+ /**
+ * The loading of the document has completed.
+ */
+ const unsigned long EVENT_DOCUMENT_LOAD_COMPLETE = 0x0015;
+
+ /**
+ * The document contents are being reloaded.
+ */
+ const unsigned long EVENT_DOCUMENT_RELOAD = 0x0016;
+
+ /**
+ * The loading of the document was interrupted.
+ */
+ const unsigned long EVENT_DOCUMENT_LOAD_STOPPED = 0x0017;
+
+ /**
+ * A text object's attributes changed.
+ * Also see EVENT_OBJECT_ATTRIBUTE_CHANGED.
+ */
+ const unsigned long EVENT_TEXT_ATTRIBUTE_CHANGED = 0x0018;
+
+ /**
+ * The caret has moved to a new position.
+ */
+ const unsigned long EVENT_TEXT_CARET_MOVED = 0x0019;
+
+ /**
+ * Text was inserted.
+ */
+ const unsigned long EVENT_TEXT_INSERTED = 0x001A;
+
+ /**
+ * Text was removed.
+ */
+ const unsigned long EVENT_TEXT_REMOVED = 0x001B;
+
+ /**
+ * The text selection changed.
+ */
+ const unsigned long EVENT_TEXT_SELECTION_CHANGED = 0x001C;
+
+ const unsigned long EVENT_WINDOW_ACTIVATE = 0x001D;
+ const unsigned long EVENT_WINDOW_DEACTIVATE = 0x001E;
+ const unsigned long EVENT_WINDOW_MAXIMIZE = 0x001F;
+ const unsigned long EVENT_WINDOW_MINIMIZE = 0x0020;
+ const unsigned long EVENT_WINDOW_RESTORE = 0x0021;
+
+ /**
+ * An object's attributes changed. Also see EVENT_TEXT_ATTRIBUTE_CHANGED.
+ */
+ const unsigned long EVENT_OBJECT_ATTRIBUTE_CHANGED = 0x0022;
+
+ /**
+ * An object's text Value has changed.
+ */
+ const unsigned long EVENT_TEXT_VALUE_CHANGE = 0x0023;
+
+ /**
+ * An accessible's viewport is scrolling.
+ */
+ const unsigned long EVENT_SCROLLING = 0x0024;
+
+ /**
+ * An accessible is making an explicit announcement.
+ */
+ const unsigned long EVENT_ANNOUNCEMENT = 0x0025;
+
+ /**
+ * A live region has been introduced. Mac only.
+ */
+ const unsigned long EVENT_LIVE_REGION_ADDED = 0x0026;
+
+ /**
+ * A live region has been removed (aria-live attribute changed). Mac Only.
+ */
+ const unsigned long EVENT_LIVE_REGION_REMOVED = 0x0027;
+
+ /**
+ * A reorder event that has been coalesced into a mutation
+ * of an ancestor's subtree.
+ */
+ const unsigned long EVENT_INNER_REORDER = 0x0028;
+
+ /**
+ * Help make sure event map does not get out-of-line.
+ */
+ const unsigned long EVENT_LAST_ENTRY = 0x0029;
+
+ /**
+ * The type of event, based on the enumerated event values
+ * defined in this interface.
+ */
+ readonly attribute unsigned long eventType;
+
+ /**
+ * The nsIAccessible associated with the event.
+ * May return null if no accessible is available
+ */
+ readonly attribute nsIAccessible accessible;
+
+ /**
+ * The nsIAccessibleDocument that the event target nsIAccessible
+ * resides in. This can be used to get the DOM window,
+ * the DOM document and the window handler, among other things.
+ */
+ readonly attribute nsIAccessibleDocument accessibleDocument;
+
+ /**
+ * The Node associated with the event
+ * May return null if accessible for event has been shut down
+ */
+ readonly attribute Node DOMNode;
+
+ /**
+ * Returns true if the event was caused by explicit user input,
+ * as opposed to purely originating from a timer or mouse movement
+ */
+ readonly attribute boolean isFromUserInput;
+};
diff --git a/accessible/interfaces/nsIAccessibleHideEvent.idl b/accessible/interfaces/nsIAccessibleHideEvent.idl
new file mode 100644
index 0000000000..820b8914c9
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleHideEvent.idl
@@ -0,0 +1,28 @@
+/* -*- 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 "nsIAccessibleEvent.idl"
+
+/**
+ * Fired when a accessible and its subtree are removed from the tree.
+ */
+[scriptable, builtinclass, uuid(2051709a-4e0d-4be5-873d-b49d1dee35fa)]
+interface nsIAccessibleHideEvent: nsIAccessibleEvent
+{
+ /**
+ * Return an accessible that was a parent of the target.
+ */
+ readonly attribute nsIAccessible targetParent;
+
+ /**
+ * Return an accessible that was a next sibling of the target
+ */
+ readonly attribute nsIAccessible targetNextSibling;
+
+ /**
+ * Return an accessible that was a parent of the target
+ */
+ readonly attribute nsIAccessible targetPrevSibling;
+};
diff --git a/accessible/interfaces/nsIAccessibleHyperLink.idl b/accessible/interfaces/nsIAccessibleHyperLink.idl
new file mode 100644
index 0000000000..e2926d4f0f
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleHyperLink.idl
@@ -0,0 +1,86 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIURI;
+interface nsIAccessible;
+
+/**
+ * A cross-platform interface that supports hyperlink-specific properties and
+ * methods. Anchors, image maps, xul:labels with class="text-link" implement this interface.
+ */
+[scriptable, builtinclass, uuid(883643d4-93a5-4f32-922c-6f06e01363c1)]
+interface nsIAccessibleHyperLink : nsISupports
+{
+ /**
+ * Returns the offset of the link within the parent accessible.
+ */
+ readonly attribute long startIndex;
+
+ /**
+ * Returns the end index of the link within the parent accessible.
+ *
+ * @note The link itself is represented by one embedded character within the
+ * parent text, so the endIndex should be startIndex + 1.
+ */
+ readonly attribute long endIndex;
+
+ /**
+ * Determines whether the link is valid (e. g. points to a valid URL).
+ *
+ * @note XXX Currently only used with ARIA links, and the author has to
+ * specify that the link is invalid via the aria-invalid="true" attribute.
+ * In all other cases, TRUE is returned.
+ */
+ readonly attribute boolean valid;
+
+ /**
+ * The numbber of anchors within this Hyperlink. Is normally 1 for anchors.
+ * This anchor is, for example, the visible output of the html:a tag.
+ * With an Image Map, reflects the actual areas within the map.
+ */
+ readonly attribute long anchorCount;
+
+ /**
+ * Returns the URI at the given index.
+ *
+ * @note ARIA hyperlinks do not have an URI to point to, since clicks are
+ * processed via JavaScript. Therefore this property does not work on ARIA
+ * links.
+ *
+ * @param index The 0-based index of the URI to be returned.
+ *
+ * @return the nsIURI object containing the specifications for the URI.
+ */
+ nsIURI getURI (in long index);
+
+ /**
+ * Returns a reference to the object at the given index.
+ *
+ * @param index The 0-based index whose object is to be returned.
+ *
+ * @return the nsIAccessible object at the desired index.
+ */
+ nsIAccessible getAnchor (in long index);
+};
+
+/*
+ Assumptions:
+
+ The object associated with object or anchor index
+ is an nsIAccessible.
+ A URI can be represented by the nsIURI interface
+ (or nsIURL interface).
+
+ Note that an object which supports nsIAccessibleHyperlink
+ does *not* generally implement nsIAccessible, unlike the
+ case of the other nsiAccessible* interfaces in this directory.
+
+ Aaron: would the nsISupports return from
+ getObject be queryable for nsIURI and nsIURL directly?
+
+*/
diff --git a/accessible/interfaces/nsIAccessibleHyperText.idl b/accessible/interfaces/nsIAccessibleHyperText.idl
new file mode 100644
index 0000000000..6e34f2a909
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleHyperText.idl
@@ -0,0 +1,54 @@
+/* -*- 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 "nsISupports.idl"
+#include "nsIAccessibleHyperLink.idl"
+
+/**
+ * A cross-platform interface that deals with text which contains hyperlinks.
+ * Each link is an embedded object representing exactly 1 character within
+ * the hypertext.
+ *
+ * Current implementation assumes every embedded object is a link.
+ */
+
+[scriptable, builtinclass, uuid(b33684e2-090c-4e1d-a3d9-f4b46f4237b9)]
+interface nsIAccessibleHyperText : nsISupports
+{
+ /**
+ * Return the number of links contained within this hypertext object.
+ */
+ readonly attribute long linkCount;
+
+ /**
+ * Return link accessible at the given index.
+ *
+ * @param index [in] 0-based index of the link that is to be retrieved
+ *
+ * @return link accessible or null if there is no link at that index
+ */
+ nsIAccessibleHyperLink getLinkAt(in long index);
+
+ /**
+ * Return index of the given link.
+ *
+ * @param link [in] link accessible the index is requested for
+ *
+ * @return index of the given link or null if there's no link within
+ * hypertext accessible
+ */
+ long getLinkIndex(in nsIAccessibleHyperLink link);
+
+ /*
+ * Return link index at the given offset within hypertext accessible.
+ *
+ * @param offset [in] the 0-based character index
+ *
+ * @return 0-based link's index or -1 if no link is present at that
+ * offset
+ */
+ long getLinkIndexAtOffset(in long offset);
+};
diff --git a/accessible/interfaces/nsIAccessibleImage.idl b/accessible/interfaces/nsIAccessibleImage.idl
new file mode 100644
index 0000000000..296216e70d
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleImage.idl
@@ -0,0 +1,30 @@
+/* -*- 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 "nsISupports.idl"
+
+[scriptable, builtinclass, uuid(09086623-0f09-4310-ac56-c2cda7c29648)]
+interface nsIAccessibleImage : nsISupports
+{
+ /**
+ * Returns the coordinates of the image.
+ *
+ * @param coordType specifies coordinates origin (for available constants
+ * refer to nsIAccessibleCoordinateType)
+ * @param x the x coordinate
+ * @param y the y coordinate
+ */
+ void getImagePosition(in unsigned long coordType,
+ out long x,
+ out long y);
+
+ /**
+ * Returns the size of the image.
+ *
+ * @param width the heigth
+ * @param height the width
+ */
+ void getImageSize(out long width, out long height);
+};
diff --git a/accessible/interfaces/nsIAccessibleMacInterface.idl b/accessible/interfaces/nsIAccessibleMacInterface.idl
new file mode 100644
index 0000000000..384d09d5f0
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleMacInterface.idl
@@ -0,0 +1,87 @@
+/* -*- 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 "nsISupports.idl"
+
+%{C++
+#include <objc/objc.h>
+
+#define NS_ACCESSIBLE_MAC_EVENT_TOPIC "accessible-mac-event"
+%}
+
+native NativeObjectId(id);
+
+/**
+ * A generic NSISupports wrapper for native NSObjects.
+ */
+[scriptable, builtinclass, uuid(4582bb77-de03-4ed1-a9b4-d482d97c0129)]
+interface nsIAccessibleMacNSObjectWrapper : nsISupports
+{
+ [noscript, notxpcom, nostdcall] NativeObjectId GetNativeObject();
+};
+
+[scriptable, builtinclass, uuid(9d27cf21-66fc-47dc-8a07-98edb18707b1)]
+interface nsIAccessibleMacInterface : nsISupports
+{
+ /**
+ * List of available attribute names for the object.
+ * Emulates `AXUIElementCopyAttributeNames`.
+ */
+ readonly attribute Array<AString> attributeNames;
+
+ /**
+ * List of available parameterized attribute names for the object.
+ * Emulates `AXUIElementCopyParameterizedAttributeNames`.
+ */
+ readonly attribute Array<AString> parameterizedAttributeNames;
+
+ /**
+ * List of available action names for tthe object.
+ * Emulates `AXUIElementCopyActionNames`.
+ */
+ readonly attribute Array<AString> actionNames;
+
+ /**
+ * Returns the value of an attribute.
+ * Emulates `AXUIElementCopyAttributeValue`.
+ */
+ [implicit_jscontext]
+ jsval getAttributeValue(in AString attributeName);
+
+ /**
+ * Returns the value of an attribute given a name and a parameter.
+ * Emulates `AXUIElementCopyParameterizedAttributeValue`.
+ **/
+ [implicit_jscontext]
+ jsval getParameterizedAttributeValue(in AString attributeName, in jsval parameter);
+
+ /**
+ * Requests that the accessibility object perform the specified action.
+ * Emulatets `AXUIElementPerformAction`.
+ */
+ void performAction(in AString actionName);
+
+ /**
+ * Returns true if the given attribute is settable on the object.
+ * Emulates `AXUIElementIsAttributeSettable`.
+ **/
+ bool isAttributeSettable(in AString attributeName);
+
+ /**
+ * Sets the given attribute with the given value on the object.
+ * Emulates `AXUIElementSetAttributeValue`.
+ **/
+ [implicit_jscontext]
+ void setAttributeValue(in AString attributeName, in jsval attributeValue);
+};
+
+[scriptable, builtinclass, uuid(6153f07b-2260-495b-9899-9699d9fe323e)]
+interface nsIAccessibleMacEvent : nsISupports
+{
+ readonly attribute nsIAccessibleMacInterface macIface;
+
+ [implicit_jscontext]
+ readonly attribute jsval data;
+};
diff --git a/accessible/interfaces/nsIAccessibleObjectAttributeChangedEvent.idl b/accessible/interfaces/nsIAccessibleObjectAttributeChangedEvent.idl
new file mode 100644
index 0000000000..28597491a1
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleObjectAttributeChangedEvent.idl
@@ -0,0 +1,19 @@
+/* -*- 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 "nsIAccessibleEvent.idl"
+
+/**
+ * Fired when an attribute of an accessible changes.
+ */
+[scriptable, builtinclass, uuid(ce41add2-096e-4606-b1ca-7408c6d5b4c3)]
+interface nsIAccessibleObjectAttributeChangedEvent : nsIAccessibleEvent
+{
+ /**
+ * Return the accessible attribute that changed.
+ */
+ readonly attribute AString changedAttribute;
+};
diff --git a/accessible/interfaces/nsIAccessiblePivot.idl b/accessible/interfaces/nsIAccessiblePivot.idl
new file mode 100644
index 0000000000..f48dc149c6
--- /dev/null
+++ b/accessible/interfaces/nsIAccessiblePivot.idl
@@ -0,0 +1,96 @@
+/* -*- 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 "nsISupports.idl"
+
+typedef short PivotMoveReason;
+
+interface nsIAccessible;
+interface nsIAccessibleTraversalRule;
+
+/**
+ * The pivot interface encapsulates a reference to a single place in an accessible
+ * subtree. The pivot is a point or a range in the accessible tree. This interface
+ * provides traversal methods to move the pivot to next/prev state that complies
+ * to a given rule.
+ */
+[scriptable, uuid(81fe5144-059b-42db-bd3a-f6ce3158d5e9)]
+interface nsIAccessiblePivot : nsISupports
+{
+ /**
+ * Move pivot to next object, from current position or given anchor,
+ * complying to given traversal rule.
+ *
+ * @param aRule [in] traversal rule to use.
+ * @param aAnchor [in] accessible to start search from, if not provided,
+ * current position will be used.
+ * @param aIncludeStart [in] include anchor accessible in search.
+ * @return next accessible node that matches rule in preorder.
+ */
+ [optional_argc] nsIAccessible next(in nsIAccessible aAnchor,
+ in nsIAccessibleTraversalRule aRule,
+ [optional] in boolean aIncludeStart);
+
+ /**
+ * Move pivot to previous object, from current position or given anchor,
+ * complying to given traversal rule.
+ *
+ * @param aRule [in] traversal rule to use.
+ * @param aAnchor [in] accessible to start search from, if not provided,
+ * current position will be used.
+ * @param aIncludeStart [in] include anchor accessible in search.
+ * @return previous accessible node that matches rule in preorder.
+ */
+ [optional_argc] nsIAccessible prev(in nsIAccessible aAnchor,
+ in nsIAccessibleTraversalRule aRule,
+ [optional] in boolean aIncludeStart);
+
+ /**
+ * Move pivot to first object in subtree complying to given traversal rule.
+ *
+ * @param aRule [in] traversal rule to use.
+ * @return first accessible node in subtree that matches rule in preorder.
+ */
+ nsIAccessible first(in nsIAccessibleTraversalRule aRule);
+
+ /**
+ * Move pivot to last object in subtree complying to given traversal rule.
+ *
+ * @param aRule [in] traversal rule to use.
+ * @return last accessible node in subtree that matches rule in preorder.
+ */
+ nsIAccessible last(in nsIAccessibleTraversalRule aRule);
+
+ /**
+ * Move pivot to given coordinate in screen pixels.
+ *
+ * @param aX [in] screen's x coordinate
+ * @param aY [in] screen's y coordinate
+ * @param aRule [in] raversal rule to use.
+ * @return highest accessible in subtree that matches rule at given point.
+ */
+ nsIAccessible atPoint(in long aX, in long aY,
+ in nsIAccessibleTraversalRule aRule);
+};
+
+[scriptable, uuid(e197460d-1eff-4247-b4bb-a43be1840dae)]
+interface nsIAccessibleTraversalRule : nsISupports
+{
+ /* Ignore this accessible object */
+ const unsigned short FILTER_IGNORE = 0x0;
+ /* Accept this accessible object */
+ const unsigned short FILTER_MATCH = 0x1;
+ /* Don't traverse accessibles children */
+ const unsigned short FILTER_IGNORE_SUBTREE = 0x2;
+
+ /**
+ * Determines if a given accessible is to be accepted in our traversal rule
+ *
+ * @param aAccessible [in] accessible to examine.
+ * @return a bitfield of FILTER_MATCH and FILTER_IGNORE_SUBTREE.
+ */
+ unsigned short match(in nsIAccessible aAccessible);
+};
diff --git a/accessible/interfaces/nsIAccessibleRelation.idl b/accessible/interfaces/nsIAccessibleRelation.idl
new file mode 100644
index 0000000000..00575804cd
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleRelation.idl
@@ -0,0 +1,183 @@
+/* -*- 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 "nsISupports.idl"
+#include "nsIArray.idl"
+
+interface nsIAccessible;
+
+/**
+ * This interface gives access to an accessible's set of relations.
+ */
+[scriptable, builtinclass, uuid(55b308c4-2ae4-46bc-b4cd-4d4370e0a660)]
+interface nsIAccessibleRelation : nsISupports
+{
+ /**
+ * This object is labelled by a target object.
+ */
+ const unsigned long RELATION_LABELLED_BY = 0x00;
+
+ /**
+ * This object is label for a target object.
+ */
+ const unsigned long RELATION_LABEL_FOR = 0x01;
+
+ /**
+ * This object is described by the target object.
+ */
+ const unsigned long RELATION_DESCRIBED_BY = 0x02;
+
+ /**
+ * This object is describes the target object.
+ */
+ const unsigned long RELATION_DESCRIPTION_FOR = 0x3;
+
+ /**
+ * This object is a child of a target object.
+ */
+ const unsigned long RELATION_NODE_CHILD_OF = 0x4;
+
+ /**
+ * This object is a parent of a target object. A dual relation to
+ * RELATION_NODE_CHILD_OF
+ */
+ const unsigned long RELATION_NODE_PARENT_OF = 0x5;
+
+ /**
+ * Some attribute of this object is affected by a target object.
+ */
+ const unsigned long RELATION_CONTROLLED_BY = 0x06;
+
+ /**
+ * This object is interactive and controls some attribute of a target object.
+ */
+ const unsigned long RELATION_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.
+ */
+ const unsigned long RELATION_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.
+ */
+ const unsigned long RELATION_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.
+ */
+ const unsigned long RELATION_MEMBER_OF = 0x0a;
+
+ /**
+ * This object is a sub window of a target object.
+ */
+ const unsigned long RELATION_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.
+ */
+ const unsigned long RELATION_EMBEDS = 0x0c;
+
+ /**
+ * This object is embedded by a target object.
+ */
+ const unsigned long RELATION_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.
+ */
+ const unsigned long RELATION_POPUP_FOR = 0x0e;
+
+ /**
+ * This object is a parent window of the target object.
+ */
+ const unsigned long RELATION_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.
+ */
+ const unsigned long RELATION_DEFAULT_BUTTON = 0x10;
+
+ /**
+ * The target object is the containing document object.
+ */
+ const unsigned long RELATION_CONTAINING_DOCUMENT = 0x11;
+
+ /**
+ * The target object is the topmost containing document object in the tab pane.
+ */
+ const unsigned long RELATION_CONTAINING_TAB_PANE = 0x12;
+
+ /**
+ * The target object is the containing window object.
+ */
+ const unsigned long RELATION_CONTAINING_WINDOW = 0x13;
+
+ /**
+ * The target object is the containing application object.
+ */
+ const unsigned long RELATION_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.
+ */
+ const unsigned long RELATION_DETAILS = 0x15;
+
+ /**
+ * This object provides the detailed, extended description for the target
+ * object. See DETAILS relation.
+ */
+ const unsigned long RELATION_DETAILS_FOR = 0x16;
+
+ /**
+ * The target object is the error message for this object.
+ */
+ const unsigned long RELATION_ERRORMSG = 0x17;
+
+ /**
+ * This object is the error message for the target object.
+ */
+ const unsigned long RELATION_ERRORMSG_FOR = 0x18;
+
+ /**
+ * The target object is the anchor referenced by this link.
+ */
+ const unsigned long RELATION_LINKS_TO = 0x19;
+
+ /**
+ * Returns the type of the relation.
+ */
+ readonly attribute unsigned long relationType;
+
+ /**
+ * Returns the number of targets for this relation.
+ */
+ readonly attribute unsigned long targetsCount;
+
+ /**
+ * Returns one accessible relation target.
+ * @param index - 0 based index of relation target.
+ */
+ nsIAccessible getTarget(in unsigned long index);
+
+ /**
+ * Returns multiple accessible relation targets.
+ */
+ nsIArray getTargets();
+};
diff --git a/accessible/interfaces/nsIAccessibleRole.idl b/accessible/interfaces/nsIAccessibleRole.idl
new file mode 100644
index 0000000000..e8024063c1
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleRole.idl
@@ -0,0 +1,802 @@
+/* -*- 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 "nsISupports.idl"
+
+/**
+ * Defines cross platform (Gecko) roles.
+ */
+[scriptable, builtinclass, uuid(ad7f32a5-6d5f-4154-a5b8-0fa7aed48936)]
+interface nsIAccessibleRole : nsISupports
+{
+ /**
+ * Used when the accessible has no strongly-defined role.
+ */
+ const unsigned long ROLE_NOTHING = 0;
+
+ /**
+ * 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".
+ */
+ const unsigned long ROLE_MENUBAR = 1;
+
+ /**
+ * Represents a vertical or horizontal scroll bar, which is part of the client
+ * area or used in a control.
+ */
+ const unsigned long ROLE_SCROLLBAR = 2;
+
+ /**
+ * 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".
+ */
+ const unsigned long ROLE_ALERT = 3;
+
+ /**
+ * A sub-document (<frame> or <iframe>)
+ */
+ const unsigned long ROLE_INTERNAL_FRAME = 4;
+
+ /**
+ * Represents a menu, which presents a list of options from which the user can
+ * make a selection to perform an action. It is used for role="menu".
+ */
+ const unsigned long ROLE_MENUPOPUP = 5;
+
+ /**
+ * Represents a menu item, which is an entry in a menu that a user can choose
+ * to carry out a command, select an option. It is used for xul:menuitem,
+ * role="menuitem".
+ */
+ const unsigned long ROLE_MENUITEM = 6;
+
+ /**
+ * Represents a ToolTip that provides helpful hints.
+ */
+ const unsigned long ROLE_TOOLTIP = 7;
+
+ /**
+ * Represents a main window for an application. It is used for
+ * role="application". Also refer to ROLE_APP_ROOT
+ */
+ const unsigned long ROLE_APPLICATION = 8;
+
+ /**
+ * Represents a document window. A document window is always contained within
+ * an application window. For role="document", see NON_NATIVE_DOCUMENT.
+ */
+ const unsigned long ROLE_DOCUMENT = 9;
+
+ /**
+ * Represents a pane within a frame or document window. Users can navigate
+ * between panes and within the contents of the current pane, but cannot
+ * navigate between items in different panes. Thus, panes represent a level
+ * of grouping lower than frame windows or documents, but above individual
+ * controls. It is used for the first child of a <frame> or <iframe>.
+ */
+ const unsigned long ROLE_PANE = 10;
+
+ /**
+ * Represents a dialog box or message box. It is used for xul:dialog,
+ * role="dialog".
+ */
+ const unsigned long ROLE_DIALOG = 11;
+
+ /**
+ * Logically groups other objects. There is not always a parent-child
+ * relationship between the grouping object and the objects it contains. It
+ * is used for html:textfield, xul:groupbox, role="group".
+ */
+ const unsigned long ROLE_GROUPING = 12;
+
+ /**
+ * Used to visually divide a space into two regions, such as a separator menu
+ * item or a bar that divides split panes within a window. It is used for
+ * xul:separator, html:hr, role="separator".
+ */
+ const unsigned long ROLE_SEPARATOR = 13;
+
+ /**
+ * Represents a toolbar, which is a grouping of controls (push buttons or
+ * toggle buttons) that provides easy access to frequently used features. It
+ * is used for xul:toolbar, role="toolbar".
+ */
+ const unsigned long ROLE_TOOLBAR = 14;
+
+ /**
+ * Represents a status bar, which is an area at the bottom of a window that
+ * displays information about the current operation, state of the application,
+ * or selected object. The status bar has multiple fields, which display
+ * different kinds of information. It is used for xul:statusbar.
+ */
+ const unsigned long ROLE_STATUSBAR = 15;
+
+ /**
+ * Represents a table that contains rows and columns of cells, and optionally,
+ * row headers and column headers. It is used for html:table,
+ * role="grid". Also refer to the following roles: ROLE_COLUMNHEADER,
+ * ROLE_ROWHEADER, ROLE_ROW, ROLE_CELL.
+ */
+ const unsigned long ROLE_TABLE = 16;
+
+ /**
+ * Represents a column header, providing a visual label for a column in
+ * a table. It is used for XUL tree column headers, html:th,
+ * role="colheader". Also refer to ROLE_TABLE.
+ */
+ const unsigned long ROLE_COLUMNHEADER = 17;
+
+ /**
+ * Represents a row header, which provides a visual label for a table row.
+ * It is used for role="rowheader". Also, see ROLE_TABLE.
+ */
+ const unsigned long ROLE_ROWHEADER = 18;
+
+ /**
+ * Represents a row of cells within a table. Also, see ROLE_TABLE.
+ */
+ const unsigned long ROLE_ROW = 19;
+
+ /**
+ * Represents a cell within a table. It is used for html:td and xul:tree cell.
+ * Also, see ROLE_TABLE.
+ */
+ const unsigned long ROLE_CELL = 20;
+
+ /**
+ * Represents a link to something else. This object might look like text or
+ * a graphic, but it acts like a button. It is used for
+ * xul:label@class="text-link", html:a, html:area.
+ */
+ const unsigned long ROLE_LINK = 21;
+
+ /**
+ * Represents a list box, allowing the user to select one or more items. It
+ * is used for xul:listbox, html:select@size, role="list". See also
+ * ROLE_LIST_ITEM.
+ */
+ const unsigned long ROLE_LIST = 22;
+
+ /**
+ * Represents an item in a list. See also ROLE_LIST.
+ */
+ const unsigned long ROLE_LISTITEM = 23;
+
+ /**
+ * Represents an outline or tree structure, such as a tree view control,
+ * that displays a hierarchical list and allows the user to expand and
+ * collapse branches. Is is used for role="tree".
+ */
+ const unsigned long ROLE_OUTLINE = 24;
+
+ /**
+ * Represents an item in an outline or tree structure. It is used for
+ * role="treeitem".
+ */
+ const unsigned long ROLE_OUTLINEITEM = 25;
+
+ /**
+ * Represents a page tab, it is a child of a page tab list. It is used for
+ * xul:tab, role="treeitem". Also refer to ROLE_PAGETABLIST.
+ */
+ const unsigned long ROLE_PAGETAB = 26;
+
+ /**
+ * Represents a property sheet. It is used for xul:tabpanel,
+ * role="tabpanel".
+ */
+ const unsigned long ROLE_PROPERTYPAGE = 27;
+
+ /**
+ * Represents a picture. Is is used for xul:image, html:img.
+ */
+ const unsigned long ROLE_GRAPHIC = 28;
+
+ /**
+ * Represents read-only text, such as labels for other controls or
+ * instructions in a dialog box. Static text cannot be modified or selected.
+ * Is is used for xul:label, xul:description, html:label, role="label".
+ */
+ const unsigned long ROLE_STATICTEXT = 29;
+
+ /**
+ * Represents selectable text that allows edits or is designated read-only.
+ */
+ const unsigned long ROLE_TEXT_LEAF = 30;
+
+ /**
+ * Represents a push button control. It is used for xul:button, html:button,
+ * role="button".
+ */
+ const unsigned long ROLE_PUSHBUTTON = 31;
+
+ /**
+ * Represents a check box control. It is used for xul:checkbox,
+ * html:input@type="checkbox", role="checkbox".
+ */
+ const unsigned long ROLE_CHECKBUTTON = 32;
+
+ /**
+ * Represents an option button, also called a radio button. It is one of a
+ * group of mutually exclusive options. All objects sharing a single parent
+ * that have this attribute are assumed to be part of single mutually
+ * exclusive group. It is used for xul:radio, html:input@type="radio",
+ * role="radio".
+ */
+ const unsigned long ROLE_RADIOBUTTON = 33;
+
+ /**
+ * Represents a combo box; a popup button with an associated list box that
+ * provides a set of predefined choices. It is used for html:select with a
+ * size of 1 and xul:menulist. See also ROLE_EDITCOMBOBOX.
+ */
+ const unsigned long ROLE_COMBOBOX = 34;
+
+ /**
+ * Represents a progress bar, dynamically showing the user the percent
+ * complete of an operation in progress. It is used for html:progress,
+ * role="progressbar".
+ */
+ const unsigned long ROLE_PROGRESSBAR = 35;
+
+ /**
+ * Represents a slider, which allows the user to adjust a setting in given
+ * increments between minimum and maximum values. It is used by xul:scale,
+ * role="slider".
+ */
+ const unsigned long ROLE_SLIDER = 36;
+
+ /**
+ * Represents a spin box, which is a control that allows the user to increment
+ * or decrement the value displayed in a separate "buddy" control associated
+ * with the spin box. It is used for input[type=number] spin buttons.
+ */
+ const unsigned long ROLE_SPINBUTTON = 37;
+
+ /**
+ * Represents a graphical image used to diagram data. It is used for svg:svg.
+ */
+ const unsigned long ROLE_DIAGRAM = 38;
+
+ /**
+ * Represents an animation control, which contains content that changes over
+ * time, such as a control that displays a series of bitmap frames.
+ */
+ const unsigned long ROLE_ANIMATION = 39;
+
+ /**
+ * Represents a button that drops down a list of items.
+ */
+ const unsigned long ROLE_BUTTONDROPDOWN = 40;
+
+ /**
+ * Represents a button that drops down a menu.
+ */
+ const unsigned long ROLE_BUTTONMENU = 41;
+
+ /**
+ * Represents blank space between other objects.
+ */
+ const unsigned long ROLE_WHITESPACE = 42;
+
+ /**
+ * Represents a container of page tab controls. Is it used for xul:tabs,
+ * DHTML: role="tabs". Also refer to ROLE_PAGETAB.
+ */
+ const unsigned long ROLE_PAGETABLIST = 43;
+
+ /**
+ * Represents a control that can be drawn into and is used to trap events.
+ * It is used for html:canvas.
+ */
+ const unsigned long ROLE_CANVAS = 44;
+
+ /**
+ * Represents a menu item with a check box.
+ */
+ const unsigned long ROLE_CHECK_MENU_ITEM = 45;
+
+ /**
+ * Represents control whose purpose is to allow a user to edit a date.
+ */
+ const unsigned long ROLE_DATE_EDITOR = 46;
+
+ /**
+ * Frame role. A top level window with a title bar, border, menu bar, etc.
+ * It is often used as the primary window for an application.
+ */
+ const unsigned long ROLE_CHROME_WINDOW = 47;
+
+ /**
+ * Presents an icon or short string in an interface.
+ */
+ const unsigned long ROLE_LABEL = 48;
+
+ /**
+ * A text object uses for passwords, or other places where the text content
+ * is not shown visibly to the user.
+ */
+ const unsigned long ROLE_PASSWORD_TEXT = 49;
+
+ /**
+ * A radio button that is a menu item.
+ */
+ const unsigned long ROLE_RADIO_MENU_ITEM = 50;
+
+ /**
+ * Collection of objects that constitute a logical text entity.
+ */
+ const unsigned long ROLE_TEXT_CONTAINER = 51;
+
+ /**
+ * A toggle button. A specialized push button that can be checked or
+ * unchecked, but does not provide a separate indicator for the current state.
+ */
+ const unsigned long ROLE_TOGGLE_BUTTON = 52;
+
+ /**
+ * Representas a control that is capable of expanding and collapsing rows as
+ * well as showing multiple columns of data.
+ * XXX: it looks like this role is dupe of ROLE_OUTLINE.
+ */
+ const unsigned long ROLE_TREE_TABLE = 53;
+
+ /**
+ * A paragraph of text.
+ */
+ const unsigned long ROLE_PARAGRAPH = 54;
+
+ /**
+ * An control whose textual content may be entered or modified by the user.
+ */
+ const unsigned long ROLE_ENTRY = 55;
+
+ /**
+ * A caption describing another object.
+ */
+ const unsigned long ROLE_CAPTION = 56;
+
+ /**
+ * An element containing content that assistive technology users may want to
+ * browse in a reading mode, rather than a focus/interactive/application mode.
+ * This role is used for role="document". For the container which holds the
+ * content of a web page, see ROLE_DOCUMENT.
+ */
+ const unsigned long ROLE_NON_NATIVE_DOCUMENT = 57;
+
+ /**
+ * Heading.
+ */
+ const unsigned long ROLE_HEADING = 58;
+
+ /**
+ * A container of document content. An example of the use of this role is to
+ * represent an html:div.
+ */
+ const unsigned long ROLE_SECTION = 59;
+
+ /**
+ * A container of form controls. An example of the use of this role is to
+ * represent an html:form.
+ */
+ const unsigned long ROLE_FORM = 60;
+
+ /**
+ * XXX: document this.
+ */
+ const unsigned long ROLE_APP_ROOT = 61;
+
+ /**
+ * Represents a menu item, which is an entry in a menu that a user can choose
+ * to display another menu.
+ */
+ const unsigned long ROLE_PARENT_MENUITEM = 62;
+
+ /**
+ * A list of items that is shown by combobox.
+ */
+ const unsigned long ROLE_COMBOBOX_LIST = 63;
+
+ /**
+ * A item of list that is shown by combobox;
+ */
+ const unsigned long ROLE_COMBOBOX_OPTION = 64;
+
+ /**
+ * An image map -- has child links representing the areas
+ */
+ const unsigned long ROLE_IMAGE_MAP = 65;
+
+ /**
+ * An option in a listbox
+ */
+ const unsigned long ROLE_OPTION = 66;
+
+ /**
+ * A rich option in a listbox, it can have other widgets as children
+ */
+ const unsigned long ROLE_RICH_OPTION = 67;
+
+ /**
+ * A list of options
+ */
+ const unsigned long ROLE_LISTBOX = 68;
+
+ /**
+ * Represents a mathematical equation in the accessible name
+ */
+ const unsigned long ROLE_FLAT_EQUATION = 69;
+
+ /**
+ * Represents a cell within a grid. It is used for role="gridcell". Unlike
+ * ROLE_CELL, it allows the calculation of the accessible name from subtree.
+ * Also, see ROLE_TABLE.
+ */
+ const unsigned long ROLE_GRID_CELL = 70;
+
+ /**
+ * A note. Originally intended to be hidden until activated, but now also used
+ * for things like html 'aside'.
+ */
+ const unsigned long ROLE_NOTE = 71;
+
+ /**
+ * A figure. Used for things like HTML5 figure element.
+ */
+ const unsigned long ROLE_FIGURE = 72;
+
+ /**
+ * Represents a rich item with a check box.
+ */
+ const unsigned long ROLE_CHECK_RICH_OPTION = 73;
+
+ /**
+ * An HTML definition list <dl>
+ */
+ const unsigned long ROLE_DEFINITION_LIST = 74;
+
+ /**
+ * An HTML definition term <dt>
+ */
+ const unsigned long ROLE_TERM = 75;
+
+ /**
+ * An HTML definition <dd>
+ */
+ const unsigned long ROLE_DEFINITION = 76;
+
+ /**
+ * A keyboard or keypad key.
+ */
+ const unsigned long ROLE_KEY = 77;
+
+ /**
+ * A switch control widget.
+ */
+ const unsigned long ROLE_SWITCH = 78;
+
+ /**
+ * A block of MathML code (math).
+ */
+ const unsigned long ROLE_MATHML_MATH = 79;
+
+ /**
+ * A MathML identifier (mi in MathML).
+ */
+ const unsigned long ROLE_MATHML_IDENTIFIER = 80;
+
+ /**
+ * A MathML number (mn in MathML).
+ */
+ const unsigned long ROLE_MATHML_NUMBER = 81;
+
+ /**
+ * A MathML operator (mo in MathML).
+ */
+ const unsigned long ROLE_MATHML_OPERATOR = 82;
+
+ /**
+ * A MathML text (mtext in MathML).
+ */
+ const unsigned long ROLE_MATHML_TEXT = 83;
+
+ /**
+ * A MathML string literal (ms in MathML).
+ */
+ const unsigned long ROLE_MATHML_STRING_LITERAL = 84;
+
+ /**
+ * A MathML glyph (mglyph in MathML).
+ */
+ const unsigned long ROLE_MATHML_GLYPH = 85;
+
+ /**
+ * A MathML row (mrow in MathML).
+ */
+ const unsigned long ROLE_MATHML_ROW = 86;
+
+ /**
+ * A MathML fraction (mfrac in MathML).
+ */
+ const unsigned long ROLE_MATHML_FRACTION = 87;
+
+ /**
+ * A MathML square root (msqrt in MathML).
+ */
+ const unsigned long ROLE_MATHML_SQUARE_ROOT = 88;
+
+ /**
+ * A MathML root (mroot in MathML).
+ */
+ const unsigned long ROLE_MATHML_ROOT = 89;
+
+ /**
+ * A MathML enclosed element (menclose in MathML).
+ */
+ const unsigned long ROLE_MATHML_ENCLOSED = 90;
+
+ /**
+ * A MathML styling element (mstyle in MathML).
+ */
+ const unsigned long ROLE_MATHML_STYLE = 91;
+
+ /**
+ * A MathML subscript (msub in MathML).
+ */
+ const unsigned long ROLE_MATHML_SUB = 92;
+
+ /**
+ * A MathML superscript (msup in MathML).
+ */
+ const unsigned long ROLE_MATHML_SUP = 93;
+
+ /**
+ * A MathML subscript and superscript (msubsup in MathML).
+ */
+ const unsigned long ROLE_MATHML_SUB_SUP = 94;
+
+ /**
+ * A MathML underscript (munder in MathML).
+ */
+ const unsigned long ROLE_MATHML_UNDER = 95;
+
+ /**
+ * A MathML overscript (mover in MathML).
+ */
+ const unsigned long ROLE_MATHML_OVER = 96;
+
+ /**
+ * A MathML underscript and overscript (munderover in MathML).
+ */
+ const unsigned long ROLE_MATHML_UNDER_OVER = 97;
+
+ /**
+ * A MathML multiple subscript and superscript element (mmultiscripts in
+ * MathML).
+ */
+ const unsigned long ROLE_MATHML_MULTISCRIPTS = 98;
+
+ /**
+ * A MathML table (mtable in MathML).
+ */
+ const unsigned long ROLE_MATHML_TABLE = 99;
+
+ /**
+ * A MathML labelled table row (mlabeledtr in MathML).
+ */
+ const unsigned long ROLE_MATHML_LABELED_ROW = 100;
+
+ /**
+ * A MathML table row (mtr in MathML).
+ */
+ const unsigned long ROLE_MATHML_TABLE_ROW = 101;
+
+ /**
+ * A MathML table entry or cell (mtd in MathML).
+ */
+ const unsigned long ROLE_MATHML_CELL = 102;
+
+ /**
+ * A MathML interactive element (maction in MathML).
+ */
+ const unsigned long ROLE_MATHML_ACTION = 103;
+
+ /**
+ * A MathML error message (merror in MathML).
+ */
+ const unsigned long ROLE_MATHML_ERROR = 104;
+
+ /**
+ * A MathML stacked (rows of numbers) element (mstack in MathML).
+ */
+ const unsigned long ROLE_MATHML_STACK = 105;
+
+ /**
+ * A MathML long division element (mlongdiv in MathML).
+ */
+ const unsigned long ROLE_MATHML_LONG_DIVISION = 106;
+
+ /**
+ * A MathML stack group (msgroup in MathML).
+ */
+ const unsigned long ROLE_MATHML_STACK_GROUP = 107;
+
+ /**
+ * A MathML stack row (msrow in MathML).
+ */
+ const unsigned long ROLE_MATHML_STACK_ROW = 108;
+
+ /**
+ * MathML carries, borrows, or crossouts for a row (mscarries in MathML).
+ */
+ const unsigned long ROLE_MATHML_STACK_CARRIES = 109;
+
+ /**
+ * A MathML carry, borrow, or crossout for a column (mscarry in MathML).
+ */
+ const unsigned long ROLE_MATHML_STACK_CARRY = 110;
+
+ /**
+ * A MathML line in a stack (msline in MathML).
+ */
+ const unsigned long ROLE_MATHML_STACK_LINE = 111;
+
+ /**
+ * A group containing radio buttons
+ */
+ const unsigned long ROLE_RADIO_GROUP = 112;
+
+ /**
+ * A text container exposing brief amount of information. See related
+ * TEXT_CONTAINER role.
+ */
+ const unsigned long ROLE_TEXT = 113;
+
+ /**
+ * A text container exposing brief amount of information. See related
+ * DETAILS role.
+ */
+ const unsigned long ROLE_DETAILS = 114;
+
+ /**
+ * A text container exposing brief amount of information. See related
+ * SUMMARY role.
+ */
+ const unsigned long ROLE_SUMMARY = 115;
+
+ /**
+ * An ARIA landmark. See related NAVIGATION role.
+ */
+ const unsigned long ROLE_LANDMARK = 116;
+
+ /**
+ * A specific type of ARIA landmark. The ability to distinguish navigation
+ * landmarks from other types of landmarks is needed because macOS has a
+ * specific AXSubrole and AXRoleDescription for navigation landmarks.
+ */
+ const unsigned long ROLE_NAVIGATION = 117;
+
+ /**
+ * An object that contains the text of a footnote.
+ */
+ const unsigned long ROLE_FOOTNOTE = 118;
+
+ /**
+ * A complete or self-contained composition in a document, page, application,
+ * or site and that is, in principle, independently distributable or reusable,
+ * e.g. in syndication.
+ */
+ const unsigned long ROLE_ARTICLE = 119;
+
+ /**
+ * A perceivable section containing content that is relevant to a specific,
+ * author-specified purpose and sufficiently important that users will likely
+ * want to be able to navigate to the section easily and to have it listed in
+ * a summary of the page.
+ */
+ const unsigned long ROLE_REGION = 120;
+
+ /**
+ * Represents a control with a text input and a popup with a set of predefined
+ * choices. It is used for ARIA's combobox role. See also ROLE_COMBOBOX.
+ */
+ const unsigned long ROLE_EDITCOMBOBOX = 121;
+
+ /**
+ * A section of content that is quoted from another source.
+ */
+ const unsigned long ROLE_BLOCKQUOTE = 122;
+
+ /**
+ * Content previously deleted or proposed for deletion, e.g. in revision
+ * history or a content view providing suggestions from reviewers.
+ */
+ const unsigned long ROLE_CONTENT_DELETION = 123;
+
+ /**
+ * Content previously inserted or proposed for insertion, e.g. in revision
+ * history or a content view providing suggestions from reviewers.
+ */
+ const unsigned long ROLE_CONTENT_INSERTION = 124;
+
+ /**
+ * An html:form element with a label provided by WAI-ARIA.
+ * This may also be used if role="form" with a label should be exposed
+ * differently in the future.
+ */
+ const unsigned long ROLE_FORM_LANDMARK = 125;
+
+ /**
+ * The html:mark element.
+ * May also be used if WAI-ARIA gets an equivalent role.
+ */
+ const unsigned long ROLE_MARK = 126;
+
+ /**
+ * The WAI-ARIA suggestion role.
+ */
+ const unsigned long ROLE_SUGGESTION = 127;
+
+ /**
+ * The WAI-ARIA comment role.
+ */
+ const unsigned long ROLE_COMMENT = 128;
+
+ /**
+ * A snippet of program code. ATs might want to treat this differently.
+ */
+ const unsigned long ROLE_CODE = 129;
+
+ /**
+ * Represents control whose purpose is to allow a user to edit a time.
+ */
+ const unsigned long ROLE_TIME_EDITOR = 130;
+
+ /**
+ * Represents the marker associated with a list item. In unordered lists,
+ * this is a bullet, while in ordered lists this is a number.
+ */
+ const unsigned long ROLE_LISTITEM_MARKER = 131;
+
+ /**
+ * Essentially, this is a progress bar with a contextually defined
+ * scale, ex. the strength of a password entered in an input.
+ */
+ const unsigned long ROLE_METER = 132;
+
+ /**
+ * Represents phrasing content that is presented with vertical alignment
+ * lower than the baseline and a smaller font size. For example, the "2" in
+ * the chemical formula H2O.
+ */
+ const unsigned long ROLE_SUBSCRIPT = 133;
+
+ /**
+ * Represents phrasing content that is presented with vertical alignment
+ * higher than the baseline and a smaller font size. For example, the
+ * exponent in a math expression.
+ */
+ const unsigned long ROLE_SUPERSCRIPT = 134;
+
+ /**
+ * Represents one or more emphasized characters. Use this role to stress or
+ * emphasize content.
+ */
+ const unsigned long ROLE_EMPHASIS = 135;
+
+ /**
+ * Represents content that is important, serious, or urgent.
+ */
+ const unsigned long ROLE_STRONG = 136;
+
+ /**
+ * Represents a specific point in time.
+ */
+ const unsigned long ROLE_TIME = 137;
+};
diff --git a/accessible/interfaces/nsIAccessibleScrollingEvent.idl b/accessible/interfaces/nsIAccessibleScrollingEvent.idl
new file mode 100644
index 0000000000..229d745793
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleScrollingEvent.idl
@@ -0,0 +1,34 @@
+/* -*- 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 "nsIAccessibleEvent.idl"
+
+/*
+ * An interface scroll events.
+ * Stores new scroll position and max scroll position.
+ */
+[scriptable, builtinclass, uuid(f75f0b32-5342-4d60-b1a5-b7bd6888eef5)]
+interface nsIAccessibleScrollingEvent : nsIAccessibleEvent
+{
+ /**
+ * New X scroll position within a scrollable container in device pixels.
+ */
+ readonly attribute unsigned long scrollX;
+
+ /**
+ * New Y scroll position within a scrollable container in device pixels.
+ */
+ readonly attribute unsigned long scrollY;
+
+ /**
+ * Max X scroll position within a scrollable container in device pixels.
+ */
+ readonly attribute unsigned long maxScrollX;
+
+ /**
+ * Max Y scroll position within a scrollable container in device pixels.
+ */
+ readonly attribute unsigned long maxScrollY;
+};
diff --git a/accessible/interfaces/nsIAccessibleSelectable.idl b/accessible/interfaces/nsIAccessibleSelectable.idl
new file mode 100644
index 0000000000..dff2677e20
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleSelectable.idl
@@ -0,0 +1,59 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIAccessible;
+interface nsIArray;
+
+/**
+ * An accessibility interface for selectable widgets.
+ */
+[scriptable, builtinclass, uuid(8efb03d4-1354-4875-94cf-261336057626)]
+interface nsIAccessibleSelectable : nsISupports
+{
+ /**
+ * Return an nsIArray of selected items within the widget.
+ */
+ readonly attribute nsIArray selectedItems;
+
+ /**
+ * Return the number of currently selected items.
+ */
+ readonly attribute unsigned long selectedItemCount;
+
+ /**
+ * Return a nth selected item within the widget.
+ */
+ nsIAccessible getSelectedItemAt(in unsigned long index);
+
+ /**
+ * Return true if the given item is selected.
+ */
+ boolean isItemSelected(in unsigned long index);
+
+ /**
+ * Adds the specified item to the widget's selection.
+ */
+ void addItemToSelection(in unsigned long index);
+
+ /**
+ * Removes the specified item from the widget's selection.
+ */
+ void removeItemFromSelection(in unsigned long index);
+
+ /**
+ * Select all items.
+ *
+ * @return false if the object does not accept multiple selection,
+ * otherwise true.
+ */
+ boolean selectAll();
+
+ /**
+ * Unselect all items.
+ */
+ void unselectAll();
+};
diff --git a/accessible/interfaces/nsIAccessibleStateChangeEvent.idl b/accessible/interfaces/nsIAccessibleStateChangeEvent.idl
new file mode 100644
index 0000000000..4d7505d5bb
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleStateChangeEvent.idl
@@ -0,0 +1,29 @@
+/* -*- 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 "nsIAccessibleEvent.idl"
+
+/**
+ * Fired when a state of an accessible changes.
+ */
+[scriptable, builtinclass, uuid(58b74954-1835-46ed-9ccd-c906490106f6)]
+interface nsIAccessibleStateChangeEvent : nsIAccessibleEvent
+{
+ /**
+ * Returns the state of accessible (see constants declared
+ * in nsIAccessibleStates).
+ */
+ readonly attribute unsigned long state;
+
+ /**
+ * Returns true if the state is extra state.
+ */
+ readonly attribute boolean isExtraState;
+
+ /**
+ * Returns true if the state is turned on.
+ */
+ readonly attribute boolean isEnabled;
+};
diff --git a/accessible/interfaces/nsIAccessibleStates.idl b/accessible/interfaces/nsIAccessibleStates.idl
new file mode 100644
index 0000000000..6d25e469c6
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleStates.idl
@@ -0,0 +1,76 @@
+/* -*- 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 "nsISupports.idl"
+
+[scriptable, uuid(f1e0fbb7-fde4-4519-9383-2bcbee428513)]
+interface nsIAccessibleStates : nsISupports
+{
+ /**
+ * MSAA State flags - used for bitfield. More than 1 allowed.
+ */
+ const unsigned long STATE_UNAVAILABLE = 0x00000001; // Disabled, maps to opposite of Java ENABLED, Gnome/ATK SENSITIVE?
+ const unsigned long STATE_SELECTED = 0x00000002;
+ const unsigned long STATE_FOCUSED = 0x00000004;
+ const unsigned long STATE_PRESSED = 0x00000008;
+ const unsigned long STATE_CHECKED = 0x00000010;
+ const unsigned long STATE_MIXED = 0x00000020; // 3-state checkbox or toolbar button
+ const unsigned long STATE_READONLY = 0x00000040; // Maps to opposite of Java/Gnome/ATK EDITABLE state
+ const unsigned long STATE_HOTTRACKED = 0x00000080;
+ const unsigned long STATE_DEFAULT = 0x00000100;
+ const unsigned long STATE_EXPANDED = 0x00000200;
+ const unsigned long STATE_COLLAPSED = 0x00000400;
+ const unsigned long STATE_BUSY = 0x00000800;
+ const unsigned long STATE_FLOATING = 0x00001000; // Children "owned" not "contained" by parent
+ const unsigned long STATE_MARQUEED = 0x00002000;
+ const unsigned long STATE_ANIMATED = 0x00004000;
+ const unsigned long STATE_INVISIBLE = 0x00008000; // Programatically hidden
+ const unsigned long STATE_OFFSCREEN = 0x00010000; // Scrolled off
+ const unsigned long STATE_SIZEABLE = 0x00020000;
+ const unsigned long STATE_MOVEABLE = 0x00040000;
+ const unsigned long STATE_SELFVOICING = 0x00080000;
+ const unsigned long STATE_FOCUSABLE = 0x00100000;
+ const unsigned long STATE_SELECTABLE = 0x00200000;
+ const unsigned long STATE_LINKED = 0x00400000;
+ const unsigned long STATE_TRAVERSED = 0x00800000;
+ const unsigned long STATE_MULTISELECTABLE = 0x01000000; // Supports multiple selection
+ const unsigned long STATE_EXTSELECTABLE = 0x02000000; // Supports extended selection
+ const unsigned long STATE_ALERT_LOW = 0x04000000; // This information is of low priority
+ const unsigned long STATE_ALERT_MEDIUM = 0x08000000; // This information is of medium priority
+ const unsigned long STATE_ALERT_HIGH = 0x10000000; // This information is of high priority
+ const unsigned long STATE_PROTECTED = 0x20000000; // Maps to Gnome's *Role* ATK_ROLE_PASSWD_TEXT, nothing for Java?
+ const unsigned long STATE_HASPOPUP = 0x40000000; // New in MSAA 2.0
+
+ // Mapping important states that we don't have to unused alert states on MSAA
+ // as per discussions with AT vendors. On ATK there will be legitimate states for
+ // STATE_REQUIRED AND STATE_INVALID
+ const unsigned long STATE_REQUIRED = STATE_ALERT_LOW;
+ const unsigned long STATE_IMPORTANT = STATE_ALERT_MEDIUM; // Not currently used
+ const unsigned long STATE_INVALID = STATE_ALERT_HIGH;
+ const unsigned long STATE_CHECKABLE = STATE_MARQUEED;
+
+/**
+ * Extended state flags (for now non-MSAA, for Java and Gnome/ATK support)
+ * "Extended state flags" has separate value space from "MSAA State flags".
+ */
+ const unsigned long EXT_STATE_SUPPORTS_AUTOCOMPLETION = 0x00000001; // For editable areas that have any kind of autocompletion
+ const unsigned long EXT_STATE_DEFUNCT = 0x00000002; // Object no longer exists
+ const unsigned long EXT_STATE_SELECTABLE_TEXT = 0x00000004; // For text which is selectable, object must implement nsIAccessibleText
+ const unsigned long EXT_STATE_EDITABLE = 0x00000008; // Implements nsIAccessibleEditableText
+ const unsigned long EXT_STATE_ACTIVE = 0x00000010; // This window is currently the active window
+ const unsigned long EXT_STATE_MODAL = 0x00000020; // Must do something with control before leaving it
+ const unsigned long EXT_STATE_MULTI_LINE = 0x00000040; // Edit control that can take multiple lines
+ const unsigned long EXT_STATE_HORIZONTAL = 0x00000080; // Uses horizontal layout
+ const unsigned long EXT_STATE_OPAQUE = 0x00000100; // Indicates this object paints every pixel within its rectangular region.
+ const unsigned long EXT_STATE_SINGLE_LINE = 0x00000200; // This text object can only contain 1 line of text
+ const unsigned long EXT_STATE_TRANSIENT = 0x00000400; //
+ const unsigned long EXT_STATE_VERTICAL = 0x00000800; // Especially used for sliders and scrollbars
+ const unsigned long EXT_STATE_STALE = 0x00001000; // Object not dead, but not up-to-date either
+ const unsigned long EXT_STATE_ENABLED = 0x00002000; // A widget that is not unavailable
+ const unsigned long EXT_STATE_SENSITIVE = 0x00004000; // Same as ENABLED for now
+ const unsigned long EXT_STATE_EXPANDABLE = 0x00008000; // If COLLAPSED or EXPANDED
+ const unsigned long EXT_STATE_PINNED = 0x00010000; // Indicates object is pinned.
+ const unsigned long EXT_STATE_CURRENT = 0x00020000; // Indicates object is the current item in its container
+};
diff --git a/accessible/interfaces/nsIAccessibleTable.idl b/accessible/interfaces/nsIAccessibleTable.idl
new file mode 100644
index 0000000000..ab4b60a5cc
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleTable.idl
@@ -0,0 +1,234 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIAccessible;
+interface nsIArray;
+
+[scriptable, builtinclass, uuid(cb0bf7b9-117e-40e2-9e46-189c3d43ce4a)]
+interface nsIAccessibleTable : nsISupports
+{
+ /**
+ * Return the caption accessible for the table. For example, html:caption
+ * element of html:table element.
+ */
+ readonly attribute nsIAccessible caption;
+
+ /**
+ * Return summary description for the table. For example, @summary attribute
+ * on html:table element.
+ */
+ readonly attribute AString summary;
+
+ /**
+ * Return columns count in the table.
+ */
+ readonly attribute long columnCount;
+
+ /**
+ * Return rows count in the table.
+ */
+ readonly attribute long rowCount;
+
+ /**
+ * Return the accessible object at the specified row and column in the table.
+ * If both row and column index are valid then the corresponding accessible
+ * object is returned that represents the requested cell regardless of whether
+ * the cell is currently visible (on the screen).
+ *
+ * @param rowIndex [in] the row index to retrieve the cell at
+ * @param columnIndex [in] the column index to retrieve the cell at
+ */
+ nsIAccessible getCellAt(in long rowIndex, in long columnIndex);
+
+ /**
+ * Translate the given row and column indices into the corresponding cell
+ * index.
+ *
+ * @param rowIndex [in] the row index to return cell index at
+ * @param columnIndex [in] the column index to return cell index at
+ */
+ long getCellIndexAt(in long rowIndex, in long columnIndex);
+
+ /**
+ * Translate the given cell index into the corresponding column index.
+ *
+ * @param cellIndex [in] index of the table cell to return column index for
+ */
+ long getColumnIndexAt(in long cellIndex);
+
+ /**
+ * Translate the given cell index into the corresponding row index.
+ *
+ * @param cellIndex [in] index of the table cell to return row index for
+ */
+ long getRowIndexAt(in long cellIndex);
+
+ /**
+ * Translate the given cell index into the corresponding row and column
+ * indices.
+ *
+ * @param cellIndex [in] cell index to return row and column indices for
+ * @param rowIndex [out] row index at the given cell index
+ * @param columnIndex [out] column index at the given cell index
+ */
+ void getRowAndColumnIndicesAt(in long cellIndex,
+ out long rowIndex, out long columnIndex);
+
+ /**
+ * Return the number of columns occupied by the accessible cell at
+ * the specified row and column in the table. The result differs from 1 if
+ * the specified cell spans multiple columns.
+ *
+ * @param row [in] row index of the cell to return the column extent for
+ * @param column [in] column index of the cell to return the column extent
+ * for
+ */
+ long getColumnExtentAt(in long row, in long column);
+
+ /**
+ * Return the number of rows occupied by the accessible cell at the specified
+ * row and column in the table. The result differs from 1 if the specified
+ * cell spans multiple rows.
+ *
+ * @param row [in] row index of the cell to return the column extent for
+ * @param column [in] column index of the cell to return the column extent
+ * for
+ */
+ long getRowExtentAt(in long row, in long column);
+
+ /**
+ * Return the description text of the specified column in the table.
+ *
+ * @param columnIndex [in] the column index to retrieve description for
+ */
+ AString getColumnDescription(in long columnIndex);
+
+ /**
+ * Return the description text of the specified row in the table.
+ *
+ * @param rowIndex [in] the row index to retrieve description for
+ */
+ AString getRowDescription(in long rowIndex);
+
+ /**
+ * Return a boolean value indicating whether the specified column is
+ * selected, i.e. all cells within the column are selected.
+ *
+ * @param columnIndex [in] the column index to determine if it's selected
+ */
+ boolean isColumnSelected(in long columnIndex);
+
+ /**
+ * Return a boolean value indicating whether the specified row is selected,
+ * i.e. all cells within the row are selected.
+ *
+ * @param rowIndex [in] the row index to determine whether it's selected
+ */
+ boolean isRowSelected(in long rowIndex);
+
+ /**
+ * Return a boolean value indicating whether the specified cell is selected.
+ *
+ * @param rowIndex [in] the row index of the cell
+ * @param columnIndex [in] the column index of the cell
+ */
+ boolean isCellSelected(in long rowIndex, in long columnIndex);
+
+ /**
+ * Return the total number of selected cells.
+ */
+ readonly attribute unsigned long selectedCellCount;
+
+ /**
+ * Return the total number of selected columns.
+ */
+ readonly attribute unsigned long selectedColumnCount;
+
+ /**
+ * Return the total number of selected rows.
+ */
+ readonly attribute unsigned long selectedRowCount;
+
+ /**
+ * Return an array of selected cells.
+ */
+ readonly attribute nsIArray selectedCells;
+
+ /**
+ * Return an array of cell indices currently selected.
+ *
+ * @return array of indexes of selected cells
+ */
+ Array<uint32_t> getSelectedCellIndices();
+
+ /**
+ * Return an array of column indices currently selected.
+ *
+ * @return array of indices of selected columns
+ */
+ Array<uint32_t> getSelectedColumnIndices();
+
+ /**
+ * Return an array of row indices currently selected.
+ *
+ * @return array of indices of selected rows
+ */
+ Array<uint32_t> getSelectedRowIndices();
+
+ /**
+ * Use heuristics to determine if table is most likely used for layout.
+ */
+ boolean isProbablyForLayout();
+};
+
+
+[scriptable, builtinclass, uuid(654e296d-fae6-452b-987d-746b20b9514b)]
+interface nsIAccessibleTableCell : nsISupports
+{
+ /**
+ * Return host table accessible.
+ */
+ readonly attribute nsIAccessibleTable table;
+
+ /**
+ * Return column index of this cell.
+ */
+ readonly attribute long columnIndex;
+
+ /**
+ * Return row index of this cell.
+ */
+ readonly attribute long rowIndex;
+
+ /**
+ * Return the number of columns occupied by this cell. The result differs
+ * from 1 if the specified cell spans multiple columns.
+ */
+ readonly attribute long columnExtent;
+
+ /**
+ * Return the number of rows occupied by this accessible cell. The result
+ * differs from 1 if the specified cell spans multiple rows.
+ */
+ readonly attribute long rowExtent;
+
+ /**
+ * Return an array of column header cells for this cell.
+ */
+ readonly attribute nsIArray columnHeaderCells;
+
+ /**
+ * Return an array of row header cells for this cell.
+ */
+ readonly attribute nsIArray rowHeaderCells;
+
+ /**
+ * Return a boolean value indicating whether this cell is selected.
+ */
+ boolean isSelected();
+};
diff --git a/accessible/interfaces/nsIAccessibleTableChangeEvent.idl b/accessible/interfaces/nsIAccessibleTableChangeEvent.idl
new file mode 100644
index 0000000000..f8804e6dae
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleTableChangeEvent.idl
@@ -0,0 +1,20 @@
+/* -*- 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 "nsIAccessibleEvent.idl"
+
+[scriptable, builtinclass, uuid(9fb3a8a4-d254-43d3-80a5-20e171d52b21)]
+interface nsIAccessibleTableChangeEvent: nsIAccessibleEvent
+{
+ /**
+ * Return the row or column index.
+ */
+ readonly attribute long rowOrColIndex;
+
+ /**
+ * Return the number of rows or cols
+ */
+ readonly attribute long RowsOrColsCount;
+};
diff --git a/accessible/interfaces/nsIAccessibleText.idl b/accessible/interfaces/nsIAccessibleText.idl
new file mode 100644
index 0000000000..5bd125c304
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleText.idl
@@ -0,0 +1,220 @@
+/* -*- 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 "nsISupports.idl"
+
+typedef long AccessibleTextBoundary;
+
+interface nsIAccessible;
+interface nsIArray;
+interface nsIPersistentProperties;
+interface nsIAccessibleTextRange;
+
+[scriptable, builtinclass, uuid(a4cc7576-45bb-44c5-b347-d9cb3ca4de9f)]
+interface nsIAccessibleText : nsISupports
+{
+ // In parameters for character offsets:
+ // -1 will be treated as the equal to the end of the text
+ // -2 will be treated as the caret position
+ const int32_t TEXT_OFFSET_END_OF_TEXT = -1;
+ const int32_t TEXT_OFFSET_CARET = -2;
+
+ const AccessibleTextBoundary BOUNDARY_CHAR = 0;
+ const AccessibleTextBoundary BOUNDARY_WORD_START = 1;
+ const AccessibleTextBoundary BOUNDARY_WORD_END = 2;
+ const AccessibleTextBoundary BOUNDARY_SENTENCE_START = 3; // don't use, deprecated
+ const AccessibleTextBoundary BOUNDARY_SENTENCE_END = 4; // don't use, deprecated
+ const AccessibleTextBoundary BOUNDARY_LINE_START = 5;
+ const AccessibleTextBoundary BOUNDARY_LINE_END = 6;
+ const AccessibleTextBoundary BOUNDARY_PARAGRAPH = 7;
+
+ /**
+ * The current current caret offset.
+ * If set < 0 then caret will be placed at the end of the text
+ */
+ attribute long caretOffset;
+
+ void getCaretRect(out long x, out long y, out long width, out long height);
+
+ readonly attribute long characterCount;
+ readonly attribute long selectionCount;
+
+ /**
+ * String methods may need to return multibyte-encoded strings,
+ * since some locales can't be encoded using 16-bit chars.
+ * So the methods below might return UTF-16 strings, or they could
+ * return "string" values which are UTF-8.
+ */
+ AString getText (in long startOffset, in long endOffset);
+
+ AString getTextAfterOffset (in long offset,
+ in AccessibleTextBoundary boundaryType,
+ out long startOffset,
+ out long endOffset);
+
+ AString getTextAtOffset (in long offset,
+ in AccessibleTextBoundary boundaryType,
+ out long startOffset,
+ out long endOffset);
+
+ AString getTextBeforeOffset (in long offset,
+ in AccessibleTextBoundary boundaryType,
+ out long startOffset,
+ out long endOffset);
+
+ /**
+ * It would be better to return an unsigned long here,
+ * to allow unicode chars > 16 bits
+ */
+ wchar getCharacterAtOffset (in long offset);
+
+ /**
+ * Get the accessible start/end offsets around the given offset,
+ * return the text attributes for this range of text.
+ *
+ * @param includeDefAttrs [in] points whether text attributes applied to
+ * the entire accessible should be included or not.
+ * @param offset [in] text offset
+ * @param rangeStartOffset [out] start offset of the range of text
+ * @param rangeEndOffset [out] end offset of the range of text
+ */
+ nsIPersistentProperties getTextAttributes(in boolean includeDefAttrs,
+ in long offset,
+ out long rangeStartOffset,
+ out long rangeEndOffset);
+
+ /**
+ * Return the text attributes that apply to the entire accessible.
+ */
+ readonly attribute nsIPersistentProperties defaultTextAttributes;
+
+ /**
+ * Returns the bounding box of the specified position.
+ *
+ * The virtual character after the last character of the represented text,
+ * i.e. the one at position length is a special case. It represents the
+ * current input position and will therefore typically be queried by AT more
+ * often than other positions. Because it does not represent an existing
+ * character its bounding box is defined in relation to preceding characters.
+ * It should be roughly equivalent to the bounding box of some character when
+ * inserted at the end of the text. Its height typically being the maximal
+ * height of all the characters in the text or the height of the preceding
+ * character, its width being at least one pixel so that the bounding box is
+ * not degenerate.
+ *
+ * @param offset - Index of the character for which to return its bounding
+ * box. The valid range is 0..length.
+ * @param x - X coordinate of the bounding box of the referenced character.
+ * @param y - Y coordinate of the bounding box of the referenced character.
+ * @param width - Width of the bounding box of the referenced character.
+ * @param height - Height of the bounding box of the referenced character.
+ * @param coordType - Specifies if the coordinates are relative to the screen
+ * or to the parent window (see constants declared in
+ * nsIAccessibleCoordinateType).
+ */
+ void getCharacterExtents (in long offset,
+ out long x,
+ out long y,
+ out long width,
+ out long height,
+ in unsigned long coordType);
+
+ void getRangeExtents (in long startOffset,
+ in long endOffset,
+ out long x,
+ out long y,
+ out long width,
+ out long height,
+ in unsigned long coordType);
+
+ /**
+ * Get the text offset at the given point, or return -1
+ * if no character exists at that point
+ *
+ * @param x - The position's x value for which to look up the index of the
+ * character that is rendered on to the display at that point.
+ * @param y - The position's y value for which to look up the index of the
+ * character that is rendered on to the display at that point.
+ * @param coordType - Screen coordinates or window coordinates (see constants
+ * declared in nsIAccessibleCoordinateType).
+ * @return offset - Index of the character under the given point or -1 if
+ * the point is invalid or there is no character under
+ * the point.
+ */
+ long getOffsetAtPoint (in long x, in long y,
+ in unsigned long coordType);
+
+ void getSelectionBounds (in long selectionNum,
+ out long startOffset,
+ out long endOffset);
+
+ /**
+ * Set the bounds for the given selection range.
+ * A reverse range where the start offset is larger than the end offset is
+ * acceptable. The caretOffset will be set to the endOffset argument.
+ */
+ void setSelectionBounds (in long selectionNum,
+ in long startOffset,
+ in long endOffset);
+
+ void addSelection (in long startOffset, in long endOffset);
+
+ void removeSelection (in long selectionNum);
+
+
+ /**
+ * Makes a specific part of string visible on screen.
+ *
+ * @param startIndex 0-based character offset
+ * @param endIndex 0-based character offset - the offset of the
+ * character just past the last character of the
+ * string
+ * @param scrollType defines how to scroll (see nsIAccessibleScrollType for
+ * available constants)
+ */
+ void scrollSubstringTo(in long startIndex, in long endIndex,
+ in unsigned long scrollType);
+
+ /**
+ * Moves the top left of a substring to a specified location.
+ *
+ * @param startIndex 0-based character offset
+ * @param endIndex 0-based character offset - the offset of the
+ * character just past the last character of
+ * the string
+ * @param coordinateType specifies the coordinates origin (for available
+ * constants refer to nsIAccessibleCoordinateType)
+ * @param x defines the x coordinate
+ * @param y defines the y coordinate
+ */
+ void scrollSubstringToPoint(in long startIndex, in long endIndex,
+ in unsigned long coordinateType,
+ in long x, in long y);
+
+ /**
+ * Return an array of disjoint ranges for selected text within the text control
+ * or otherwise the document this accessible belongs to.
+ */
+ readonly attribute nsIArray selectionRanges;
+};
+
+/*
+ Assumptions:
+
+ Using wstring (UCS2) instead of string encoded in UTF-8.
+ Multibyte encodings (or at least potentially multi-byte
+ encodings) would be preferred for the reasons cited above.
+
+ The following methods will throw an exception on failure
+ (since not every text component will allow every operation):
+ setSelectionBounds, addSelection, removeSelection, setCaretOffset.
+
+ we assume that all text components support the idea of
+ a caret offset, whether visible or "virtual". If this
+ isn't the case, caretOffset can be made readonly and
+ a setCaretOffset method provided which throws an exception
+ on failure (as with *selection methods above).
+*/
diff --git a/accessible/interfaces/nsIAccessibleTextChangeEvent.idl b/accessible/interfaces/nsIAccessibleTextChangeEvent.idl
new file mode 100644
index 0000000000..4aaadb8671
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleTextChangeEvent.idl
@@ -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/. */
+
+#include "nsIAccessibleEvent.idl"
+
+/**
+ * Fired when an accessible's text changes.
+ */
+[scriptable, builtinclass, uuid(1fcc0dfa-93e6-48f4-bbd4-f80eb1d9f2e6)]
+interface nsIAccessibleTextChangeEvent : nsIAccessibleEvent
+{
+ /**
+ * Returns offset of changed text in accessible.
+ */
+ readonly attribute long start;
+
+ /**
+ * Returns length of changed text.
+ */
+ readonly attribute unsigned long length;
+
+ /**
+ * Returns true if text was inserted, otherwise false.
+ */
+ readonly attribute boolean isInserted;
+
+ /**
+ * The inserted or removed text
+ */
+ readonly attribute AString modifiedText;
+};
diff --git a/accessible/interfaces/nsIAccessibleTextLeafRange.idl b/accessible/interfaces/nsIAccessibleTextLeafRange.idl
new file mode 100644
index 0000000000..00b9cc4cad
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleTextLeafRange.idl
@@ -0,0 +1,43 @@
+/* -*- 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 "nsISupports.idl"
+#include "nsIArray.idl"
+#include "nsIAccessibleText.idl"
+
+interface nsIAccessible;
+
+[scriptable, builtinclass, uuid(9181e777-8954-4f8f-8cee-32f9771e40d7)]
+interface nsIAccessibleTextLeafPoint : nsISupports
+{
+ attribute nsIAccessible accessible;
+
+ attribute long offset;
+
+ /**
+ * Find a boundary (word start, line start, etc.) in a specific direction.
+ * If no boundary is found, the start/end of the document is returned
+ * (depending on the direction).
+ *
+ * @param aBoundaryType [in] the boundary type to search for
+ * @param aDirection [in] search next or previous
+ * @param aFlags [in] optional flags for search
+ */
+ nsIAccessibleTextLeafPoint findBoundary(in AccessibleTextBoundary aBoundaryType,
+ in unsigned long aDirection,
+ in unsigned long aFlags);
+
+ const long DIRECTION_NEXT = 0x0;
+ const long DIRECTION_PREVIOUS = 0x1;
+
+ // Keep these flags up to date with the ones in TextLeafPoint.
+ const unsigned long BOUNDARY_FLAG_DEFAULT = 0x0;
+ // If current point is a matching boundary, return unchanged.
+ const unsigned long BOUNDARY_FLAG_INCLUDE_ORIGIN = 0x1;
+ // Do not search past end of editables.
+ const unsigned long BOUNDARY_FLAG_STOP_IN_EDITABLE = 0x2;
+ // Skip over list items in searches and don't consider them line or paragraph starts.
+ const unsigned long BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER = 0x4;
+};
diff --git a/accessible/interfaces/nsIAccessibleTextRange.idl b/accessible/interfaces/nsIAccessibleTextRange.idl
new file mode 100644
index 0000000000..5a3e6facaf
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleTextRange.idl
@@ -0,0 +1,57 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIAccessible;
+interface nsIAccessibleText;
+interface nsIArray;
+interface nsIVariant;
+
+/**
+ * A range representing a piece of text in the document.
+ */
+[scriptable, builtinclass, uuid(c4515623-55f9-4543-a3d5-c1e9afa588f4)]
+interface nsIAccessibleTextRange : nsISupports
+{
+ readonly attribute nsIAccessibleText startContainer;
+ readonly attribute long startOffset;
+ readonly attribute nsIAccessibleText endContainer;
+ readonly attribute long endOffset;
+
+ /**
+ * Return an accessible containing the whole range
+ */
+ readonly attribute nsIAccessible container;
+
+ /**
+ * Return true if this range has the same end points of the given range.
+ */
+ boolean compare(in nsIAccessibleTextRange aOtherRange);
+
+ /**
+ * The two endpoints of the range (starting and ending).
+ */
+ const unsigned long EndPoint_Start = 1;
+ const unsigned long EndPoint_End = 2;
+
+ /**
+ * Compare this and given ranges end points.
+ *
+ * @return -1/0/1 if this range end point is before/equal/after the given
+ * range end point.
+ */
+ long compareEndPoints(in unsigned long aEndPoint,
+ in nsIAccessibleTextRange aOtherRange,
+ in unsigned long aOtherRangeEndPoint);
+
+ /**
+ * Crops the range by the given accessible element.
+ */
+ boolean crop(in nsIAccessible aContainer);
+
+ const unsigned long AlignToTop = 0;
+ const unsigned long AlignToBottom = 1;
+};
diff --git a/accessible/interfaces/nsIAccessibleTextSelectionChangeEvent.idl b/accessible/interfaces/nsIAccessibleTextSelectionChangeEvent.idl
new file mode 100644
index 0000000000..6ca09192ef
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleTextSelectionChangeEvent.idl
@@ -0,0 +1,21 @@
+/* -*- 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 "nsIAccessibleEvent.idl"
+
+interface nsIArray;
+
+/**
+ * Fired when the caret changes position in text.
+ */
+[scriptable, builtinclass, uuid(011f98e2-2beb-4ec3-97a5-f154f624e112)]
+interface nsIAccessibleTextSelectionChangeEvent: nsIAccessibleEvent
+{
+ /**
+ * Return an array of disjoint ranges for selected text within the
+ * source of this event.
+ */
+ readonly attribute nsIArray selectionRanges;
+};
diff --git a/accessible/interfaces/nsIAccessibleTypes.idl b/accessible/interfaces/nsIAccessibleTypes.idl
new file mode 100644
index 0000000000..913636d2b0
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleTypes.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * These constants control the scrolling of an object or substring into a
+ * window. Note, keep them synchronized with IA2ScrollType.
+ */
+[scriptable, builtinclass, uuid(05cd38b1-94b3-4cdf-8371-3935a9611405)]
+interface nsIAccessibleScrollType : nsISupports
+{
+ /**
+ * Scroll the top left of the object or substring to the top left of the
+ * window (or as close as possible).
+ */
+ const unsigned long SCROLL_TYPE_TOP_LEFT =0x00;
+
+ /**
+ * Scroll the bottom right of the object or substring to the bottom right of
+ * the window (or as close as possible).
+ */
+ const unsigned long SCROLL_TYPE_BOTTOM_RIGHT = 0x01;
+
+ /**
+ * Scroll the top edge of the object or substring to the top edge of the
+ * window (or as close as possible).
+ */
+ const unsigned long SCROLL_TYPE_TOP_EDGE = 0x02;
+
+ /**
+ * Scroll the bottom edge of the object or substring to the bottom edge of
+ * the window (or as close as possible).
+ */
+ const unsigned long SCROLL_TYPE_BOTTOM_EDGE = 0x03;
+
+ /**
+ * Scroll the left edge of the object or substring to the left edge of the
+ * window (or as close as possible).
+ */
+ const unsigned long SCROLL_TYPE_LEFT_EDGE =0x04;
+
+ /**
+ * Scroll the right edge of the object or substring to the right edge of the
+ * window (or as close as possible).
+ */
+ const unsigned long SCROLL_TYPE_RIGHT_EDGE = 0x05;
+
+ /**
+ * Scroll an object the minimum amount necessary in order for the entire
+ * frame to be visible (if possible).
+ */
+ const unsigned long SCROLL_TYPE_ANYWHERE = 0x06;
+};
+
+
+/**
+ * These constants define which coordinate system a point is located in.
+ */
+[scriptable, builtinclass, uuid(c9fbdf10-619e-436f-bf4b-8566686f1577)]
+interface nsIAccessibleCoordinateType : nsISupports
+{
+ /**
+ * The coordinates are relative to the screen.
+ */
+ const unsigned long COORDTYPE_SCREEN_RELATIVE = 0x00;
+
+ /**
+ * The coordinates are relative to the window.
+ */
+ const unsigned long COORDTYPE_WINDOW_RELATIVE = 0x01;
+
+ /**
+ * The coordinates are relative to the upper left corner of the bounding box
+ * of the immediate parent.
+ */
+ const unsigned long COORDTYPE_PARENT_RELATIVE = 0x02;
+};
diff --git a/accessible/interfaces/nsIAccessibleValue.idl b/accessible/interfaces/nsIAccessibleValue.idl
new file mode 100644
index 0000000000..886fcb8fed
--- /dev/null
+++ b/accessible/interfaces/nsIAccessibleValue.idl
@@ -0,0 +1,35 @@
+/* -*- 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 "nsISupports.idl"
+
+[scriptable, builtinclass, uuid(42a1e1dc-58cf-419d-bff0-ed3314c70016)]
+interface nsIAccessibleValue : nsISupports
+{
+ readonly attribute double maximumValue;
+ readonly attribute double minimumValue;
+ attribute double currentValue;
+ readonly attribute double minimumIncrement;
+};
+
+/*
+ Assumptions:
+
+ The attribute currentValue will throw an exception
+ if it cannot be set i.e. if the value is not a
+ member of the interval.
+ This may not be the 'desired' behaviour given gObject
+ equivalent. Thus it could be changed to be:
+
+ readonly attribute double currentValue;
+ boolean setCurrentValue (double long value);
+
+ GValue can represent many basic types.
+ Since this interface is designed to represent
+ an interval and a member of double should
+ cover the cases of char int and float.
+
+*/
diff --git a/accessible/ipc/DocAccessibleChild.cpp b/accessible/ipc/DocAccessibleChild.cpp
new file mode 100644
index 0000000000..5f9d1a0a63
--- /dev/null
+++ b/accessible/ipc/DocAccessibleChild.cpp
@@ -0,0 +1,431 @@
+/* -*- 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 "chrome/common/ipc_channel.h"
+#include "mozilla/a11y/DocAccessibleChild.h"
+#include "mozilla/a11y/CacheConstants.h"
+#include "mozilla/a11y/FocusManager.h"
+#include "mozilla/ipc/ProcessChild.h"
+#include "nsAccessibilityService.h"
+
+#include "LocalAccessible-inl.h"
+#ifdef A11Y_LOG
+# include "Logging.h"
+#endif
+#include "TextLeafRange.h"
+
+namespace mozilla {
+namespace a11y {
+
+/* static */
+void DocAccessibleChild::FlattenTree(LocalAccessible* aRoot,
+ nsTArray<LocalAccessible*>& aTree) {
+ MOZ_ASSERT(!aRoot->IsDoc(), "documents shouldn't be serialized");
+
+ aTree.AppendElement(aRoot);
+ // OuterDocAccessibles are special because we don't want to serialize the
+ // child doc here, we'll call PDocAccessibleConstructor in
+ // NotificationController.
+ uint32_t childCount = aRoot->IsOuterDoc() ? 0 : aRoot->ChildCount();
+
+ for (uint32_t i = 0; i < childCount; i++) {
+ FlattenTree(aRoot->LocalChildAt(i), aTree);
+ }
+}
+
+/* static */
+AccessibleData DocAccessibleChild::SerializeAcc(LocalAccessible* aAcc) {
+ uint32_t genericTypes = aAcc->mGenericTypes;
+ if (aAcc->ARIAHasNumericValue()) {
+ // XXX: We need to do this because this requires a state check.
+ genericTypes |= eNumericValue;
+ }
+ if (aAcc->IsTextLeaf() || aAcc->IsImage()) {
+ // Ideally, we'd set eActionable for any Accessible with an ancedstor
+ // action. However, that requires an ancestor walk which is too expensive
+ // here. eActionable is only used by ATK. For now, we only expose ancestor
+ // actions on text leaf and image Accessibles. This means that we don't
+ // support "click ancestor" for ATK.
+ if (aAcc->ActionCount()) {
+ genericTypes |= eActionable;
+ }
+ } else if (aAcc->HasPrimaryAction()) {
+ genericTypes |= eActionable;
+ }
+
+ RefPtr<AccAttributes> fields;
+ // Even though we send moves as a hide and a show, we don't want to
+ // push the cache again for moves.
+ if (!aAcc->Document()->IsAccessibleBeingMoved(aAcc)) {
+ fields =
+ aAcc->BundleFieldsForCache(CacheDomain::All, CacheUpdateType::Initial);
+ if (fields->Count() == 0) {
+ fields = nullptr;
+ }
+ }
+
+ return AccessibleData(aAcc->ID(), aAcc->Role(), aAcc->LocalParent()->ID(),
+ static_cast<int32_t>(aAcc->IndexInParent()),
+ static_cast<AccType>(aAcc->mType),
+ static_cast<AccGenericType>(genericTypes),
+ aAcc->mRoleMapEntryIndex, fields);
+}
+
+void DocAccessibleChild::InsertIntoIpcTree(LocalAccessible* aChild,
+ bool aSuppressShowEvent) {
+ nsTArray<LocalAccessible*> shownTree;
+ FlattenTree(aChild, shownTree);
+ uint32_t totalAccs = shownTree.Length();
+ // Exceeding the IPDL maximum message size will cause a crash. Try to avoid
+ // this by only including kMaxAccsPerMessage Accessibels in a single IPDL
+ // call. If there are Accessibles beyond this, they will be split across
+ // multiple calls.
+ constexpr uint32_t kMaxAccsPerMessage =
+ IPC::Channel::kMaximumMessageSize / (2 * 1024);
+ nsTArray<AccessibleData> data(std::min(kMaxAccsPerMessage, totalAccs));
+ for (LocalAccessible* child : shownTree) {
+ if (data.Length() == kMaxAccsPerMessage) {
+ if (ipc::ProcessChild::ExpectingShutdown()) {
+ return;
+ }
+ SendShowEvent(data, aSuppressShowEvent, false, false);
+ data.ClearAndRetainStorage();
+ }
+ data.AppendElement(SerializeAcc(child));
+ }
+ if (ipc::ProcessChild::ExpectingShutdown()) {
+ return;
+ }
+ if (!data.IsEmpty()) {
+ SendShowEvent(data, aSuppressShowEvent, true, false);
+ }
+}
+
+void DocAccessibleChild::ShowEvent(AccShowEvent* aShowEvent) {
+ LocalAccessible* child = aShowEvent->GetAccessible();
+ InsertIntoIpcTree(child, false);
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvTakeFocus(const uint64_t& aID) {
+ LocalAccessible* acc = IdToAccessible(aID);
+ if (acc) {
+ acc->TakeFocus();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvScrollTo(
+ const uint64_t& aID, const uint32_t& aScrollType) {
+ LocalAccessible* acc = IdToAccessible(aID);
+ if (acc) {
+ RefPtr<PresShell> presShell = acc->Document()->PresShellPtr();
+ nsCOMPtr<nsIContent> content = acc->GetContent();
+ nsCoreUtils::ScrollTo(presShell, content, aScrollType);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvTakeSelection(
+ const uint64_t& aID) {
+ LocalAccessible* acc = IdToAccessible(aID);
+ if (acc) {
+ acc->TakeSelection();
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvSetSelected(
+ const uint64_t& aID, const bool& aSelect) {
+ LocalAccessible* acc = IdToAccessible(aID);
+ if (acc) {
+ acc->SetSelected(aSelect);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvVerifyCache(
+ const uint64_t& aID, const uint64_t& aCacheDomain, AccAttributes* aFields) {
+#ifdef A11Y_LOG
+ LocalAccessible* acc = IdToAccessible(aID);
+ if (!acc) {
+ return IPC_OK();
+ }
+
+ RefPtr<AccAttributes> localFields =
+ acc->BundleFieldsForCache(aCacheDomain, CacheUpdateType::Update);
+ bool mismatches = false;
+
+ for (auto prop : *localFields) {
+ if (prop.Value<DeleteEntry>()) {
+ if (aFields->HasAttribute(prop.Name())) {
+ if (!mismatches) {
+ logging::MsgBegin("Mismatch!", "Local and remote values differ");
+ logging::AccessibleInfo("", acc);
+ mismatches = true;
+ }
+ nsAutoCString propName;
+ prop.Name()->ToUTF8String(propName);
+ nsAutoString val;
+ aFields->GetAttribute(prop.Name(), val);
+ logging::MsgEntry(
+ "Remote value for %s should be empty, but instead it is '%s'",
+ propName.get(), NS_ConvertUTF16toUTF8(val).get());
+ }
+ continue;
+ }
+
+ nsAutoString localVal;
+ prop.ValueAsString(localVal);
+ nsAutoString remoteVal;
+ aFields->GetAttribute(prop.Name(), remoteVal);
+ if (!localVal.Equals(remoteVal)) {
+ if (!mismatches) {
+ logging::MsgBegin("Mismatch!", "Local and remote values differ");
+ logging::AccessibleInfo("", acc);
+ mismatches = true;
+ }
+ nsAutoCString propName;
+ prop.Name()->ToUTF8String(propName);
+ logging::MsgEntry("Fields differ: %s '%s' != '%s'", propName.get(),
+ NS_ConvertUTF16toUTF8(remoteVal).get(),
+ NS_ConvertUTF16toUTF8(localVal).get());
+ }
+ }
+ if (mismatches) {
+ logging::MsgEnd();
+ }
+#endif // A11Y_LOG
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvDoActionAsync(
+ const uint64_t& aID, const uint8_t& aIndex) {
+ if (LocalAccessible* acc = IdToAccessible(aID)) {
+ Unused << acc->DoAction(aIndex);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvSetCaretOffset(
+ const uint64_t& aID, const int32_t& aOffset) {
+ HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
+ if (acc && acc->IsTextRole() && acc->IsValidOffset(aOffset)) {
+ acc->SetCaretOffset(aOffset);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvSetTextSelection(
+ const uint64_t& aStartID, const int32_t& aStartOffset,
+ const uint64_t& aEndID, const int32_t& aEndOffset,
+ const int32_t& aSelectionNum) {
+ TextLeafRange range(TextLeafPoint(IdToAccessible(aStartID), aStartOffset),
+ TextLeafPoint(IdToAccessible(aEndID), aEndOffset));
+ if (range) {
+ range.SetSelection(aSelectionNum);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvScrollTextLeafRangeIntoView(
+ const uint64_t& aStartID, const int32_t& aStartOffset,
+ const uint64_t& aEndID, const int32_t& aEndOffset,
+ const uint32_t& aScrollType) {
+ TextLeafRange range(TextLeafPoint(IdToAccessible(aStartID), aStartOffset),
+ TextLeafPoint(IdToAccessible(aEndID), aEndOffset));
+ if (range) {
+ range.ScrollIntoView(aScrollType);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvRemoveTextSelection(
+ const uint64_t& aID, const int32_t& aSelectionNum) {
+ HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
+ if (acc && acc->IsTextRole()) {
+ acc->RemoveFromSelection(aSelectionNum);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvSetCurValue(
+ const uint64_t& aID, const double& aValue) {
+ LocalAccessible* acc = IdToAccessible(aID);
+ if (acc) {
+ acc->SetCurValue(aValue);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvReplaceText(
+ const uint64_t& aID, const nsAString& aText) {
+ HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
+ if (acc && acc->IsTextRole()) {
+ acc->ReplaceText(aText);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvInsertText(
+ const uint64_t& aID, const nsAString& aText, const int32_t& aPosition) {
+ HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
+ if (acc && acc->IsTextRole()) {
+ acc->InsertText(aText, aPosition);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvCopyText(
+ const uint64_t& aID, const int32_t& aStartPos, const int32_t& aEndPos) {
+ HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
+ if (acc && acc->IsTextRole()) {
+ acc->CopyText(aStartPos, aEndPos);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvCutText(
+ const uint64_t& aID, const int32_t& aStartPos, const int32_t& aEndPos) {
+ HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
+ if (acc && acc->IsTextRole()) {
+ acc->CutText(aStartPos, aEndPos);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvDeleteText(
+ const uint64_t& aID, const int32_t& aStartPos, const int32_t& aEndPos) {
+ HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
+ if (acc && acc->IsTextRole()) {
+ acc->DeleteText(aStartPos, aEndPos);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvPasteText(
+ const uint64_t& aID, const int32_t& aPosition) {
+ RefPtr<HyperTextAccessible> acc = IdToHyperTextAccessible(aID);
+ if (acc && acc->IsTextRole()) {
+ acc->PasteText(aPosition);
+ }
+
+ return IPC_OK();
+}
+
+ipc::IPCResult DocAccessibleChild::RecvRestoreFocus() {
+ if (FocusManager* focusMgr = FocusMgr()) {
+ focusMgr->ForceFocusEvent();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvScrollToPoint(
+ const uint64_t& aID, const uint32_t& aScrollType, const int32_t& aX,
+ const int32_t& aY) {
+ LocalAccessible* acc = IdToAccessible(aID);
+ if (acc) {
+ acc->ScrollToPoint(aScrollType, aX, aY);
+ }
+
+ return IPC_OK();
+}
+
+LayoutDeviceIntRect DocAccessibleChild::GetCaretRectFor(const uint64_t& aID) {
+#if defined(XP_WIN)
+ LocalAccessible* target;
+
+ if (aID) {
+ target = reinterpret_cast<LocalAccessible*>(aID);
+ } else {
+ target = mDoc;
+ }
+
+ MOZ_ASSERT(target);
+
+ HyperTextAccessible* text = target->AsHyperText();
+ if (!text) {
+ return LayoutDeviceIntRect();
+ }
+
+ nsIWidget* widget = nullptr;
+ return text->GetCaretRect(&widget);
+#else
+ // The caret rect is only used on Windows, so just return an empty rect
+ // on other platforms.
+ return LayoutDeviceIntRect();
+#endif // defined(XP_WIN)
+}
+
+bool DocAccessibleChild::SendFocusEvent(const uint64_t& aID) {
+ return PDocAccessibleChild::SendFocusEvent(aID, GetCaretRectFor(aID));
+}
+
+bool DocAccessibleChild::SendCaretMoveEvent(const uint64_t& aID,
+ const int32_t& aOffset,
+ const bool& aIsSelectionCollapsed,
+ const bool& aIsAtEndOfLine,
+ const int32_t& aGranularity,
+ bool aFromUser) {
+ return PDocAccessibleChild::SendCaretMoveEvent(
+ aID, GetCaretRectFor(aID), aOffset, aIsSelectionCollapsed, aIsAtEndOfLine,
+ aGranularity, aFromUser);
+}
+
+#if !defined(XP_WIN)
+mozilla::ipc::IPCResult DocAccessibleChild::RecvAnnounce(
+ const uint64_t& aID, const nsAString& aAnnouncement,
+ const uint16_t& aPriority) {
+ LocalAccessible* acc = IdToAccessible(aID);
+ if (acc) {
+ acc->Announce(aAnnouncement, aPriority);
+ }
+
+ return IPC_OK();
+}
+#endif // !defined(XP_WIN)
+
+mozilla::ipc::IPCResult DocAccessibleChild::RecvScrollSubstringToPoint(
+ const uint64_t& aID, const int32_t& aStartOffset, const int32_t& aEndOffset,
+ const uint32_t& aCoordinateType, const int32_t& aX, const int32_t& aY) {
+ HyperTextAccessible* acc = IdToHyperTextAccessible(aID);
+ if (acc) {
+ acc->ScrollSubstringToPoint(aStartOffset, aEndOffset, aCoordinateType, aX,
+ aY);
+ }
+
+ return IPC_OK();
+}
+
+LocalAccessible* DocAccessibleChild::IdToAccessible(const uint64_t& aID) const {
+ if (!aID) return mDoc;
+
+ if (!mDoc) return nullptr;
+
+ return mDoc->GetAccessibleByUniqueID(reinterpret_cast<void*>(aID));
+}
+
+HyperTextAccessible* DocAccessibleChild::IdToHyperTextAccessible(
+ const uint64_t& aID) const {
+ LocalAccessible* acc = IdToAccessible(aID);
+ return acc && acc->IsHyperText() ? acc->AsHyperText() : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/ipc/DocAccessibleChild.h b/accessible/ipc/DocAccessibleChild.h
new file mode 100644
index 0000000000..0a6164cce8
--- /dev/null
+++ b/accessible/ipc/DocAccessibleChild.h
@@ -0,0 +1,176 @@
+/* -*- 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_DocAccessibleChild_h
+#define mozilla_a11y_DocAccessibleChild_h
+
+#include "mozilla/a11y/DocAccessible.h"
+#include "mozilla/a11y/PDocAccessibleChild.h"
+#include "mozilla/Unused.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+namespace a11y {
+
+class LocalAccessible;
+class AccShowEvent;
+
+/**
+ * These objects handle content side communication for an accessible document,
+ * and their lifetime is the same as the document they represent.
+ */
+class DocAccessibleChild : public PDocAccessibleChild {
+ public:
+ DocAccessibleChild(DocAccessible* aDoc, IProtocol* aManager) : mDoc(aDoc) {
+ MOZ_COUNT_CTOR(DocAccessibleChild);
+ SetManager(aManager);
+ }
+
+ ~DocAccessibleChild() {
+ // Shutdown() should have been called, but maybe it isn't if the process is
+ // killed?
+ MOZ_ASSERT(!mDoc);
+ if (mDoc) {
+ mDoc->SetIPCDoc(nullptr);
+ }
+
+ MOZ_COUNT_DTOR(DocAccessibleChild);
+ }
+
+ virtual void Shutdown() {
+ DetachDocument();
+ SendShutdown();
+ }
+
+ /**
+ * Serializes a shown tree and sends it to the chrome process.
+ */
+ void InsertIntoIpcTree(LocalAccessible* aChild, bool aSuppressShowEvent);
+ void ShowEvent(AccShowEvent* aShowEvent);
+
+ virtual void ActorDestroy(ActorDestroyReason) override {
+ if (!mDoc) {
+ return;
+ }
+
+ mDoc->SetIPCDoc(nullptr);
+ mDoc = nullptr;
+ }
+
+ virtual mozilla::ipc::IPCResult RecvTakeFocus(const uint64_t& aID) override;
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual mozilla::ipc::IPCResult RecvScrollTo(
+ const uint64_t& aID, const uint32_t& aScrollType) override;
+
+ virtual mozilla::ipc::IPCResult RecvTakeSelection(
+ const uint64_t& aID) override;
+ virtual mozilla::ipc::IPCResult RecvSetSelected(const uint64_t& aID,
+ const bool& aSelect) override;
+
+ virtual mozilla::ipc::IPCResult RecvVerifyCache(
+ const uint64_t& aID, const uint64_t& aCacheDomain,
+ AccAttributes* aFields) override;
+
+ virtual mozilla::ipc::IPCResult RecvDoActionAsync(
+ const uint64_t& aID, const uint8_t& aIndex) override;
+
+ virtual mozilla::ipc::IPCResult RecvSetCaretOffset(
+ const uint64_t& aID, const int32_t& aOffset) override;
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual mozilla::ipc::IPCResult RecvSetTextSelection(
+ const uint64_t& aStartID, const int32_t& aStartOffset,
+ const uint64_t& aEndID, const int32_t& aEndOffset,
+ const int32_t& aSelectionNum) override;
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual mozilla::ipc::IPCResult RecvScrollTextLeafRangeIntoView(
+ const uint64_t& aStartID, const int32_t& aStartOffset,
+ const uint64_t& aEndID, const int32_t& aEndOffset,
+ const uint32_t& aScrollType) override;
+
+ virtual mozilla::ipc::IPCResult RecvRemoveTextSelection(
+ const uint64_t& aID, const int32_t& aSelectionNum) override;
+
+ virtual mozilla::ipc::IPCResult RecvSetCurValue(
+ const uint64_t& aID, const double& aValue) override;
+
+ virtual mozilla::ipc::IPCResult RecvReplaceText(
+ const uint64_t& aID, const nsAString& aText) override;
+
+ virtual mozilla::ipc::IPCResult RecvInsertText(
+ const uint64_t& aID, const nsAString& aText,
+ const int32_t& aPosition) override;
+
+ virtual mozilla::ipc::IPCResult RecvCopyText(const uint64_t& aID,
+ const int32_t& aStartPos,
+ const int32_t& aEndPos) override;
+
+ virtual mozilla::ipc::IPCResult RecvCutText(const uint64_t& aID,
+ const int32_t& aStartPos,
+ const int32_t& aEndPos) override;
+
+ virtual mozilla::ipc::IPCResult RecvDeleteText(
+ const uint64_t& aID, const int32_t& aStartPos,
+ const int32_t& aEndPos) override;
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual mozilla::ipc::IPCResult RecvPasteText(
+ const uint64_t& aID, const int32_t& aPosition) override;
+
+ virtual mozilla::ipc::IPCResult RecvRestoreFocus() override;
+
+ virtual mozilla::ipc::IPCResult RecvScrollToPoint(const uint64_t& aID,
+ const uint32_t& aScrollType,
+ const int32_t& aX,
+ const int32_t& aY) override;
+
+ bool SendCaretMoveEvent(const uint64_t& aID, const int32_t& aOffset,
+ const bool& aIsSelectionCollapsed,
+ const bool& aIsAtEndOfLine,
+ const int32_t& aGranularity, bool aFromUser);
+ bool SendFocusEvent(const uint64_t& aID);
+
+#if !defined(XP_WIN)
+ virtual mozilla::ipc::IPCResult RecvAnnounce(
+ const uint64_t& aID, const nsAString& aAnnouncement,
+ const uint16_t& aPriority) override;
+#endif // !defined(XP_WIN)
+
+ virtual mozilla::ipc::IPCResult RecvScrollSubstringToPoint(
+ const uint64_t& aID, const int32_t& aStartOffset,
+ const int32_t& aEndOffset, const uint32_t& aCoordinateType,
+ const int32_t& aX, const int32_t& aY) override;
+
+ private:
+ LayoutDeviceIntRect GetCaretRectFor(const uint64_t& aID);
+
+ protected:
+ static void FlattenTree(LocalAccessible* aRoot,
+ nsTArray<LocalAccessible*>& aTree);
+
+ static AccessibleData SerializeAcc(LocalAccessible* aAcc);
+
+ void DetachDocument() {
+ if (mDoc) {
+ mDoc->SetIPCDoc(nullptr);
+ mDoc = nullptr;
+ }
+ }
+
+ LocalAccessible* IdToAccessible(const uint64_t& aID) const;
+ HyperTextAccessible* IdToHyperTextAccessible(const uint64_t& aID) const;
+
+ DocAccessible* mDoc;
+
+ friend void DocAccessible::DoInitialUpdate();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_DocAccessibleChild_h
diff --git a/accessible/ipc/DocAccessibleParent.cpp b/accessible/ipc/DocAccessibleParent.cpp
new file mode 100644
index 0000000000..1c96bd38cd
--- /dev/null
+++ b/accessible/ipc/DocAccessibleParent.cpp
@@ -0,0 +1,1266 @@
+/* -*- 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 "CachedTableAccessible.h"
+#include "DocAccessibleParent.h"
+#include "mozilla/a11y/Platform.h"
+#include "mozilla/Components.h" // for mozilla::components
+#include "mozilla/dom/BrowserBridgeParent.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "nsAccessibilityService.h"
+#include "xpcAccessibleDocument.h"
+#include "xpcAccEvents.h"
+#include "nsAccUtils.h"
+#include "nsIIOService.h"
+#include "TextRange.h"
+#include "Relation.h"
+#include "RootAccessible.h"
+
+#if defined(XP_WIN)
+# include "Compatibility.h"
+# include "nsWinUtils.h"
+#endif
+
+#if defined(ANDROID)
+# define ACQUIRE_ANDROID_LOCK \
+ MonitorAutoLock mal(nsAccessibilityService::GetAndroidMonitor());
+#else
+# define ACQUIRE_ANDROID_LOCK \
+ do { \
+ } while (0);
+#endif
+
+namespace mozilla {
+
+namespace a11y {
+uint64_t DocAccessibleParent::sMaxDocID = 0;
+
+DocAccessibleParent::DocAccessibleParent()
+ : RemoteAccessible(this),
+ mParentDoc(kNoParentDoc),
+#if defined(XP_WIN)
+ mEmulatedWindowHandle(nullptr),
+#endif // defined(XP_WIN)
+ mTopLevel(false),
+ mTopLevelInContentProcess(false),
+ mShutdown(false),
+ mFocus(0),
+ mCaretId(0),
+ mCaretOffset(-1),
+ mIsCaretAtEndOfLine(false) {
+ sMaxDocID++;
+ mActorID = sMaxDocID;
+ MOZ_ASSERT(!LiveDocs().Get(mActorID));
+ LiveDocs().InsertOrUpdate(mActorID, this);
+}
+
+DocAccessibleParent::~DocAccessibleParent() {
+ UnregisterWeakMemoryReporter(this);
+ LiveDocs().Remove(mActorID);
+ MOZ_ASSERT(mChildDocs.Length() == 0);
+ MOZ_ASSERT(!ParentDoc());
+}
+
+already_AddRefed<DocAccessibleParent> DocAccessibleParent::New() {
+ RefPtr<DocAccessibleParent> dap(new DocAccessibleParent());
+ // We need to do this with a non-zero reference count. The easiest way is to
+ // do it in this static method and hide the constructor.
+ RegisterWeakMemoryReporter(dap);
+ return dap.forget();
+}
+
+void DocAccessibleParent::SetBrowsingContext(
+ dom::CanonicalBrowsingContext* aBrowsingContext) {
+ mBrowsingContext = aBrowsingContext;
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvShowEvent(
+ nsTArray<AccessibleData>&& aNewTree, const bool& aEventSuppressed,
+ const bool& aComplete, const bool& aFromUser) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) return IPC_OK();
+
+ MOZ_ASSERT(CheckDocTree());
+
+ if (aNewTree.IsEmpty()) {
+ return IPC_FAIL(this, "No children being added");
+ }
+
+ RemoteAccessible* root = nullptr;
+ RemoteAccessible* rootParent = nullptr;
+ RemoteAccessible* lastParent = this;
+ uint64_t lastParentID = 0;
+ for (const auto& accData : aNewTree) {
+ RemoteAccessible* parent = accData.ParentID() == lastParentID
+ ? lastParent
+ : GetAccessible(accData.ParentID());
+ // XXX This should really never happen, but sometimes we fail to fire the
+ // required show events.
+ if (!parent) {
+ NS_ERROR("adding child to unknown accessible");
+#ifdef DEBUG
+ return IPC_FAIL(this, "unknown parent accessible");
+#else
+ return IPC_OK();
+#endif
+ }
+
+ uint32_t childIdx = accData.IndexInParent();
+ if (childIdx > parent->ChildCount()) {
+ NS_ERROR("invalid index to add child at");
+#ifdef DEBUG
+ return IPC_FAIL(this, "invalid index");
+#else
+ return IPC_OK();
+#endif
+ }
+
+ RemoteAccessible* child = CreateAcc(accData);
+ if (!child) {
+ // This shouldn't happen.
+ return IPC_FAIL(this, "failed to add children");
+ }
+ if (!root && !mPendingShowChild) {
+ // This is the first Accessible, which is the root of the shown subtree.
+ root = child;
+ rootParent = parent;
+ }
+ // If this show event has been split across multiple messages and this is
+ // not the last message, don't attach the shown root to the tree yet.
+ // Otherwise, clients might crawl the incomplete subtree and they won't get
+ // mutation events for the remaining pieces.
+ if (aComplete || root != child) {
+ AttachChild(parent, childIdx, child);
+ }
+ }
+
+ MOZ_ASSERT(CheckDocTree());
+
+ if (!aComplete && !mPendingShowChild) {
+ // This is the first message for a show event split across multiple
+ // messages. Save the show target for subsequent messages and return.
+ const auto& accData = aNewTree[0];
+ mPendingShowChild = accData.ID();
+ mPendingShowParent = accData.ParentID();
+ mPendingShowIndex = accData.IndexInParent();
+ return IPC_OK();
+ }
+ if (!aComplete) {
+ // This show event has been split into multiple messages, but this is
+ // neither the first nor the last message. There's nothing more to do here.
+ return IPC_OK();
+ }
+ MOZ_ASSERT(aComplete);
+ if (mPendingShowChild) {
+ // This is the last message for a show event split across multiple
+ // messages. Retrieve the saved show target, attach it to the tree and fire
+ // an event if appropriate.
+ rootParent = GetAccessible(mPendingShowParent);
+ MOZ_ASSERT(rootParent);
+ root = GetAccessible(mPendingShowChild);
+ MOZ_ASSERT(root);
+ AttachChild(rootParent, mPendingShowIndex, root);
+ mPendingShowChild = 0;
+ mPendingShowParent = 0;
+ mPendingShowIndex = 0;
+ }
+
+ // Just update, no events.
+ if (aEventSuppressed) {
+ return IPC_OK();
+ }
+
+ PlatformShowHideEvent(root, rootParent, true, aFromUser);
+
+ if (nsCOMPtr<nsIObserverService> obsService =
+ services::GetObserverService()) {
+ obsService->NotifyObservers(nullptr, NS_ACCESSIBLE_CACHE_TOPIC, nullptr);
+ }
+
+ if (!nsCoreUtils::AccEventObserversExist()) {
+ return IPC_OK();
+ }
+
+ uint32_t type = nsIAccessibleEvent::EVENT_SHOW;
+ xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(root);
+ xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this);
+ nsINode* node = nullptr;
+ RefPtr<xpcAccEvent> event =
+ new xpcAccEvent(type, xpcAcc, doc, node, aFromUser);
+ nsCoreUtils::DispatchAccEvent(std::move(event));
+
+ return IPC_OK();
+}
+
+RemoteAccessible* DocAccessibleParent::CreateAcc(
+ const AccessibleData& aAccData) {
+ RemoteAccessible* newProxy;
+ if ((newProxy = GetAccessible(aAccData.ID()))) {
+ // This is a move. Reuse the Accessible; don't destroy it.
+ MOZ_ASSERT(!newProxy->RemoteParent());
+ return newProxy;
+ }
+
+ if (!aria::IsRoleMapIndexValid(aAccData.RoleMapEntryIndex())) {
+ MOZ_ASSERT_UNREACHABLE("Invalid role map entry index");
+ return nullptr;
+ }
+
+ newProxy = new RemoteAccessible(aAccData.ID(), this, aAccData.Role(),
+ aAccData.Type(), aAccData.GenericTypes(),
+ aAccData.RoleMapEntryIndex());
+ mAccessibles.PutEntry(aAccData.ID())->mProxy = newProxy;
+
+ if (RefPtr<AccAttributes> fields = aAccData.CacheFields()) {
+ newProxy->ApplyCache(CacheUpdateType::Initial, fields);
+ }
+
+ return newProxy;
+}
+
+void DocAccessibleParent::AttachChild(RemoteAccessible* aParent,
+ uint32_t aIndex,
+ RemoteAccessible* aChild) {
+ aParent->AddChildAt(aIndex, aChild);
+ aChild->SetParent(aParent);
+ // ProxyCreated might have already been called if aChild is being moved.
+ if (!aChild->GetWrapper()) {
+ ProxyCreated(aChild);
+ }
+ if (aChild->IsTableCell()) {
+ CachedTableAccessible::Invalidate(aChild);
+ }
+ if (aChild->IsOuterDoc()) {
+ // We can only do this after ProxyCreated is called because it will fire an
+ // event on aChild.
+ mPendingOOPChildDocs.RemoveIf([&](dom::BrowserBridgeParent* bridge) {
+ MOZ_ASSERT(bridge->GetBrowserParent(),
+ "Pending BrowserBridgeParent should be alive");
+ if (bridge->GetEmbedderAccessibleId() != aChild->ID()) {
+ return false;
+ }
+ MOZ_ASSERT(bridge->GetEmbedderAccessibleDoc() == this);
+ if (DocAccessibleParent* childDoc = bridge->GetDocAccessibleParent()) {
+ AddChildDoc(childDoc, aChild->ID(), false);
+ }
+ return true;
+ });
+ }
+}
+
+void DocAccessibleParent::ShutdownOrPrepareForMove(RemoteAccessible* aAcc) {
+ // Children might be removed or moved. Handle them the same way. We do this
+ // before checking the moving IDs set in order to ensure that we handle moved
+ // descendants properly. Avoid descending into the children of outer documents
+ // for moves since they are added and removed differently to normal children.
+ if (!aAcc->IsOuterDoc()) {
+ // Even if some children are kept, those will be re-attached when we handle
+ // the show event. For now, clear all of them by moving them to a temporary.
+ auto children{std::move(aAcc->mChildren)};
+ for (RemoteAccessible* child : children) {
+ ShutdownOrPrepareForMove(child);
+ }
+ }
+
+ const uint64_t id = aAcc->ID();
+ if (!mMovingIDs.Contains(id)) {
+ // This Accessible is being removed.
+ aAcc->Shutdown();
+ return;
+ }
+ // This is a move. Moves are sent as a hide and then a show, but for a move,
+ // we want to keep the Accessible alive for reuse later.
+ if (aAcc->IsTable() || aAcc->IsTableCell()) {
+ // For table cells, it's important that we do this before the parent is
+ // cleared because CachedTableAccessible::Invalidate needs the ancestry.
+ CachedTableAccessible::Invalidate(aAcc);
+ }
+ if (aAcc->IsHyperText()) {
+ aAcc->InvalidateCachedHyperTextOffsets();
+ }
+ aAcc->SetParent(nullptr);
+ mMovingIDs.EnsureRemoved(id);
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvHideEvent(
+ const uint64_t& aRootID, const bool& aFromUser) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) return IPC_OK();
+
+ MOZ_ASSERT(CheckDocTree());
+
+ // We shouldn't actually need this because mAccessibles shouldn't have an
+ // entry for the document itself, but it doesn't hurt to be explicit.
+ if (!aRootID) {
+ return IPC_FAIL(this, "Trying to hide entire document?");
+ }
+
+ ProxyEntry* rootEntry = mAccessibles.GetEntry(aRootID);
+ if (!rootEntry) {
+ NS_ERROR("invalid root being removed!");
+ return IPC_OK();
+ }
+
+ RemoteAccessible* root = rootEntry->mProxy;
+ if (!root) {
+ NS_ERROR("invalid root being removed!");
+ return IPC_OK();
+ }
+
+ RemoteAccessible* parent = root->RemoteParent();
+ PlatformShowHideEvent(root, parent, false, aFromUser);
+
+ RefPtr<xpcAccHideEvent> event = nullptr;
+ if (nsCoreUtils::AccEventObserversExist()) {
+ uint32_t type = nsIAccessibleEvent::EVENT_HIDE;
+ xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(root);
+ xpcAccessibleGeneric* xpcParent = GetXPCAccessible(parent);
+ RemoteAccessible* next = root->RemoteNextSibling();
+ xpcAccessibleGeneric* xpcNext = next ? GetXPCAccessible(next) : nullptr;
+ RemoteAccessible* prev = root->RemotePrevSibling();
+ xpcAccessibleGeneric* xpcPrev = prev ? GetXPCAccessible(prev) : nullptr;
+ xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this);
+ nsINode* node = nullptr;
+ event = new xpcAccHideEvent(type, xpcAcc, doc, node, aFromUser, xpcParent,
+ xpcNext, xpcPrev);
+ }
+
+ parent->RemoveChild(root);
+ ShutdownOrPrepareForMove(root);
+
+ MOZ_ASSERT(CheckDocTree());
+
+ if (event) {
+ nsCoreUtils::DispatchAccEvent(std::move(event));
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvEvent(
+ const uint64_t& aID, const uint32_t& aEventType) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) {
+ return IPC_OK();
+ }
+ if (aEventType == 0 || aEventType >= nsIAccessibleEvent::EVENT_LAST_ENTRY) {
+ MOZ_ASSERT_UNREACHABLE("Invalid event");
+ return IPC_FAIL(this, "Invalid event");
+ }
+
+ RemoteAccessible* remote = GetAccessible(aID);
+ if (!remote) {
+ NS_ERROR("no proxy for event!");
+ return IPC_OK();
+ }
+
+ FireEvent(remote, aEventType);
+ return IPC_OK();
+}
+
+void DocAccessibleParent::FireEvent(RemoteAccessible* aAcc,
+ const uint32_t& aEventType) {
+ if (aEventType == nsIAccessibleEvent::EVENT_REORDER ||
+ aEventType == nsIAccessibleEvent::EVENT_INNER_REORDER) {
+ uint32_t count = aAcc->ChildCount();
+ for (uint32_t c = 0; c < count; ++c) {
+ aAcc->RemoteChildAt(c)->InvalidateGroupInfo();
+ }
+ } else if (aEventType == nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE &&
+ aAcc == this) {
+ // A DocAccessible gets the STALE state while it is still loading, but we
+ // don't fire a state change for that. That state might have been
+ // included in the initial cache push, so clear it here.
+ // We also clear the BUSY state here. Although we do fire a state change
+ // for that, we fire it after doc load complete. It doesn't make sense
+ // for the document to report BUSY after doc load complete and doing so
+ // confuses JAWS.
+ UpdateStateCache(states::STALE | states::BUSY, false);
+ }
+
+ PlatformEvent(aAcc, aEventType);
+
+ if (!nsCoreUtils::AccEventObserversExist()) {
+ return;
+ }
+
+ xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(aAcc);
+ xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this);
+ nsINode* node = nullptr;
+ bool fromUser = true; // XXX fix me
+ RefPtr<xpcAccEvent> event =
+ new xpcAccEvent(aEventType, xpcAcc, doc, node, fromUser);
+ nsCoreUtils::DispatchAccEvent(std::move(event));
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvStateChangeEvent(
+ const uint64_t& aID, const uint64_t& aState, const bool& aEnabled) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) {
+ return IPC_OK();
+ }
+
+ RemoteAccessible* target = GetAccessible(aID);
+ if (!target) {
+ NS_ERROR("we don't know about the target of a state change event!");
+ return IPC_OK();
+ }
+
+ target->UpdateStateCache(aState, aEnabled);
+ if (nsCOMPtr<nsIObserverService> obsService =
+ services::GetObserverService()) {
+ obsService->NotifyObservers(nullptr, NS_ACCESSIBLE_CACHE_TOPIC, nullptr);
+ }
+ PlatformStateChangeEvent(target, aState, aEnabled);
+
+ if (!nsCoreUtils::AccEventObserversExist()) {
+ return IPC_OK();
+ }
+
+ xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(target);
+ xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this);
+ uint32_t type = nsIAccessibleEvent::EVENT_STATE_CHANGE;
+ bool extra;
+ uint32_t state = nsAccUtils::To32States(aState, &extra);
+ bool fromUser = true; // XXX fix this
+ nsINode* node = nullptr; // XXX can we do better?
+ RefPtr<xpcAccStateChangeEvent> event = new xpcAccStateChangeEvent(
+ type, xpcAcc, doc, node, fromUser, state, extra, aEnabled);
+ nsCoreUtils::DispatchAccEvent(std::move(event));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvCaretMoveEvent(
+ const uint64_t& aID, const LayoutDeviceIntRect& aCaretRect,
+ const int32_t& aOffset, const bool& aIsSelectionCollapsed,
+ const bool& aIsAtEndOfLine, const int32_t& aGranularity,
+ const bool& aFromUser) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) {
+ return IPC_OK();
+ }
+
+ RemoteAccessible* proxy = GetAccessible(aID);
+ if (!proxy) {
+ NS_ERROR("unknown caret move event target!");
+ return IPC_OK();
+ }
+
+ mCaretId = aID;
+ mCaretOffset = aOffset;
+ mIsCaretAtEndOfLine = aIsAtEndOfLine;
+ if (aIsSelectionCollapsed) {
+ // We don't fire selection events for collapsed selections, but we need to
+ // ensure we don't have a stale cached selection; e.g. when selecting
+ // forward and then unselecting backward.
+ mTextSelections.ClearAndRetainStorage();
+ mTextSelections.AppendElement(TextRangeData(aID, aID, aOffset, aOffset));
+ }
+
+ PlatformCaretMoveEvent(proxy, aOffset, aIsSelectionCollapsed, aGranularity,
+ aCaretRect, aFromUser);
+
+ if (!nsCoreUtils::AccEventObserversExist()) {
+ return IPC_OK();
+ }
+
+ xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(proxy);
+ xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this);
+ nsINode* node = nullptr;
+ bool fromUser = true; // XXX fix me
+ uint32_t type = nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED;
+ RefPtr<xpcAccCaretMoveEvent> event = new xpcAccCaretMoveEvent(
+ type, xpcAcc, doc, node, fromUser, aOffset, aIsSelectionCollapsed,
+ aIsAtEndOfLine, aGranularity);
+ nsCoreUtils::DispatchAccEvent(std::move(event));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvTextChangeEvent(
+ const uint64_t& aID, const nsAString& aStr, const int32_t& aStart,
+ const uint32_t& aLen, const bool& aIsInsert, const bool& aFromUser) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) {
+ return IPC_OK();
+ }
+
+ RemoteAccessible* target = GetAccessible(aID);
+ if (!target) {
+ NS_ERROR("text change event target is unknown!");
+ return IPC_OK();
+ }
+
+ PlatformTextChangeEvent(target, aStr, aStart, aLen, aIsInsert, aFromUser);
+
+ if (!nsCoreUtils::AccEventObserversExist()) {
+ return IPC_OK();
+ }
+
+ xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(target);
+ xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this);
+ uint32_t type = aIsInsert ? nsIAccessibleEvent::EVENT_TEXT_INSERTED
+ : nsIAccessibleEvent::EVENT_TEXT_REMOVED;
+ nsINode* node = nullptr;
+ RefPtr<xpcAccTextChangeEvent> event = new xpcAccTextChangeEvent(
+ type, xpcAcc, doc, node, aFromUser, aStart, aLen, aIsInsert, aStr);
+ nsCoreUtils::DispatchAccEvent(std::move(event));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvSelectionEvent(
+ const uint64_t& aID, const uint64_t& aWidgetID, const uint32_t& aType) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) {
+ return IPC_OK();
+ }
+ if (aType == 0 || aType >= nsIAccessibleEvent::EVENT_LAST_ENTRY) {
+ MOZ_ASSERT_UNREACHABLE("Invalid event");
+ return IPC_FAIL(this, "Invalid event");
+ }
+
+ RemoteAccessible* target = GetAccessible(aID);
+ RemoteAccessible* widget = GetAccessible(aWidgetID);
+ if (!target || !widget) {
+ NS_ERROR("invalid id in selection event");
+ return IPC_OK();
+ }
+
+ PlatformSelectionEvent(target, widget, aType);
+ if (!nsCoreUtils::AccEventObserversExist()) {
+ return IPC_OK();
+ }
+ xpcAccessibleGeneric* xpcTarget = GetXPCAccessible(target);
+ xpcAccessibleDocument* xpcDoc = GetAccService()->GetXPCDocument(this);
+ RefPtr<xpcAccEvent> event =
+ new xpcAccEvent(aType, xpcTarget, xpcDoc, nullptr, false);
+ nsCoreUtils::DispatchAccEvent(std::move(event));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvScrollingEvent(
+ const uint64_t& aID, const uint64_t& aType, const uint32_t& aScrollX,
+ const uint32_t& aScrollY, const uint32_t& aMaxScrollX,
+ const uint32_t& aMaxScrollY) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) {
+ return IPC_OK();
+ }
+ if (aType == 0 || aType >= nsIAccessibleEvent::EVENT_LAST_ENTRY) {
+ MOZ_ASSERT_UNREACHABLE("Invalid event");
+ return IPC_FAIL(this, "Invalid event");
+ }
+
+ RemoteAccessible* target = GetAccessible(aID);
+ if (!target) {
+ NS_ERROR("no proxy for event!");
+ return IPC_OK();
+ }
+
+#if defined(ANDROID)
+ PlatformScrollingEvent(target, aType, aScrollX, aScrollY, aMaxScrollX,
+ aMaxScrollY);
+#else
+ PlatformEvent(target, aType);
+#endif
+
+ if (!nsCoreUtils::AccEventObserversExist()) {
+ return IPC_OK();
+ }
+
+ xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(target);
+ xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this);
+ nsINode* node = nullptr;
+ bool fromUser = true; // XXX: Determine if this was from user input.
+ RefPtr<xpcAccScrollingEvent> event =
+ new xpcAccScrollingEvent(aType, xpcAcc, doc, node, fromUser, aScrollX,
+ aScrollY, aMaxScrollX, aMaxScrollY);
+ nsCoreUtils::DispatchAccEvent(std::move(event));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvCache(
+ const mozilla::a11y::CacheUpdateType& aUpdateType,
+ nsTArray<CacheData>&& aData) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) {
+ return IPC_OK();
+ }
+
+ for (auto& entry : aData) {
+ RemoteAccessible* remote = GetAccessible(entry.ID());
+ if (!remote) {
+ MOZ_ASSERT_UNREACHABLE("No remote found!");
+ continue;
+ }
+
+ remote->ApplyCache(aUpdateType, entry.Fields());
+ }
+
+ if (nsCOMPtr<nsIObserverService> obsService =
+ services::GetObserverService()) {
+ obsService->NotifyObservers(nullptr, NS_ACCESSIBLE_CACHE_TOPIC, nullptr);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvSelectedAccessiblesChanged(
+ nsTArray<uint64_t>&& aSelectedIDs, nsTArray<uint64_t>&& aUnselectedIDs) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) {
+ return IPC_OK();
+ }
+
+ for (auto& id : aSelectedIDs) {
+ RemoteAccessible* remote = GetAccessible(id);
+ if (!remote) {
+ MOZ_ASSERT_UNREACHABLE("No remote found!");
+ continue;
+ }
+
+ remote->UpdateStateCache(states::SELECTED, true);
+ }
+
+ for (auto& id : aUnselectedIDs) {
+ RemoteAccessible* remote = GetAccessible(id);
+ if (!remote) {
+ MOZ_ASSERT_UNREACHABLE("No remote found!");
+ continue;
+ }
+
+ remote->UpdateStateCache(states::SELECTED, false);
+ }
+
+ if (nsCOMPtr<nsIObserverService> obsService =
+ services::GetObserverService()) {
+ obsService->NotifyObservers(nullptr, NS_ACCESSIBLE_CACHE_TOPIC, nullptr);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvAccessiblesWillMove(
+ nsTArray<uint64_t>&& aIDs) {
+ for (uint64_t id : aIDs) {
+ mMovingIDs.EnsureInserted(id);
+ }
+ return IPC_OK();
+}
+
+#if !defined(XP_WIN)
+mozilla::ipc::IPCResult DocAccessibleParent::RecvAnnouncementEvent(
+ const uint64_t& aID, const nsAString& aAnnouncement,
+ const uint16_t& aPriority) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) {
+ return IPC_OK();
+ }
+
+ RemoteAccessible* target = GetAccessible(aID);
+ if (!target) {
+ NS_ERROR("no proxy for event!");
+ return IPC_OK();
+ }
+
+# if defined(ANDROID)
+ PlatformAnnouncementEvent(target, aAnnouncement, aPriority);
+# endif
+
+ if (!nsCoreUtils::AccEventObserversExist()) {
+ return IPC_OK();
+ }
+
+ xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(target);
+ xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this);
+ RefPtr<xpcAccAnnouncementEvent> event = new xpcAccAnnouncementEvent(
+ nsIAccessibleEvent::EVENT_ANNOUNCEMENT, xpcAcc, doc, nullptr, false,
+ aAnnouncement, aPriority);
+ nsCoreUtils::DispatchAccEvent(std::move(event));
+
+ return IPC_OK();
+}
+#endif // !defined(XP_WIN)
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvTextSelectionChangeEvent(
+ const uint64_t& aID, nsTArray<TextRangeData>&& aSelection) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) {
+ return IPC_OK();
+ }
+
+ RemoteAccessible* target = GetAccessible(aID);
+ if (!target) {
+ NS_ERROR("no proxy for event!");
+ return IPC_OK();
+ }
+
+ mTextSelections.ClearAndRetainStorage();
+ mTextSelections.AppendElements(aSelection);
+
+#ifdef MOZ_WIDGET_COCOA
+ AutoTArray<TextRange, 1> ranges;
+ SelectionRanges(&ranges);
+ PlatformTextSelectionChangeEvent(target, ranges);
+#else
+ PlatformEvent(target, nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED);
+#endif
+
+ if (!nsCoreUtils::AccEventObserversExist()) {
+ return IPC_OK();
+ }
+ xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(target);
+ xpcAccessibleDocument* doc = nsAccessibilityService::GetXPCDocument(this);
+ nsINode* node = nullptr;
+ bool fromUser = true; // XXX fix me
+ RefPtr<xpcAccEvent> event =
+ new xpcAccEvent(nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED, xpcAcc,
+ doc, node, fromUser);
+ nsCoreUtils::DispatchAccEvent(std::move(event));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvRoleChangedEvent(
+ const a11y::role& aRole, const uint8_t& aRoleMapEntryIndex) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) {
+ return IPC_OK();
+ }
+ if (!aria::IsRoleMapIndexValid(aRoleMapEntryIndex)) {
+ MOZ_ASSERT_UNREACHABLE("Invalid role map entry index");
+ return IPC_FAIL(this, "Invalid role map entry index");
+ }
+
+ mRole = aRole;
+ mRoleMapEntryIndex = aRoleMapEntryIndex;
+
+#ifdef MOZ_WIDGET_COCOA
+ PlatformRoleChangedEvent(this, aRole, aRoleMapEntryIndex);
+#endif
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvBindChildDoc(
+ NotNull<PDocAccessibleParent*> aChildDoc, const uint64_t& aID) {
+ ACQUIRE_ANDROID_LOCK
+ // One document should never directly be the child of another.
+ // We should always have at least an outer doc accessible in between.
+ MOZ_ASSERT(aID);
+ if (!aID) return IPC_FAIL(this, "ID is 0!");
+
+ if (mShutdown) {
+ return IPC_OK();
+ }
+
+ MOZ_ASSERT(CheckDocTree());
+
+ auto childDoc = static_cast<DocAccessibleParent*>(aChildDoc.get());
+ childDoc->Unbind();
+ ipc::IPCResult result = AddChildDoc(childDoc, aID, false);
+ MOZ_ASSERT(result);
+ MOZ_ASSERT(CheckDocTree());
+#ifdef DEBUG
+ if (!result) {
+ return result;
+ }
+#else
+ result = IPC_OK();
+#endif
+
+ return result;
+}
+
+ipc::IPCResult DocAccessibleParent::AddChildDoc(DocAccessibleParent* aChildDoc,
+ uint64_t aParentID,
+ bool aCreating) {
+ // We do not use GetAccessible here because we want to be sure to not get the
+ // document it self.
+ ProxyEntry* e = mAccessibles.GetEntry(aParentID);
+ if (!e) {
+#ifndef FUZZING_SNAPSHOT
+ // This diagnostic assert and the one down below expect a well-behaved
+ // child process. In IPC fuzzing, we directly fuzz parameters of each
+ // method over IPDL and the asserts are not valid under these conditions.
+ MOZ_DIAGNOSTIC_ASSERT(false, "Binding to nonexistent proxy!");
+#endif
+ return IPC_FAIL(this, "binding to nonexistant proxy!");
+ }
+
+ RemoteAccessible* outerDoc = e->mProxy;
+ MOZ_ASSERT(outerDoc);
+
+ // OuterDocAccessibles are expected to only have a document as a child.
+ // However for compatibility we tolerate replacing one document with another
+ // here.
+ if (!outerDoc->IsOuterDoc() || outerDoc->ChildCount() > 1 ||
+ (outerDoc->ChildCount() == 1 && !outerDoc->RemoteChildAt(0)->IsDoc())) {
+#ifndef FUZZING_SNAPSHOT
+ MOZ_DIAGNOSTIC_ASSERT(false,
+ "Binding to parent that isn't a valid OuterDoc!");
+#endif
+ return IPC_FAIL(this, "Binding to parent that isn't a valid OuterDoc!");
+ }
+
+ if (outerDoc->ChildCount() == 1) {
+ MOZ_ASSERT(outerDoc->RemoteChildAt(0)->AsDoc());
+ outerDoc->RemoteChildAt(0)->AsDoc()->Unbind();
+ }
+
+ aChildDoc->SetParent(outerDoc);
+ outerDoc->SetChildDoc(aChildDoc);
+ mChildDocs.AppendElement(aChildDoc->mActorID);
+ aChildDoc->mParentDoc = mActorID;
+
+ if (aCreating) {
+ ProxyCreated(aChildDoc);
+ }
+
+ if (aChildDoc->IsTopLevelInContentProcess()) {
+ // aChildDoc is an embedded document in a different content process to
+ // this document.
+ auto embeddedBrowser =
+ static_cast<dom::BrowserParent*>(aChildDoc->Manager());
+ dom::BrowserBridgeParent* bridge =
+ embeddedBrowser->GetBrowserBridgeParent();
+ if (bridge) {
+#if defined(XP_WIN)
+ if (nsWinUtils::IsWindowEmulationStarted()) {
+ aChildDoc->SetEmulatedWindowHandle(mEmulatedWindowHandle);
+ }
+#endif // defined(XP_WIN)
+ // We need to fire a reorder event on the outer doc accessible.
+ // For same-process documents, this is fired by the content process, but
+ // this isn't possible when the document is in a different process to its
+ // embedder.
+ // FireEvent fires both OS and XPCOM events.
+ FireEvent(outerDoc, nsIAccessibleEvent::EVENT_REORDER);
+ }
+ }
+
+ return IPC_OK();
+}
+
+ipc::IPCResult DocAccessibleParent::AddChildDoc(
+ dom::BrowserBridgeParent* aBridge) {
+ MOZ_ASSERT(aBridge->GetEmbedderAccessibleDoc() == this);
+ uint64_t parentId = aBridge->GetEmbedderAccessibleId();
+ MOZ_ASSERT(parentId);
+ if (!mAccessibles.GetEntry(parentId)) {
+ // Sometimes, this gets called before the embedder sends us the
+ // OuterDocAccessible. We must add the child when the OuterDocAccessible
+ // gets created later.
+ mPendingOOPChildDocs.Insert(aBridge);
+ return IPC_OK();
+ }
+ return AddChildDoc(aBridge->GetDocAccessibleParent(), parentId,
+ /* aCreating */ false);
+}
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvShutdown() {
+ ACQUIRE_ANDROID_LOCK
+ Destroy();
+
+ auto mgr = static_cast<dom::BrowserParent*>(Manager());
+ if (!mgr->IsDestroyed()) {
+ if (!PDocAccessibleParent::Send__delete__(this)) {
+ return IPC_FAIL_NO_REASON(mgr);
+ }
+ }
+
+ return IPC_OK();
+}
+
+void DocAccessibleParent::Destroy() {
+ // If we are already shutdown that is because our containing tab parent is
+ // shutting down in which case we don't need to do anything.
+ if (mShutdown) {
+ return;
+ }
+
+ mShutdown = true;
+ mBrowsingContext = nullptr;
+
+#ifdef ANDROID
+ if (FocusMgr() && FocusMgr()->IsFocusedRemoteDoc(this)) {
+ FocusMgr()->SetFocusedRemoteDoc(nullptr);
+ }
+#endif
+
+ MOZ_DIAGNOSTIC_ASSERT(LiveDocs().Contains(mActorID));
+ uint32_t childDocCount = mChildDocs.Length();
+ for (uint32_t i = 0; i < childDocCount; i++) {
+ for (uint32_t j = i + 1; j < childDocCount; j++) {
+ MOZ_DIAGNOSTIC_ASSERT(mChildDocs[i] != mChildDocs[j]);
+ }
+ }
+
+ // XXX This indirection through the hash map of live documents shouldn't be
+ // needed, but be paranoid for now.
+ int32_t actorID = mActorID;
+ for (uint32_t i = childDocCount - 1; i < childDocCount; i--) {
+ DocAccessibleParent* thisDoc = LiveDocs().Get(actorID);
+ MOZ_ASSERT(thisDoc);
+ if (!thisDoc) {
+ return;
+ }
+
+ thisDoc->ChildDocAt(i)->Destroy();
+ }
+
+ for (auto iter = mAccessibles.Iter(); !iter.Done(); iter.Next()) {
+ RemoteAccessible* acc = iter.Get()->mProxy;
+ MOZ_ASSERT(acc != this);
+ if (acc->IsTable()) {
+ CachedTableAccessible::Invalidate(acc);
+ }
+ ProxyDestroyed(acc);
+ iter.Remove();
+ }
+
+ DocAccessibleParent* thisDoc = LiveDocs().Get(actorID);
+ MOZ_ASSERT(thisDoc);
+ if (!thisDoc) {
+ return;
+ }
+
+ mChildren.Clear();
+ // The code above should have already completely cleared these, but to be
+ // extra safe make sure they are cleared here.
+ thisDoc->mAccessibles.Clear();
+ thisDoc->mChildDocs.Clear();
+
+ DocManager::NotifyOfRemoteDocShutdown(thisDoc);
+ thisDoc = LiveDocs().Get(actorID);
+ MOZ_ASSERT(thisDoc);
+ if (!thisDoc) {
+ return;
+ }
+
+ ProxyDestroyed(thisDoc);
+ thisDoc = LiveDocs().Get(actorID);
+ MOZ_ASSERT(thisDoc);
+ if (!thisDoc) {
+ return;
+ }
+
+ if (DocAccessibleParent* parentDoc = thisDoc->ParentDoc()) {
+ parentDoc->RemoveChildDoc(thisDoc);
+ } else if (IsTopLevel()) {
+ GetAccService()->RemoteDocShutdown(this);
+ }
+}
+
+void DocAccessibleParent::ActorDestroy(ActorDestroyReason aWhy) {
+ MOZ_ASSERT(CheckDocTree());
+ if (!mShutdown) {
+ ACQUIRE_ANDROID_LOCK
+ Destroy();
+ }
+}
+
+DocAccessibleParent* DocAccessibleParent::ParentDoc() const {
+ if (mParentDoc == kNoParentDoc) {
+ return nullptr;
+ }
+
+ return LiveDocs().Get(mParentDoc);
+}
+
+bool DocAccessibleParent::CheckDocTree() const {
+ size_t childDocs = mChildDocs.Length();
+ for (size_t i = 0; i < childDocs; i++) {
+ const DocAccessibleParent* childDoc = ChildDocAt(i);
+ if (!childDoc || childDoc->ParentDoc() != this) return false;
+
+ if (!childDoc->CheckDocTree()) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+xpcAccessibleGeneric* DocAccessibleParent::GetXPCAccessible(
+ RemoteAccessible* aProxy) {
+ xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this);
+ MOZ_ASSERT(doc);
+
+ return doc->GetAccessible(aProxy);
+}
+
+#if defined(XP_WIN)
+void DocAccessibleParent::MaybeInitWindowEmulation() {
+ if (!nsWinUtils::IsWindowEmulationStarted()) {
+ return;
+ }
+
+ // XXX get the bounds from the browserParent instead of poking at accessibles
+ // which might not exist yet.
+ LocalAccessible* outerDoc = OuterDocOfRemoteBrowser();
+ if (!outerDoc) {
+ return;
+ }
+
+ RootAccessible* rootDocument = outerDoc->RootAccessible();
+ MOZ_ASSERT(rootDocument);
+
+ bool isActive = true;
+ LayoutDeviceIntRect rect(CW_USEDEFAULT, CW_USEDEFAULT, 0, 0);
+ if (Compatibility::IsDolphin()) {
+ rect = Bounds();
+ LayoutDeviceIntRect rootRect = rootDocument->Bounds();
+ rect.MoveToX(rootRect.X() - rect.X());
+ rect.MoveToY(rect.Y() - rootRect.Y());
+
+ auto browserParent = static_cast<dom::BrowserParent*>(Manager());
+ isActive = browserParent->GetDocShellIsActive();
+ }
+
+ // onCreate is guaranteed to be called synchronously by
+ // nsWinUtils::CreateNativeWindow, so this reference isn't really necessary.
+ // However, static analysis complains without it.
+ RefPtr<DocAccessibleParent> thisRef = this;
+ nsWinUtils::NativeWindowCreateProc onCreate([thisRef](HWND aHwnd) -> void {
+ ::SetPropW(aHwnd, kPropNameDocAccParent,
+ reinterpret_cast<HANDLE>(thisRef.get()));
+ thisRef->SetEmulatedWindowHandle(aHwnd);
+ });
+
+ HWND parentWnd = reinterpret_cast<HWND>(rootDocument->GetNativeWindow());
+ DebugOnly<HWND> hWnd = nsWinUtils::CreateNativeWindow(
+ kClassNameTabContent, parentWnd, rect.X(), rect.Y(), rect.Width(),
+ rect.Height(), isActive, &onCreate);
+ MOZ_ASSERT(hWnd);
+}
+
+void DocAccessibleParent::SetEmulatedWindowHandle(HWND aWindowHandle) {
+ if (!aWindowHandle && mEmulatedWindowHandle && IsTopLevel()) {
+ ::DestroyWindow(mEmulatedWindowHandle);
+ }
+ mEmulatedWindowHandle = aWindowHandle;
+}
+#endif // defined(XP_WIN)
+
+mozilla::ipc::IPCResult DocAccessibleParent::RecvFocusEvent(
+ const uint64_t& aID, const LayoutDeviceIntRect& aCaretRect) {
+ ACQUIRE_ANDROID_LOCK
+ if (mShutdown) {
+ return IPC_OK();
+ }
+
+ RemoteAccessible* proxy = GetAccessible(aID);
+ if (!proxy) {
+ NS_ERROR("no proxy for event!");
+ return IPC_OK();
+ }
+
+#ifdef ANDROID
+ if (FocusMgr()) {
+ FocusMgr()->SetFocusedRemoteDoc(this);
+ }
+#endif
+
+ mFocus = aID;
+ PlatformFocusEvent(proxy, aCaretRect);
+
+ if (!nsCoreUtils::AccEventObserversExist()) {
+ return IPC_OK();
+ }
+
+ xpcAccessibleGeneric* xpcAcc = GetXPCAccessible(proxy);
+ xpcAccessibleDocument* doc = GetAccService()->GetXPCDocument(this);
+ nsINode* node = nullptr;
+ bool fromUser = true; // XXX fix me
+ RefPtr<xpcAccEvent> event = new xpcAccEvent(nsIAccessibleEvent::EVENT_FOCUS,
+ xpcAcc, doc, node, fromUser);
+ nsCoreUtils::DispatchAccEvent(std::move(event));
+
+ return IPC_OK();
+}
+
+void DocAccessibleParent::SelectionRanges(nsTArray<TextRange>* aRanges) const {
+ aRanges->SetCapacity(mTextSelections.Length());
+ for (const auto& data : mTextSelections) {
+ // Selection ranges should usually be in sync with the tree. However, tree
+ // and selection updates happen using separate IPDL calls, so it's possible
+ // for a client selection query to arrive between them. Thus, we validate
+ // the Accessibles and offsets here.
+ auto* startAcc =
+ const_cast<RemoteAccessible*>(GetAccessible(data.StartID()));
+ auto* endAcc = const_cast<RemoteAccessible*>(GetAccessible(data.EndID()));
+ if (!startAcc || !endAcc) {
+ continue;
+ }
+ uint32_t startCount = startAcc->CharacterCount();
+ if (startCount == 0 ||
+ data.StartOffset() > static_cast<int32_t>(startCount)) {
+ continue;
+ }
+ uint32_t endCount = endAcc->CharacterCount();
+ if (endCount == 0 || data.EndOffset() > static_cast<int32_t>(endCount)) {
+ continue;
+ }
+ aRanges->AppendElement(TextRange(const_cast<DocAccessibleParent*>(this),
+ startAcc, data.StartOffset(), endAcc,
+ data.EndOffset()));
+ }
+}
+
+Accessible* DocAccessibleParent::FocusedChild() {
+ LocalAccessible* outerDoc = OuterDocOfRemoteBrowser();
+ if (!outerDoc) {
+ return nullptr;
+ }
+
+ RootAccessible* rootDocument = outerDoc->RootAccessible();
+ return rootDocument->FocusedChild();
+}
+
+void DocAccessibleParent::URL(nsACString& aURL) const {
+ if (!mBrowsingContext) {
+ return;
+ }
+ nsCOMPtr<nsIURI> uri = mBrowsingContext->GetCurrentURI();
+ if (!uri) {
+ return;
+ }
+ // Let's avoid treating too long URI in the main process for avoiding
+ // memory fragmentation as far as possible.
+ if (uri->SchemeIs("data") || uri->SchemeIs("blob")) {
+ return;
+ }
+ nsCOMPtr<nsIIOService> io = mozilla::components::IO::Service();
+ if (NS_WARN_IF(!io)) {
+ return;
+ }
+ nsCOMPtr<nsIURI> exposableURI;
+ if (NS_FAILED(io->CreateExposableURI(uri, getter_AddRefs(exposableURI))) ||
+ MOZ_UNLIKELY(!exposableURI)) {
+ return;
+ }
+ exposableURI->GetSpec(aURL);
+}
+
+void DocAccessibleParent::URL(nsAString& aURL) const {
+ nsAutoCString url;
+ URL(url);
+ CopyUTF8toUTF16(url, aURL);
+}
+
+void DocAccessibleParent::MimeType(nsAString& aMime) const {
+ if (mCachedFields) {
+ mCachedFields->GetAttribute(CacheKey::MimeType, aMime);
+ }
+}
+
+Relation DocAccessibleParent::RelationByType(RelationType aType) const {
+ // If the accessible is top-level, provide the NODE_CHILD_OF relation so that
+ // MSAA clients can easily get to true parent instead of getting to oleacc's
+ // ROLE_WINDOW accessible when window emulation is enabled which will prevent
+ // us from going up further (because it is system generated and has no idea
+ // about the hierarchy above it).
+ if (aType == RelationType::NODE_CHILD_OF && IsTopLevel()) {
+ return Relation(Parent());
+ }
+
+ return RemoteAccessible::RelationByType(aType);
+}
+
+DocAccessibleParent* DocAccessibleParent::GetFrom(
+ dom::BrowsingContext* aBrowsingContext) {
+ if (!aBrowsingContext) {
+ return nullptr;
+ }
+
+ dom::BrowserParent* bp = aBrowsingContext->Canonical()->GetBrowserParent();
+ if (!bp) {
+ return nullptr;
+ }
+
+ const ManagedContainer<PDocAccessibleParent>& docs =
+ bp->ManagedPDocAccessibleParent();
+ for (auto* key : docs) {
+ // Iterate over our docs until we find one with a browsing
+ // context that matches the one we passed in. Return that
+ // document.
+ auto* doc = static_cast<a11y::DocAccessibleParent*>(key);
+ if (doc->GetBrowsingContext() == aBrowsingContext) {
+ return doc;
+ }
+ }
+
+ return nullptr;
+}
+
+size_t DocAccessibleParent::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) {
+ size_t size = 0;
+
+ size += RemoteAccessible::SizeOfExcludingThis(aMallocSizeOf);
+
+ size += mReverseRelations.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto i = mReverseRelations.Iter(); !i.Done(); i.Next()) {
+ size += i.Data().ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto j = i.Data().Iter(); !j.Done(); j.Next()) {
+ size += j.Data().ShallowSizeOfExcludingThis(aMallocSizeOf);
+ }
+ }
+
+ size += mOnScreenAccessibles.ShallowSizeOfExcludingThis(aMallocSizeOf);
+
+ size += mChildDocs.ShallowSizeOfExcludingThis(aMallocSizeOf);
+
+ size += mAccessibles.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto i = mAccessibles.Iter(); !i.Done(); i.Next()) {
+ size += i.Get()->mProxy->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ size += mPendingOOPChildDocs.ShallowSizeOfExcludingThis(aMallocSizeOf);
+
+ // The mTextSelections array contains structs of integers. We can count them
+ // by counting the size of the array - there's no deep structure here.
+ size += mTextSelections.ShallowSizeOfExcludingThis(aMallocSizeOf);
+
+ return size;
+}
+
+MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOfAccessibilityCache);
+
+NS_IMETHODIMP
+DocAccessibleParent::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnon) {
+ nsAutoCString path;
+
+ if (aAnon) {
+ path = nsPrintfCString("explicit/a11y/cache(%" PRIu64 ")", mActorID);
+ } else {
+ nsCString url;
+ URL(url);
+ url.ReplaceChar(
+ '/', '\\'); // Tell the memory reporter this is not a path seperator.
+ path = nsPrintfCString("explicit/a11y/cache(%s)", url.get());
+ }
+
+ aHandleReport->Callback(
+ /* process */ ""_ns, path, KIND_HEAP, UNITS_BYTES,
+ SizeOfIncludingThis(MallocSizeOfAccessibilityCache),
+ nsLiteralCString("Size of the accessability cache for this document."),
+ aData);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(DocAccessibleParent, nsIMemoryReporter);
+
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/ipc/DocAccessibleParent.h b/accessible/ipc/DocAccessibleParent.h
new file mode 100644
index 0000000000..bb05fbafad
--- /dev/null
+++ b/accessible/ipc/DocAccessibleParent.h
@@ -0,0 +1,400 @@
+/* -*- 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_DocAccessibleParent_h
+#define mozilla_a11y_DocAccessibleParent_h
+
+#include "nsAccessibilityService.h"
+#include "mozilla/a11y/PDocAccessibleParent.h"
+#include "mozilla/a11y/RemoteAccessible.h"
+#include "mozilla/dom/BrowserBridgeParent.h"
+#include "nsClassHashtable.h"
+#include "nsHashKeys.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+namespace dom {
+class CanonicalBrowsingContext;
+}
+
+namespace a11y {
+
+class TextRange;
+class xpcAccessibleGeneric;
+
+/*
+ * These objects live in the main process and comunicate with and represent
+ * an accessible document in a content process.
+ */
+class DocAccessibleParent : public RemoteAccessible,
+ public PDocAccessibleParent,
+ public nsIMemoryReporter {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMEMORYREPORTER
+
+ private:
+ DocAccessibleParent();
+
+ public:
+ static already_AddRefed<DocAccessibleParent> New();
+
+ /**
+ * Set this as a top level document; i.e. it is not embedded by another remote
+ * document. This also means it is a top level document in its content
+ * process.
+ * Tab documents are top level documents.
+ */
+ void SetTopLevel() {
+ mTopLevel = true;
+ mTopLevelInContentProcess = true;
+ }
+ bool IsTopLevel() const { return mTopLevel; }
+
+ /**
+ * Set this as a top level document in its content process.
+ * Note that this could be an out-of-process iframe embedded by a remote
+ * embedder document. In that case, IsToplevel() will return false, but
+ * IsTopLevelInContentProcess() will return true.
+ */
+ void SetTopLevelInContentProcess() { mTopLevelInContentProcess = true; }
+ bool IsTopLevelInContentProcess() const { return mTopLevelInContentProcess; }
+
+ /**
+ * Determine whether this is an out-of-process iframe document, embedded by a
+ * remote embedder document.
+ */
+ bool IsOOPIframeDoc() const {
+ return !mTopLevel && mTopLevelInContentProcess;
+ }
+
+ bool IsShutdown() const { return mShutdown; }
+
+ /**
+ * Mark this actor as shutdown without doing any cleanup. This should only
+ * be called on actors that have just been initialized, so probably only from
+ * RecvPDocAccessibleConstructor.
+ */
+ void MarkAsShutdown() {
+ MOZ_ASSERT(mChildDocs.IsEmpty());
+ MOZ_ASSERT(mAccessibles.Count() == 0);
+ MOZ_ASSERT(!mBrowsingContext);
+ mShutdown = true;
+ }
+
+ void SetBrowsingContext(dom::CanonicalBrowsingContext* aBrowsingContext);
+
+ dom::CanonicalBrowsingContext* GetBrowsingContext() const {
+ return mBrowsingContext;
+ }
+
+ /*
+ * Called when a message from a document in a child process notifies the main
+ * process it is firing an event.
+ */
+ virtual mozilla::ipc::IPCResult RecvEvent(const uint64_t& aID,
+ const uint32_t& aType) override;
+
+ virtual mozilla::ipc::IPCResult RecvShowEvent(
+ nsTArray<AccessibleData>&& aNewTree, const bool& aEventSuppressed,
+ const bool& aComplete, const bool& aFromUser) override;
+ virtual mozilla::ipc::IPCResult RecvHideEvent(const uint64_t& aRootID,
+ const bool& aFromUser) override;
+ mozilla::ipc::IPCResult RecvStateChangeEvent(const uint64_t& aID,
+ const uint64_t& aState,
+ const bool& aEnabled) final;
+
+ mozilla::ipc::IPCResult RecvCaretMoveEvent(
+ const uint64_t& aID, const LayoutDeviceIntRect& aCaretRect,
+ const int32_t& aOffset, const bool& aIsSelectionCollapsed,
+ const bool& aIsAtEndOfLine, const int32_t& aGranularity,
+ const bool& aFromUser) final;
+
+ virtual mozilla::ipc::IPCResult RecvTextChangeEvent(
+ const uint64_t& aID, const nsAString& aStr, const int32_t& aStart,
+ const uint32_t& aLen, const bool& aIsInsert,
+ const bool& aFromUser) override;
+
+ virtual mozilla::ipc::IPCResult RecvFocusEvent(
+ const uint64_t& aID, const LayoutDeviceIntRect& aCaretRect) override;
+
+ virtual mozilla::ipc::IPCResult RecvSelectionEvent(
+ const uint64_t& aID, const uint64_t& aWidgetID,
+ const uint32_t& aType) override;
+
+ virtual mozilla::ipc::IPCResult RecvScrollingEvent(
+ const uint64_t& aID, const uint64_t& aType, const uint32_t& aScrollX,
+ const uint32_t& aScrollY, const uint32_t& aMaxScrollX,
+ const uint32_t& aMaxScrollY) override;
+
+ virtual mozilla::ipc::IPCResult RecvCache(
+ const mozilla::a11y::CacheUpdateType& aUpdateType,
+ nsTArray<CacheData>&& aData) override;
+
+ virtual mozilla::ipc::IPCResult RecvSelectedAccessiblesChanged(
+ nsTArray<uint64_t>&& aSelectedIDs,
+ nsTArray<uint64_t>&& aUnselectedIDs) override;
+
+ virtual mozilla::ipc::IPCResult RecvAccessiblesWillMove(
+ nsTArray<uint64_t>&& aIDs) override;
+
+#if !defined(XP_WIN)
+ virtual mozilla::ipc::IPCResult RecvAnnouncementEvent(
+ const uint64_t& aID, const nsAString& aAnnouncement,
+ const uint16_t& aPriority) override;
+#endif
+
+ virtual mozilla::ipc::IPCResult RecvTextSelectionChangeEvent(
+ const uint64_t& aID, nsTArray<TextRangeData>&& aSelection) override;
+
+ mozilla::ipc::IPCResult RecvRoleChangedEvent(
+ const a11y::role& aRole, const uint8_t& aRoleMapEntryIndex) final;
+
+ virtual mozilla::ipc::IPCResult RecvBindChildDoc(
+ NotNull<PDocAccessibleParent*> aChildDoc, const uint64_t& aID) override;
+
+ void Unbind() {
+ if (DocAccessibleParent* parent = ParentDoc()) {
+ parent->RemoveChildDoc(this);
+ }
+
+ SetParent(nullptr);
+ }
+
+ virtual mozilla::ipc::IPCResult RecvShutdown() override;
+ void Destroy();
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ /*
+ * Return the main processes representation of the parent document (if any)
+ * of the document this object represents.
+ */
+ DocAccessibleParent* ParentDoc() const;
+ static const uint64_t kNoParentDoc = UINT64_MAX;
+
+ /**
+ * Called when a document in a content process notifies the main process of a
+ * new child document.
+ * Although this is called internally for OOP child documents, these should be
+ * added via the BrowserBridgeParent version of this method, as the parent id
+ * might not exist yet in that case.
+ */
+ ipc::IPCResult AddChildDoc(DocAccessibleParent* aChildDoc, uint64_t aParentID,
+ bool aCreating = true);
+
+ /**
+ * Called when a document in a content process notifies the main process of a
+ * new OOP child document.
+ */
+ ipc::IPCResult AddChildDoc(dom::BrowserBridgeParent* aBridge);
+
+ void RemovePendingOOPChildDoc(dom::BrowserBridgeParent* aBridge) {
+ mPendingOOPChildDocs.Remove(aBridge);
+ }
+
+ /*
+ * Called when the document in the content process this object represents
+ * notifies the main process a child document has been removed.
+ */
+ void RemoveChildDoc(DocAccessibleParent* aChildDoc) {
+ RemoteAccessible* parent = aChildDoc->RemoteParent();
+ MOZ_ASSERT(parent);
+ if (parent) {
+ aChildDoc->RemoteParent()->ClearChildDoc(aChildDoc);
+ }
+ DebugOnly<bool> result = mChildDocs.RemoveElement(aChildDoc->mActorID);
+ aChildDoc->mParentDoc = kNoParentDoc;
+ MOZ_ASSERT(result);
+ }
+
+ void RemoveAccessible(RemoteAccessible* aAccessible) {
+ MOZ_DIAGNOSTIC_ASSERT(mAccessibles.GetEntry(aAccessible->ID()));
+ mAccessibles.RemoveEntry(aAccessible->ID());
+ }
+
+ /**
+ * Return the accessible for given id.
+ */
+ RemoteAccessible* GetAccessible(uintptr_t aID) {
+ if (!aID) return this;
+
+ ProxyEntry* e = mAccessibles.GetEntry(aID);
+ return e ? e->mProxy : nullptr;
+ }
+
+ const RemoteAccessible* GetAccessible(uintptr_t aID) const {
+ return const_cast<DocAccessibleParent*>(this)->GetAccessible(aID);
+ }
+
+ size_t ChildDocCount() const { return mChildDocs.Length(); }
+ const DocAccessibleParent* ChildDocAt(size_t aIdx) const {
+ return const_cast<DocAccessibleParent*>(this)->ChildDocAt(aIdx);
+ }
+ DocAccessibleParent* ChildDocAt(size_t aIdx) {
+ return LiveDocs().Get(mChildDocs[aIdx]);
+ }
+
+#if defined(XP_WIN)
+ void MaybeInitWindowEmulation();
+
+ /**
+ * Set emulated native window handle for a document.
+ * @param aWindowHandle emulated native window handle
+ */
+ void SetEmulatedWindowHandle(HWND aWindowHandle);
+ HWND GetEmulatedWindowHandle() const { return mEmulatedWindowHandle; }
+#endif
+
+ // Accessible
+ virtual Accessible* Parent() const override {
+ if (IsTopLevel()) {
+ return OuterDocOfRemoteBrowser();
+ }
+ return RemoteParent();
+ }
+
+ virtual int32_t IndexInParent() const override {
+ if (IsTopLevel() && OuterDocOfRemoteBrowser()) {
+ // An OuterDoc can only have 1 child.
+ return 0;
+ }
+ return RemoteAccessible::IndexInParent();
+ }
+
+ /**
+ * Get the focused Accessible in this document, if any.
+ */
+ RemoteAccessible* GetFocusedAcc() const {
+ return const_cast<DocAccessibleParent*>(this)->GetAccessible(mFocus);
+ }
+
+ /**
+ * Get the HyperText Accessible containing the caret and the offset of the
+ * caret within. If there is no caret in this document, returns
+ * {nullptr, -1}.
+ */
+ std::pair<RemoteAccessible*, int32_t> GetCaret() const {
+ if (mCaretOffset == -1) {
+ return {nullptr, -1};
+ }
+ RemoteAccessible* acc =
+ const_cast<DocAccessibleParent*>(this)->GetAccessible(mCaretId);
+ if (!acc) {
+ return {nullptr, -1};
+ }
+ return {acc, mCaretOffset};
+ }
+
+ bool IsCaretAtEndOfLine() const { return mIsCaretAtEndOfLine; }
+
+ virtual void SelectionRanges(nsTArray<TextRange>* aRanges) const override;
+
+ virtual Accessible* FocusedChild() override;
+
+ void URL(nsAString& aURL) const;
+ void URL(nsACString& aURL) const;
+
+ void MimeType(nsAString& aURL) const;
+
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ // Tracks cached reverse relations (ie. those not set explicitly by an
+ // attribute like aria-labelledby) for accessibles in this doc. This map is of
+ // the form: {accID, {relationType, [targetAccID, targetAccID, ...]}}
+ nsTHashMap<uint64_t, nsTHashMap<RelationType, nsTArray<uint64_t>>>
+ mReverseRelations;
+
+ // Computed from the viewport cache, the accs referenced by these ids
+ // are currently on screen (making any acc not in this list offscreen).
+ nsTHashSet<uint64_t> mOnScreenAccessibles;
+
+ static DocAccessibleParent* GetFrom(dom::BrowsingContext* aBrowsingContext);
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) override;
+
+ private:
+ ~DocAccessibleParent();
+
+ class ProxyEntry : public PLDHashEntryHdr {
+ public:
+ explicit ProxyEntry(const void*) : mProxy(nullptr) {}
+ ProxyEntry(ProxyEntry&& aOther) : mProxy(aOther.mProxy) {
+ aOther.mProxy = nullptr;
+ }
+ ~ProxyEntry() { delete mProxy; }
+
+ typedef uint64_t KeyType;
+ typedef const void* KeyTypePointer;
+
+ bool KeyEquals(const void* aKey) const {
+ return mProxy->ID() == (uint64_t)aKey;
+ }
+
+ static const void* KeyToPointer(uint64_t aKey) { return (void*)aKey; }
+
+ static PLDHashNumber HashKey(const void* aKey) { return (uint64_t)aKey; }
+
+ enum { ALLOW_MEMMOVE = true };
+
+ RemoteAccessible* mProxy;
+ };
+
+ RemoteAccessible* CreateAcc(const AccessibleData& aAccData);
+ void AttachChild(RemoteAccessible* aParent, uint32_t aIndex,
+ RemoteAccessible* aChild);
+ [[nodiscard]] bool CheckDocTree() const;
+ xpcAccessibleGeneric* GetXPCAccessible(RemoteAccessible* aProxy);
+
+ void FireEvent(RemoteAccessible* aAcc, const uint32_t& aType);
+
+ /**
+ * If this Accessible is being moved, prepare it for reuse. Otherwise, it is
+ * being removed, so shut it down.
+ */
+ void ShutdownOrPrepareForMove(RemoteAccessible* aAcc);
+
+ nsTArray<uint64_t> mChildDocs;
+ uint64_t mParentDoc;
+
+#if defined(XP_WIN)
+ // The handle associated with the emulated window that contains this document
+ HWND mEmulatedWindowHandle;
+#endif // defined(XP_WIN)
+
+ /*
+ * Conceptually this is a map from IDs to proxies, but we store the ID in the
+ * proxy object so we can't use a real map.
+ */
+ nsTHashtable<ProxyEntry> mAccessibles;
+ uint64_t mPendingShowChild = 0;
+ uint64_t mPendingShowParent = 0;
+ uint32_t mPendingShowIndex = 0;
+ nsTHashSet<uint64_t> mMovingIDs;
+ uint64_t mActorID;
+ bool mTopLevel;
+ bool mTopLevelInContentProcess;
+ bool mShutdown;
+ RefPtr<dom::CanonicalBrowsingContext> mBrowsingContext;
+
+ nsTHashSet<RefPtr<dom::BrowserBridgeParent>> mPendingOOPChildDocs;
+
+ uint64_t mFocus;
+ uint64_t mCaretId;
+ int32_t mCaretOffset;
+ bool mIsCaretAtEndOfLine;
+ nsTArray<TextRangeData> mTextSelections;
+
+ static uint64_t sMaxDocID;
+ static nsTHashMap<nsUint64HashKey, DocAccessibleParent*>& LiveDocs() {
+ static nsTHashMap<nsUint64HashKey, DocAccessibleParent*> sLiveDocs;
+ return sLiveDocs;
+ }
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/ipc/DocAccessibleTypes.ipdlh b/accessible/ipc/DocAccessibleTypes.ipdlh
new file mode 100644
index 0000000000..3ef67fc431
--- /dev/null
+++ b/accessible/ipc/DocAccessibleTypes.ipdlh
@@ -0,0 +1,19 @@
+/* -*- 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/. */
+
+[RefCounted] using mozilla::a11y::AccAttributes from "mozilla/a11y/IPCTypes.h";
+
+namespace mozilla {
+namespace a11y {
+
+struct CacheData
+{
+ uint64_t ID;
+ nullable AccAttributes Fields;
+};
+
+}
+}
diff --git a/accessible/ipc/IPCTypes.h b/accessible/ipc/IPCTypes.h
new file mode 100644
index 0000000000..2e911724bb
--- /dev/null
+++ b/accessible/ipc/IPCTypes.h
@@ -0,0 +1,189 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_IPCTypes_h
+#define mozilla_a11y_IPCTypes_h
+
+#ifdef ACCESSIBILITY
+# include "mozilla/a11y/AccAttributes.h"
+# include "mozilla/a11y/AccTypes.h"
+# include "mozilla/a11y/CacheConstants.h"
+# include "mozilla/a11y/Role.h"
+# include "mozilla/a11y/AccGroupInfo.h"
+# include "mozilla/GfxMessageUtils.h"
+# include "ipc/EnumSerializer.h"
+# include "ipc/IPCMessageUtilsSpecializations.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::a11y::role>
+ : public ContiguousEnumSerializerInclusive<mozilla::a11y::role,
+ mozilla::a11y::role::NOTHING,
+ mozilla::a11y::role::LAST_ROLE> {
+};
+
+template <>
+struct ParamTraits<mozilla::a11y::AccType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::a11y::AccType, mozilla::a11y::AccType::eNoType,
+ mozilla::a11y::AccType::eLastAccType> {};
+
+template <>
+struct ParamTraits<mozilla::a11y::AccGenericType>
+ : public BitFlagsEnumSerializer<
+ mozilla::a11y::AccGenericType,
+ mozilla::a11y::AccGenericType::eAllGenericTypes> {};
+
+template <>
+struct ParamTraits<mozilla::a11y::CacheUpdateType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::a11y::CacheUpdateType,
+ mozilla::a11y::CacheUpdateType::Initial,
+ mozilla::a11y::CacheUpdateType::Update> {};
+
+template <>
+struct ParamTraits<mozilla::a11y::FontSize> {
+ typedef mozilla::a11y::FontSize paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mValue);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &(aResult->mValue));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::a11y::DeleteEntry> {
+ typedef mozilla::a11y::DeleteEntry paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mValue);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &(aResult->mValue));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::a11y::AccGroupInfo> {
+ typedef mozilla::a11y::AccGroupInfo paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ MOZ_ASSERT_UNREACHABLE("Cannot serialize AccGroupInfo");
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ MOZ_ASSERT_UNREACHABLE("Cannot de-serialize AccGroupInfo");
+ return false;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::a11y::Color> {
+ typedef mozilla::a11y::Color paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mValue);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &(aResult->mValue));
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::a11y::AccAttributes*> {
+ typedef mozilla::a11y::AccAttributes paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType* aParam) {
+ if (!aParam) {
+ WriteParam(aWriter, true);
+ return;
+ }
+
+ WriteParam(aWriter, false);
+ uint32_t count = aParam->mData.Count();
+ WriteParam(aWriter, count);
+ for (auto iter = aParam->mData.ConstIter(); !iter.Done(); iter.Next()) {
+ RefPtr<nsAtom> key = iter.Key();
+ WriteParam(aWriter, key);
+ const paramType::AttrValueType& data = iter.Data();
+ WriteParam(aWriter, data);
+ }
+ }
+
+ static bool Read(MessageReader* aReader, RefPtr<paramType>* aResult) {
+ bool isNull = false;
+ if (!ReadParam(aReader, &isNull)) {
+ return false;
+ }
+
+ if (isNull) {
+ *aResult = nullptr;
+ return true;
+ }
+
+ *aResult = mozilla::MakeRefPtr<mozilla::a11y::AccAttributes>();
+ uint32_t count;
+ if (!ReadParam(aReader, &count)) {
+ return false;
+ }
+ for (uint32_t i = 0; i < count; ++i) {
+ RefPtr<nsAtom> key;
+ if (!ReadParam(aReader, &key)) {
+ return false;
+ }
+ paramType::AttrValueType val(0);
+ if (!ReadParam(aReader, &val)) {
+ return false;
+ }
+ (*aResult)->mData.InsertOrUpdate(key, std::move(val));
+ }
+ return true;
+ }
+};
+
+} // namespace IPC
+#else
+namespace mozilla {
+namespace a11y {
+typedef uint32_t role;
+} // namespace a11y
+} // namespace mozilla
+#endif // ACCESSIBILITY
+
+/**
+ * Since IPDL does not support preprocessing, this header file allows us to
+ * define types used by PDocAccessible differently depending on platform.
+ */
+
+#if defined(MOZ_WIDGET_COCOA)
+# if defined(ACCESSIBILITY)
+# include "mozilla/a11y/PlatformExtTypes.h"
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::a11y::EWhichRange>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::a11y::EWhichRange, mozilla::a11y::EWhichRange::eLeftWord,
+ mozilla::a11y::EWhichRange::eStyle> {};
+
+} // namespace IPC
+
+# else
+namespace mozilla {
+namespace a11y {
+typedef uint32_t EWhichRange;
+} // namespace a11y
+} // namespace mozilla
+# endif // defined(ACCESSIBILITY)
+#endif // defined(MOZ_WIDGET_COCOA)
+
+#endif // mozilla_a11y_IPCTypes_h
diff --git a/accessible/ipc/PDocAccessible.ipdl b/accessible/ipc/PDocAccessible.ipdl
new file mode 100644
index 0000000000..d43fad823e
--- /dev/null
+++ b/accessible/ipc/PDocAccessible.ipdl
@@ -0,0 +1,164 @@
+/* -*- 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 protocol PBrowser;
+
+include DocAccessibleTypes;
+
+include "mozilla/GfxMessageUtils.h";
+
+using mozilla::LayoutDeviceIntRect from "Units.h";
+using mozilla::LayoutDeviceIntPoint from "Units.h";
+using mozilla::LayoutDeviceIntSize from "Units.h";
+using mozilla::a11y::role from "mozilla/a11y/IPCTypes.h";
+using mozilla::a11y::AccType from "mozilla/a11y/IPCTypes.h";
+using mozilla::a11y::AccGenericType from "mozilla/a11y/IPCTypes.h";
+[RefCounted] using mozilla::a11y::AccAttributes from "mozilla/a11y/IPCTypes.h";
+using mozilla::a11y::CacheUpdateType from "mozilla/a11y/IPCTypes.h";
+using mozilla::gfx::Matrix4x4 from "mozilla/gfx/Matrix.h";
+
+namespace mozilla {
+namespace a11y {
+
+struct AccessibleData
+{
+ uint64_t ID;
+ role Role;
+ uint64_t ParentID;
+ uint32_t IndexInParent;
+ AccType Type;
+ AccGenericType GenericTypes;
+ uint8_t RoleMapEntryIndex;
+ nullable AccAttributes CacheFields;
+};
+
+struct TextRangeData
+{
+ uint64_t StartID;
+ uint64_t EndID;
+ int32_t StartOffset;
+ int32_t EndOffset;
+};
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+protocol PDocAccessible
+{
+ manager PBrowser;
+
+parent:
+ async Shutdown();
+
+ /*
+ * Notify the parent process the document in the child process is firing an
+ * event.
+ */
+ async Event(uint64_t aID, uint32_t type);
+ async ShowEvent(AccessibleData[] aNewTree, bool aEventSuppressed,
+ bool aComplete, bool aFromuser);
+ async HideEvent(uint64_t aRootID, bool aFromUser);
+ async StateChangeEvent(uint64_t aID, uint64_t aState, bool aEnabled);
+ async CaretMoveEvent(uint64_t aID,
+ LayoutDeviceIntRect aCaretRect,
+ int32_t aOffset,
+ bool aIsSelectionCollapsed, bool aIsAtEndOfLine,
+ int32_t aGranularity, bool aFromUser);
+ async TextChangeEvent(uint64_t aID, nsString aStr, int32_t aStart, uint32_t aLen,
+ bool aIsInsert, bool aFromUser);
+ async SelectionEvent(uint64_t aID, uint64_t aWidgetID, uint32_t aType);
+ async RoleChangedEvent(role aRole, uint8_t aRoleMapEntryIndex);
+ async FocusEvent(uint64_t aID, LayoutDeviceIntRect aCaretRect);
+ async ScrollingEvent(uint64_t aID, uint64_t aType,
+ uint32_t aScrollX, uint32_t aScrollY,
+ uint32_t aMaxScrollX, uint32_t aMaxScrollY);
+#ifndef XP_WIN
+ async AnnouncementEvent(uint64_t aID,
+ nsString aAnnouncement,
+ uint16_t aPriority);
+#endif
+ async TextSelectionChangeEvent(uint64_t aID, TextRangeData[] aSelection);
+
+ /*
+ * Tell the parent document to bind the existing document as a new child
+ * document.
+ */
+ async BindChildDoc(PDocAccessible aChildDoc, uint64_t aID);
+
+ /*
+ * Cache The World
+ */
+ async Cache(CacheUpdateType aUpdateType, CacheData[] aData);
+
+ /*
+ * Lists of accessibles that either gained or lost a selected state.
+ */
+ async SelectedAccessiblesChanged(uint64_t[] aSelectedIDs, uint64_t[] aUnselectedIDs);
+
+ /*
+ * Tell the parent process that the given Accessibles are about to be moved
+ * via subsequent hide and show events.
+ */
+ async AccessiblesWillMove(uint64_t[] aIDs);
+
+child:
+ async __delete__();
+
+ /*
+ * Called as a result of focus shifting from chrome to content
+ * elements through keyboard navigation.
+ */
+ async RestoreFocus();
+
+ // LocalAccessible
+ async ScrollTo(uint64_t aID, uint32_t aScrollType);
+ async ScrollToPoint(uint64_t aID, uint32_t aScrollType, int32_t aX,
+ int32_t aY);
+#ifndef XP_WIN
+ async Announce(uint64_t aID, nsString aAnnouncement, uint16_t aPriority);
+#endif
+
+ // AccessibleText
+
+ async SetCaretOffset(uint64_t aID, int32_t aOffset);
+
+ async SetTextSelection(uint64_t aStartID, int32_t aStartOffset,
+ uint64_t aEndID, int32_t aEndOffset,
+ int32_t aSelectionNum);
+ async RemoveTextSelection(uint64_t aID, int32_t aSelectionNum);
+
+ async ScrollTextLeafRangeIntoView(uint64_t aStartID, int32_t aStartOffset,
+ uint64_t aEndID, int32_t aEndOffset,
+ uint32_t aScrollType);
+ async ScrollSubstringToPoint(uint64_t aID,
+ int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aCoordinateType,
+ int32_t aX, int32_t aY);
+
+ async ReplaceText(uint64_t aID, nsString aText);
+ async InsertText(uint64_t aID, nsString aText, int32_t aPosition);
+ async CopyText(uint64_t aID, int32_t aStartPos, int32_t aEndPos);
+ async CutText(uint64_t aID, int32_t aStartPos, int32_t aEndPos);
+ async DeleteText(uint64_t aID, int32_t aStartPos, int32_t aEndPos);
+ async PasteText(uint64_t aID, int32_t aPosition);
+
+ async TakeSelection(uint64_t aID);
+ async SetSelected(uint64_t aID, bool aSelected);
+
+ async DoActionAsync(uint64_t aID, uint8_t aIndex);
+
+ async SetCurValue(uint64_t aID, double aValue);
+
+ async TakeFocus(uint64_t aID);
+
+ /*
+ * Verify the cache. Used for testing purposes.
+ */
+ async VerifyCache(uint64_t aID, uint64_t aCacheDomain, nullable AccAttributes aFields);
+
+};
+
+}
+}
diff --git a/accessible/ipc/RemoteAccessible.cpp b/accessible/ipc/RemoteAccessible.cpp
new file mode 100644
index 0000000000..772fc58776
--- /dev/null
+++ b/accessible/ipc/RemoteAccessible.cpp
@@ -0,0 +1,2092 @@
+/* -*- 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 "CachedTableAccessible.h"
+#include "RemoteAccessible.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/a11y/DocManager.h"
+#include "mozilla/a11y/Platform.h"
+#include "mozilla/a11y/TableAccessible.h"
+#include "mozilla/a11y/TableCellAccessible.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/gfx/Matrix.h"
+#include "nsAccessibilityService.h"
+#include "mozilla/Unused.h"
+#include "nsAccUtils.h"
+#include "nsTextEquivUtils.h"
+#include "Pivot.h"
+#include "Relation.h"
+#include "mozilla/a11y/RelationType.h"
+#include "xpcAccessibleDocument.h"
+
+#ifdef A11Y_LOG
+# include "Logging.h"
+# define VERIFY_CACHE(domain) \
+ if (logging::IsEnabled(logging::eCache)) { \
+ Unused << mDoc->SendVerifyCache(mID, domain, mCachedFields); \
+ }
+#else
+# define VERIFY_CACHE(domain) \
+ do { \
+ } while (0)
+
+#endif
+
+namespace mozilla {
+namespace a11y {
+
+void RemoteAccessible::Shutdown() {
+ MOZ_DIAGNOSTIC_ASSERT(!IsDoc());
+ xpcAccessibleDocument* xpcDoc =
+ GetAccService()->GetCachedXPCDocument(Document());
+ if (xpcDoc) {
+ xpcDoc->NotifyOfShutdown(static_cast<RemoteAccessible*>(this));
+ }
+
+ if (IsTable() || IsTableCell()) {
+ CachedTableAccessible::Invalidate(this);
+ }
+
+ // Remove this acc's relation map from the doc's map of
+ // reverse relations. Prune forward relations associated with this
+ // acc's reverse relations. This also removes the acc's map of reverse
+ // rels from the mDoc's mReverseRelations.
+ PruneRelationsOnShutdown();
+
+ // XXX Ideally this wouldn't be necessary, but it seems OuterDoc
+ // accessibles can be destroyed before the doc they own.
+ uint32_t childCount = mChildren.Length();
+ if (!IsOuterDoc()) {
+ for (uint32_t idx = 0; idx < childCount; idx++) mChildren[idx]->Shutdown();
+ } else {
+ if (childCount > 1) {
+ MOZ_CRASH("outer doc has too many documents!");
+ } else if (childCount == 1) {
+ mChildren[0]->AsDoc()->Unbind();
+ }
+ }
+
+ mChildren.Clear();
+ ProxyDestroyed(static_cast<RemoteAccessible*>(this));
+ mDoc->RemoveAccessible(static_cast<RemoteAccessible*>(this));
+}
+
+void RemoteAccessible::SetChildDoc(DocAccessibleParent* aChildDoc) {
+ MOZ_ASSERT(aChildDoc);
+ MOZ_ASSERT(mChildren.Length() == 0);
+ mChildren.AppendElement(aChildDoc);
+}
+
+void RemoteAccessible::ClearChildDoc(DocAccessibleParent* aChildDoc) {
+ MOZ_ASSERT(aChildDoc);
+ // This is possible if we're replacing one document with another: Doc 1
+ // has not had a chance to remove itself, but was already replaced by Doc 2
+ // in SetChildDoc(). This could result in two subsequent calls to
+ // ClearChildDoc() even though mChildren.Length() == 1.
+ MOZ_ASSERT(mChildren.Length() <= 1);
+ mChildren.RemoveElement(aChildDoc);
+}
+
+uint32_t RemoteAccessible::EmbeddedChildCount() {
+ size_t count = 0, kids = mChildren.Length();
+ for (size_t i = 0; i < kids; i++) {
+ if (mChildren[i]->IsEmbeddedObject()) {
+ count++;
+ }
+ }
+
+ return count;
+}
+
+int32_t RemoteAccessible::IndexOfEmbeddedChild(Accessible* aChild) {
+ size_t index = 0, kids = mChildren.Length();
+ for (size_t i = 0; i < kids; i++) {
+ if (mChildren[i]->IsEmbeddedObject()) {
+ if (mChildren[i] == aChild) {
+ return index;
+ }
+
+ index++;
+ }
+ }
+
+ return -1;
+}
+
+Accessible* RemoteAccessible::EmbeddedChildAt(uint32_t aChildIdx) {
+ size_t index = 0, kids = mChildren.Length();
+ for (size_t i = 0; i < kids; i++) {
+ if (!mChildren[i]->IsEmbeddedObject()) {
+ continue;
+ }
+
+ if (index == aChildIdx) {
+ return mChildren[i];
+ }
+
+ index++;
+ }
+
+ return nullptr;
+}
+
+LocalAccessible* RemoteAccessible::OuterDocOfRemoteBrowser() const {
+ auto tab = static_cast<dom::BrowserParent*>(mDoc->Manager());
+ dom::Element* frame = tab->GetOwnerElement();
+ NS_ASSERTION(frame, "why isn't the tab in a frame!");
+ if (!frame) return nullptr;
+
+ DocAccessible* chromeDoc = GetExistingDocAccessible(frame->OwnerDoc());
+
+ return chromeDoc ? chromeDoc->GetAccessible(frame) : nullptr;
+}
+
+void RemoteAccessible::SetParent(RemoteAccessible* aParent) {
+ if (!aParent) {
+ mParent = kNoParent;
+ } else {
+ MOZ_ASSERT(!IsDoc() || !aParent->IsDoc());
+ mParent = aParent->ID();
+ }
+}
+
+RemoteAccessible* RemoteAccessible::RemoteParent() const {
+ if (mParent == kNoParent) {
+ return nullptr;
+ }
+
+ // if we are not a document then are parent is another proxy in the same
+ // document. That means we can just ask our document for the proxy with our
+ // parent id.
+ if (!IsDoc()) {
+ return Document()->GetAccessible(mParent);
+ }
+
+ // If we are a top level document then our parent is not a proxy.
+ if (AsDoc()->IsTopLevel()) {
+ return nullptr;
+ }
+
+ // Finally if we are a non top level document then our parent id is for a
+ // proxy in our parent document so get the proxy from there.
+ DocAccessibleParent* parentDoc = AsDoc()->ParentDoc();
+ MOZ_ASSERT(parentDoc);
+ MOZ_ASSERT(mParent);
+ return parentDoc->GetAccessible(mParent);
+}
+
+void RemoteAccessible::ApplyCache(CacheUpdateType aUpdateType,
+ AccAttributes* aFields) {
+ if (!aFields) {
+ MOZ_ASSERT_UNREACHABLE("ApplyCache called with aFields == null");
+ return;
+ }
+
+ const nsTArray<bool> relUpdatesNeeded = PreProcessRelations(aFields);
+ if (auto maybeViewportCache =
+ aFields->GetAttribute<nsTArray<uint64_t>>(CacheKey::Viewport)) {
+ // Updating the viewport cache means the offscreen state of this
+ // document's accessibles has changed. Update the HashSet we use for
+ // checking offscreen state here.
+ MOZ_ASSERT(IsDoc(),
+ "Fetched the viewport cache from a non-doc accessible?");
+ AsDoc()->mOnScreenAccessibles.Clear();
+ for (auto id : *maybeViewportCache) {
+ AsDoc()->mOnScreenAccessibles.Insert(id);
+ }
+ }
+
+ if (aUpdateType == CacheUpdateType::Initial) {
+ mCachedFields = aFields;
+ } else {
+ if (!mCachedFields) {
+ // The fields cache can be uninitialized if there were no cache-worthy
+ // fields in the initial cache push.
+ // We don't do a simple assign because we don't want to store the
+ // DeleteEntry entries.
+ mCachedFields = new AccAttributes();
+ }
+ mCachedFields->Update(aFields);
+ }
+
+ if (IsTextLeaf()) {
+ RemoteAccessible* parent = RemoteParent();
+ if (parent && parent->IsHyperText()) {
+ parent->InvalidateCachedHyperTextOffsets();
+ }
+ }
+
+ PostProcessRelations(relUpdatesNeeded);
+}
+
+ENameValueFlag RemoteAccessible::Name(nsString& aName) const {
+ ENameValueFlag nameFlag = eNameOK;
+ if (mCachedFields) {
+ if (IsText()) {
+ mCachedFields->GetAttribute(CacheKey::Text, aName);
+ return eNameOK;
+ }
+ auto cachedNameFlag =
+ mCachedFields->GetAttribute<int32_t>(CacheKey::NameValueFlag);
+ if (cachedNameFlag) {
+ nameFlag = static_cast<ENameValueFlag>(*cachedNameFlag);
+ }
+ if (mCachedFields->GetAttribute(CacheKey::Name, aName)) {
+ VERIFY_CACHE(CacheDomain::NameAndDescription);
+ return nameFlag;
+ }
+ }
+
+ MOZ_ASSERT(aName.IsEmpty());
+ aName.SetIsVoid(true);
+ return nameFlag;
+}
+
+void RemoteAccessible::Description(nsString& aDescription) const {
+ if (mCachedFields) {
+ mCachedFields->GetAttribute(CacheKey::Description, aDescription);
+ VERIFY_CACHE(CacheDomain::NameAndDescription);
+ }
+}
+
+void RemoteAccessible::Value(nsString& aValue) const {
+ if (mCachedFields) {
+ if (mCachedFields->HasAttribute(CacheKey::TextValue)) {
+ mCachedFields->GetAttribute(CacheKey::TextValue, aValue);
+ VERIFY_CACHE(CacheDomain::Value);
+ return;
+ }
+
+ if (HasNumericValue()) {
+ double checkValue = CurValue();
+ if (!std::isnan(checkValue)) {
+ aValue.AppendFloat(checkValue);
+ }
+ return;
+ }
+
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ // Value of textbox is a textified subtree.
+ if (roleMapEntry && roleMapEntry->Is(nsGkAtoms::textbox)) {
+ nsTextEquivUtils::GetTextEquivFromSubtree(this, aValue);
+ return;
+ }
+
+ if (IsCombobox()) {
+ // For combo boxes, rely on selection state to determine the value.
+ const Accessible* option =
+ const_cast<RemoteAccessible*>(this)->GetSelectedItem(0);
+ if (option) {
+ option->Name(aValue);
+ } else {
+ // If no selected item, determine the value from descendant elements.
+ nsTextEquivUtils::GetTextEquivFromSubtree(this, aValue);
+ }
+ return;
+ }
+
+ if (IsTextLeaf() || IsImage()) {
+ if (const Accessible* actionAcc = ActionAncestor()) {
+ if (const_cast<Accessible*>(actionAcc)->State() & states::LINKED) {
+ // Text and image descendants of links expose the link URL as the
+ // value.
+ return actionAcc->Value(aValue);
+ }
+ }
+ }
+ }
+}
+
+double RemoteAccessible::CurValue() const {
+ if (mCachedFields) {
+ if (auto value =
+ mCachedFields->GetAttribute<double>(CacheKey::NumericValue)) {
+ VERIFY_CACHE(CacheDomain::Value);
+ return *value;
+ }
+ }
+
+ return UnspecifiedNaN<double>();
+}
+
+double RemoteAccessible::MinValue() const {
+ if (mCachedFields) {
+ if (auto min = mCachedFields->GetAttribute<double>(CacheKey::MinValue)) {
+ VERIFY_CACHE(CacheDomain::Value);
+ return *min;
+ }
+ }
+
+ return UnspecifiedNaN<double>();
+}
+
+double RemoteAccessible::MaxValue() const {
+ if (mCachedFields) {
+ if (auto max = mCachedFields->GetAttribute<double>(CacheKey::MaxValue)) {
+ VERIFY_CACHE(CacheDomain::Value);
+ return *max;
+ }
+ }
+
+ return UnspecifiedNaN<double>();
+}
+
+double RemoteAccessible::Step() const {
+ if (mCachedFields) {
+ if (auto step = mCachedFields->GetAttribute<double>(CacheKey::Step)) {
+ VERIFY_CACHE(CacheDomain::Value);
+ return *step;
+ }
+ }
+
+ return UnspecifiedNaN<double>();
+}
+
+bool RemoteAccessible::SetCurValue(double aValue) {
+ if (!HasNumericValue() || IsProgress()) {
+ return false;
+ }
+
+ const uint32_t kValueCannotChange = states::READONLY | states::UNAVAILABLE;
+ if (State() & kValueCannotChange) {
+ return false;
+ }
+
+ double checkValue = MinValue();
+ if (!std::isnan(checkValue) && aValue < checkValue) {
+ return false;
+ }
+
+ checkValue = MaxValue();
+ if (!std::isnan(checkValue) && aValue > checkValue) {
+ return false;
+ }
+
+ Unused << mDoc->SendSetCurValue(mID, aValue);
+ return true;
+}
+
+bool RemoteAccessible::ContainsPoint(int32_t aX, int32_t aY) {
+ if (!BoundsWithOffset(Nothing(), true).Contains(aX, aY)) {
+ return false;
+ }
+ if (!IsTextLeaf()) {
+ if (IsImage() || IsImageMap() || !HasChildren() ||
+ RefPtr{DisplayStyle()} != nsGkAtoms::inlinevalue) {
+ // This isn't an inline element that might contain text, so we don't need
+ // to walk lines. It's enough that our rect contains the point.
+ return true;
+ }
+ // Non-image inline elements with children can wrap across lines just like
+ // text leaves; see below.
+ // Walk the children, which will walk the lines of text in any text leaves.
+ uint32_t count = ChildCount();
+ for (uint32_t c = 0; c < count; ++c) {
+ RemoteAccessible* child = RemoteChildAt(c);
+ if (child->Role() == roles::TEXT_CONTAINER && child->IsClipped()) {
+ // There is a clipped child. This is a candidate for fuzzy hit testing.
+ // See RemoteAccessible::DoFuzzyHittesting.
+ return true;
+ }
+ if (child->ContainsPoint(aX, aY)) {
+ return true;
+ }
+ }
+ // None of our descendants contain the point, so nor do we.
+ return false;
+ }
+ // This is a text leaf. The text might wrap across lines, which means our
+ // rect might cover a wider area than the actual text. For example, if the
+ // text begins in the middle of the first line and wraps on to the second,
+ // the rect will cover the start of the first line and the end of the second.
+ auto lines = GetCachedTextLines();
+ if (!lines) {
+ // This means the text is empty or occupies a single line (but does not
+ // begin the line). In that case, the Bounds check above is sufficient,
+ // since there's only one rect.
+ return true;
+ }
+ uint32_t length = lines->Length();
+ MOZ_ASSERT(length > 0,
+ "Line starts shouldn't be in cache if there aren't any");
+ if (length == 0 || (length == 1 && (*lines)[0] == 0)) {
+ // This means the text begins and occupies a single line. Again, the Bounds
+ // check above is sufficient.
+ return true;
+ }
+ // Walk the lines of the text. Even if this text doesn't start at the
+ // beginning of a line (i.e. lines[0] > 0), we always want to consider its
+ // first line.
+ int32_t lineStart = 0;
+ for (uint32_t index = 0; index <= length; ++index) {
+ int32_t lineEnd;
+ if (index < length) {
+ int32_t nextLineStart = (*lines)[index];
+ if (nextLineStart == 0) {
+ // This Accessible starts at the beginning of a line. Here, we always
+ // treat 0 as the first line start anyway.
+ MOZ_ASSERT(index == 0);
+ continue;
+ }
+ lineEnd = nextLineStart - 1;
+ } else {
+ // This is the last line.
+ lineEnd = static_cast<int32_t>(nsAccUtils::TextLength(this)) - 1;
+ }
+ MOZ_ASSERT(lineEnd >= lineStart);
+ nsRect lineRect = GetCachedCharRect(lineStart);
+ if (lineEnd > lineStart) {
+ lineRect.UnionRect(lineRect, GetCachedCharRect(lineEnd));
+ }
+ if (BoundsWithOffset(Some(lineRect), true).Contains(aX, aY)) {
+ return true;
+ }
+ lineStart = lineEnd + 1;
+ }
+ return false;
+}
+
+RemoteAccessible* RemoteAccessible::DoFuzzyHittesting() {
+ uint32_t childCount = ChildCount();
+ if (!childCount) {
+ return nullptr;
+ }
+ // Check if this match has a clipped child.
+ // This usually indicates invisible text, and we're
+ // interested in returning the inner text content
+ // even if it doesn't contain the point we're hittesting.
+ RemoteAccessible* clippedContainer = nullptr;
+ for (uint32_t i = 0; i < childCount; i++) {
+ RemoteAccessible* child = RemoteChildAt(i);
+ if (child->Role() == roles::TEXT_CONTAINER) {
+ if (child->IsClipped()) {
+ clippedContainer = child;
+ break;
+ }
+ }
+ }
+ // If we found a clipped container, descend it in search of
+ // meaningful text leaves. Ignore non-text-leaf/text-container
+ // siblings.
+ RemoteAccessible* container = clippedContainer;
+ while (container) {
+ RemoteAccessible* textLeaf = nullptr;
+ bool continueSearch = false;
+ childCount = container->ChildCount();
+ for (uint32_t i = 0; i < childCount; i++) {
+ RemoteAccessible* child = container->RemoteChildAt(i);
+ if (child->Role() == roles::TEXT_CONTAINER) {
+ container = child;
+ continueSearch = true;
+ break;
+ }
+ if (child->IsTextLeaf()) {
+ textLeaf = child;
+ // Don't break here -- it's possible a text container
+ // exists as another sibling, and we should descend as
+ // deep as possible.
+ }
+ }
+ if (textLeaf) {
+ return textLeaf;
+ }
+ if (!continueSearch) {
+ // We didn't find anything useful in this set of siblings.
+ // Don't keep searching
+ break;
+ }
+ }
+ return nullptr;
+}
+
+Accessible* RemoteAccessible::ChildAtPoint(
+ int32_t aX, int32_t aY, LocalAccessible::EWhichChildAtPoint aWhichChild) {
+ // Elements that are partially on-screen should have their bounds masked by
+ // their containing scroll area so hittesting yields results that are
+ // consistent with the content's visual representation. Pass this value to
+ // bounds calculation functions to indicate that we're hittesting.
+ const bool hitTesting = true;
+
+ if (IsOuterDoc() && aWhichChild == EWhichChildAtPoint::DirectChild) {
+ // This is an iframe, which is as deep as the viewport cache goes. The
+ // caller wants a direct child, which can only be the embedded document.
+ if (BoundsWithOffset(Nothing(), hitTesting).Contains(aX, aY)) {
+ return RemoteFirstChild();
+ }
+ return nullptr;
+ }
+
+ RemoteAccessible* lastMatch = nullptr;
+ // If `this` is a document, use its viewport cache instead of
+ // the cache of its parent document.
+ if (DocAccessibleParent* doc = IsDoc() ? AsDoc() : mDoc) {
+ if (!doc->mCachedFields) {
+ // A client call might arrive after we've constructed doc but before we
+ // get a cache push for it.
+ return nullptr;
+ }
+ if (auto maybeViewportCache =
+ doc->mCachedFields->GetAttribute<nsTArray<uint64_t>>(
+ CacheKey::Viewport)) {
+ // The retrieved viewport cache contains acc IDs in hittesting order.
+ // That is, items earlier in the list have z-indexes that are larger than
+ // those later in the list. If you were to build a tree by z-index, where
+ // chilren have larger z indices than their parents, iterating this list
+ // is essentially a postorder tree traversal.
+ const nsTArray<uint64_t>& viewportCache = *maybeViewportCache;
+
+ for (auto id : viewportCache) {
+ RemoteAccessible* acc = doc->GetAccessible(id);
+ if (!acc) {
+ // This can happen if the acc died in between
+ // pushing the viewport cache and doing this hittest
+ continue;
+ }
+
+ if (acc->IsOuterDoc() &&
+ aWhichChild == EWhichChildAtPoint::DeepestChild &&
+ acc->BoundsWithOffset(Nothing(), hitTesting).Contains(aX, aY)) {
+ // acc is an iframe, which is as deep as the viewport cache goes. This
+ // iframe contains the requested point.
+ RemoteAccessible* innerDoc = acc->RemoteFirstChild();
+ if (innerDoc) {
+ MOZ_ASSERT(innerDoc->IsDoc());
+ // Search the embedded document's viewport cache so we return the
+ // deepest descendant in that embedded document.
+ Accessible* deepestAcc = innerDoc->ChildAtPoint(
+ aX, aY, EWhichChildAtPoint::DeepestChild);
+ MOZ_ASSERT(!deepestAcc || deepestAcc->IsRemote());
+ lastMatch = deepestAcc ? deepestAcc->AsRemote() : nullptr;
+ break;
+ }
+ // If there is no embedded document, the iframe itself is the deepest
+ // descendant.
+ lastMatch = acc;
+ break;
+ }
+
+ if (acc == this) {
+ MOZ_ASSERT(!acc->IsOuterDoc());
+ // Even though we're searching from the doc's cache
+ // this call shouldn't pass the boundary defined by
+ // the acc this call originated on. If we hit `this`,
+ // return our most recent match.
+ if (!lastMatch &&
+ BoundsWithOffset(Nothing(), hitTesting).Contains(aX, aY)) {
+ // If we haven't found a match, but `this` contains the point we're
+ // looking for, set it as our temp last match so we can
+ // (potentially) do fuzzy hittesting on it below.
+ lastMatch = acc;
+ }
+ break;
+ }
+
+ if (acc->ContainsPoint(aX, aY)) {
+ // Because our rects are in hittesting order, the
+ // first match we encounter is guaranteed to be the
+ // deepest match.
+ lastMatch = acc;
+ break;
+ }
+ }
+ if (lastMatch) {
+ RemoteAccessible* fuzzyMatch = lastMatch->DoFuzzyHittesting();
+ lastMatch = fuzzyMatch ? fuzzyMatch : lastMatch;
+ }
+ }
+ }
+
+ if (aWhichChild == EWhichChildAtPoint::DirectChild && lastMatch) {
+ // lastMatch is the deepest match. Walk up to the direct child of this.
+ RemoteAccessible* parent = lastMatch->RemoteParent();
+ for (;;) {
+ if (parent == this) {
+ break;
+ }
+ if (!parent || parent->IsDoc()) {
+ // `this` is not an ancestor of lastMatch. Ignore lastMatch.
+ lastMatch = nullptr;
+ break;
+ }
+ lastMatch = parent;
+ parent = parent->RemoteParent();
+ }
+ } else if (aWhichChild == EWhichChildAtPoint::DeepestChild && lastMatch &&
+ !IsDoc() && !IsAncestorOf(lastMatch)) {
+ // If we end up with a match that is not in the ancestor chain
+ // of the accessible this call originated on, we should ignore it.
+ // This can happen when the aX, aY given are outside `this`.
+ lastMatch = nullptr;
+ }
+
+ if (!lastMatch && BoundsWithOffset(Nothing(), hitTesting).Contains(aX, aY)) {
+ // Even though the hit target isn't inside `this`, the point is still
+ // within our bounds, so fall back to `this`.
+ return this;
+ }
+
+ return lastMatch;
+}
+
+Maybe<nsRect> RemoteAccessible::RetrieveCachedBounds() const {
+ if (!mCachedFields) {
+ return Nothing();
+ }
+
+ Maybe<const nsTArray<int32_t>&> maybeArray =
+ mCachedFields->GetAttribute<nsTArray<int32_t>>(
+ CacheKey::ParentRelativeBounds);
+ if (maybeArray) {
+ const nsTArray<int32_t>& relativeBoundsArr = *maybeArray;
+ MOZ_ASSERT(relativeBoundsArr.Length() == 4,
+ "Incorrectly sized bounds array");
+ nsRect relativeBoundsRect(relativeBoundsArr[0], relativeBoundsArr[1],
+ relativeBoundsArr[2], relativeBoundsArr[3]);
+ return Some(relativeBoundsRect);
+ }
+
+ return Nothing();
+}
+
+void RemoteAccessible::ApplyCrossDocOffset(nsRect& aBounds) const {
+ if (!IsDoc()) {
+ // We should only apply cross-doc offsets to documents. If we're anything
+ // else, return early here.
+ return;
+ }
+
+ RemoteAccessible* parentAcc = RemoteParent();
+ if (!parentAcc || !parentAcc->IsOuterDoc()) {
+ return;
+ }
+
+ Maybe<const nsTArray<int32_t>&> maybeOffset =
+ parentAcc->mCachedFields->GetAttribute<nsTArray<int32_t>>(
+ CacheKey::CrossDocOffset);
+ if (!maybeOffset) {
+ return;
+ }
+
+ MOZ_ASSERT(maybeOffset->Length() == 2);
+ const nsTArray<int32_t>& offset = *maybeOffset;
+ // Our retrieved value is in app units, so we don't need to do any
+ // unit conversion here.
+ aBounds.MoveBy(offset[0], offset[1]);
+}
+
+bool RemoteAccessible::ApplyTransform(nsRect& aCumulativeBounds) const {
+ // First, attempt to retrieve the transform from the cache.
+ Maybe<const UniquePtr<gfx::Matrix4x4>&> maybeTransform =
+ mCachedFields->GetAttribute<UniquePtr<gfx::Matrix4x4>>(
+ CacheKey::TransformMatrix);
+ if (!maybeTransform) {
+ return false;
+ }
+
+ auto mtxInPixels = gfx::Matrix4x4Typed<CSSPixel, CSSPixel>::FromUnknownMatrix(
+ *(*maybeTransform));
+
+ // Our matrix is in CSS Pixels, so we need our rect to be in CSS
+ // Pixels too. Convert before applying.
+ auto boundsInPixels = CSSRect::FromAppUnits(aCumulativeBounds);
+ boundsInPixels = mtxInPixels.TransformBounds(boundsInPixels);
+ aCumulativeBounds = CSSRect::ToAppUnits(boundsInPixels);
+
+ return true;
+}
+
+bool RemoteAccessible::ApplyScrollOffset(nsRect& aBounds) const {
+ Maybe<const nsTArray<int32_t>&> maybeScrollPosition =
+ mCachedFields->GetAttribute<nsTArray<int32_t>>(CacheKey::ScrollPosition);
+
+ if (!maybeScrollPosition || maybeScrollPosition->Length() != 2) {
+ return false;
+ }
+ // Our retrieved value is in app units, so we don't need to do any
+ // unit conversion here.
+ const nsTArray<int32_t>& scrollPosition = *maybeScrollPosition;
+
+ // Scroll position is an inverse representation of scroll offset (since the
+ // further the scroll bar moves down the page, the further the page content
+ // moves up/closer to the origin).
+ nsPoint scrollOffset(-scrollPosition[0], -scrollPosition[1]);
+
+ aBounds.MoveBy(scrollOffset.x, scrollOffset.y);
+
+ // Return true here even if the scroll offset was 0,0 because the RV is used
+ // as a scroll container indicator. Non-scroll containers won't have cached
+ // scroll position.
+ return true;
+}
+
+nsRect RemoteAccessible::BoundsInAppUnits() const {
+ if (dom::CanonicalBrowsingContext* cbc = mDoc->GetBrowsingContext()->Top()) {
+ if (dom::BrowserParent* bp = cbc->GetBrowserParent()) {
+ DocAccessibleParent* topDoc = bp->GetTopLevelDocAccessible();
+ if (topDoc && topDoc->mCachedFields) {
+ auto appUnitsPerDevPixel = topDoc->mCachedFields->GetAttribute<int32_t>(
+ CacheKey::AppUnitsPerDevPixel);
+ MOZ_ASSERT(appUnitsPerDevPixel);
+ return LayoutDeviceIntRect::ToAppUnits(Bounds(), *appUnitsPerDevPixel);
+ }
+ }
+ }
+ return LayoutDeviceIntRect::ToAppUnits(Bounds(), AppUnitsPerCSSPixel());
+}
+
+bool RemoteAccessible::IsFixedPos() const {
+ MOZ_ASSERT(mCachedFields);
+ if (auto maybePosition =
+ mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::CssPosition)) {
+ return *maybePosition == nsGkAtoms::fixed;
+ }
+
+ return false;
+}
+
+bool RemoteAccessible::IsOverflowHidden() const {
+ MOZ_ASSERT(mCachedFields);
+ if (auto maybeOverflow =
+ mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::CSSOverflow)) {
+ return *maybeOverflow == nsGkAtoms::hidden;
+ }
+
+ return false;
+}
+
+bool RemoteAccessible::IsClipped() const {
+ MOZ_ASSERT(mCachedFields);
+ if (mCachedFields->GetAttribute<bool>(CacheKey::IsClipped)) {
+ return true;
+ }
+
+ return false;
+}
+
+LayoutDeviceIntRect RemoteAccessible::BoundsWithOffset(
+ Maybe<nsRect> aOffset, bool aBoundsAreForHittesting) const {
+ Maybe<nsRect> maybeBounds = RetrieveCachedBounds();
+ if (maybeBounds) {
+ nsRect bounds = *maybeBounds;
+ // maybeBounds is parent-relative. However, the transform matrix we cache
+ // (if any) is meant to operate on self-relative rects. Therefore, make
+ // bounds self-relative until after we transform.
+ bounds.MoveTo(0, 0);
+ const DocAccessibleParent* topDoc = IsDoc() ? AsDoc() : nullptr;
+
+ if (aOffset.isSome()) {
+ // The rect we've passed in is in app units, so no conversion needed.
+ nsRect internalRect = *aOffset;
+ bounds.SetRectX(bounds.x + internalRect.x, internalRect.width);
+ bounds.SetRectY(bounds.y + internalRect.y, internalRect.height);
+ }
+
+ Unused << ApplyTransform(bounds);
+ // Now apply the parent-relative offset.
+ bounds.MoveBy(maybeBounds->TopLeft());
+
+ ApplyCrossDocOffset(bounds);
+
+ LayoutDeviceIntRect devPxBounds;
+ const Accessible* acc = Parent();
+ bool encounteredFixedContainer = IsFixedPos();
+ while (acc && acc->IsRemote()) {
+ // Return early if we're hit testing and our cumulative bounds are empty,
+ // since walking the ancestor chain won't produce any hits.
+ if (aBoundsAreForHittesting && bounds.IsEmpty()) {
+ return LayoutDeviceIntRect{};
+ }
+
+ RemoteAccessible* remoteAcc = const_cast<Accessible*>(acc)->AsRemote();
+
+ if (Maybe<nsRect> maybeRemoteBounds = remoteAcc->RetrieveCachedBounds()) {
+ nsRect remoteBounds = *maybeRemoteBounds;
+ // We need to take into account a non-1 resolution set on the
+ // presshell. This happens with async pinch zooming, among other
+ // things. We can't reliably query this value in the parent process,
+ // so we retrieve it from the document's cache.
+ if (remoteAcc->IsDoc()) {
+ // Apply the document's resolution to the bounds we've gathered
+ // thus far. We do this before applying the document's offset
+ // because document accs should not have their bounds scaled by
+ // their own resolution. They should be scaled by the resolution
+ // of their containing document (if any).
+ Maybe<float> res =
+ remoteAcc->AsDoc()->mCachedFields->GetAttribute<float>(
+ CacheKey::Resolution);
+ MOZ_ASSERT(res, "No cached document resolution found.");
+ bounds.ScaleRoundOut(res.valueOr(1.0f));
+
+ topDoc = remoteAcc->AsDoc();
+ }
+
+ // We don't account for the document offset of iframes when
+ // computing parent-relative bounds. Instead, we store this value
+ // separately on all iframes and apply it here. See the comments in
+ // LocalAccessible::BundleFieldsForCache where we set the
+ // nsGkAtoms::crossorigin attribute.
+ remoteAcc->ApplyCrossDocOffset(remoteBounds);
+ if (!encounteredFixedContainer) {
+ // Apply scroll offset, if applicable. Only the contents of an
+ // element are affected by its scroll offset, which is why this call
+ // happens in this loop instead of both inside and outside of
+ // the loop (like ApplyTransform).
+ // Never apply scroll offsets past a fixed container.
+ const bool hasScrollArea = remoteAcc->ApplyScrollOffset(bounds);
+
+ // If we are hit testing and the Accessible has a scroll area, ensure
+ // that the bounds we've calculated so far are constrained to the
+ // bounds of the scroll area. Without this, we'll "hit" the off-screen
+ // portions of accs that are are partially (but not fully) within the
+ // scroll area. This is also a problem for accs with overflow:hidden;
+ if (aBoundsAreForHittesting &&
+ (hasScrollArea || remoteAcc->IsOverflowHidden())) {
+ nsRect selfRelativeVisibleBounds(0, 0, remoteBounds.width,
+ remoteBounds.height);
+ bounds = bounds.SafeIntersect(selfRelativeVisibleBounds);
+ }
+ }
+ if (remoteAcc->IsDoc()) {
+ // Fixed elements are document relative, so if we've hit a
+ // document we're now subject to that document's styling
+ // (including scroll offsets that operate on it).
+ // This ordering is important, we don't want to apply scroll
+ // offsets on this doc's content.
+ encounteredFixedContainer = false;
+ }
+ if (!encounteredFixedContainer) {
+ // The transform matrix we cache (if any) is meant to operate on
+ // self-relative rects. Therefore, we must apply the transform before
+ // we make bounds parent-relative.
+ Unused << remoteAcc->ApplyTransform(bounds);
+ // Regardless of whether this is a doc, we should offset `bounds`
+ // by the bounds retrieved here. This is how we build screen
+ // coordinates from relative coordinates.
+ bounds.MoveBy(remoteBounds.X(), remoteBounds.Y());
+ }
+
+ if (remoteAcc->IsFixedPos()) {
+ encounteredFixedContainer = true;
+ }
+ // we can't just break here if we're scroll suppressed because we still
+ // need to find the top doc.
+ }
+ acc = acc->Parent();
+ }
+
+ MOZ_ASSERT(topDoc);
+ if (topDoc) {
+ // We use the top documents app-units-per-dev-pixel even though
+ // theoretically nested docs can have different values. Practically,
+ // that isn't likely since we only offer zoom controls for the top
+ // document and all subdocuments inherit from it.
+ auto appUnitsPerDevPixel = topDoc->mCachedFields->GetAttribute<int32_t>(
+ CacheKey::AppUnitsPerDevPixel);
+ MOZ_ASSERT(appUnitsPerDevPixel);
+ if (appUnitsPerDevPixel) {
+ // Convert our existing `bounds` rect from app units to dev pixels
+ devPxBounds = LayoutDeviceIntRect::FromAppUnitsToNearest(
+ bounds, *appUnitsPerDevPixel);
+ }
+ }
+
+#if !defined(ANDROID)
+ // This block is not thread safe because it queries a LocalAccessible.
+ // It is also not needed in Android since the only local accessible is
+ // the outer doc browser that has an offset of 0.
+ // acc could be null if the OuterDocAccessible died before the top level
+ // DocAccessibleParent.
+ if (LocalAccessible* localAcc =
+ acc ? const_cast<Accessible*>(acc)->AsLocal() : nullptr) {
+ // LocalAccessible::Bounds returns screen-relative bounds in
+ // dev pixels.
+ LayoutDeviceIntRect localBounds = localAcc->Bounds();
+
+ // The root document will always have an APZ resolution of 1,
+ // so we don't factor in its scale here. We also don't scale
+ // by GetFullZoom because LocalAccessible::Bounds already does
+ // that.
+ devPxBounds.MoveBy(localBounds.X(), localBounds.Y());
+ }
+#endif
+
+ return devPxBounds;
+ }
+
+ return LayoutDeviceIntRect();
+}
+
+LayoutDeviceIntRect RemoteAccessible::Bounds() const {
+ return BoundsWithOffset(Nothing());
+}
+
+Relation RemoteAccessible::RelationByType(RelationType aType) const {
+ // We are able to handle some relations completely in the
+ // parent process, without the help of the cache. Those
+ // relations are enumerated here. Other relations, whose
+ // types are stored in kRelationTypeAtoms, are processed
+ // below using the cache.
+ if (aType == RelationType::CONTAINING_TAB_PANE) {
+ if (dom::CanonicalBrowsingContext* cbc = mDoc->GetBrowsingContext()) {
+ if (dom::CanonicalBrowsingContext* topCbc = cbc->Top()) {
+ if (dom::BrowserParent* bp = topCbc->GetBrowserParent()) {
+ return Relation(bp->GetTopLevelDocAccessible());
+ }
+ }
+ }
+ return Relation();
+ }
+
+ if (aType == RelationType::LINKS_TO && Role() == roles::LINK) {
+ Pivot p = Pivot(mDoc);
+ nsString href;
+ Value(href);
+ int32_t i = href.FindChar('#');
+ int32_t len = static_cast<int32_t>(href.Length());
+ if (i != -1 && i < (len - 1)) {
+ nsDependentSubstring anchorName = Substring(href, i + 1, len);
+ MustPruneSameDocRule rule;
+ Accessible* nameMatch = nullptr;
+ for (Accessible* match = p.Next(mDoc, rule); match;
+ match = p.Next(match, rule)) {
+ nsString currID;
+ match->DOMNodeID(currID);
+ MOZ_ASSERT(match->IsRemote());
+ if (anchorName.Equals(currID)) {
+ return Relation(match->AsRemote());
+ }
+ if (!nameMatch) {
+ nsString currName = match->AsRemote()->GetCachedHTMLNameAttribute();
+ if (match->TagName() == nsGkAtoms::a && anchorName.Equals(currName)) {
+ // If we find an element with a matching ID, we should return
+ // that, but if we don't we should return the first anchor with
+ // a matching name. To avoid doing two traversals, store the first
+ // name match here.
+ nameMatch = match;
+ }
+ }
+ }
+ return nameMatch ? Relation(nameMatch->AsRemote()) : Relation();
+ }
+
+ return Relation();
+ }
+
+ // Handle ARIA tree, treegrid parent/child relations. Each of these cases
+ // relies on cached group info. To find the parent of an accessible, use the
+ // unified conceptual parent.
+ if (aType == RelationType::NODE_CHILD_OF) {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (roleMapEntry && (roleMapEntry->role == roles::OUTLINEITEM ||
+ roleMapEntry->role == roles::LISTITEM ||
+ roleMapEntry->role == roles::ROW)) {
+ if (const AccGroupInfo* groupInfo =
+ const_cast<RemoteAccessible*>(this)->GetOrCreateGroupInfo()) {
+ return Relation(groupInfo->ConceptualParent());
+ }
+ }
+ return Relation();
+ }
+
+ // To find the children of a parent, provide an iterator through its items.
+ if (aType == RelationType::NODE_PARENT_OF) {
+ const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
+ if (roleMapEntry && (roleMapEntry->role == roles::OUTLINEITEM ||
+ roleMapEntry->role == roles::LISTITEM ||
+ roleMapEntry->role == roles::ROW ||
+ roleMapEntry->role == roles::OUTLINE ||
+ roleMapEntry->role == roles::LIST ||
+ roleMapEntry->role == roles::TREE_TABLE)) {
+ return Relation(new ItemIterator(this));
+ }
+ return Relation();
+ }
+
+ if (aType == RelationType::MEMBER_OF) {
+ Relation rel = Relation();
+ // HTML radio buttons with cached names should be grouped.
+ if (IsHTMLRadioButton()) {
+ nsString name = GetCachedHTMLNameAttribute();
+ if (name.IsEmpty()) {
+ return rel;
+ }
+
+ RemoteAccessible* ancestor = RemoteParent();
+ while (ancestor && ancestor->Role() != roles::FORM && ancestor != mDoc) {
+ ancestor = ancestor->RemoteParent();
+ }
+ if (ancestor) {
+ // Sometimes we end up with an unparented acc here, potentially
+ // because the acc is being moved. See bug 1807639.
+ // Pivot expects to be created with a non-null mRoot.
+ Pivot p = Pivot(ancestor);
+ PivotRadioNameRule rule(name);
+ Accessible* match = p.Next(ancestor, rule);
+ while (match) {
+ rel.AppendTarget(match->AsRemote());
+ match = p.Next(match, rule);
+ }
+ }
+ return rel;
+ }
+
+ if (IsARIARole(nsGkAtoms::radio)) {
+ // ARIA radio buttons should be grouped by their radio group
+ // parent, if one exists.
+ RemoteAccessible* currParent = RemoteParent();
+ while (currParent && currParent->Role() != roles::RADIO_GROUP) {
+ currParent = currParent->RemoteParent();
+ }
+
+ if (currParent && currParent->Role() == roles::RADIO_GROUP) {
+ // If we found a radiogroup parent, search for all
+ // roles::RADIOBUTTON children and add them to our relation.
+ // This search will include the radio button this method
+ // was called from, which is expected.
+ Pivot p = Pivot(currParent);
+ PivotRoleRule rule(roles::RADIOBUTTON);
+ Accessible* match = p.Next(currParent, rule);
+ while (match) {
+ MOZ_ASSERT(match->IsRemote(),
+ "We should only be traversing the remote tree.");
+ rel.AppendTarget(match->AsRemote());
+ match = p.Next(match, rule);
+ }
+ }
+ }
+ // By webkit's standard, aria radio buttons do not get grouped
+ // if they lack a group parent, so we return an empty
+ // relation here if the above check fails.
+ return rel;
+ }
+
+ Relation rel;
+ if (!mCachedFields) {
+ return rel;
+ }
+
+ for (const auto& data : kRelationTypeAtoms) {
+ if (data.mType != aType ||
+ (data.mValidTag && TagName() != data.mValidTag)) {
+ continue;
+ }
+
+ if (auto maybeIds =
+ mCachedFields->GetAttribute<nsTArray<uint64_t>>(data.mAtom)) {
+ rel.AppendIter(new RemoteAccIterator(*maybeIds, Document()));
+ }
+ // Each relation type has only one relevant cached attribute,
+ // so break after we've handled the attr for this type,
+ // even if we didn't find any targets.
+ break;
+ }
+
+ if (auto accRelMapEntry = mDoc->mReverseRelations.Lookup(ID())) {
+ if (auto reverseIdsEntry = accRelMapEntry.Data().Lookup(aType)) {
+ rel.AppendIter(new RemoteAccIterator(reverseIdsEntry.Data(), Document()));
+ }
+ }
+
+ // We handle these relations here rather than before cached relations because
+ // the cached relations need to take precedence. For example, a <figure> with
+ // both aria-labelledby and a <figcaption> must return two LABELLED_BY
+ // targets: the aria-labelledby and then the <figcaption>.
+ if (aType == RelationType::LABELLED_BY && TagName() == nsGkAtoms::figure) {
+ uint32_t count = ChildCount();
+ for (uint32_t c = 0; c < count; ++c) {
+ RemoteAccessible* child = RemoteChildAt(c);
+ MOZ_ASSERT(child);
+ if (child->TagName() == nsGkAtoms::figcaption) {
+ rel.AppendTarget(child);
+ }
+ }
+ } else if (aType == RelationType::LABEL_FOR &&
+ TagName() == nsGkAtoms::figcaption) {
+ if (RemoteAccessible* parent = RemoteParent()) {
+ if (parent->TagName() == nsGkAtoms::figure) {
+ rel.AppendTarget(parent);
+ }
+ }
+ }
+
+ return rel;
+}
+
+void RemoteAccessible::AppendTextTo(nsAString& aText, uint32_t aStartOffset,
+ uint32_t aLength) {
+ if (IsText()) {
+ if (mCachedFields) {
+ if (auto text = mCachedFields->GetAttribute<nsString>(CacheKey::Text)) {
+ aText.Append(Substring(*text, aStartOffset, aLength));
+ }
+ VERIFY_CACHE(CacheDomain::Text);
+ }
+ return;
+ }
+
+ if (aStartOffset != 0 || aLength == 0) {
+ return;
+ }
+
+ if (IsHTMLBr()) {
+ aText += kForcedNewLineChar;
+ } else if (RemoteParent() && nsAccUtils::MustPrune(RemoteParent())) {
+ // Expose the embedded object accessible as imaginary embedded object
+ // character if its parent hypertext accessible doesn't expose children to
+ // AT.
+ aText += kImaginaryEmbeddedObjectChar;
+ } else {
+ aText += kEmbeddedObjectChar;
+ }
+}
+
+nsTArray<bool> RemoteAccessible::PreProcessRelations(AccAttributes* aFields) {
+ nsTArray<bool> updateTracker(ArrayLength(kRelationTypeAtoms));
+ for (auto const& data : kRelationTypeAtoms) {
+ if (data.mValidTag) {
+ // The relation we're currently processing only applies to particular
+ // elements. Check to see if we're one of them.
+ nsAtom* tag = TagName();
+ if (!tag) {
+ // TagName() returns null on an initial cache push -- check aFields
+ // for a tag name instead.
+ if (auto maybeTag =
+ aFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::TagName)) {
+ tag = *maybeTag;
+ }
+ }
+ MOZ_ASSERT(
+ tag || IsTextLeaf() || IsDoc(),
+ "Could not fetch tag via TagName() or from initial cache push!");
+ if (tag != data.mValidTag) {
+ // If this rel doesn't apply to us, do no pre-processing. Also,
+ // note in our updateTracker that we should do no post-processing.
+ updateTracker.AppendElement(false);
+ continue;
+ }
+ }
+
+ nsStaticAtom* const relAtom = data.mAtom;
+ auto newRelationTargets =
+ aFields->GetAttribute<nsTArray<uint64_t>>(relAtom);
+ bool shouldAddNewImplicitRels =
+ newRelationTargets && newRelationTargets->Length();
+
+ // Remove existing implicit relations if we need to perform an update, or
+ // if we've received a DeleteEntry(). Only do this if mCachedFields is
+ // initialized. If mCachedFields is not initialized, we still need to
+ // construct the update array so we correctly handle reverse rels in
+ // PostProcessRelations.
+ if ((shouldAddNewImplicitRels ||
+ aFields->GetAttribute<DeleteEntry>(relAtom)) &&
+ mCachedFields) {
+ if (auto maybeOldIDs =
+ mCachedFields->GetAttribute<nsTArray<uint64_t>>(relAtom)) {
+ for (uint64_t id : *maybeOldIDs) {
+ // For each target, fetch its reverse relation map
+ // We need to call `Lookup` here instead of `LookupOrInsert` because
+ // it's possible the ID we're querying is from an acc that has since
+ // been Shutdown(), and so has intentionally removed its reverse rels
+ // from the doc's reverse rel cache.
+ if (auto reverseRels = Document()->mReverseRelations.Lookup(id)) {
+ // Then fetch its reverse relation's ID list. This should be safe
+ // to do via LookupOrInsert because by the time we've gotten here,
+ // we know the acc and `this` are still alive in the doc. If we hit
+ // the following assert, we don't have parity on implicit/explicit
+ // rels and something is wrong.
+ nsTArray<uint64_t>& reverseRelIDs =
+ reverseRels->LookupOrInsert(data.mReverseType);
+ // There might be other reverse relations stored for this acc, so
+ // remove our ID instead of deleting the array entirely.
+ DebugOnly<bool> removed = reverseRelIDs.RemoveElement(ID());
+ MOZ_ASSERT(removed, "Can't find old reverse relation");
+ }
+ }
+ }
+ }
+
+ updateTracker.AppendElement(shouldAddNewImplicitRels);
+ }
+
+ return updateTracker;
+}
+
+void RemoteAccessible::PostProcessRelations(const nsTArray<bool>& aToUpdate) {
+ size_t updateCount = aToUpdate.Length();
+ MOZ_ASSERT(updateCount == ArrayLength(kRelationTypeAtoms),
+ "Did not note update status for every relation type!");
+ for (size_t i = 0; i < updateCount; i++) {
+ if (aToUpdate.ElementAt(i)) {
+ // Since kRelationTypeAtoms was used to generate aToUpdate, we
+ // know the ith entry of aToUpdate corresponds to the relation type in
+ // the ith entry of kRelationTypeAtoms. Fetch the related data here.
+ auto const& data = kRelationTypeAtoms[i];
+
+ const nsTArray<uint64_t>& newIDs =
+ *mCachedFields->GetAttribute<nsTArray<uint64_t>>(data.mAtom);
+ for (uint64_t id : newIDs) {
+ nsTHashMap<RelationType, nsTArray<uint64_t>>& relations =
+ Document()->mReverseRelations.LookupOrInsert(id);
+ nsTArray<uint64_t>& ids = relations.LookupOrInsert(data.mReverseType);
+ ids.AppendElement(ID());
+ }
+ }
+ }
+}
+
+void RemoteAccessible::PruneRelationsOnShutdown() {
+ auto reverseRels = mDoc->mReverseRelations.Lookup(ID());
+ if (!reverseRels) {
+ return;
+ }
+ for (auto const& data : kRelationTypeAtoms) {
+ // Fetch the list of targets for this reverse relation
+ auto reverseTargetList = reverseRels->Lookup(data.mReverseType);
+ if (!reverseTargetList) {
+ continue;
+ }
+ for (uint64_t id : *reverseTargetList) {
+ // For each target, retrieve its corresponding forward relation target
+ // list
+ RemoteAccessible* affectedAcc = mDoc->GetAccessible(id);
+ if (!affectedAcc) {
+ // It's possible the affect acc also shut down, in which case
+ // we don't have anything to update.
+ continue;
+ }
+ if (auto forwardTargetList =
+ affectedAcc->mCachedFields
+ ->GetMutableAttribute<nsTArray<uint64_t>>(data.mAtom)) {
+ forwardTargetList->RemoveElement(ID());
+ if (!forwardTargetList->Length()) {
+ // The ID we removed was the only thing in the list, so remove the
+ // entry from the cache entirely -- don't leave an empty array.
+ affectedAcc->mCachedFields->Remove(data.mAtom);
+ }
+ }
+ }
+ }
+ // Remove this ID from the document's map of reverse relations.
+ reverseRels.Remove();
+}
+
+uint32_t RemoteAccessible::GetCachedTextLength() {
+ MOZ_ASSERT(!HasChildren());
+ if (!mCachedFields) {
+ return 0;
+ }
+ VERIFY_CACHE(CacheDomain::Text);
+ auto text = mCachedFields->GetAttribute<nsString>(CacheKey::Text);
+ if (!text) {
+ return 0;
+ }
+ return text->Length();
+}
+
+Maybe<const nsTArray<int32_t>&> RemoteAccessible::GetCachedTextLines() {
+ MOZ_ASSERT(!HasChildren());
+ if (!mCachedFields) {
+ return Nothing();
+ }
+ VERIFY_CACHE(CacheDomain::Text);
+ return mCachedFields->GetAttribute<nsTArray<int32_t>>(
+ CacheKey::TextLineStarts);
+}
+
+nsRect RemoteAccessible::GetCachedCharRect(int32_t aOffset) {
+ MOZ_ASSERT(IsText());
+ if (!mCachedFields) {
+ return nsRect();
+ }
+
+ if (Maybe<const nsTArray<int32_t>&> maybeCharData =
+ mCachedFields->GetAttribute<nsTArray<int32_t>>(
+ CacheKey::TextBounds)) {
+ const nsTArray<int32_t>& charData = *maybeCharData;
+ const int32_t index = aOffset * kNumbersInRect;
+ if (index < static_cast<int32_t>(charData.Length())) {
+ return nsRect(charData[index], charData[index + 1], charData[index + 2],
+ charData[index + 3]);
+ }
+ // It is valid for a client to call this with an offset 1 after the last
+ // character because of the insertion point at the end of text boxes.
+ MOZ_ASSERT(index == static_cast<int32_t>(charData.Length()));
+ }
+
+ return nsRect();
+}
+
+void RemoteAccessible::DOMNodeID(nsString& aID) const {
+ if (mCachedFields) {
+ mCachedFields->GetAttribute(CacheKey::DOMNodeID, aID);
+ VERIFY_CACHE(CacheDomain::DOMNodeIDAndClass);
+ }
+}
+
+void RemoteAccessible::ScrollToPoint(uint32_t aScrollType, int32_t aX,
+ int32_t aY) {
+ Unused << mDoc->SendScrollToPoint(mID, aScrollType, aX, aY);
+}
+
+#if !defined(XP_WIN)
+void RemoteAccessible::Announce(const nsString& aAnnouncement,
+ uint16_t aPriority) {
+ Unused << mDoc->SendAnnounce(mID, aAnnouncement, aPriority);
+}
+#endif // !defined(XP_WIN)
+
+void RemoteAccessible::ScrollSubstringToPoint(int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aCoordinateType,
+ int32_t aX, int32_t aY) {
+ Unused << mDoc->SendScrollSubstringToPoint(mID, aStartOffset, aEndOffset,
+ aCoordinateType, aX, aY);
+}
+
+RefPtr<const AccAttributes> RemoteAccessible::GetCachedTextAttributes() {
+ MOZ_ASSERT(IsText() || IsHyperText());
+ if (mCachedFields) {
+ auto attrs = mCachedFields->GetAttributeRefPtr<AccAttributes>(
+ CacheKey::TextAttributes);
+ VERIFY_CACHE(CacheDomain::Text);
+ return attrs;
+ }
+ return nullptr;
+}
+
+already_AddRefed<AccAttributes> RemoteAccessible::DefaultTextAttributes() {
+ RefPtr<const AccAttributes> attrs = GetCachedTextAttributes();
+ RefPtr<AccAttributes> result = new AccAttributes();
+ if (attrs) {
+ attrs->CopyTo(result);
+ }
+ return result.forget();
+}
+
+RefPtr<const AccAttributes> RemoteAccessible::GetCachedARIAAttributes() const {
+ if (mCachedFields) {
+ auto attrs = mCachedFields->GetAttributeRefPtr<AccAttributes>(
+ CacheKey::ARIAAttributes);
+ VERIFY_CACHE(CacheDomain::ARIA);
+ return attrs;
+ }
+ return nullptr;
+}
+
+nsString RemoteAccessible::GetCachedHTMLNameAttribute() const {
+ if (mCachedFields) {
+ if (auto maybeName =
+ mCachedFields->GetAttribute<nsString>(CacheKey::DOMName)) {
+ return *maybeName;
+ }
+ }
+ return nsString();
+}
+
+uint64_t RemoteAccessible::State() {
+ uint64_t state = 0;
+ if (mCachedFields) {
+ if (auto rawState =
+ mCachedFields->GetAttribute<uint64_t>(CacheKey::State)) {
+ VERIFY_CACHE(CacheDomain::State);
+ state = *rawState;
+ // Handle states that are derived from other states.
+ if (!(state & states::UNAVAILABLE)) {
+ state |= states::ENABLED | states::SENSITIVE;
+ }
+ if (state & states::EXPANDABLE && !(state & states::EXPANDED)) {
+ state |= states::COLLAPSED;
+ }
+ }
+
+ ApplyImplicitState(state);
+
+ auto* cbc = mDoc->GetBrowsingContext();
+ if (cbc && !cbc->IsActive()) {
+ // If our browsing context is _not_ active, we're in a background tab
+ // and inherently offscreen.
+ state |= states::OFFSCREEN;
+ } else {
+ // If we're in an active browsing context, there are a few scenarios we
+ // need to address:
+ // - We are an iframe document in the visual viewport
+ // - We are an iframe document out of the visual viewport
+ // - We are non-iframe content in the visual viewport
+ // - We are non-iframe content out of the visual viewport
+ // We assume top level tab docs are on screen if their BC is active, so
+ // we don't need additional handling for them here.
+ if (!mDoc->IsTopLevel()) {
+ // Here we handle iframes and iframe content.
+ // We use an iframe's outer doc's position in the embedding document's
+ // viewport to determine if the iframe has been scrolled offscreen.
+ Accessible* docParent = mDoc->Parent();
+ // In rare cases, we might not have an outer doc yet. Return if that's
+ // the case.
+ if (NS_WARN_IF(!docParent || !docParent->IsRemote())) {
+ return state;
+ }
+
+ RemoteAccessible* outerDoc = docParent->AsRemote();
+ DocAccessibleParent* embeddingDocument = outerDoc->Document();
+ if (embeddingDocument &&
+ !embeddingDocument->mOnScreenAccessibles.Contains(outerDoc->ID())) {
+ // Our embedding document's viewport cache doesn't contain the ID of
+ // our outer doc, so this iframe (and any of its content) is
+ // offscreen.
+ state |= states::OFFSCREEN;
+ } else if (this != mDoc && !mDoc->mOnScreenAccessibles.Contains(ID())) {
+ // Our embedding document's viewport cache contains the ID of our
+ // outer doc, but the iframe's viewport cache doesn't contain our ID.
+ // We are offscreen.
+ state |= states::OFFSCREEN;
+ }
+ } else if (this != mDoc && !mDoc->mOnScreenAccessibles.Contains(ID())) {
+ // We are top level tab content (but not a top level tab doc).
+ // If our tab doc's viewport cache doesn't contain our ID, we're
+ // offscreen.
+ state |= states::OFFSCREEN;
+ }
+ }
+ }
+
+ return state;
+}
+
+already_AddRefed<AccAttributes> RemoteAccessible::Attributes() {
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+ nsAccessibilityService* accService = GetAccService();
+ if (!accService) {
+ // The service can be shut down before RemoteAccessibles. If it is shut
+ // down, we can't calculate some attributes. We're about to die anyway.
+ return attributes.forget();
+ }
+
+ if (mCachedFields) {
+ // We use GetAttribute instead of GetAttributeRefPtr because we need
+ // nsAtom, not const nsAtom.
+ if (auto tag =
+ mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::TagName)) {
+ attributes->SetAttribute(nsGkAtoms::tag, *tag);
+ }
+
+ GroupPos groupPos = GroupPosition();
+ nsAccUtils::SetAccGroupAttrs(attributes, groupPos.level, groupPos.setSize,
+ groupPos.posInSet);
+
+ bool hierarchical = false;
+ uint32_t itemCount = AccGroupInfo::TotalItemCount(this, &hierarchical);
+ if (itemCount) {
+ attributes->SetAttribute(nsGkAtoms::child_item_count,
+ static_cast<int32_t>(itemCount));
+ }
+
+ if (hierarchical) {
+ attributes->SetAttribute(nsGkAtoms::tree, true);
+ }
+
+ if (auto inputType =
+ mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::InputType)) {
+ attributes->SetAttribute(nsGkAtoms::textInputType, *inputType);
+ }
+
+ if (RefPtr<nsAtom> display = DisplayStyle()) {
+ attributes->SetAttribute(nsGkAtoms::display, display);
+ }
+
+ if (TableCellAccessible* cell = AsTableCell()) {
+ TableAccessible* table = cell->Table();
+ uint32_t row = cell->RowIdx();
+ uint32_t col = cell->ColIdx();
+ int32_t cellIdx = table->CellIndexAt(row, col);
+ if (cellIdx != -1) {
+ attributes->SetAttribute(nsGkAtoms::tableCellIndex, cellIdx);
+ }
+ }
+
+ if (bool layoutGuess = TableIsProbablyForLayout()) {
+ attributes->SetAttribute(nsGkAtoms::layout_guess, layoutGuess);
+ }
+
+ accService->MarkupAttributes(this, attributes);
+
+ const nsRoleMapEntry* roleMap = ARIARoleMap();
+ nsAutoString role;
+ mCachedFields->GetAttribute(CacheKey::ARIARole, role);
+ if (role.IsEmpty()) {
+ if (roleMap && roleMap->roleAtom != nsGkAtoms::_empty) {
+ // Single, known role.
+ attributes->SetAttribute(nsGkAtoms::xmlroles, roleMap->roleAtom);
+ } else if (nsAtom* landmark = LandmarkRole()) {
+ // Landmark role from markup; e.g. HTML <main>.
+ attributes->SetAttribute(nsGkAtoms::xmlroles, landmark);
+ }
+ } else {
+ // Unknown role or multiple roles.
+ attributes->SetAttribute(nsGkAtoms::xmlroles, std::move(role));
+ }
+
+ if (roleMap) {
+ nsAutoString live;
+ if (nsAccUtils::GetLiveAttrValue(roleMap->liveAttRule, live)) {
+ attributes->SetAttribute(nsGkAtoms::aria_live, std::move(live));
+ }
+ }
+
+ if (auto ariaAttrs = GetCachedARIAAttributes()) {
+ ariaAttrs->CopyTo(attributes);
+ }
+
+ nsAccUtils::SetLiveContainerAttributes(attributes, this);
+
+ nsString id;
+ DOMNodeID(id);
+ if (!id.IsEmpty()) {
+ attributes->SetAttribute(nsGkAtoms::id, std::move(id));
+ }
+
+ nsString className;
+ mCachedFields->GetAttribute(CacheKey::DOMNodeClass, className);
+ if (!className.IsEmpty()) {
+ attributes->SetAttribute(nsGkAtoms::_class, std::move(className));
+ }
+
+ if (IsImage()) {
+ nsString src;
+ mCachedFields->GetAttribute(CacheKey::SrcURL, src);
+ if (!src.IsEmpty()) {
+ attributes->SetAttribute(nsGkAtoms::src, std::move(src));
+ }
+ }
+
+ if (IsTextField()) {
+ nsString placeholder;
+ mCachedFields->GetAttribute(CacheKey::HTMLPlaceholder, placeholder);
+ if (!placeholder.IsEmpty()) {
+ attributes->SetAttribute(nsGkAtoms::placeholder,
+ std::move(placeholder));
+ attributes->Remove(nsGkAtoms::aria_placeholder);
+ }
+ }
+
+ nsString popupType;
+ mCachedFields->GetAttribute(CacheKey::PopupType, popupType);
+ if (!popupType.IsEmpty()) {
+ attributes->SetAttribute(nsGkAtoms::ispopup, std::move(popupType));
+ }
+ }
+
+ nsAutoString name;
+ if (Name(name) != eNameFromSubtree && !name.IsVoid()) {
+ attributes->SetAttribute(nsGkAtoms::explicit_name, true);
+ }
+
+ // Expose the string value via the valuetext attribute. We test for the value
+ // interface because we don't want to expose traditional Value() information
+ // such as URLs on links and documents, or text in an input.
+ // XXX This is only needed for ATK, since other APIs have native ways to
+ // retrieve value text. We should probably move this into ATK specific code.
+ // For now, we do this because LocalAccessible does it.
+ if (HasNumericValue()) {
+ nsString valuetext;
+ Value(valuetext);
+ attributes->SetAttribute(nsGkAtoms::aria_valuetext, std::move(valuetext));
+ }
+
+ return attributes.forget();
+}
+
+nsAtom* RemoteAccessible::TagName() const {
+ if (mCachedFields) {
+ if (auto tag =
+ mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::TagName)) {
+ return *tag;
+ }
+ }
+
+ return nullptr;
+}
+
+already_AddRefed<nsAtom> RemoteAccessible::InputType() const {
+ if (mCachedFields) {
+ if (auto inputType =
+ mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::InputType)) {
+ RefPtr<nsAtom> result = *inputType;
+ return result.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+already_AddRefed<nsAtom> RemoteAccessible::DisplayStyle() const {
+ if (mCachedFields) {
+ if (auto display =
+ mCachedFields->GetAttribute<RefPtr<nsAtom>>(CacheKey::CSSDisplay)) {
+ RefPtr<nsAtom> result = *display;
+ return result.forget();
+ }
+ }
+ return nullptr;
+}
+
+float RemoteAccessible::Opacity() const {
+ if (mCachedFields) {
+ if (auto opacity = mCachedFields->GetAttribute<float>(CacheKey::Opacity)) {
+ return *opacity;
+ }
+ }
+
+ return 1.0f;
+}
+
+void RemoteAccessible::LiveRegionAttributes(nsAString* aLive,
+ nsAString* aRelevant,
+ Maybe<bool>* aAtomic,
+ nsAString* aBusy) const {
+ if (!mCachedFields) {
+ return;
+ }
+ RefPtr<const AccAttributes> attrs = GetCachedARIAAttributes();
+ if (!attrs) {
+ return;
+ }
+ if (aLive) {
+ attrs->GetAttribute(nsGkAtoms::aria_live, *aLive);
+ }
+ if (aRelevant) {
+ attrs->GetAttribute(nsGkAtoms::aria_relevant, *aRelevant);
+ }
+ if (aAtomic) {
+ if (auto value =
+ attrs->GetAttribute<RefPtr<nsAtom>>(nsGkAtoms::aria_atomic)) {
+ *aAtomic = Some(*value == nsGkAtoms::_true);
+ }
+ }
+ if (aBusy) {
+ attrs->GetAttribute(nsGkAtoms::aria_busy, *aBusy);
+ }
+}
+
+Maybe<bool> RemoteAccessible::ARIASelected() const {
+ if (mCachedFields) {
+ return mCachedFields->GetAttribute<bool>(CacheKey::ARIASelected);
+ }
+ return Nothing();
+}
+
+nsAtom* RemoteAccessible::GetPrimaryAction() const {
+ if (mCachedFields) {
+ if (auto action = mCachedFields->GetAttribute<RefPtr<nsAtom>>(
+ CacheKey::PrimaryAction)) {
+ return *action;
+ }
+ }
+
+ return nullptr;
+}
+
+uint8_t RemoteAccessible::ActionCount() const {
+ uint8_t actionCount = 0;
+ if (mCachedFields) {
+ if (HasPrimaryAction() || ActionAncestor()) {
+ actionCount++;
+ }
+
+ if (mCachedFields->HasAttribute(CacheKey::HasLongdesc)) {
+ actionCount++;
+ }
+ VERIFY_CACHE(CacheDomain::Actions);
+ }
+
+ return actionCount;
+}
+
+void RemoteAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (mCachedFields) {
+ aName.Truncate();
+ nsAtom* action = GetPrimaryAction();
+ bool hasActionAncestor = !action && ActionAncestor();
+
+ switch (aIndex) {
+ case 0:
+ if (action) {
+ action->ToString(aName);
+ } else if (hasActionAncestor) {
+ aName.AssignLiteral("click ancestor");
+ } else if (mCachedFields->HasAttribute(CacheKey::HasLongdesc)) {
+ aName.AssignLiteral("showlongdesc");
+ }
+ break;
+ case 1:
+ if ((action || hasActionAncestor) &&
+ mCachedFields->HasAttribute(CacheKey::HasLongdesc)) {
+ aName.AssignLiteral("showlongdesc");
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ VERIFY_CACHE(CacheDomain::Actions);
+}
+
+bool RemoteAccessible::DoAction(uint8_t aIndex) const {
+ if (ActionCount() < aIndex + 1) {
+ return false;
+ }
+
+ Unused << mDoc->SendDoActionAsync(mID, aIndex);
+ return true;
+}
+
+KeyBinding RemoteAccessible::AccessKey() const {
+ if (mCachedFields) {
+ if (auto value =
+ mCachedFields->GetAttribute<uint64_t>(CacheKey::AccessKey)) {
+ return KeyBinding(*value);
+ }
+ }
+ return KeyBinding();
+}
+
+void RemoteAccessible::SelectionRanges(nsTArray<TextRange>* aRanges) const {
+ Document()->SelectionRanges(aRanges);
+}
+
+bool RemoteAccessible::RemoveFromSelection(int32_t aSelectionNum) {
+ MOZ_ASSERT(IsHyperText());
+ if (SelectionCount() <= aSelectionNum) {
+ return false;
+ }
+
+ Unused << mDoc->SendRemoveTextSelection(mID, aSelectionNum);
+
+ return true;
+}
+
+void RemoteAccessible::ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize,
+ int32_t* aPosInSet) const {
+ if (!mCachedFields) {
+ return;
+ }
+
+ if (aLevel) {
+ if (auto level =
+ mCachedFields->GetAttribute<int32_t>(nsGkAtoms::aria_level)) {
+ *aLevel = *level;
+ }
+ }
+ if (aSetSize) {
+ if (auto setsize =
+ mCachedFields->GetAttribute<int32_t>(nsGkAtoms::aria_setsize)) {
+ *aSetSize = *setsize;
+ }
+ }
+ if (aPosInSet) {
+ if (auto posinset =
+ mCachedFields->GetAttribute<int32_t>(nsGkAtoms::aria_posinset)) {
+ *aPosInSet = *posinset;
+ }
+ }
+}
+
+AccGroupInfo* RemoteAccessible::GetGroupInfo() const {
+ if (!mCachedFields) {
+ return nullptr;
+ }
+
+ if (auto groupInfo = mCachedFields->GetAttribute<UniquePtr<AccGroupInfo>>(
+ CacheKey::GroupInfo)) {
+ return groupInfo->get();
+ }
+
+ return nullptr;
+}
+
+AccGroupInfo* RemoteAccessible::GetOrCreateGroupInfo() {
+ AccGroupInfo* groupInfo = GetGroupInfo();
+ if (groupInfo) {
+ return groupInfo;
+ }
+
+ groupInfo = AccGroupInfo::CreateGroupInfo(this);
+ if (groupInfo) {
+ if (!mCachedFields) {
+ mCachedFields = new AccAttributes();
+ }
+
+ mCachedFields->SetAttribute(CacheKey::GroupInfo, groupInfo);
+ }
+
+ return groupInfo;
+}
+
+void RemoteAccessible::InvalidateGroupInfo() {
+ if (mCachedFields) {
+ mCachedFields->Remove(CacheKey::GroupInfo);
+ }
+}
+
+void RemoteAccessible::GetPositionAndSetSize(int32_t* aPosInSet,
+ int32_t* aSetSize) {
+ if (IsHTMLRadioButton()) {
+ *aSetSize = 0;
+ Relation rel = RelationByType(RelationType::MEMBER_OF);
+ while (Accessible* radio = rel.Next()) {
+ ++*aSetSize;
+ if (radio == this) {
+ *aPosInSet = *aSetSize;
+ }
+ }
+ return;
+ }
+
+ Accessible::GetPositionAndSetSize(aPosInSet, aSetSize);
+}
+
+bool RemoteAccessible::HasPrimaryAction() const {
+ return mCachedFields && mCachedFields->HasAttribute(CacheKey::PrimaryAction);
+}
+
+void RemoteAccessible::TakeFocus() const { Unused << mDoc->SendTakeFocus(mID); }
+
+void RemoteAccessible::ScrollTo(uint32_t aHow) const {
+ Unused << mDoc->SendScrollTo(mID, aHow);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// SelectAccessible
+
+void RemoteAccessible::SelectedItems(nsTArray<Accessible*>* aItems) {
+ Pivot p = Pivot(this);
+ PivotStateRule rule(states::SELECTED);
+ for (Accessible* selected = p.First(rule); selected;
+ selected = p.Next(selected, rule)) {
+ aItems->AppendElement(selected);
+ }
+}
+
+uint32_t RemoteAccessible::SelectedItemCount() {
+ uint32_t count = 0;
+ Pivot p = Pivot(this);
+ PivotStateRule rule(states::SELECTED);
+ for (Accessible* selected = p.First(rule); selected;
+ selected = p.Next(selected, rule)) {
+ count++;
+ }
+
+ return count;
+}
+
+Accessible* RemoteAccessible::GetSelectedItem(uint32_t aIndex) {
+ uint32_t index = 0;
+ Accessible* selected = nullptr;
+ Pivot p = Pivot(this);
+ PivotStateRule rule(states::SELECTED);
+ for (selected = p.First(rule); selected && index < aIndex;
+ selected = p.Next(selected, rule)) {
+ index++;
+ }
+
+ return selected;
+}
+
+bool RemoteAccessible::IsItemSelected(uint32_t aIndex) {
+ uint32_t index = 0;
+ Accessible* selectable = nullptr;
+ Pivot p = Pivot(this);
+ PivotStateRule rule(states::SELECTABLE);
+ for (selectable = p.First(rule); selectable && index < aIndex;
+ selectable = p.Next(selectable, rule)) {
+ index++;
+ }
+
+ return selectable && selectable->State() & states::SELECTED;
+}
+
+bool RemoteAccessible::AddItemToSelection(uint32_t aIndex) {
+ uint32_t index = 0;
+ Accessible* selectable = nullptr;
+ Pivot p = Pivot(this);
+ PivotStateRule rule(states::SELECTABLE);
+ for (selectable = p.First(rule); selectable && index < aIndex;
+ selectable = p.Next(selectable, rule)) {
+ index++;
+ }
+
+ if (selectable) selectable->SetSelected(true);
+
+ return static_cast<bool>(selectable);
+}
+
+bool RemoteAccessible::RemoveItemFromSelection(uint32_t aIndex) {
+ uint32_t index = 0;
+ Accessible* selectable = nullptr;
+ Pivot p = Pivot(this);
+ PivotStateRule rule(states::SELECTABLE);
+ for (selectable = p.First(rule); selectable && index < aIndex;
+ selectable = p.Next(selectable, rule)) {
+ index++;
+ }
+
+ if (selectable) selectable->SetSelected(false);
+
+ return static_cast<bool>(selectable);
+}
+
+bool RemoteAccessible::SelectAll() {
+ if ((State() & states::MULTISELECTABLE) == 0) {
+ return false;
+ }
+
+ bool success = false;
+ Accessible* selectable = nullptr;
+ Pivot p = Pivot(this);
+ PivotStateRule rule(states::SELECTABLE);
+ for (selectable = p.First(rule); selectable;
+ selectable = p.Next(selectable, rule)) {
+ success = true;
+ selectable->SetSelected(true);
+ }
+ return success;
+}
+
+bool RemoteAccessible::UnselectAll() {
+ if ((State() & states::MULTISELECTABLE) == 0) {
+ return false;
+ }
+
+ bool success = false;
+ Accessible* selectable = nullptr;
+ Pivot p = Pivot(this);
+ PivotStateRule rule(states::SELECTABLE);
+ for (selectable = p.First(rule); selectable;
+ selectable = p.Next(selectable, rule)) {
+ success = true;
+ selectable->SetSelected(false);
+ }
+ return success;
+}
+
+void RemoteAccessible::TakeSelection() {
+ Unused << mDoc->SendTakeSelection(mID);
+}
+
+void RemoteAccessible::SetSelected(bool aSelect) {
+ Unused << mDoc->SendSetSelected(mID, aSelect);
+}
+
+TableAccessible* RemoteAccessible::AsTable() {
+ if (IsTable()) {
+ return CachedTableAccessible::GetFrom(this);
+ }
+ return nullptr;
+}
+
+TableCellAccessible* RemoteAccessible::AsTableCell() {
+ if (IsTableCell()) {
+ return CachedTableCellAccessible::GetFrom(this);
+ }
+ return nullptr;
+}
+
+bool RemoteAccessible::TableIsProbablyForLayout() {
+ if (mCachedFields) {
+ if (auto layoutGuess =
+ mCachedFields->GetAttribute<bool>(CacheKey::TableLayoutGuess)) {
+ return *layoutGuess;
+ }
+ }
+ return false;
+}
+
+nsTArray<int32_t>& RemoteAccessible::GetCachedHyperTextOffsets() {
+ if (mCachedFields) {
+ if (auto offsets = mCachedFields->GetMutableAttribute<nsTArray<int32_t>>(
+ CacheKey::HyperTextOffsets)) {
+ return *offsets;
+ }
+ }
+ nsTArray<int32_t> newOffsets;
+ if (!mCachedFields) {
+ mCachedFields = new AccAttributes();
+ }
+ mCachedFields->SetAttribute(CacheKey::HyperTextOffsets,
+ std::move(newOffsets));
+ return *mCachedFields->GetMutableAttribute<nsTArray<int32_t>>(
+ CacheKey::HyperTextOffsets);
+}
+
+void RemoteAccessible::SetCaretOffset(int32_t aOffset) {
+ Unused << mDoc->SendSetCaretOffset(mID, aOffset);
+}
+
+Maybe<int32_t> RemoteAccessible::GetIntARIAAttr(nsAtom* aAttrName) const {
+ if (RefPtr<const AccAttributes> attrs = GetCachedARIAAttributes()) {
+ if (auto val = attrs->GetAttribute<int32_t>(aAttrName)) {
+ return val;
+ }
+ }
+ return Nothing();
+}
+
+void RemoteAccessible::Language(nsAString& aLocale) {
+ if (!IsHyperText()) {
+ return;
+ }
+ if (auto attrs = GetCachedTextAttributes()) {
+ attrs->GetAttribute(nsGkAtoms::language, aLocale);
+ }
+}
+
+void RemoteAccessible::ReplaceText(const nsAString& aText) {
+ Unused << mDoc->SendReplaceText(mID, aText);
+}
+
+void RemoteAccessible::InsertText(const nsAString& aText, int32_t aPosition) {
+ Unused << mDoc->SendInsertText(mID, aText, aPosition);
+}
+
+void RemoteAccessible::CopyText(int32_t aStartPos, int32_t aEndPos) {
+ Unused << mDoc->SendCopyText(mID, aStartPos, aEndPos);
+}
+
+void RemoteAccessible::CutText(int32_t aStartPos, int32_t aEndPos) {
+ Unused << mDoc->SendCutText(mID, aStartPos, aEndPos);
+}
+
+void RemoteAccessible::DeleteText(int32_t aStartPos, int32_t aEndPos) {
+ Unused << mDoc->SendDeleteText(mID, aStartPos, aEndPos);
+}
+
+void RemoteAccessible::PasteText(int32_t aPosition) {
+ Unused << mDoc->SendPasteText(mID, aPosition);
+}
+
+size_t RemoteAccessible::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+size_t RemoteAccessible::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) {
+ size_t size = 0;
+
+ // Count attributes.
+ if (mCachedFields) {
+ size += mCachedFields->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ // We don't recurse into mChildren because they're already counted in their
+ // document's mAccessibles.
+ size += mChildren.ShallowSizeOfExcludingThis(aMallocSizeOf);
+
+ return size;
+}
+
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/ipc/RemoteAccessible.h b/accessible/ipc/RemoteAccessible.h
new file mode 100644
index 0000000000..9215fd7bc5
--- /dev/null
+++ b/accessible/ipc/RemoteAccessible.h
@@ -0,0 +1,511 @@
+/* -*- 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_RemoteAccessible_h
+#define mozilla_a11y_RemoteAccessible_h
+
+#include "mozilla/a11y/Accessible.h"
+#include "mozilla/a11y/CacheConstants.h"
+#include "mozilla/a11y/HyperTextAccessibleBase.h"
+#include "mozilla/a11y/Role.h"
+#include "AccAttributes.h"
+#include "nsIAccessibleText.h"
+#include "nsIAccessibleTypes.h"
+#include "nsTArray.h"
+#include "nsRect.h"
+#include "LocalAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+class Attribute;
+class DocAccessibleParent;
+class RemoteAccessible;
+enum class RelationType;
+
+/**
+ * The class for an accessibility tree node that originated in the parent
+ * process.
+ */
+class RemoteAccessible : public Accessible, public HyperTextAccessibleBase {
+ public:
+ virtual ~RemoteAccessible() {
+ MOZ_ASSERT(!mWrapper);
+ MOZ_COUNT_DTOR(RemoteAccessible);
+ }
+
+ virtual bool IsRemote() const override { return true; }
+
+ void AddChildAt(uint32_t aIdx, RemoteAccessible* aChild) {
+ mChildren.InsertElementAt(aIdx, aChild);
+ if (IsHyperText()) {
+ InvalidateCachedHyperTextOffsets();
+ }
+ }
+
+ virtual uint32_t ChildCount() const override { return mChildren.Length(); }
+ RemoteAccessible* RemoteChildAt(uint32_t aIdx) const {
+ return mChildren.SafeElementAt(aIdx);
+ }
+ RemoteAccessible* RemoteFirstChild() const {
+ return mChildren.Length() ? mChildren[0] : nullptr;
+ }
+ RemoteAccessible* RemoteLastChild() const {
+ return mChildren.Length() ? mChildren[mChildren.Length() - 1] : nullptr;
+ }
+ RemoteAccessible* RemotePrevSibling() const {
+ if (IsDoc()) {
+ // The normal code path doesn't work for documents because the parent
+ // might be a local OuterDoc, but IndexInParent() will return 1.
+ // A document is always a single child of an OuterDoc anyway.
+ return nullptr;
+ }
+ int32_t idx = IndexInParent();
+ if (idx == -1) {
+ return nullptr; // No parent.
+ }
+ return idx > 0 ? RemoteParent()->mChildren[idx - 1] : nullptr;
+ }
+ RemoteAccessible* RemoteNextSibling() const {
+ if (IsDoc()) {
+ // The normal code path doesn't work for documents because the parent
+ // might be a local OuterDoc, but IndexInParent() will return 1.
+ // A document is always a single child of an OuterDoc anyway.
+ return nullptr;
+ }
+ int32_t idx = IndexInParent();
+ if (idx == -1) {
+ return nullptr; // No parent.
+ }
+ MOZ_ASSERT(idx >= 0);
+ size_t newIdx = idx + 1;
+ return newIdx < RemoteParent()->mChildren.Length()
+ ? RemoteParent()->mChildren[newIdx]
+ : nullptr;
+ }
+
+ // Accessible hierarchy method overrides
+
+ virtual Accessible* Parent() const override { return RemoteParent(); }
+
+ virtual Accessible* ChildAt(uint32_t aIndex) const override {
+ return RemoteChildAt(aIndex);
+ }
+
+ virtual Accessible* NextSibling() const override {
+ return RemoteNextSibling();
+ }
+
+ virtual Accessible* PrevSibling() const override {
+ return RemotePrevSibling();
+ }
+
+ // XXX evaluate if this is fast enough.
+ virtual int32_t IndexInParent() const override {
+ RemoteAccessible* parent = RemoteParent();
+ if (!parent) {
+ return -1;
+ }
+ return parent->mChildren.IndexOf(
+ static_cast<const RemoteAccessible*>(this));
+ }
+ virtual uint32_t EmbeddedChildCount() override;
+ virtual int32_t IndexOfEmbeddedChild(Accessible* aChild) override;
+ virtual Accessible* EmbeddedChildAt(uint32_t aChildIdx) override;
+
+ void Shutdown();
+
+ void SetChildDoc(DocAccessibleParent* aChildDoc);
+ void ClearChildDoc(DocAccessibleParent* aChildDoc);
+
+ /**
+ * Remove The given child.
+ */
+ void RemoveChild(RemoteAccessible* aChild) {
+ mChildren.RemoveElement(aChild);
+ if (IsHyperText()) {
+ InvalidateCachedHyperTextOffsets();
+ }
+ }
+
+ /**
+ * Return the proxy for the parent of the wrapped accessible.
+ */
+ RemoteAccessible* RemoteParent() const;
+
+ LocalAccessible* OuterDocOfRemoteBrowser() const;
+
+ /**
+ * Get the role of the accessible we're proxying.
+ */
+ virtual role Role() const override { return mRole; }
+
+ /**
+ * Return true if this is an embedded object.
+ */
+ bool IsEmbeddedObject() const { return !IsText(); }
+
+ virtual bool IsLink() const override {
+ if (IsHTMLLink()) {
+ // XXX: HTML links always return true for IsLink.
+ return true;
+ }
+
+ if (IsText()) {
+ return false;
+ }
+
+ if (Accessible* parent = Parent()) {
+ return parent->IsHyperText();
+ }
+
+ return false;
+ }
+
+ virtual bool HasNumericValue() const override {
+ // XXX: We combine the aria and native "has numeric value" field
+ // when we serialize the local accessible into eNumericValue.
+ return HasGenericType(eNumericValue);
+ }
+
+ // Methods that potentially access a cache.
+
+ virtual ENameValueFlag Name(nsString& aName) const override;
+ virtual void Description(nsString& aDescription) const override;
+ virtual void Value(nsString& aValue) const override;
+
+ virtual double CurValue() const override;
+ virtual double MinValue() const override;
+ virtual double MaxValue() const override;
+ virtual double Step() const override;
+ virtual bool SetCurValue(double aValue) override;
+
+ virtual Accessible* ChildAtPoint(
+ int32_t aX, int32_t aY,
+ LocalAccessible::EWhichChildAtPoint aWhichChild) override;
+
+ virtual LayoutDeviceIntRect Bounds() const override;
+
+ virtual nsRect BoundsInAppUnits() const override;
+
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ virtual uint64_t State() override;
+
+ virtual already_AddRefed<AccAttributes> Attributes() override;
+
+ virtual nsAtom* TagName() const override;
+
+ virtual already_AddRefed<nsAtom> InputType() const override;
+
+ virtual already_AddRefed<nsAtom> DisplayStyle() const override;
+
+ virtual float Opacity() const override;
+
+ virtual void LiveRegionAttributes(nsAString* aLive, nsAString* aRelevant,
+ Maybe<bool>* aAtomic,
+ nsAString* aBusy) const override;
+
+ virtual Maybe<bool> ARIASelected() const override;
+
+ virtual uint8_t ActionCount() const override;
+
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+
+ virtual bool DoAction(uint8_t aIndex) const override;
+
+ virtual KeyBinding AccessKey() const override;
+
+ virtual void SelectionRanges(nsTArray<TextRange>* aRanges) const override;
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual bool RemoveFromSelection(
+ int32_t aSelectionNum) override;
+
+ virtual Maybe<int32_t> GetIntARIAAttr(nsAtom* aAttrName) const override;
+
+ virtual void Language(nsAString& aLocale) override;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // EditableTextAccessible
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void ReplaceText(
+ const nsAString& aText) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void InsertText(
+ const nsAString& aText, int32_t aPosition) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void CopyText(int32_t aStartPos,
+ int32_t aEndPos) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void CutText(int32_t aStartPos,
+ int32_t aEndPos) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual void DeleteText(int32_t aStartPos,
+ int32_t aEndPos) override;
+ MOZ_CAN_RUN_SCRIPT virtual void PasteText(int32_t aPosition) override;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // SelectAccessible
+
+ virtual void SelectedItems(nsTArray<Accessible*>* aItems) override;
+
+ virtual uint32_t SelectedItemCount() override;
+
+ virtual Accessible* GetSelectedItem(uint32_t aIndex) override;
+
+ virtual bool IsItemSelected(uint32_t aIndex) override;
+
+ virtual bool AddItemToSelection(uint32_t aIndex) override;
+
+ virtual bool RemoveItemFromSelection(uint32_t aIndex) override;
+
+ virtual bool SelectAll() override;
+
+ virtual bool UnselectAll() override;
+
+ virtual void TakeSelection() override;
+
+ virtual void SetSelected(bool aSelect) override;
+
+ // Methods that interact with content.
+
+ virtual void TakeFocus() const override;
+ virtual void ScrollTo(uint32_t aHow) const override;
+ virtual void SetCaretOffset(int32_t aOffset) override;
+
+ /**
+ * Allow the platform to store a pointers worth of data on us.
+ */
+ uintptr_t GetWrapper() const { return mWrapper; }
+ void SetWrapper(uintptr_t aWrapper) { mWrapper = aWrapper; }
+
+ virtual uint64_t ID() const override { return mID; }
+
+ /**
+ * Return the document containing this proxy, or the proxy itself if it is a
+ * document.
+ */
+ DocAccessibleParent* Document() const { return mDoc; }
+
+ DocAccessibleParent* AsDoc() const { return IsDoc() ? mDoc : nullptr; }
+
+ void ApplyCache(CacheUpdateType aUpdateType, AccAttributes* aFields);
+
+ void UpdateStateCache(uint64_t aState, bool aEnabled) {
+ if (aState & kRemoteCalculatedStates) {
+ return;
+ }
+ uint64_t state = 0;
+ if (mCachedFields) {
+ if (auto oldState =
+ mCachedFields->GetAttribute<uint64_t>(CacheKey::State)) {
+ state = *oldState;
+ }
+ } else {
+ mCachedFields = new AccAttributes();
+ }
+ if (aEnabled) {
+ state |= aState;
+ } else {
+ state &= ~aState;
+ }
+ mCachedFields->SetAttribute(CacheKey::State, state);
+ }
+
+ void InvalidateGroupInfo();
+
+ virtual void AppendTextTo(nsAString& aText, uint32_t aStartOffset = 0,
+ uint32_t aLength = UINT32_MAX) override;
+
+ virtual bool TableIsProbablyForLayout();
+
+ /**
+ * Iterates through each atom in kRelationTypeAtoms, checking to see
+ * if it is present in aFields. If it is present (or if aFields contains
+ * a DeleteEntry() for this atom) and mCachedFields is initialized,
+ * fetches the old rel targets and removes their existing reverse relations
+ * stored in mReverseRelations.
+ * Returns an array of bools where the ith array entry corresponds
+ * to whether or not the rel at the ith entry of kRelationTypeAtoms
+ * requires a post-processing update.
+ */
+ nsTArray<bool> PreProcessRelations(AccAttributes* aFields);
+
+ /**
+ * Takes in the array returned from PreProcessRelations.
+ * For each entry requiring an update, fetches the new relation
+ * targets stored in mCachedFields and appropriately
+ * updates their reverse relations in mReverseRelations.
+ */
+ void PostProcessRelations(const nsTArray<bool>& aToUpdate);
+
+ /**
+ * This method is called during shutdown, before we clear our
+ * reverse rel map from the document's mReverseRelations cache.
+ * Here, we traverse our reverse relations, removing our ID from
+ * the corresponding forward relation's target list. This ensures
+ * the stored forward relations do not reference defunct accessibles.
+ */
+ void PruneRelationsOnShutdown();
+
+ uint32_t GetCachedTextLength();
+ Maybe<const nsTArray<int32_t>&> GetCachedTextLines();
+ nsRect GetCachedCharRect(int32_t aOffset);
+ RefPtr<const AccAttributes> GetCachedTextAttributes();
+ RefPtr<const AccAttributes> GetCachedARIAAttributes() const;
+
+ nsString GetCachedHTMLNameAttribute() const;
+
+ virtual HyperTextAccessibleBase* AsHyperTextBase() override {
+ return IsHyperText() ? static_cast<HyperTextAccessibleBase*>(this)
+ : nullptr;
+ }
+
+ virtual TableAccessible* AsTable() override;
+ virtual TableCellAccessible* AsTableCell() override;
+
+ virtual void DOMNodeID(nsString& aID) const override;
+
+ virtual void ScrollToPoint(uint32_t aScrollType, int32_t aX,
+ int32_t aY) override;
+
+#if !defined(XP_WIN)
+ void Announce(const nsString& aAnnouncement, uint16_t aPriority);
+#endif // !defined(XP_WIN)
+
+ // HyperTextAccessibleBase
+ virtual already_AddRefed<AccAttributes> DefaultTextAttributes() override;
+
+ virtual void ScrollSubstringToPoint(int32_t aStartOffset, int32_t aEndOffset,
+ uint32_t aCoordinateType, int32_t aX,
+ int32_t aY) override;
+
+ /**
+ * Invalidate cached HyperText offsets. This should be called whenever a
+ * child is added or removed or the text of a text leaf child is changed.
+ * Although GetChildOffset can either fully or partially invalidate the
+ * offsets cache, calculating which offset to invalidate is not worthwhile
+ * because a client might not even query offsets. This is in contrast to
+ * LocalAccessible, where the offsets are always needed to fire text change
+ * events. For RemoteAccessible, it's cheaper overall to just rebuild the
+ * offsets cache when a client next needs it.
+ */
+ void InvalidateCachedHyperTextOffsets() {
+ if (mCachedFields) {
+ mCachedFields->Remove(CacheKey::HyperTextOffsets);
+ }
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf);
+ virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf);
+
+ protected:
+ RemoteAccessible(uint64_t aID, DocAccessibleParent* aDoc, role aRole,
+ AccType aType, AccGenericType aGenericTypes,
+ uint8_t aRoleMapEntryIndex)
+ : Accessible(aType, aGenericTypes, aRoleMapEntryIndex),
+ mParent(kNoParent),
+ mDoc(aDoc),
+ mWrapper(0),
+ mID(aID),
+ mCachedFields(nullptr),
+ mRole(aRole) {
+ MOZ_COUNT_CTOR(RemoteAccessible);
+ }
+
+ explicit RemoteAccessible(DocAccessibleParent* aThisAsDoc)
+ : mParent(kNoParent),
+ mDoc(aThisAsDoc),
+ mWrapper(0),
+ mID(0),
+ mCachedFields(nullptr),
+ mRole(roles::DOCUMENT) {
+ mGenericTypes = eDocument | eHyperText;
+ MOZ_COUNT_CTOR(RemoteAccessible);
+ }
+
+ protected:
+ void SetParent(RemoteAccessible* aParent);
+ Maybe<nsRect> RetrieveCachedBounds() const;
+ bool ApplyTransform(nsRect& aCumulativeBounds) const;
+ bool ApplyScrollOffset(nsRect& aBounds) const;
+ void ApplyCrossDocOffset(nsRect& aBounds) const;
+ LayoutDeviceIntRect BoundsWithOffset(
+ Maybe<nsRect> aOffset, bool aBoundsAreForHittesting = false) const;
+ bool IsFixedPos() const;
+ bool IsOverflowHidden() const;
+
+ /**
+ * Returns true if an accessible's frame has no scrollable overflow, and
+ * false otherwise.
+ * Does not return true for partially clipped accessibles.
+ */
+ bool IsClipped() const;
+
+ /**
+ * Checks if our hittesting match has any clipped children and, if so
+ * descends it and subsequent TEXT_CONTAINERs in search of a text leaf.
+ * We do this because some sites use clipping to hide text that is only
+ * visible to a11y, while displaying a visual version of the same text on
+ * the web page. We want a hittest of the visible text to resolve to the
+ * hidden, a11y-only text node.
+ */
+ RemoteAccessible* DoFuzzyHittesting();
+
+ // This function is used exclusively for hit testing.
+ bool ContainsPoint(int32_t aX, int32_t aY);
+
+ virtual void ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize,
+ int32_t* aPosInSet) const override;
+
+ virtual AccGroupInfo* GetGroupInfo() const override;
+
+ virtual AccGroupInfo* GetOrCreateGroupInfo() override;
+
+ virtual void GetPositionAndSetSize(int32_t* aPosInSet,
+ int32_t* aSetSize) override;
+
+ virtual bool HasPrimaryAction() const override;
+
+ nsAtom* GetPrimaryAction() const;
+
+ virtual nsTArray<int32_t>& GetCachedHyperTextOffsets() override;
+
+ private:
+ uintptr_t mParent;
+ static const uintptr_t kNoParent = UINTPTR_MAX;
+
+ friend DocAccessibleParent;
+ friend TextLeafPoint;
+ friend HyperTextAccessibleBase;
+ friend class xpcAccessible;
+ friend class CachedTableCellAccessible;
+#ifdef XP_WIN
+ friend class sdnAccessible;
+#endif
+
+ nsTArray<RemoteAccessible*> mChildren;
+ DocAccessibleParent* mDoc;
+ uintptr_t mWrapper;
+ uint64_t mID;
+
+ protected:
+ virtual const Accessible* Acc() const override { return this; }
+
+ RefPtr<AccAttributes> mCachedFields;
+
+ // XXX DocAccessibleParent gets to change this to change the role of
+ // documents.
+ role mRole : 27;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// RemoteAccessible downcasting method
+
+inline RemoteAccessible* Accessible::AsRemote() {
+ return IsRemote() ? static_cast<RemoteAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/ipc/moz.build b/accessible/ipc/moz.build
new file mode 100644
index 0000000000..b8ff3b9c4f
--- /dev/null
+++ b/accessible/ipc/moz.build
@@ -0,0 +1,62 @@
+# -*- 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/.
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ LOCAL_INCLUDES += [
+ "/accessible/windows/ia2",
+ "/accessible/windows/msaa",
+ ]
+else:
+ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ LOCAL_INCLUDES += [
+ "/accessible/atk",
+ ]
+ elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ LOCAL_INCLUDES += [
+ "/accessible/mac",
+ ]
+ else:
+ LOCAL_INCLUDES += [
+ "/accessible/other",
+ ]
+
+if CONFIG["ACCESSIBILITY"]:
+ PREPROCESSED_IPDL_SOURCES += [
+ "PDocAccessible.ipdl",
+ ]
+ IPDL_SOURCES += [
+ "DocAccessibleTypes.ipdlh",
+ ]
+
+EXPORTS.mozilla.a11y += [
+ "IPCTypes.h",
+]
+
+if CONFIG["ACCESSIBILITY"]:
+ EXPORTS.mozilla.a11y += [
+ "DocAccessibleChild.h",
+ "DocAccessibleParent.h",
+ "RemoteAccessible.h",
+ ]
+
+ UNIFIED_SOURCES += [
+ "DocAccessibleChild.cpp",
+ "DocAccessibleParent.cpp",
+ "RemoteAccessible.cpp",
+ ]
+
+ LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/generic",
+ "/accessible/xpcom",
+ ]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/accessible/mac/.clang-format b/accessible/mac/.clang-format
new file mode 100644
index 0000000000..269bce4d0f
--- /dev/null
+++ b/accessible/mac/.clang-format
@@ -0,0 +1,11 @@
+---
+# Objective C formatting rules.
+# Since this doesn't derive from the Cpp section, we need to redifine the root rules here.
+Language: ObjC
+BasedOnStyle: Google
+
+DerivePointerAlignment: false
+PointerAlignment: Left
+SortIncludes: false
+ColumnLimit: 80
+IndentPPDirectives: AfterHash
diff --git a/accessible/mac/AccessibleWrap.h b/accessible/mac/AccessibleWrap.h
new file mode 100644
index 0000000000..eb78b9417f
--- /dev/null
+++ b/accessible/mac/AccessibleWrap.h
@@ -0,0 +1,95 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 _AccessibleWrap_H_
+#define _AccessibleWrap_H_
+
+#include <objc/objc.h>
+
+#include "LocalAccessible.h"
+#include "PlatformExtTypes.h"
+#include "States.h"
+
+#include "nsCOMPtr.h"
+
+#include "nsTArray.h"
+
+#if defined(__OBJC__)
+@class mozAccessible;
+#endif
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Mac specific functionality for an accessibility tree node that originated in
+ * mDoc's content process.
+ */
+class AccessibleWrap : public LocalAccessible {
+ public: // construction, destruction
+ AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc);
+ virtual ~AccessibleWrap();
+
+ /**
+ * Get the native Obj-C object (mozAccessible).
+ */
+ virtual void GetNativeInterface(void** aOutAccessible) override;
+
+ /**
+ * The objective-c |Class| type that this accessible's native object
+ * should be instantied with. used on runtime to determine the
+ * right type for this accessible's associated native object.
+ */
+ virtual Class GetNativeType();
+
+ virtual void Shutdown() override;
+
+ virtual nsresult HandleAccEvent(AccEvent* aEvent) override;
+
+ protected:
+ friend class xpcAccessibleMacInterface;
+
+ /**
+ * Get the native object. Create it if needed.
+ */
+#if defined(__OBJC__)
+ mozAccessible* GetNativeObject();
+#else
+ id GetNativeObject();
+#endif
+
+ private:
+ /**
+ * Our native object. Private because its creation is done lazily.
+ * Don't access it directly. Ever. Unless you are GetNativeObject() or
+ * Shutdown()
+ */
+#if defined(__OBJC__)
+ // if we are in Objective-C, we use the actual Obj-C class.
+ mozAccessible* mNativeObject;
+#else
+ id mNativeObject;
+#endif
+
+ /**
+ * We have created our native. This does not mean there is one.
+ * This can never go back to false.
+ * We need it because checking whether we need a native object cost time.
+ */
+ bool mNativeInited;
+};
+
+Class GetTypeFromRole(roles::Role aRole);
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/mac/AccessibleWrap.mm b/accessible/mac/AccessibleWrap.mm
new file mode 100644
index 0000000000..ef2f4ba779
--- /dev/null
+++ b/accessible/mac/AccessibleWrap.mm
@@ -0,0 +1,281 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsObjCExceptions.h"
+#include "nsCocoaUtils.h"
+#include "nsUnicharUtils.h"
+
+#include "LocalAccessible-inl.h"
+#include "nsAccUtils.h"
+#include "mozilla/a11y/Role.h"
+#include "TextRange.h"
+#include "gfxPlatform.h"
+
+#import "MOXLandmarkAccessibles.h"
+#import "MOXMathAccessibles.h"
+#import "MOXTextMarkerDelegate.h"
+#import "MOXWebAreaAccessible.h"
+#import "mozAccessible.h"
+#import "mozActionElements.h"
+#import "mozHTMLAccessible.h"
+#import "mozSelectableElements.h"
+#import "mozTableAccessible.h"
+#import "mozTextAccessible.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
+ : LocalAccessible(aContent, aDoc),
+ mNativeObject(nil),
+ mNativeInited(false) {
+ if (aContent && aContent->IsElement() && aDoc) {
+ // Check if this accessible is a live region and queue it
+ // it for dispatching an event after it has been inserted.
+ DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(aDoc);
+ static const dom::Element::AttrValuesArray sLiveRegionValues[] = {
+ nsGkAtoms::OFF, nsGkAtoms::polite, nsGkAtoms::assertive, nullptr};
+ int32_t attrValue = nsAccUtils::FindARIAAttrValueIn(
+ aContent->AsElement(), nsGkAtoms::aria_live, sLiveRegionValues,
+ eIgnoreCase);
+ if (attrValue == 0) {
+ // aria-live is "off", do nothing.
+ } else if (attrValue > 0) {
+ // aria-live attribute is polite or assertive. It's live!
+ doc->QueueNewLiveRegion(this);
+ } else if (const nsRoleMapEntry* roleMap =
+ aria::GetRoleMap(aContent->AsElement())) {
+ // aria role defines it as a live region. It's live!
+ if (roleMap->liveAttRule == ePoliteLiveAttr ||
+ roleMap->liveAttRule == eAssertiveLiveAttr) {
+ doc->QueueNewLiveRegion(this);
+ }
+ } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
+ aContent, nsGkAtoms::aria_live)) {
+ // HTML element defines it as a live region. It's live!
+ if (value == nsGkAtoms::polite || value == nsGkAtoms::assertive) {
+ doc->QueueNewLiveRegion(this);
+ }
+ }
+ }
+}
+
+AccessibleWrap::~AccessibleWrap() {}
+
+mozAccessible* AccessibleWrap::GetNativeObject() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mNativeInited && !mNativeObject) {
+ // We don't creat OSX accessibles for xul tooltips, defunct accessibles,
+ // <br> (whitespace) elements, or pruned children.
+ //
+ // To maintain a scripting environment where the XPCOM accessible hierarchy
+ // look the same on all platforms, we still let the C++ objects be created
+ // though.
+ if (!IsXULTooltip() && !IsDefunct() && Role() != roles::WHITESPACE) {
+ mNativeObject = [[GetNativeType() alloc] initWithAccessible:this];
+ }
+ }
+
+ mNativeInited = true;
+
+ return mNativeObject;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+void AccessibleWrap::GetNativeInterface(void** aOutInterface) {
+ *aOutInterface = static_cast<void*>(GetNativeObject());
+}
+
+// overridden in subclasses to create the right kind of object. by default we
+// create a generic 'mozAccessible' node.
+Class AccessibleWrap::GetNativeType() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (IsXULTabpanels()) {
+ return [mozPaneAccessible class];
+ }
+
+ if (IsTable()) {
+ return [mozTableAccessible class];
+ }
+
+ if (IsTableRow()) {
+ return [mozTableRowAccessible class];
+ }
+
+ if (IsTableCell()) {
+ return [mozTableCellAccessible class];
+ }
+
+ if (IsDoc()) {
+ return [MOXWebAreaAccessible class];
+ }
+
+ return GetTypeFromRole(Role());
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// this method is very important. it is fired when an accessible object "dies".
+// after this point the object might still be around (because some 3rd party
+// still has a ref to it), but it is in fact 'dead'.
+void AccessibleWrap::Shutdown() {
+ // this ensure we will not try to re-create the native object.
+ mNativeInited = true;
+
+ // we really intend to access the member directly.
+ if (mNativeObject) {
+ [mNativeObject expire];
+ [mNativeObject release];
+ mNativeObject = nil;
+ }
+
+ LocalAccessible::Shutdown();
+}
+
+nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsresult rv = LocalAccessible::HandleAccEvent(aEvent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (IsDefunct()) {
+ // The accessible can become defunct after their events are handled.
+ return NS_OK;
+ }
+
+ uint32_t eventType = aEvent->GetEventType();
+
+ if (eventType == nsIAccessibleEvent::EVENT_SHOW) {
+ DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(Document());
+ doc->ProcessNewLiveRegions();
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// AccessibleWrap protected
+
+Class a11y::GetTypeFromRole(roles::Role aRole) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ switch (aRole) {
+ case roles::COMBOBOX:
+ return [mozPopupButtonAccessible class];
+
+ case roles::PUSHBUTTON:
+ return [mozButtonAccessible class];
+
+ case roles::PAGETAB:
+ return [mozTabAccessible class];
+
+ case roles::DATE_EDITOR:
+ return [mozDatePickerAccessible class];
+
+ case roles::CHECKBUTTON:
+ case roles::TOGGLE_BUTTON:
+ case roles::SWITCH:
+ case roles::CHECK_MENU_ITEM:
+ return [mozCheckboxAccessible class];
+
+ case roles::RADIOBUTTON:
+ case roles::RADIO_MENU_ITEM:
+ return [mozRadioButtonAccessible class];
+
+ case roles::SPINBUTTON:
+ case roles::SLIDER:
+ return [mozIncrementableAccessible class];
+
+ case roles::HEADING:
+ return [mozHeadingAccessible class];
+
+ case roles::PAGETABLIST:
+ return [mozTabGroupAccessible class];
+
+ case roles::ENTRY:
+ case roles::CAPTION:
+ case roles::EDITCOMBOBOX:
+ case roles::PASSWORD_TEXT:
+ // normal textfield (static or editable)
+ return [mozTextAccessible class];
+
+ case roles::TEXT_LEAF:
+ case roles::STATICTEXT:
+ return [mozTextLeafAccessible class];
+
+ case roles::LANDMARK:
+ return [MOXLandmarkAccessible class];
+
+ case roles::LINK:
+ return [mozLinkAccessible class];
+
+ case roles::LISTBOX:
+ return [mozListboxAccessible class];
+
+ case roles::LISTITEM:
+ return [MOXListItemAccessible class];
+
+ case roles::OPTION: {
+ return [mozOptionAccessible class];
+ }
+
+ case roles::RICH_OPTION: {
+ return [mozSelectableChildAccessible class];
+ }
+
+ case roles::COMBOBOX_LIST:
+ case roles::MENUBAR:
+ case roles::MENUPOPUP: {
+ return [mozMenuAccessible class];
+ }
+
+ case roles::COMBOBOX_OPTION:
+ case roles::PARENT_MENUITEM:
+ case roles::MENUITEM: {
+ return [mozMenuItemAccessible class];
+ }
+
+ case roles::MATHML_ROOT:
+ return [MOXMathRootAccessible class];
+
+ case roles::MATHML_SQUARE_ROOT:
+ return [MOXMathSquareRootAccessible class];
+
+ case roles::MATHML_FRACTION:
+ return [MOXMathFractionAccessible class];
+
+ case roles::MATHML_SUB:
+ case roles::MATHML_SUP:
+ case roles::MATHML_SUB_SUP:
+ return [MOXMathSubSupAccessible class];
+
+ case roles::MATHML_UNDER:
+ case roles::MATHML_OVER:
+ case roles::MATHML_UNDER_OVER:
+ return [MOXMathUnderOverAccessible class];
+
+ case roles::OUTLINE:
+ case roles::TREE_TABLE:
+ return [mozOutlineAccessible class];
+
+ case roles::OUTLINEITEM:
+ return [mozOutlineRowAccessible class];
+
+ default:
+ return [mozAccessible class];
+ }
+
+ return nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
diff --git a/accessible/mac/ApplicationAccessibleWrap.h b/accessible/mac/ApplicationAccessibleWrap.h
new file mode 100644
index 0000000000..a4b2fd70c7
--- /dev/null
+++ b/accessible/mac/ApplicationAccessibleWrap.h
@@ -0,0 +1,21 @@
+/* -*- 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/mac/DocAccessibleWrap.h b/accessible/mac/DocAccessibleWrap.h
new file mode 100644
index 0000000000..4526fb2b80
--- /dev/null
+++ b/accessible/mac/DocAccessibleWrap.h
@@ -0,0 +1,46 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "nsTHashSet.h"
+
+namespace mozilla {
+
+class PresShell;
+
+namespace a11y {
+
+class DocAccessibleWrap : public DocAccessible {
+ public:
+ DocAccessibleWrap(dom::Document* aDocument, PresShell* aPresShell);
+
+ virtual ~DocAccessibleWrap();
+
+ virtual void Shutdown() override;
+
+ virtual void AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue) override;
+
+ void QueueNewLiveRegion(LocalAccessible* aAccessible);
+
+ void ProcessNewLiveRegions();
+
+ protected:
+ virtual void DoInitialUpdate() override;
+
+ private:
+ nsTHashSet<void*> mNewLiveRegions;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/mac/DocAccessibleWrap.mm b/accessible/mac/DocAccessibleWrap.mm
new file mode 100644
index 0000000000..9b21251e1e
--- /dev/null
+++ b/accessible/mac/DocAccessibleWrap.mm
@@ -0,0 +1,105 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "ARIAMap.h"
+#include "DocAccessible-inl.h"
+#include "nsAccUtils.h"
+
+#import "mozAccessible.h"
+#import "MOXTextMarkerDelegate.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+DocAccessibleWrap::DocAccessibleWrap(dom::Document* aDocument,
+ PresShell* aPresShell)
+ : DocAccessible(aDocument, aPresShell) {}
+
+void DocAccessibleWrap::Shutdown() {
+ [MOXTextMarkerDelegate destroyForDoc:this];
+ DocAccessible::Shutdown();
+}
+
+DocAccessibleWrap::~DocAccessibleWrap() {}
+
+void DocAccessibleWrap::AttributeChanged(dom::Element* aElement,
+ int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue) {
+ DocAccessible::AttributeChanged(aElement, aNameSpaceID, aAttribute, aModType,
+ aOldValue);
+ if (aAttribute == nsGkAtoms::aria_live) {
+ LocalAccessible* accessible =
+ mContent != aElement ? GetAccessible(aElement) : this;
+ if (!accessible) {
+ return;
+ }
+
+ static const dom::Element::AttrValuesArray sLiveRegionValues[] = {
+ nsGkAtoms::OFF, nsGkAtoms::polite, nsGkAtoms::assertive, nullptr};
+ int32_t attrValue = nsAccUtils::FindARIAAttrValueIn(
+ aElement, nsGkAtoms::aria_live, sLiveRegionValues, eIgnoreCase);
+ if (attrValue > 0) {
+ if (!aOldValue || aOldValue->IsEmptyString() ||
+ aOldValue->Equals(nsGkAtoms::OFF, eIgnoreCase)) {
+ // This element just got an active aria-live attribute value
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED,
+ accessible);
+ }
+ } else {
+ if (aOldValue && (aOldValue->Equals(nsGkAtoms::polite, eIgnoreCase) ||
+ aOldValue->Equals(nsGkAtoms::assertive, eIgnoreCase))) {
+ // This element lost an active live region
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED,
+ accessible);
+ } else if (attrValue == 0) {
+ // aria-live="off", check if its a role-based live region that
+ // needs to be removed.
+ if (const nsRoleMapEntry* roleMap = accessible->ARIARoleMap()) {
+ // aria role defines it as a live region. It's live!
+ if (roleMap->liveAttRule == ePoliteLiveAttr ||
+ roleMap->liveAttRule == eAssertiveLiveAttr) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED,
+ accessible);
+ }
+ } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute(
+ aElement, nsGkAtoms::aria_live)) {
+ // HTML element defines it as a live region. It's live!
+ if (value == nsGkAtoms::polite || value == nsGkAtoms::assertive) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED,
+ accessible);
+ }
+ }
+ }
+ }
+ }
+}
+
+void DocAccessibleWrap::QueueNewLiveRegion(LocalAccessible* aAccessible) {
+ if (!aAccessible) {
+ return;
+ }
+
+ mNewLiveRegions.Insert(aAccessible->UniqueID());
+}
+
+void DocAccessibleWrap::ProcessNewLiveRegions() {
+ for (const auto& uniqueID : mNewLiveRegions) {
+ if (LocalAccessible* liveRegion =
+ GetAccessibleByUniqueID(const_cast<void*>(uniqueID))) {
+ FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED, liveRegion);
+ }
+ }
+
+ mNewLiveRegions.Clear();
+}
+
+void DocAccessibleWrap::DoInitialUpdate() {
+ DocAccessible::DoInitialUpdate();
+ ProcessNewLiveRegions();
+}
diff --git a/accessible/mac/GeckoTextMarker.h b/accessible/mac/GeckoTextMarker.h
new file mode 100644
index 0000000000..0e8f660694
--- /dev/null
+++ b/accessible/mac/GeckoTextMarker.h
@@ -0,0 +1,138 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* 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 _GeckoTextMarker_H_
+#define _GeckoTextMarker_H_
+
+#include <ApplicationServices/ApplicationServices.h>
+#include <Foundation/Foundation.h>
+
+#include "TextLeafRange.h"
+
+namespace mozilla {
+namespace a11y {
+
+class Accessible;
+class GeckoTextMarkerRange;
+
+class GeckoTextMarker final {
+ public:
+ GeckoTextMarker(Accessible* aAcc, int32_t aOffset);
+
+ explicit GeckoTextMarker(const TextLeafPoint& aTextLeafPoint)
+ : mPoint(aTextLeafPoint) {}
+
+ GeckoTextMarker() : mPoint() {}
+
+ static GeckoTextMarker MarkerFromAXTextMarker(Accessible* aDoc,
+ AXTextMarkerRef aTextMarker);
+
+ static GeckoTextMarker MarkerFromIndex(Accessible* aRoot, int32_t aIndex);
+
+ AXTextMarkerRef CreateAXTextMarker();
+
+ bool Next();
+
+ bool Previous();
+
+ GeckoTextMarkerRange LeftWordRange() const;
+
+ GeckoTextMarkerRange RightWordRange() const;
+
+ GeckoTextMarkerRange LineRange() const;
+
+ GeckoTextMarkerRange LeftLineRange() const;
+
+ GeckoTextMarkerRange RightLineRange() const;
+
+ GeckoTextMarkerRange ParagraphRange() const;
+
+ GeckoTextMarkerRange StyleRange() const;
+
+ int32_t& Offset() { return mPoint.mOffset; }
+
+ Accessible* Leaf();
+
+ Accessible* Acc() const { return mPoint.mAcc; }
+
+ bool IsValid() const { return !!mPoint; };
+
+ bool operator<(const GeckoTextMarker& aOther) const {
+ return mPoint < aOther.mPoint;
+ }
+
+ bool operator==(const GeckoTextMarker& aOther) const {
+ return mPoint == aOther.mPoint;
+ }
+
+ TextLeafPoint mPoint;
+};
+
+class GeckoTextMarkerRange final {
+ public:
+ GeckoTextMarkerRange(const GeckoTextMarker& aStart,
+ const GeckoTextMarker& aEnd)
+ : mRange(aStart.mPoint, aEnd.mPoint) {}
+
+ GeckoTextMarkerRange(const TextLeafPoint& aStart, const TextLeafPoint& aEnd)
+ : mRange(aStart, aEnd) {}
+
+ GeckoTextMarkerRange() {}
+
+ explicit GeckoTextMarkerRange(Accessible* aAccessible);
+
+ static GeckoTextMarkerRange MarkerRangeFromAXTextMarkerRange(
+ Accessible* aDoc, AXTextMarkerRangeRef aTextMarkerRange);
+
+ AXTextMarkerRangeRef CreateAXTextMarkerRange();
+
+ bool IsValid() const { return !!mRange.Start() && !!mRange.End(); };
+
+ GeckoTextMarker Start() { return GeckoTextMarker(mRange.Start()); }
+
+ GeckoTextMarker End() { return GeckoTextMarker(mRange.End()); }
+
+ /**
+ * Return text enclosed by the range.
+ */
+ NSString* Text() const;
+
+ /**
+ * Return the attributed text enclosed by the range.
+ */
+ NSAttributedString* AttributedText() const;
+
+ /**
+ * Return length of characters enclosed by the range.
+ */
+ int32_t Length() const;
+
+ /**
+ * Return screen bounds of range.
+ */
+ NSValue* Bounds() const;
+
+ /**
+ * Set the current range as the DOM selection.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void Select() const;
+
+ /**
+ * Crops the range if it overlaps the given accessible element boundaries.
+ * Return true if successfully cropped. false if the range does not intersect
+ * with the container.
+ */
+ bool Crop(Accessible* aContainer) { return mRange.Crop(aContainer); }
+
+ TextLeafRange mRange;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/mac/GeckoTextMarker.mm b/accessible/mac/GeckoTextMarker.mm
new file mode 100644
index 0000000000..ba3a6e6231
--- /dev/null
+++ b/accessible/mac/GeckoTextMarker.mm
@@ -0,0 +1,514 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "GeckoTextMarker.h"
+
+#import "MacUtils.h"
+
+#include "AccAttributes.h"
+#include "DocAccessible.h"
+#include "DocAccessibleParent.h"
+#include "nsCocoaUtils.h"
+#include "HyperTextAccessible.h"
+#include "States.h"
+#include "nsAccUtils.h"
+
+namespace mozilla {
+namespace a11y {
+
+struct TextMarkerData {
+ TextMarkerData(uintptr_t aDoc, uintptr_t aID, int32_t aOffset)
+ : mDoc(aDoc), mID(aID), mOffset(aOffset) {}
+ TextMarkerData() {}
+ uintptr_t mDoc;
+ uintptr_t mID;
+ int32_t mOffset;
+};
+
+// GeckoTextMarker
+
+GeckoTextMarker::GeckoTextMarker(Accessible* aAcc, int32_t aOffset) {
+ HyperTextAccessibleBase* ht = aAcc->AsHyperTextBase();
+ if (ht && aOffset != nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT &&
+ aOffset <= static_cast<int32_t>(ht->CharacterCount())) {
+ mPoint = aAcc->AsHyperTextBase()->ToTextLeafPoint(aOffset);
+ } else {
+ mPoint = TextLeafPoint(aAcc, aOffset);
+ }
+}
+
+GeckoTextMarker GeckoTextMarker::MarkerFromAXTextMarker(
+ Accessible* aDoc, AXTextMarkerRef aTextMarker) {
+ MOZ_ASSERT(aDoc);
+ if (!aTextMarker) {
+ return GeckoTextMarker();
+ }
+
+ if (AXTextMarkerGetLength(aTextMarker) != sizeof(TextMarkerData)) {
+ MOZ_ASSERT_UNREACHABLE("Malformed AXTextMarkerRef");
+ return GeckoTextMarker();
+ }
+
+ TextMarkerData markerData;
+ memcpy(&markerData, AXTextMarkerGetBytePtr(aTextMarker),
+ sizeof(TextMarkerData));
+
+ if (!utils::DocumentExists(aDoc, markerData.mDoc)) {
+ return GeckoTextMarker();
+ }
+
+ Accessible* doc = reinterpret_cast<Accessible*>(markerData.mDoc);
+ MOZ_ASSERT(doc->IsDoc());
+ int32_t offset = markerData.mOffset;
+ Accessible* acc = nullptr;
+ if (doc->IsRemote()) {
+ acc = doc->AsRemote()->AsDoc()->GetAccessible(markerData.mID);
+ } else {
+ acc = doc->AsLocal()->AsDoc()->GetAccessibleByUniqueID(
+ reinterpret_cast<void*>(markerData.mID));
+ }
+
+ if (!acc) {
+ return GeckoTextMarker();
+ }
+
+ return GeckoTextMarker(acc, offset);
+}
+
+GeckoTextMarker GeckoTextMarker::MarkerFromIndex(Accessible* aRoot,
+ int32_t aIndex) {
+ TextLeafRange range(
+ TextLeafPoint(aRoot, 0),
+ TextLeafPoint(aRoot, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT));
+ int32_t index = aIndex;
+ // Iterate through all segments until we exhausted the index sum
+ // so we can find the segment the index lives in.
+ for (TextLeafRange segment : range) {
+ if (segment.End().mAcc->Role() == roles::LISTITEM_MARKER) {
+ // XXX: MacOS expects bullets to be in the range's text, but not in
+ // the calculated length!
+ continue;
+ }
+
+ index -= segment.End().mOffset - segment.Start().mOffset;
+ if (index <= 0) {
+ // The index is in the current segment.
+ return GeckoTextMarker(segment.Start().mAcc,
+ segment.End().mOffset + index);
+ }
+ }
+
+ return GeckoTextMarker();
+}
+
+AXTextMarkerRef GeckoTextMarker::CreateAXTextMarker() {
+ if (!IsValid()) {
+ return nil;
+ }
+
+ Accessible* doc = nsAccUtils::DocumentFor(mPoint.mAcc);
+ TextMarkerData markerData(reinterpret_cast<uintptr_t>(doc), mPoint.mAcc->ID(),
+ mPoint.mOffset);
+ AXTextMarkerRef cf_text_marker = AXTextMarkerCreate(
+ kCFAllocatorDefault, reinterpret_cast<const UInt8*>(&markerData),
+ sizeof(TextMarkerData));
+
+ return (__bridge AXTextMarkerRef)[(__bridge id)(cf_text_marker)autorelease];
+}
+
+bool GeckoTextMarker::Next() {
+ TextLeafPoint next =
+ mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirNext,
+ TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
+
+ if (next && next != mPoint) {
+ mPoint = next;
+ return true;
+ }
+
+ return false;
+}
+
+bool GeckoTextMarker::Previous() {
+ TextLeafPoint prev =
+ mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
+ TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
+ if (prev && mPoint != prev) {
+ mPoint = prev;
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Return true if the given point is inside editable content.
+ */
+static bool IsPointInEditable(const TextLeafPoint& aPoint) {
+ if (aPoint.mAcc) {
+ if (aPoint.mAcc->State() & states::EDITABLE) {
+ return true;
+ }
+
+ Accessible* parent = aPoint.mAcc->Parent();
+ if (parent && (parent->State() & states::EDITABLE)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+GeckoTextMarkerRange GeckoTextMarker::LeftWordRange() const {
+ bool includeCurrentInStart = !mPoint.IsParagraphStart(true);
+ if (includeCurrentInStart) {
+ TextLeafPoint prevChar =
+ mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
+ if (!prevChar.IsSpace()) {
+ includeCurrentInStart = false;
+ }
+ }
+
+ TextLeafPoint start = mPoint.FindBoundary(
+ nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious,
+ includeCurrentInStart
+ ? (TextLeafPoint::BoundaryFlags::eIncludeOrigin |
+ TextLeafPoint::BoundaryFlags::eStopInEditable |
+ TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker)
+ : (TextLeafPoint::BoundaryFlags::eStopInEditable |
+ TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker));
+
+ TextLeafPoint end;
+ if (start == mPoint) {
+ end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirPrevious,
+ TextLeafPoint::BoundaryFlags::eStopInEditable);
+ }
+
+ if (start != mPoint || end == start) {
+ end = start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
+ TextLeafPoint::BoundaryFlags::eStopInEditable);
+ if (end < mPoint && IsPointInEditable(end) && !IsPointInEditable(mPoint)) {
+ start = end;
+ end = mPoint;
+ }
+ }
+
+ return GeckoTextMarkerRange(start < end ? start : end,
+ start < end ? end : start);
+}
+
+GeckoTextMarkerRange GeckoTextMarker::RightWordRange() const {
+ TextLeafPoint prevChar =
+ mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
+ TextLeafPoint::BoundaryFlags::eStopInEditable);
+
+ if (prevChar != mPoint && mPoint.IsParagraphStart(true)) {
+ return GeckoTextMarkerRange(mPoint, mPoint);
+ }
+
+ TextLeafPoint end =
+ mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
+ TextLeafPoint::BoundaryFlags::eStopInEditable);
+
+ if (end == mPoint) {
+ // No word to the right of this point.
+ return GeckoTextMarkerRange(mPoint, mPoint);
+ }
+
+ TextLeafPoint start =
+ end.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_START, eDirPrevious,
+ TextLeafPoint::BoundaryFlags::eStopInEditable);
+
+ if (start.FindBoundary(nsIAccessibleText::BOUNDARY_WORD_END, eDirNext,
+ TextLeafPoint::BoundaryFlags::eStopInEditable) <
+ mPoint) {
+ // Word end is inside of an input to the left of this.
+ return GeckoTextMarkerRange(mPoint, mPoint);
+ }
+
+ if (mPoint < start) {
+ end = start;
+ start = mPoint;
+ }
+
+ return GeckoTextMarkerRange(start < end ? start : end,
+ start < end ? end : start);
+}
+
+GeckoTextMarkerRange GeckoTextMarker::LineRange() const {
+ TextLeafPoint start = mPoint.FindBoundary(
+ nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
+ TextLeafPoint::BoundaryFlags::eStopInEditable |
+ TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker |
+ TextLeafPoint::BoundaryFlags::eIncludeOrigin);
+ // If this is a blank line containing only a line feed, the start boundary
+ // is the same as the end boundary. We do not want to walk to the end of the
+ // next line.
+ TextLeafPoint end =
+ start.IsLineFeedChar()
+ ? start
+ : start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
+ TextLeafPoint::BoundaryFlags::eStopInEditable);
+
+ return GeckoTextMarkerRange(start, end);
+}
+
+GeckoTextMarkerRange GeckoTextMarker::LeftLineRange() const {
+ TextLeafPoint start = mPoint.FindBoundary(
+ nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
+ TextLeafPoint::BoundaryFlags::eStopInEditable |
+ TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
+ TextLeafPoint end =
+ start.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
+ TextLeafPoint::BoundaryFlags::eStopInEditable);
+
+ return GeckoTextMarkerRange(start, end);
+}
+
+GeckoTextMarkerRange GeckoTextMarker::RightLineRange() const {
+ TextLeafPoint end =
+ mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_END, eDirNext,
+ TextLeafPoint::BoundaryFlags::eStopInEditable);
+ TextLeafPoint start =
+ end.FindBoundary(nsIAccessibleText::BOUNDARY_LINE_START, eDirPrevious,
+ TextLeafPoint::BoundaryFlags::eStopInEditable);
+
+ return GeckoTextMarkerRange(start, end);
+}
+
+GeckoTextMarkerRange GeckoTextMarker::ParagraphRange() const {
+ // XXX: WebKit gets trapped in inputs. Maybe we shouldn't?
+ TextLeafPoint end =
+ mPoint.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirNext,
+ TextLeafPoint::BoundaryFlags::eStopInEditable);
+ TextLeafPoint start =
+ end.FindBoundary(nsIAccessibleText::BOUNDARY_PARAGRAPH, eDirPrevious,
+ TextLeafPoint::BoundaryFlags::eStopInEditable);
+
+ return GeckoTextMarkerRange(start, end);
+}
+
+GeckoTextMarkerRange GeckoTextMarker::StyleRange() const {
+ if (mPoint.mOffset == 0) {
+ // If the marker is on the boundary between two leafs, MacOS expects the
+ // previous leaf.
+ TextLeafPoint prev = mPoint.FindBoundary(
+ nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
+ TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
+ if (prev != mPoint) {
+ return GeckoTextMarker(prev).StyleRange();
+ }
+ }
+
+ TextLeafPoint start(mPoint.mAcc, 0);
+ TextLeafPoint end(mPoint.mAcc, nsAccUtils::TextLength(mPoint.mAcc));
+ return GeckoTextMarkerRange(start, end);
+}
+
+Accessible* GeckoTextMarker::Leaf() {
+ MOZ_ASSERT(mPoint.mAcc);
+ Accessible* acc = mPoint.mAcc;
+ if (mPoint.mOffset == 0) {
+ // If the marker is on the boundary between two leafs, MacOS expects the
+ // previous leaf.
+ TextLeafPoint prev = mPoint.FindBoundary(
+ nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious,
+ TextLeafPoint::BoundaryFlags::eIgnoreListItemMarker);
+ acc = prev.mAcc;
+ }
+
+ Accessible* parent = acc->Parent();
+ return parent && nsAccUtils::MustPrune(parent) ? parent : acc;
+}
+
+// GeckoTextMarkerRange
+
+GeckoTextMarkerRange::GeckoTextMarkerRange(Accessible* aAccessible) {
+ mRange = TextLeafRange(
+ TextLeafPoint(aAccessible, 0),
+ TextLeafPoint(aAccessible, nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT));
+}
+
+GeckoTextMarkerRange GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
+ Accessible* aDoc, AXTextMarkerRangeRef aTextMarkerRange) {
+ if (!aTextMarkerRange ||
+ CFGetTypeID(aTextMarkerRange) != AXTextMarkerRangeGetTypeID()) {
+ return GeckoTextMarkerRange();
+ }
+
+ AXTextMarkerRef start_marker(
+ AXTextMarkerRangeCopyStartMarker(aTextMarkerRange));
+ AXTextMarkerRef end_marker(AXTextMarkerRangeCopyEndMarker(aTextMarkerRange));
+
+ GeckoTextMarker start =
+ GeckoTextMarker::MarkerFromAXTextMarker(aDoc, start_marker);
+ GeckoTextMarker end =
+ GeckoTextMarker::MarkerFromAXTextMarker(aDoc, end_marker);
+
+ CFRelease(start_marker);
+ CFRelease(end_marker);
+
+ return GeckoTextMarkerRange(start, end);
+}
+
+AXTextMarkerRangeRef GeckoTextMarkerRange::CreateAXTextMarkerRange() {
+ if (!IsValid()) {
+ return nil;
+ }
+
+ GeckoTextMarker start = GeckoTextMarker(mRange.Start());
+ GeckoTextMarker end = GeckoTextMarker(mRange.End());
+
+ AXTextMarkerRangeRef cf_text_marker_range =
+ AXTextMarkerRangeCreate(kCFAllocatorDefault, start.CreateAXTextMarker(),
+ end.CreateAXTextMarker());
+
+ return (__bridge AXTextMarkerRangeRef)[(__bridge id)(
+ cf_text_marker_range)autorelease];
+}
+
+NSString* GeckoTextMarkerRange::Text() const {
+ if (mRange.Start() == mRange.End()) {
+ return @"";
+ }
+
+ if ((mRange.Start().mAcc == mRange.End().mAcc) &&
+ (mRange.Start().mAcc->ChildCount() == 0) &&
+ (mRange.Start().mAcc->State() & states::EDITABLE)) {
+ return @"";
+ }
+
+ nsAutoString text;
+ TextLeafPoint prev = mRange.Start().FindBoundary(
+ nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
+ TextLeafRange range =
+ prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER
+ ? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End())
+ : mRange;
+
+ for (TextLeafRange segment : range) {
+ TextLeafPoint start = segment.Start();
+ if (start.mAcc->IsTextField() && start.mAcc->ChildCount() == 0) {
+ continue;
+ }
+
+ start.mAcc->AppendTextTo(text, start.mOffset,
+ segment.End().mOffset - start.mOffset);
+ }
+
+ return nsCocoaUtils::ToNSString(text);
+}
+
+static void AppendTextToAttributedString(
+ NSMutableAttributedString* aAttributedString, Accessible* aAccessible,
+ const nsString& aString, AccAttributes* aAttributes) {
+ NSAttributedString* substr = [[[NSAttributedString alloc]
+ initWithString:nsCocoaUtils::ToNSString(aString)
+ attributes:utils::StringAttributesFromAccAttributes(
+ aAttributes, aAccessible)] autorelease];
+
+ [aAttributedString appendAttributedString:substr];
+}
+
+NSAttributedString* GeckoTextMarkerRange::AttributedText() const {
+ NSMutableAttributedString* str =
+ [[[NSMutableAttributedString alloc] init] autorelease];
+
+ if (mRange.Start() == mRange.End()) {
+ return str;
+ }
+
+ if ((mRange.Start().mAcc == mRange.End().mAcc) &&
+ (mRange.Start().mAcc->ChildCount() == 0) &&
+ (mRange.Start().mAcc->IsTextField())) {
+ return str;
+ }
+
+ TextLeafPoint prev = mRange.Start().FindBoundary(
+ nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious);
+ TextLeafRange range =
+ prev != mRange.Start() && prev.mAcc->Role() == roles::LISTITEM_MARKER
+ ? TextLeafRange(TextLeafPoint(prev.mAcc, 0), mRange.End())
+ : mRange;
+
+ nsAutoString text;
+ RefPtr<AccAttributes> currentRun = range.Start().GetTextAttributes();
+ Accessible* runAcc = range.Start().mAcc;
+ for (TextLeafRange segment : range) {
+ TextLeafPoint start = segment.Start();
+ TextLeafPoint attributesNext;
+ do {
+ if (start.mAcc->IsText()) {
+ attributesNext = start.FindTextAttrsStart(eDirNext, false);
+ } else {
+ // If this segment isn't a text leaf, but another kind of inline element
+ // like a control, just consider this full segment one "attributes run".
+ attributesNext = segment.End();
+ }
+ if (attributesNext == start) {
+ // XXX: FindTextAttrsStart should not return the same point.
+ break;
+ }
+ RefPtr<AccAttributes> attributes = start.GetTextAttributes();
+ if (!currentRun || !attributes || !attributes->Equal(currentRun)) {
+ // If currentRun is null this is a non-text control and we will
+ // append a run with no text or attributes, just an AXAttachment
+ // referencing this accessible.
+ AppendTextToAttributedString(str, runAcc, text, currentRun);
+ text.Truncate();
+ currentRun = attributes;
+ runAcc = start.mAcc;
+ }
+ TextLeafPoint end =
+ attributesNext < segment.End() ? attributesNext : segment.End();
+ start.mAcc->AppendTextTo(text, start.mOffset,
+ end.mOffset - start.mOffset);
+ start = attributesNext;
+
+ } while (attributesNext < segment.End());
+ }
+
+ if (!text.IsEmpty()) {
+ AppendTextToAttributedString(str, runAcc, text, currentRun);
+ }
+
+ return str;
+}
+
+int32_t GeckoTextMarkerRange::Length() const {
+ int32_t length = 0;
+ for (TextLeafRange segment : mRange) {
+ if (segment.End().mAcc->Role() == roles::LISTITEM_MARKER) {
+ // XXX: MacOS expects bullets to be in the range's text, but not in
+ // the calculated length!
+ continue;
+ }
+ length += segment.End().mOffset - segment.Start().mOffset;
+ }
+
+ return length;
+}
+
+NSValue* GeckoTextMarkerRange::Bounds() const {
+ LayoutDeviceIntRect rect = mRange ? mRange.Bounds() : LayoutDeviceIntRect();
+
+ NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
+ NSRect r =
+ NSMakeRect(static_cast<CGFloat>(rect.x) / scaleFactor,
+ [mainView frame].size.height -
+ static_cast<CGFloat>(rect.y + rect.height) / scaleFactor,
+ static_cast<CGFloat>(rect.width) / scaleFactor,
+ static_cast<CGFloat>(rect.height) / scaleFactor);
+
+ return [NSValue valueWithRect:r];
+}
+
+void GeckoTextMarkerRange::Select() const { mRange.SetSelection(0); }
+
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/mac/MOXAccessibleBase.h b/accessible/mac/MOXAccessibleBase.h
new file mode 100644
index 0000000000..751fa5f28d
--- /dev/null
+++ b/accessible/mac/MOXAccessibleBase.h
@@ -0,0 +1,143 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+
+#import "mozAccessibleProtocol.h"
+#import "MOXAccessibleProtocol.h"
+
+#include "Platform.h"
+
+inline id<mozAccessible> GetObjectOrRepresentedView(id<mozAccessible> aObject) {
+ if (!mozilla::a11y::ShouldA11yBeEnabled()) {
+ // If platform a11y is not enabled, don't return represented view.
+ // This is mostly for our mochitest environment because the represented
+ // ChildView checks `ShouldA11yBeEnabled` before proxying accessibility
+ // methods to mozAccessibles.
+ return aObject;
+ }
+
+ return [aObject hasRepresentedView] ? [aObject representedView] : aObject;
+}
+
+@interface MOXAccessibleBase : NSObject <mozAccessible, MOXAccessible> {
+ BOOL mIsExpired;
+}
+
+#pragma mark - mozAccessible/widget
+
+// override
+- (BOOL)hasRepresentedView;
+
+// override
+- (id)representedView;
+
+// override
+- (BOOL)isRoot;
+
+#pragma mark - mozAccessible/NSAccessibility
+
+// The methods below interface with the platform through NSAccessibility.
+// They should not be called directly or overridden in subclasses.
+
+// override, final
+- (NSArray*)accessibilityAttributeNames;
+
+// override, final
+- (id)accessibilityAttributeValue:(NSString*)attribute;
+
+// override, final
+- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute;
+
+// override, final
+- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute;
+
+// override, final
+- (NSArray*)accessibilityActionNames;
+
+// override, final
+- (void)accessibilityPerformAction:(NSString*)action;
+
+// override, final
+- (NSString*)accessibilityActionDescription:(NSString*)action;
+
+// override, final
+- (NSArray*)accessibilityParameterizedAttributeNames;
+
+// override, final
+- (id)accessibilityAttributeValue:(NSString*)attribute
+ forParameter:(id)parameter;
+
+// override, final
+- (id)accessibilityHitTest:(NSPoint)point;
+
+// override, final
+- (id)accessibilityFocusedUIElement;
+
+// override, final
+- (BOOL)isAccessibilityElement;
+
+// final
+- (BOOL)accessibilityNotifiesWhenDestroyed;
+
+#pragma mark - MOXAccessible protocol
+
+// override
+- (id)moxHitTest:(NSPoint)point;
+
+// override
+- (id)moxFocusedUIElement;
+
+// override
+- (void)moxPostNotification:(NSString*)notification;
+
+// override
+- (void)moxPostNotification:(NSString*)notification
+ withUserInfo:(NSDictionary*)userInfo;
+
+// override
+- (BOOL)moxBlockSelector:(SEL)selector;
+
+// override
+- (NSArray*)moxUnignoredChildren;
+
+// override
+- (NSArray*)moxChildren;
+
+// override
+- (id<mozAccessible>)moxUnignoredParent;
+
+// override
+- (id<mozAccessible>)moxParent;
+
+// override
+- (NSNumber*)moxIndexForChildUIElement:(id)child;
+
+// override
+- (id)moxTopLevelUIElement;
+
+// override
+- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate;
+
+// override
+- (BOOL)moxIsLiveRegion;
+
+// override
+- (id<MOXAccessible>)moxFindAncestor:(BOOL (^)(id<MOXAccessible> moxAcc,
+ BOOL* stop))findBlock;
+
+#pragma mark -
+
+- (NSString*)description;
+
+- (BOOL)isExpired;
+
+// makes ourselves "expired". after this point, we might be around if someone
+// has retained us (e.g., a third-party), but we really contain no information.
+- (void)expire;
+
+@end
diff --git a/accessible/mac/MOXAccessibleBase.mm b/accessible/mac/MOXAccessibleBase.mm
new file mode 100644
index 0000000000..fe7c367f8c
--- /dev/null
+++ b/accessible/mac/MOXAccessibleBase.mm
@@ -0,0 +1,584 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "MOXAccessibleBase.h"
+
+#import "MacSelectorMap.h"
+
+#include "nsObjCExceptions.h"
+#include "xpcAccessibleMacInterface.h"
+#include "mozilla/Logging.h"
+#include "gfxPlatform.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+#undef LOG
+mozilla::LogModule* GetMacAccessibilityLog() {
+ static mozilla::LazyLogModule sLog("MacAccessibility");
+
+ return sLog;
+}
+#define LOG(type, format, ...) \
+ do { \
+ if (MOZ_LOG_TEST(GetMacAccessibilityLog(), type)) { \
+ NSString* msg = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \
+ MOZ_LOG(GetMacAccessibilityLog(), type, ("%s", [msg UTF8String])); \
+ } \
+ } while (0)
+
+@interface NSObject (MOXAccessible)
+
+// This NSObject conforms to MOXAccessible.
+// This is needed to we know to mutate the value
+// (get represented view, check isAccessibilityElement)
+// before forwarding it to NSAccessibility.
+- (BOOL)isMOXAccessible;
+
+// Same as above, but this checks if the NSObject is an array with
+// mozAccessible conforming objects.
+- (BOOL)hasMOXAccessibles;
+
+@end
+
+@implementation NSObject (MOXAccessible)
+
+- (BOOL)isMOXAccessible {
+ return [self conformsToProtocol:@protocol(MOXAccessible)];
+}
+
+- (BOOL)hasMOXAccessibles {
+ return [self isKindOfClass:[NSArray class]] &&
+ [[(NSArray*)self firstObject] isMOXAccessible];
+}
+
+@end
+
+// Private methods
+@interface MOXAccessibleBase ()
+
+- (BOOL)isSelectorSupported:(SEL)selector;
+
+@end
+
+@implementation MOXAccessibleBase
+
+#pragma mark - mozAccessible/widget
+
+- (BOOL)hasRepresentedView {
+ return NO;
+}
+
+- (id)representedView {
+ return nil;
+}
+
+- (BOOL)isRoot {
+ return NO;
+}
+
+#pragma mark - mozAccessible/NSAccessibility
+
+- (NSArray*)accessibilityAttributeNames {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ static NSMutableDictionary* attributesForEachClass = nil;
+
+ if (!attributesForEachClass) {
+ attributesForEachClass = [[NSMutableDictionary alloc] init];
+ }
+
+ NSMutableArray* attributes =
+ attributesForEachClass [[self class]]
+ ?: [[[NSMutableArray alloc] init] autorelease];
+
+ NSDictionary* getters = mac::AttributeGetters();
+ if (![attributes count]) {
+ // Go through all our attribute getters, if they are supported by this class
+ // advertise the attribute name.
+ for (NSString* attribute in getters) {
+ SEL selector = NSSelectorFromString(getters[attribute]);
+ if ([self isSelectorSupported:selector]) {
+ [attributes addObject:attribute];
+ }
+ }
+
+ // If we have a delegate add all the text marker attributes.
+ if ([self moxTextMarkerDelegate]) {
+ [attributes addObjectsFromArray:[mac::TextAttributeGetters() allKeys]];
+ }
+
+ // We store a hash table with types as keys, and atttribute lists as values.
+ // This lets us cache the atttribute list of each subclass so we only
+ // need to gather its MOXAccessible methods once.
+ // XXX: Uncomment when accessibilityAttributeNames is removed from all
+ // subclasses. attributesForEachClass[[self class]] = attributes;
+ }
+
+ return attributes;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)accessibilityAttributeValue:(NSString*)attribute {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ id value = nil;
+ NSDictionary* getters = mac::AttributeGetters();
+ if (getters[attribute]) {
+ SEL selector = NSSelectorFromString(getters[attribute]);
+ if ([self isSelectorSupported:selector]) {
+ value = [self performSelector:selector];
+ }
+ } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
+ // If we have a delegate, check if attribute is a text marker
+ // attribute and call the associated selector on the delegate
+ // if so.
+ NSDictionary* textMarkerGetters = mac::TextAttributeGetters();
+ if (textMarkerGetters[attribute]) {
+ SEL selector = NSSelectorFromString(textMarkerGetters[attribute]);
+ if ([textMarkerDelegate respondsToSelector:selector]) {
+ value = [textMarkerDelegate performSelector:selector];
+ }
+ }
+ }
+
+ if ([value isMOXAccessible]) {
+ // If this is a MOXAccessible, get its represented view or filter it if
+ // it should be ignored.
+ value = [value isAccessibilityElement] ? GetObjectOrRepresentedView(value)
+ : nil;
+ }
+
+ if ([value hasMOXAccessibles]) {
+ // If this is an array of mozAccessibles, get each element's represented
+ // view and remove it from the returned array if it should be ignored.
+ NSUInteger arrSize = [value count];
+ NSMutableArray* arr =
+ [[[NSMutableArray alloc] initWithCapacity:arrSize] autorelease];
+ for (NSUInteger i = 0; i < arrSize; i++) {
+ id<mozAccessible> mozAcc = GetObjectOrRepresentedView(value[i]);
+ if ([mozAcc isAccessibilityElement]) {
+ [arr addObject:mozAcc];
+ }
+ }
+
+ value = arr;
+ }
+
+ if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) {
+ if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
+ LOG(LogLevel::Verbose, @"%@ attributeValue %@ => %@", self, attribute,
+ value);
+ } else if (![attribute isEqualToString:@"AXParent"] &&
+ ![attribute isEqualToString:@"AXRole"] &&
+ ![attribute isEqualToString:@"AXSubrole"] &&
+ ![attribute isEqualToString:@"AXSize"] &&
+ ![attribute isEqualToString:@"AXPosition"] &&
+ ![attribute isEqualToString:@"AXRole"] &&
+ ![attribute isEqualToString:@"AXChildren"]) {
+ LOG(LogLevel::Debug, @"%@ attributeValue %@", self, attribute);
+ }
+ }
+
+ return value;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ([self isExpired]) {
+ return NO;
+ }
+
+ NSDictionary* setters = mac::AttributeSetters();
+ if (setters[attribute]) {
+ SEL selector = NSSelectorFromString(setters[attribute]);
+ if ([self isSelectorSupported:selector]) {
+ return YES;
+ }
+ } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
+ // If we have a delegate, check text setters on delegate
+ NSDictionary* textMarkerSetters = mac::TextAttributeSetters();
+ if (textMarkerSetters[attribute]) {
+ SEL selector = NSSelectorFromString(textMarkerSetters[attribute]);
+ if ([textMarkerDelegate respondsToSelector:selector]) {
+ return YES;
+ }
+ }
+ }
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if ([self isExpired]) {
+ return;
+ }
+
+ LOG(LogLevel::Debug, @"%@ setValueForattribute %@ = %@", self, attribute,
+ value);
+
+ NSDictionary* setters = mac::AttributeSetters();
+ if (setters[attribute]) {
+ SEL selector = NSSelectorFromString(setters[attribute]);
+ if ([self isSelectorSupported:selector]) {
+ [self performSelector:selector withObject:value];
+ }
+ } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
+ // If we have a delegate, check if attribute is a text marker
+ // attribute and call the associated selector on the delegate
+ // if so.
+ NSDictionary* textMarkerSetters = mac::TextAttributeSetters();
+ if (textMarkerSetters[attribute]) {
+ SEL selector = NSSelectorFromString(textMarkerSetters[attribute]);
+ if ([textMarkerDelegate respondsToSelector:selector]) {
+ [textMarkerDelegate performSelector:selector withObject:value];
+ }
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (NSArray*)accessibilityActionNames {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ NSMutableArray* actionNames = [[[NSMutableArray alloc] init] autorelease];
+
+ NSDictionary* actions = mac::Actions();
+ for (NSString* action in actions) {
+ SEL selector = NSSelectorFromString(actions[action]);
+ if ([self isSelectorSupported:selector]) {
+ [actionNames addObject:action];
+ }
+ }
+
+ return actionNames;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)accessibilityPerformAction:(NSString*)action {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ if ([self isExpired]) {
+ return;
+ }
+
+ LOG(LogLevel::Debug, @"%@ performAction %@ ", self, action);
+
+ NSDictionary* actions = mac::Actions();
+ if (actions[action]) {
+ SEL selector = NSSelectorFromString(actions[action]);
+ if ([self isSelectorSupported:selector]) {
+ [self performSelector:selector];
+ }
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (NSString*)accessibilityActionDescription:(NSString*)action {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ // by default we return whatever the MacOS API know about.
+ // if you have custom actions, override.
+ return NSAccessibilityActionDescription(action);
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSArray*)accessibilityParameterizedAttributeNames {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ NSMutableArray* attributeNames = [[[NSMutableArray alloc] init] autorelease];
+
+ NSDictionary* attributes = mac::ParameterizedAttributeGetters();
+ for (NSString* attribute in attributes) {
+ SEL selector = NSSelectorFromString(attributes[attribute]);
+ if ([self isSelectorSupported:selector]) {
+ [attributeNames addObject:attribute];
+ }
+ }
+
+ // If we have a delegate add all the text marker attributes.
+ if ([self moxTextMarkerDelegate]) {
+ [attributeNames
+ addObjectsFromArray:[mac::ParameterizedTextAttributeGetters() allKeys]];
+ }
+
+ return attributeNames;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)accessibilityAttributeValue:(NSString*)attribute
+ forParameter:(id)parameter {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ id value = nil;
+
+ NSDictionary* getters = mac::ParameterizedAttributeGetters();
+ if (getters[attribute]) {
+ SEL selector = NSSelectorFromString(getters[attribute]);
+ if ([self isSelectorSupported:selector]) {
+ value = [self performSelector:selector withObject:parameter];
+ }
+ } else if (id textMarkerDelegate = [self moxTextMarkerDelegate]) {
+ // If we have a delegate, check if attribute is a text marker
+ // attribute and call the associated selector on the delegate
+ // if so.
+ NSDictionary* textMarkerGetters = mac::ParameterizedTextAttributeGetters();
+ if (textMarkerGetters[attribute]) {
+ SEL selector = NSSelectorFromString(textMarkerGetters[attribute]);
+ if ([textMarkerDelegate respondsToSelector:selector]) {
+ value = [textMarkerDelegate performSelector:selector
+ withObject:parameter];
+ }
+ }
+ }
+
+ if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
+ LOG(LogLevel::Verbose, @"%@ attributeValueForParam %@(%@) => %@", self,
+ attribute, parameter, value);
+ } else {
+ LOG(LogLevel::Debug, @"%@ attributeValueForParam %@", self, attribute);
+ }
+
+ return value;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)accessibilityHitTest:(NSPoint)point {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ return GetObjectOrRepresentedView([self moxHitTest:point]);
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)accessibilityFocusedUIElement {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ return GetObjectOrRepresentedView([self moxFocusedUIElement]);
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)isAccessibilityElement {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if ([self isExpired]) {
+ return YES;
+ }
+
+ id parent = [self moxParent];
+ if (![parent isMOXAccessible]) {
+ return YES;
+ }
+
+ return ![self moxIgnoreWithParent:parent];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NO);
+}
+
+- (BOOL)accessibilityNotifiesWhenDestroyed {
+ return YES;
+}
+
+#pragma mark - MOXAccessible protocol
+
+- (NSNumber*)moxIndexForChildUIElement:(id)child {
+ return @([[self moxUnignoredChildren] indexOfObject:child]);
+}
+
+- (id)moxTopLevelUIElement {
+ return [self moxWindow];
+}
+
+- (id)moxHitTest:(NSPoint)point {
+ return self;
+}
+
+- (id)moxFocusedUIElement {
+ return self;
+}
+
+- (void)moxPostNotification:(NSString*)notification {
+ [self moxPostNotification:notification withUserInfo:nil];
+}
+
+- (void)moxPostNotification:(NSString*)notification
+ withUserInfo:(NSDictionary*)userInfo {
+ if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Verbose)) {
+ LOG(LogLevel::Verbose, @"%@ notify %@ %@", self, notification, userInfo);
+ } else {
+ LOG(LogLevel::Debug, @"%@ notify %@", self, notification);
+ }
+
+ // This sends events via nsIObserverService to be consumed by our mochitests.
+ xpcAccessibleMacEvent::FireEvent(self, notification, userInfo);
+
+ if (gfxPlatform::IsHeadless()) {
+ // Using a headless toolkit for tests and whatnot, posting accessibility
+ // notification won't work.
+ return;
+ }
+
+ if (![self isAccessibilityElement]) {
+ // If this is an ignored object, don't expose it to system.
+ return;
+ }
+
+ if (userInfo) {
+ NSAccessibilityPostNotificationWithUserInfo(
+ GetObjectOrRepresentedView(self), notification, userInfo);
+ } else {
+ NSAccessibilityPostNotification(GetObjectOrRepresentedView(self),
+ notification);
+ }
+}
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ return NO;
+}
+
+- (NSArray*)moxChildren {
+ return @[];
+}
+
+- (NSArray*)moxUnignoredChildren {
+ NSMutableArray* unignoredChildren =
+ [[[NSMutableArray alloc] init] autorelease];
+ NSArray* allChildren = [self moxChildren];
+
+ for (MOXAccessibleBase* nativeChild in allChildren) {
+ if ([nativeChild moxIgnoreWithParent:self]) {
+ // If this child should be ignored get its unignored children.
+ // This will in turn recurse to any unignored descendants if the
+ // child is ignored.
+ [unignoredChildren
+ addObjectsFromArray:[nativeChild moxUnignoredChildren]];
+ } else {
+ [unignoredChildren addObject:nativeChild];
+ }
+ }
+
+ return unignoredChildren;
+}
+
+- (id<mozAccessible>)moxParent {
+ return nil;
+}
+
+- (id<mozAccessible>)moxUnignoredParent {
+ id<mozAccessible> nativeParent = [self moxParent];
+ if (!nativeParent) {
+ return nil;
+ }
+
+ if (![nativeParent isAccessibilityElement]) {
+ if ([nativeParent conformsToProtocol:@protocol(MOXAccessible)] &&
+ [nativeParent respondsToSelector:@selector(moxUnignoredParent)]) {
+ // Cast away the protocol so we can cast to another protocol.
+ id bareNativeParent = nativeParent;
+ id<MOXAccessible> moxNativeParent = bareNativeParent;
+ return [moxNativeParent moxUnignoredParent];
+ }
+ }
+
+ return GetObjectOrRepresentedView(nativeParent);
+}
+
+- (BOOL)moxIgnoreWithParent:(MOXAccessibleBase*)parent {
+ return [parent moxIgnoreChild:self];
+}
+
+- (BOOL)moxIgnoreChild:(MOXAccessibleBase*)child {
+ return NO;
+}
+
+- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
+ return nil;
+}
+
+- (BOOL)moxIsLiveRegion {
+ return NO;
+}
+
+#pragma mark -
+
+// objc-style description (from NSObject); not to be confused with the
+// accessible description above.
+- (NSString*)description {
+ if (MOZ_LOG_TEST(GetMacAccessibilityLog(), LogLevel::Debug)) {
+ if ([self isSelectorSupported:@selector(moxMozDebugDescription)]) {
+ return [self moxMozDebugDescription];
+ }
+ }
+
+ return [NSString stringWithFormat:@"<%@: %p %@>",
+ NSStringFromClass([self class]), self,
+ [self moxRole]];
+}
+
+- (BOOL)isExpired {
+ return mIsExpired;
+}
+
+- (void)expire {
+ MOZ_ASSERT(!mIsExpired, "expire called an expired mozAccessible!");
+
+ mIsExpired = YES;
+
+ [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
+}
+
+- (id<MOXAccessible>)moxFindAncestor:(BOOL (^)(id<MOXAccessible> moxAcc,
+ BOOL* stop))findBlock {
+ for (id element = self; [element conformsToProtocol:@protocol(MOXAccessible)];
+ element = [element moxUnignoredParent]) {
+ BOOL stop = NO;
+ if (findBlock(element, &stop)) {
+ return element;
+ }
+
+ if (stop || ![element respondsToSelector:@selector(moxUnignoredParent)]) {
+ break;
+ }
+ }
+
+ return nil;
+}
+
+#pragma mark - Private
+
+- (BOOL)isSelectorSupported:(SEL)selector {
+ return
+ [self respondsToSelector:selector] && ![self moxBlockSelector:selector];
+}
+
+@end
diff --git a/accessible/mac/MOXAccessibleProtocol.h b/accessible/mac/MOXAccessibleProtocol.h
new file mode 100644
index 0000000000..e3535c1628
--- /dev/null
+++ b/accessible/mac/MOXAccessibleProtocol.h
@@ -0,0 +1,538 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@protocol MOXTextMarkerSupport;
+@protocol mozAccessible;
+
+// This protocol's primary use is for abstracting the NSAccessibility informal
+// protocol into a formal internal API. Conforming classes get to choose a
+// subset of the optional methods to implement. Those methods will be mapped to
+// NSAccessibility attributes or actions. A conforming class can implement
+// moxBlockSelector to control which of its implemented methods should be
+// exposed to NSAccessibility.
+
+@protocol MOXAccessible
+
+// The deepest descendant of the accessible subtree that contains the specified
+// point. Forwarded from accessibilityHitTest.
+- (id _Nullable)moxHitTest:(NSPoint)point;
+
+// The deepest descendant of the accessible subtree that has the focus.
+// Forwarded from accessibilityFocusedUIElement.
+- (id _Nullable)moxFocusedUIElement;
+
+// Sends a notification to any observing assistive applications.
+- (void)moxPostNotification:(NSString* _Nonnull)notification;
+
+- (void)moxPostNotification:(NSString* _Nonnull)notification
+ withUserInfo:(NSDictionary* _Nullable)userInfo;
+
+// Return YES if selector should be considered not supported.
+// Used in implementations such as:
+// - accessibilityAttributeNames
+// - accessibilityActionNames
+// - accessibilityIsAttributeSettable
+- (BOOL)moxBlockSelector:(SEL _Nonnull)selector;
+
+// Returns a list of all children, doesn't do ignore filtering.
+- (NSArray* _Nullable)moxChildren;
+
+// Returns our parent, doesn't do ignore filtering.
+- (id<mozAccessible> _Nullable)moxParent;
+
+// This is called by isAccessibilityElement. If a subclass wants
+// to alter the isAccessibilityElement return value, it must
+// override this and not isAccessibilityElement directly.
+- (BOOL)moxIgnoreWithParent:(id<MOXAccessible> _Nullable)parent;
+
+// Should the child be ignored. This allows subclasses to determine
+// what kinds of accessibles are valid children. This causes the child
+// to be skipped, but the unignored descendants will be added to the
+// container in the default children getter.
+- (BOOL)moxIgnoreChild:(id<MOXAccessible> _Nullable)child;
+
+// Return text delegate if it exists.
+- (id<MOXTextMarkerSupport> _Nullable)moxTextMarkerDelegate;
+
+// Return true if this accessible is a live region
+- (BOOL)moxIsLiveRegion;
+
+// Find the nearest ancestor that returns true with the given block function
+- (id<MOXAccessible> _Nullable)moxFindAncestor:
+ (BOOL (^_Nonnull)(id<MOXAccessible> _Nonnull moxAcc,
+ BOOL* _Nonnull stop))findBlock;
+
+@optional
+
+#pragma mark - AttributeGetters
+
+// AXChildren
+- (NSArray* _Nullable)moxUnignoredChildren;
+
+// AXParent
+- (id _Nullable)moxUnignoredParent;
+
+// AXRole
+- (NSString* _Nullable)moxRole;
+
+// AXRoleDescription
+- (NSString* _Nullable)moxRoleDescription;
+
+// AXSubrole
+- (NSString* _Nullable)moxSubrole;
+
+// AXTitle
+- (NSString* _Nullable)moxTitle;
+
+// AXDescription
+- (NSString* _Nullable)moxLabel;
+
+// AXHelp
+- (NSString* _Nullable)moxHelp;
+
+// AXValue
+- (id _Nullable)moxValue;
+
+// AXValueDescription
+- (NSString* _Nullable)moxValueDescription;
+
+// AXSize
+- (NSValue* _Nullable)moxSize;
+
+// AXPosition
+- (NSValue* _Nullable)moxPosition;
+
+// AXEnabled
+- (NSNumber* _Nullable)moxEnabled;
+
+// AXFocused
+- (NSNumber* _Nullable)moxFocused;
+
+// AXWindow
+- (id _Nullable)moxWindow;
+
+// AXFrame
+- (NSValue* _Nullable)moxFrame;
+
+// AXTitleUIElement
+- (id _Nullable)moxTitleUIElement;
+
+// AXTopLevelUIElement
+- (id _Nullable)moxTopLevelUIElement;
+
+// AXHasPopup
+- (NSNumber* _Nullable)moxHasPopup;
+
+// AXARIACurrent
+- (NSString* _Nullable)moxARIACurrent;
+
+// AXSelected
+- (NSNumber* _Nullable)moxSelected;
+
+// AXRequired
+- (NSNumber* _Nullable)moxRequired;
+
+// AXElementBusy
+- (NSNumber* _Nullable)moxElementBusy;
+
+// AXLinkedUIElements
+- (NSArray* _Nullable)moxLinkedUIElements;
+
+// AXARIAControls
+- (NSArray* _Nullable)moxARIAControls;
+
+// AXDOMIdentifier
+- (NSString* _Nullable)moxDOMIdentifier;
+
+// AXURL
+- (NSURL* _Nullable)moxURL;
+
+// AXLinkUIElements
+- (NSArray* _Nullable)moxLinkUIElements;
+
+// AXPopupValue
+- (NSString* _Nullable)moxPopupValue;
+
+// AXVisited
+- (NSNumber* _Nullable)moxVisited;
+
+// AXExpanded
+- (NSNumber* _Nullable)moxExpanded;
+
+// AXMain
+- (NSNumber* _Nullable)moxMain;
+
+// AXMinimized
+- (NSNumber* _Nullable)moxMinimized;
+
+// AXSelectedChildren
+- (NSArray* _Nullable)moxSelectedChildren;
+
+// AXTabs
+- (NSArray* _Nullable)moxTabs;
+
+// AXContents
+- (NSArray* _Nullable)moxContents;
+
+// AXOrientation
+- (NSString* _Nullable)moxOrientation;
+
+// AXMenuItemMarkChar
+- (NSString* _Nullable)moxMenuItemMarkChar;
+
+// AXLoaded
+- (NSNumber* _Nullable)moxLoaded;
+
+// AXLoadingProgress
+- (NSNumber* _Nullable)moxLoadingProgress;
+
+// AXMinValue
+- (id _Nullable)moxMinValue;
+
+// AXMaxValue
+- (id _Nullable)moxMaxValue;
+
+// Webkit also implements the following:
+// // AXCaretBrowsingEnabled
+// - (NSString* _Nullable)moxCaretBrowsingEnabled;
+
+// // AXLayoutCount
+// - (NSString* _Nullable)moxLayoutCount;
+
+// // AXWebSessionID
+// - (NSString* _Nullable)moxWebSessionID;
+
+// // AXPreventKeyboardDOMEventDispatch
+// - (NSString* _Nullable)moxPreventKeyboardDOMEventDispatch;
+
+// Table Attributes
+
+// AXRowCount
+- (NSNumber* _Nullable)moxRowCount;
+
+// AXColumnCount
+- (NSNumber* _Nullable)moxColumnCount;
+
+// AXRows
+- (NSArray* _Nullable)moxRows;
+
+// AXColumns
+- (NSArray* _Nullable)moxColumns;
+
+// AXIndex
+- (NSNumber* _Nullable)moxIndex;
+
+// AXRowIndexRange
+- (NSValue* _Nullable)moxRowIndexRange;
+
+// AXColumnIndexRange
+- (NSValue* _Nullable)moxColumnIndexRange;
+
+// AXRowHeaderUIElements
+- (NSArray* _Nullable)moxRowHeaderUIElements;
+
+// AXColumnHeaderUIElements
+- (NSArray* _Nullable)moxColumnHeaderUIElements;
+
+// AXIdentifier
+- (NSString* _Nullable)moxIdentifier;
+
+// AXVisibleChildren
+- (NSArray* _Nullable)moxVisibleChildren;
+
+// Outline Attributes
+
+// AXDisclosing
+- (NSNumber* _Nullable)moxDisclosing;
+
+// AXDisclosedByRow
+- (id _Nullable)moxDisclosedByRow;
+
+// AXDisclosureLevel
+- (NSNumber* _Nullable)moxDisclosureLevel;
+
+// AXDisclosedRows
+- (NSArray* _Nullable)moxDisclosedRows;
+
+// AXSelectedRows
+- (NSArray* _Nullable)moxSelectedRows;
+
+// AXARIAPosInSet
+- (NSNumber* _Nullable)moxARIAPosInSet;
+
+// AXARIASetSize
+- (NSNumber* _Nullable)moxARIASetSize;
+
+// Math Attributes
+
+// AXMathRootRadicand
+- (id _Nullable)moxMathRootRadicand;
+
+// AXMathRootIndex
+- (id _Nullable)moxMathRootIndex;
+
+// AXMathFractionNumerator
+- (id _Nullable)moxMathFractionNumerator;
+
+// AXMathFractionDenominator
+- (id _Nullable)moxMathFractionDenominator;
+
+// AXMathLineThickness
+- (NSNumber* _Nullable)moxMathLineThickness;
+
+// AXMathBase
+- (id _Nullable)moxMathBase;
+
+// AXMathSubscript
+- (id _Nullable)moxMathSubscript;
+
+// AXMathSuperscript
+- (id _Nullable)moxMathSuperscript;
+
+// AXMathUnder
+- (id _Nullable)moxMathUnder;
+
+// AXMathOver
+- (id _Nullable)moxMathOver;
+
+// AXInvalid
+- (NSString* _Nullable)moxInvalid;
+
+// AXSelectedText
+- (NSString* _Nullable)moxSelectedText;
+
+// AXSelectedTextRange
+- (NSValue* _Nullable)moxSelectedTextRange;
+
+// AXNumberOfCharacters
+- (NSNumber* _Nullable)moxNumberOfCharacters;
+
+// AXVisibleCharacterRange
+- (NSValue* _Nullable)moxVisibleCharacterRange;
+
+// AXInsertionPointLineNumber
+- (NSNumber* _Nullable)moxInsertionPointLineNumber;
+
+// AXEditableAncestor
+- (id _Nullable)moxEditableAncestor;
+
+// AXHighestEditableAncestor
+- (id _Nullable)moxHighestEditableAncestor;
+
+// AXFocusableAncestor
+- (id _Nullable)moxFocusableAncestor;
+
+// AXARIAAtomic
+- (NSNumber* _Nullable)moxARIAAtomic;
+
+// AXARIALive
+- (NSString* _Nullable)moxARIALive;
+
+// AXARIARelevant
+- (NSString* _Nullable)moxARIARelevant;
+
+// AXMozDebugDescription
+- (NSString* _Nullable)moxMozDebugDescription;
+
+#pragma mark - AttributeSetters
+
+// AXDisclosing
+- (void)moxSetDisclosing:(NSNumber* _Nullable)disclosing;
+
+// AXValue
+- (void)moxSetValue:(id _Nullable)value;
+
+// AXFocused
+- (void)moxSetFocused:(NSNumber* _Nullable)focused;
+
+// AXSelected
+- (void)moxSetSelected:(NSNumber* _Nullable)selected;
+
+// AXSelectedChildren
+- (void)moxSetSelectedChildren:(NSArray* _Nullable)selectedChildren;
+
+// AXSelectedText
+- (void)moxSetSelectedText:(NSString* _Nullable)selectedText;
+
+// AXSelectedTextRange
+- (void)moxSetSelectedTextRange:(NSValue* _Nullable)selectedTextRange;
+
+// AXVisibleCharacterRange
+- (void)moxSetVisibleCharacterRange:(NSValue* _Nullable)visibleCharacterRange;
+
+#pragma mark - Actions
+
+// AXPress
+- (void)moxPerformPress;
+
+// AXShowMenu
+- (void)moxPerformShowMenu;
+
+// AXScrollToVisible
+- (void)moxPerformScrollToVisible;
+
+// AXIncrement
+- (void)moxPerformIncrement;
+
+// AXDecrement
+- (void)moxPerformDecrement;
+
+#pragma mark - ParameterizedAttributeGetters
+
+// AXLineForIndex
+- (NSNumber* _Nullable)moxLineForIndex:(NSNumber* _Nonnull)index;
+
+// AXRangeForLine
+- (NSValue* _Nullable)moxRangeForLine:(NSNumber* _Nonnull)line;
+
+// AXStringForRange
+- (NSString* _Nullable)moxStringForRange:(NSValue* _Nonnull)range;
+
+// AXRangeForPosition
+- (NSValue* _Nullable)moxRangeForPosition:(NSValue* _Nonnull)position;
+
+// AXRangeForIndex
+- (NSValue* _Nullable)moxRangeForIndex:(NSNumber* _Nonnull)index;
+
+// AXBoundsForRange
+- (NSValue* _Nullable)moxBoundsForRange:(NSValue* _Nonnull)range;
+
+// AXRTFForRange
+- (NSData* _Nullable)moxRTFForRange:(NSValue* _Nonnull)range;
+
+// AXStyleRangeForIndex
+- (NSValue* _Nullable)moxStyleRangeForIndex:(NSNumber* _Nonnull)index;
+
+// AXAttributedStringForRange
+- (NSAttributedString* _Nullable)moxAttributedStringForRange:
+ (NSValue* _Nonnull)range;
+
+// AXUIElementsForSearchPredicate
+- (NSArray* _Nullable)moxUIElementsForSearchPredicate:
+ (NSDictionary* _Nonnull)searchPredicate;
+
+// AXUIElementCountForSearchPredicate
+- (NSNumber* _Nullable)moxUIElementCountForSearchPredicate:
+ (NSDictionary* _Nonnull)searchPredicate;
+
+// AXCellForColumnAndRow
+- (id _Nullable)moxCellForColumnAndRow:(NSArray* _Nonnull)columnAndRow;
+
+// AXIndexForChildUIElement
+- (NSNumber* _Nullable)moxIndexForChildUIElement:(id _Nonnull)child;
+
+@end
+
+// This protocol maps text marker and text marker range parameters to
+// methods. It is implemented by a delegate of a MOXAccessible.
+@protocol MOXTextMarkerSupport
+
+#pragma mark - TextAttributeGetters
+
+// AXStartTextMarker
+- (AXTextMarkerRef _Nullable)moxStartTextMarker;
+
+// AXEndTextMarker
+- (AXTextMarkerRef _Nullable)moxEndTextMarker;
+
+// AXSelectedTextMarkerRange
+- (AXTextMarkerRangeRef _Nullable)moxSelectedTextMarkerRange;
+
+#pragma mark - ParameterizedTextAttributeGetters
+
+// AXLengthForTextMarkerRange
+- (NSNumber* _Nullable)moxLengthForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+// AXStringForTextMarkerRange
+- (NSString* _Nullable)moxStringForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+// AXTextMarkerRangeForUnorderedTextMarkers
+- (AXTextMarkerRangeRef _Nullable)moxTextMarkerRangeForUnorderedTextMarkers:
+ (NSArray* _Nonnull)textMarkers;
+
+// AXLeftWordTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxLeftWordTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXRightWordTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxRightWordTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXStartTextMarkerForTextMarkerRange
+- (AXTextMarkerRef _Nullable)moxStartTextMarkerForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+// AXEndTextMarkerForTextMarkerRange
+- (AXTextMarkerRef _Nullable)moxEndTextMarkerForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+// AXNextTextMarkerForTextMarker
+- (AXTextMarkerRef _Nullable)moxNextTextMarkerForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXPreviousTextMarkerForTextMarker
+- (AXTextMarkerRef _Nullable)moxPreviousTextMarkerForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXAttributedStringForTextMarkerRange
+- (NSAttributedString* _Nullable)moxAttributedStringForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+// AXBoundsForTextMarkerRange
+- (NSValue* _Nullable)moxBoundsForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+// AXIndexForTextMarker
+- (NSNumber* _Nullable)moxIndexForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXTextMarkerForIndex
+- (AXTextMarkerRef _Nullable)moxTextMarkerForIndex:(NSNumber* _Nonnull)index;
+
+// AXUIElementForTextMarker
+- (id _Nullable)moxUIElementForTextMarker:(AXTextMarkerRef _Nonnull)textMarker;
+
+// AXTextMarkerRangeForUIElement
+- (AXTextMarkerRangeRef _Nullable)moxTextMarkerRangeForUIElement:
+ (id _Nonnull)element;
+
+// AXLineTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXLeftLineTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxLeftLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXRightLineTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxRightLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXParagraphTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxParagraphTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXStyleTextMarkerRangeForTextMarker
+- (AXTextMarkerRangeRef _Nullable)moxStyleTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXMozDebugDescriptionForTextMarker
+- (NSString* _Nullable)moxMozDebugDescriptionForTextMarker:
+ (AXTextMarkerRef _Nonnull)textMarker;
+
+// AXMozDebugDescriptionForTextMarkerRange
+- (NSString* _Nullable)moxMozDebugDescriptionForTextMarkerRange:
+ (AXTextMarkerRangeRef _Nonnull)textMarkerRange;
+
+#pragma mark - TextAttributeSetters
+
+// AXSelectedTextMarkerRange
+- (void)moxSetSelectedTextMarkerRange:(id _Nullable)textMarkerRange;
+
+@end
diff --git a/accessible/mac/MOXLandmarkAccessibles.h b/accessible/mac/MOXLandmarkAccessibles.h
new file mode 100644
index 0000000000..bea44e7a8f
--- /dev/null
+++ b/accessible/mac/MOXLandmarkAccessibles.h
@@ -0,0 +1,15 @@
+/* -*- Mode: Objective-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/. */
+
+#import "mozAccessible.h"
+
+@interface MOXLandmarkAccessible : mozAccessible
+// overrides
+- (NSString*)moxTitle;
+
+@end
diff --git a/accessible/mac/MOXLandmarkAccessibles.mm b/accessible/mac/MOXLandmarkAccessibles.mm
new file mode 100644
index 0000000000..4a3aa8f597
--- /dev/null
+++ b/accessible/mac/MOXLandmarkAccessibles.mm
@@ -0,0 +1,15 @@
+/* -*- (Mode: Objective-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/. */
+
+#import "MOXLandmarkAccessibles.h"
+
+@implementation MOXLandmarkAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+@end
diff --git a/accessible/mac/MOXMathAccessibles.h b/accessible/mac/MOXMathAccessibles.h
new file mode 100644
index 0000000000..7661ad5c6a
--- /dev/null
+++ b/accessible/mac/MOXMathAccessibles.h
@@ -0,0 +1,64 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+
+@interface MOXMathRootAccessible : mozAccessible
+
+// overrides
+- (id)moxMathRootRadicand;
+
+// overrides
+- (id)moxMathRootIndex;
+
+@end
+
+@interface MOXMathSquareRootAccessible : mozAccessible
+
+// overrides
+- (id)moxMathRootRadicand;
+
+@end
+
+@interface MOXMathFractionAccessible : mozAccessible
+
+// overrides
+- (id)moxMathFractionNumerator;
+
+// overrides
+- (id)moxMathFractionDenominator;
+
+// overrides
+- (NSNumber*)moxMathLineThickness;
+
+@end
+
+@interface MOXMathSubSupAccessible : mozAccessible
+
+// overrides
+- (id)moxMathBase;
+
+// overrides
+- (id)moxMathSubscript;
+
+// overrides
+- (id)moxMathSuperscript;
+
+@end
+
+@interface MOXMathUnderOverAccessible : mozAccessible
+
+// overrides
+- (id)moxMathBase;
+
+// overrides
+- (id)moxMathUnder;
+
+// overrides
+- (id)moxMathOver;
+
+@end
diff --git a/accessible/mac/MOXMathAccessibles.mm b/accessible/mac/MOXMathAccessibles.mm
new file mode 100644
index 0000000000..7bfe2e3e05
--- /dev/null
+++ b/accessible/mac/MOXMathAccessibles.mm
@@ -0,0 +1,117 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "MOXMathAccessibles.h"
+
+#import "MacUtils.h"
+
+using namespace mozilla::a11y;
+
+// XXX WebKit also defines the following attributes.
+// See bugs 1176970 and 1176983.
+// - NSAccessibilityMathFencedOpenAttribute @"AXMathFencedOpen"
+// - NSAccessibilityMathFencedCloseAttribute @"AXMathFencedClose"
+// - NSAccessibilityMathPrescriptsAttribute @"AXMathPrescripts"
+// - NSAccessibilityMathPostscriptsAttribute @"AXMathPostscripts"
+
+@implementation MOXMathRootAccessible
+
+- (id)moxMathRootRadicand {
+ return [self childAt:0];
+}
+
+- (id)moxMathRootIndex {
+ return [self childAt:1];
+}
+
+@end
+
+@implementation MOXMathSquareRootAccessible
+
+- (id)moxMathRootRadicand {
+ return [self childAt:0];
+}
+
+@end
+
+@implementation MOXMathFractionAccessible
+
+- (id)moxMathFractionNumerator {
+ return [self childAt:0];
+}
+
+- (id)moxMathFractionDenominator {
+ return [self childAt:1];
+}
+
+// Bug 1639745: This doesn't actually work.
+- (NSNumber*)moxMathLineThickness {
+ // WebKit sets line thickness to some logical value parsed in the
+ // renderer object of the <mfrac> element. It's not clear whether the
+ // exact value is relevant to assistive technologies. From a semantic
+ // point of view, the only important point is to distinguish between
+ // <mfrac> elements that have a fraction bar and those that do not.
+ // Per the MathML 3 spec, the latter happens iff the linethickness
+ // attribute is of the form [zero-float][optional-unit]. In that case we
+ // set line thickness to zero and in the other cases we set it to one.
+ if (NSString* thickness =
+ utils::GetAccAttr(self, nsGkAtoms::linethickness_)) {
+ NSNumberFormatter* formatter =
+ [[[NSNumberFormatter alloc] init] autorelease];
+ NSNumber* value = [formatter numberFromString:thickness];
+ return [NSNumber numberWithBool:[value boolValue]];
+ } else {
+ return [NSNumber numberWithInteger:0];
+ }
+}
+
+@end
+
+@implementation MOXMathSubSupAccessible
+- (id)moxMathBase {
+ return [self childAt:0];
+}
+
+- (id)moxMathSubscript {
+ if (mRole == roles::MATHML_SUP) {
+ return nil;
+ }
+
+ return [self childAt:1];
+}
+
+- (id)moxMathSuperscript {
+ if (mRole == roles::MATHML_SUB) {
+ return nil;
+ }
+
+ return [self childAt:mRole == roles::MATHML_SUP ? 1 : 2];
+}
+
+@end
+
+@implementation MOXMathUnderOverAccessible
+- (id)moxMathBase {
+ return [self childAt:0];
+}
+
+- (id)moxMathUnder {
+ if (mRole == roles::MATHML_OVER) {
+ return nil;
+ }
+
+ return [self childAt:1];
+}
+
+- (id)moxMathOver {
+ if (mRole == roles::MATHML_UNDER) {
+ return nil;
+ }
+
+ return [self childAt:mRole == roles::MATHML_OVER ? 1 : 2];
+}
+@end
diff --git a/accessible/mac/MOXSearchInfo.h b/accessible/mac/MOXSearchInfo.h
new file mode 100644
index 0000000000..8f5e6f414d
--- /dev/null
+++ b/accessible/mac/MOXSearchInfo.h
@@ -0,0 +1,43 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+#include "Pivot.h"
+
+using namespace mozilla::a11y;
+
+@interface MOXSearchInfo : NSObject {
+ // The MOX accessible of the web area, we need a reference
+ // to set the pivot's root. This is a weak ref.
+ MOXAccessibleBase* mRoot;
+
+ // The MOX accessible we should start searching from.
+ // This is a weak ref.
+ MOXAccessibleBase* mStartElem;
+
+ // The amount of matches we should return
+ int mResultLimit;
+
+ // The array of search keys to use during this search
+ NSMutableArray* mSearchKeys;
+
+ // Set to YES if we should search forward, NO if backward
+ BOOL mSearchForward;
+
+ // Set to YES if we should match on immediate descendants only, NO otherwise
+ BOOL mImmediateDescendantsOnly;
+
+ NSString* mSearchText;
+}
+
+- (id)initWithParameters:(NSDictionary*)params andRoot:(MOXAccessibleBase*)root;
+
+- (NSArray*)performSearch;
+
+- (void)dealloc;
+
+@end
diff --git a/accessible/mac/MOXSearchInfo.mm b/accessible/mac/MOXSearchInfo.mm
new file mode 100644
index 0000000000..ce7a7dedf3
--- /dev/null
+++ b/accessible/mac/MOXSearchInfo.mm
@@ -0,0 +1,374 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "MOXSearchInfo.h"
+#import "MOXWebAreaAccessible.h"
+#import "RotorRules.h"
+
+#include "nsCocoaUtils.h"
+#include "DocAccessibleParent.h"
+#include "nsAccessibilityService.h"
+
+using namespace mozilla::a11y;
+
+@interface MOXSearchInfo ()
+- (NSArray*)getMatchesForRule:(PivotRule&)rule;
+
+- (Accessible*)rootGeckoAccessible;
+
+- (Accessible*)startGeckoAccessible;
+@end
+
+@implementation MOXSearchInfo
+
+- (id)initWithParameters:(NSDictionary*)params
+ andRoot:(MOXAccessibleBase*)root {
+ if (id searchKeyParam = [params objectForKey:@"AXSearchKey"]) {
+ mSearchKeys = [searchKeyParam isKindOfClass:[NSString class]]
+ ? [[NSArray alloc] initWithObjects:searchKeyParam, nil]
+ : [searchKeyParam retain];
+ }
+
+ if (id startElemParam = [params objectForKey:@"AXStartElement"]) {
+ mStartElem = startElemParam;
+ } else {
+ mStartElem = root;
+ }
+
+ mRoot = root;
+
+ mResultLimit = [[params objectForKey:@"AXResultsLimit"] intValue];
+
+ mSearchForward =
+ [[params objectForKey:@"AXDirection"] isEqualToString:@"AXDirectionNext"];
+
+ mImmediateDescendantsOnly =
+ [[params objectForKey:@"AXImmediateDescendantsOnly"] boolValue];
+
+ mSearchText = [params objectForKey:@"AXSearchText"];
+
+ return [super init];
+}
+
+- (Accessible*)rootGeckoAccessible {
+ id root =
+ [mRoot isKindOfClass:[mozAccessible class]] ? mRoot : [mRoot moxParent];
+
+ return [static_cast<mozAccessible*>(root) geckoAccessible];
+}
+
+- (Accessible*)startGeckoAccessible {
+ if ([mStartElem isKindOfClass:[mozAccessible class]]) {
+ return [static_cast<mozAccessible*>(mStartElem) geckoAccessible];
+ }
+
+ // If it isn't a mozAccessible, it doesn't have a gecko accessible
+ // this is most likely the root group. Use the gecko doc as the start
+ // accessible.
+ return [self rootGeckoAccessible];
+}
+
+- (NSArray*)getMatchesForRule:(PivotRule&)rule {
+ int resultLimit = mResultLimit;
+
+ NSMutableArray<mozAccessible*>* matches =
+ [[[NSMutableArray alloc] init] autorelease];
+ Accessible* geckoRootAcc = [self rootGeckoAccessible];
+ Accessible* geckoStartAcc = [self startGeckoAccessible];
+ Pivot p = Pivot(geckoRootAcc);
+ Accessible* match;
+ if (mSearchForward) {
+ match = p.Next(geckoStartAcc, rule);
+ } else {
+ // Search backwards
+ if (geckoRootAcc == geckoStartAcc) {
+ // If we have no explicit start accessible, start from the last match.
+ match = p.Last(rule);
+ } else {
+ match = p.Prev(geckoStartAcc, rule);
+ }
+ }
+
+ while (match && resultLimit != 0) {
+ if (!mSearchForward && match == geckoRootAcc) {
+ // If searching backwards, don't include root.
+ break;
+ }
+
+ // we use mResultLimit != 0 to capture the case where mResultLimit is -1
+ // when it is set from the params dictionary. If that's true, we want
+ // to return all matches (ie. have no limit)
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(match);
+ if (nativeMatch) {
+ // only add/count results for which there is a matching
+ // native accessible
+ [matches addObject:nativeMatch];
+ resultLimit -= 1;
+ }
+
+ match = mSearchForward ? p.Next(match, rule) : p.Prev(match, rule);
+ }
+
+ return matches;
+}
+
+- (NSArray*)performSearch {
+ Accessible* geckoRootAcc = [self rootGeckoAccessible];
+ Accessible* geckoStartAcc = [self startGeckoAccessible];
+ NSMutableArray* matches = [[[NSMutableArray alloc] init] autorelease];
+ nsString searchText;
+ nsCocoaUtils::GetStringForNSString(mSearchText, searchText);
+ for (id key in mSearchKeys) {
+ if ([key isEqualToString:@"AXAnyTypeSearchKey"]) {
+ RotorRule rule = mImmediateDescendantsOnly
+ ? RotorRule(geckoRootAcc, searchText)
+ : RotorRule(searchText);
+
+ if (searchText.IsEmpty() &&
+ [mStartElem isKindOfClass:[MOXWebAreaAccessible class]]) {
+ // Don't include the root group when a search text is defined.
+ if (id rootGroup =
+ [static_cast<MOXWebAreaAccessible*>(mStartElem) rootGroup]) {
+ // Moving forward from web area, rootgroup; if it exists, is next.
+ [matches addObject:rootGroup];
+ if (mResultLimit == 1) {
+ // Found one match, continue in search keys for block.
+ continue;
+ }
+ }
+ }
+
+ if (mImmediateDescendantsOnly && mStartElem != mRoot &&
+ [mStartElem isKindOfClass:[MOXRootGroup class]]) {
+ // Moving forward from root group. If we don't match descendants,
+ // there is no match. Continue.
+ continue;
+ }
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingSearchKey"]) {
+ RotorRoleRule rule =
+ mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::HEADING, geckoRootAcc, searchText)
+ : RotorRoleRule(roles::HEADING, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXArticleSearchKey"]) {
+ RotorRoleRule rule =
+ mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::ARTICLE, geckoRootAcc, searchText)
+ : RotorRoleRule(roles::ARTICLE, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXTableSearchKey"]) {
+ RotorRoleRule rule =
+ mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::TABLE, geckoRootAcc, searchText)
+ : RotorRoleRule(roles::TABLE, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXLandmarkSearchKey"]) {
+ RotorRoleRule rule =
+ mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::LANDMARK, geckoRootAcc, searchText)
+ : RotorRoleRule(roles::LANDMARK, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXListSearchKey"]) {
+ RotorRoleRule rule =
+ mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::LIST, geckoRootAcc, searchText)
+ : RotorRoleRule(roles::LIST, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXLinkSearchKey"]) {
+ RotorLinkRule rule = mImmediateDescendantsOnly
+ ? RotorLinkRule(geckoRootAcc, searchText)
+ : RotorLinkRule(searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXVisitedLinkSearchKey"]) {
+ RotorVisitedLinkRule rule =
+ mImmediateDescendantsOnly
+ ? RotorVisitedLinkRule(geckoRootAcc, searchText)
+ : RotorVisitedLinkRule(searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXUnvisitedLinkSearchKey"]) {
+ RotorUnvisitedLinkRule rule =
+ mImmediateDescendantsOnly
+ ? RotorUnvisitedLinkRule(geckoRootAcc, searchText)
+ : RotorUnvisitedLinkRule(searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXButtonSearchKey"]) {
+ RotorRoleRule rule =
+ mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::PUSHBUTTON, geckoRootAcc, searchText)
+ : RotorRoleRule(roles::PUSHBUTTON, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXControlSearchKey"]) {
+ RotorControlRule rule = mImmediateDescendantsOnly
+ ? RotorControlRule(geckoRootAcc, searchText)
+ : RotorControlRule(searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXSameTypeSearchKey"]) {
+ mozAccessible* native = GetNativeFromGeckoAccessible(geckoStartAcc);
+ NSString* macRole = [native moxRole];
+ RotorMacRoleRule rule =
+ mImmediateDescendantsOnly
+ ? RotorMacRoleRule(macRole, geckoRootAcc, searchText)
+ : RotorMacRoleRule(macRole, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXDifferentTypeSearchKey"]) {
+ mozAccessible* native = GetNativeFromGeckoAccessible(geckoStartAcc);
+ NSString* macRole = [native moxRole];
+ RotorNotMacRoleRule rule =
+ mImmediateDescendantsOnly
+ ? RotorNotMacRoleRule(macRole, geckoRootAcc, searchText)
+ : RotorNotMacRoleRule(macRole, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXRadioGroupSearchKey"]) {
+ RotorRoleRule rule =
+ mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::RADIO_GROUP, geckoRootAcc, searchText)
+ : RotorRoleRule(roles::RADIO_GROUP, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXFrameSearchKey"]) {
+ RotorRoleRule rule =
+ mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::DOCUMENT, geckoRootAcc, searchText)
+ : RotorRoleRule(roles::DOCUMENT, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXImageSearchKey"] ||
+ [key isEqualToString:@"AXGraphicSearchKey"]) {
+ RotorRoleRule rule =
+ mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::GRAPHIC, geckoRootAcc, searchText)
+ : RotorRoleRule(roles::GRAPHIC, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXCheckBoxSearchKey"]) {
+ RotorRoleRule rule =
+ mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::CHECKBUTTON, geckoRootAcc, searchText)
+ : RotorRoleRule(roles::CHECKBUTTON, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXStaticTextSearchKey"]) {
+ RotorStaticTextRule rule =
+ mImmediateDescendantsOnly
+ ? RotorStaticTextRule(geckoRootAcc, searchText)
+ : RotorStaticTextRule(searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingLevel1SearchKey"]) {
+ RotorHeadingLevelRule rule =
+ mImmediateDescendantsOnly
+ ? RotorHeadingLevelRule(1, geckoRootAcc, searchText)
+ : RotorHeadingLevelRule(1, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingLevel2SearchKey"]) {
+ RotorHeadingLevelRule rule =
+ mImmediateDescendantsOnly
+ ? RotorHeadingLevelRule(2, geckoRootAcc, searchText)
+ : RotorHeadingLevelRule(2, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingLevel3SearchKey"]) {
+ RotorHeadingLevelRule rule =
+ mImmediateDescendantsOnly
+ ? RotorHeadingLevelRule(3, geckoRootAcc, searchText)
+ : RotorHeadingLevelRule(3, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingLevel4SearchKey"]) {
+ RotorHeadingLevelRule rule =
+ mImmediateDescendantsOnly
+ ? RotorHeadingLevelRule(4, geckoRootAcc, searchText)
+ : RotorHeadingLevelRule(4, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingLevel5SearchKey"]) {
+ RotorHeadingLevelRule rule =
+ mImmediateDescendantsOnly
+ ? RotorHeadingLevelRule(5, geckoRootAcc, searchText)
+ : RotorHeadingLevelRule(5, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXHeadingLevel6SearchKey"]) {
+ RotorHeadingLevelRule rule =
+ mImmediateDescendantsOnly
+ ? RotorHeadingLevelRule(6, geckoRootAcc, searchText)
+ : RotorHeadingLevelRule(6, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXBlockquoteSearchKey"]) {
+ RotorRoleRule rule =
+ mImmediateDescendantsOnly
+ ? RotorRoleRule(roles::BLOCKQUOTE, geckoRootAcc, searchText)
+ : RotorRoleRule(roles::BLOCKQUOTE, searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXTextFieldSearchKey"]) {
+ RotorTextEntryRule rule =
+ mImmediateDescendantsOnly
+ ? RotorTextEntryRule(geckoRootAcc, searchText)
+ : RotorTextEntryRule(searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+
+ if ([key isEqualToString:@"AXLiveRegionSearchKey"]) {
+ RotorLiveRegionRule rule =
+ mImmediateDescendantsOnly
+ ? RotorLiveRegionRule(geckoRootAcc, searchText)
+ : RotorLiveRegionRule(searchText);
+ [matches addObjectsFromArray:[self getMatchesForRule:rule]];
+ }
+ }
+
+ return matches;
+}
+
+- (void)dealloc {
+ [mSearchKeys release];
+ [super dealloc];
+}
+
+@end
diff --git a/accessible/mac/MOXTextMarkerDelegate.h b/accessible/mac/MOXTextMarkerDelegate.h
new file mode 100644
index 0000000000..f1d40f6ffa
--- /dev/null
+++ b/accessible/mac/MOXTextMarkerDelegate.h
@@ -0,0 +1,169 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+
+#import "MOXAccessibleProtocol.h"
+#import "GeckoTextMarker.h"
+
+@interface MOXTextMarkerDelegate : NSObject <MOXTextMarkerSupport> {
+ mozilla::a11y::Accessible* mGeckoDocAccessible;
+ AXTextMarkerRangeRef mSelection;
+ AXTextMarkerRef mCaret;
+ AXTextMarkerRef mPrevCaret;
+ int32_t mCaretMoveGranularity;
+}
+
++ (id)getOrCreateForDoc:(mozilla::a11y::Accessible*)aDoc;
+
++ (void)destroyForDoc:(mozilla::a11y::Accessible*)aDoc;
+
+- (id)initWithDoc:(mozilla::a11y::Accessible*)aDoc;
+
+- (void)dealloc;
+
+- (void)setSelectionFrom:(mozilla::a11y::Accessible*)startContainer
+ at:(int32_t)startOffset
+ to:(mozilla::a11y::Accessible*)endContainer
+ at:(int32_t)endOffset;
+
+- (void)setCaretOffset:(mozilla::a11y::Accessible*)container
+ at:(int32_t)offset
+ moveGranularity:(int32_t)granularity;
+
+- (NSDictionary*)selectionChangeInfo;
+
+- (void)invalidateSelection;
+
+- (mozilla::a11y::GeckoTextMarkerRange)selection;
+
+// override
+- (AXTextMarkerRef)moxStartTextMarker;
+
+// override
+- (AXTextMarkerRef)moxEndTextMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxSelectedTextMarkerRange;
+
+// override
+- (NSNumber*)moxLengthForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange;
+
+// override
+- (NSString*)moxStringForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange;
+
+// override
+- (AXTextMarkerRangeRef)moxTextMarkerRangeForUnorderedTextMarkers:
+ (NSArray*)textMarkers;
+
+// override
+- (AXTextMarkerRef)moxStartTextMarkerForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange;
+
+// override
+- (AXTextMarkerRef)moxEndTextMarkerForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange;
+
+// override
+- (AXTextMarkerRangeRef)moxLeftWordTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxRightWordTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxLeftLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxRightLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxParagraphTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxStyleTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRef)moxNextTextMarkerForTextMarker:(AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRef)moxPreviousTextMarkerForTextMarker:
+ (AXTextMarkerRef)textMarker;
+
+// override
+- (NSAttributedString*)moxAttributedStringForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange;
+
+// override
+- (NSValue*)moxBoundsForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange;
+
+// override
+- (id)moxUIElementForTextMarker:(AXTextMarkerRef)textMarker;
+
+// override
+- (AXTextMarkerRangeRef)moxTextMarkerRangeForUIElement:(id)element;
+
+// override
+- (NSString*)moxMozDebugDescriptionForTextMarker:(AXTextMarkerRef)textMarker;
+
+// override
+- (void)moxSetSelectedTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange;
+
+@end
+
+namespace mozilla {
+namespace a11y {
+
+enum AXTextEditType {
+ AXTextEditTypeUnknown,
+ AXTextEditTypeDelete,
+ AXTextEditTypeInsert,
+ AXTextEditTypeTyping,
+ AXTextEditTypeDictation,
+ AXTextEditTypeCut,
+ AXTextEditTypePaste,
+ AXTextEditTypeAttributesChange
+};
+
+enum AXTextStateChangeType {
+ AXTextStateChangeTypeUnknown,
+ AXTextStateChangeTypeEdit,
+ AXTextStateChangeTypeSelectionMove,
+ AXTextStateChangeTypeSelectionExtend
+};
+
+enum AXTextSelectionDirection {
+ AXTextSelectionDirectionUnknown,
+ AXTextSelectionDirectionBeginning,
+ AXTextSelectionDirectionEnd,
+ AXTextSelectionDirectionPrevious,
+ AXTextSelectionDirectionNext,
+ AXTextSelectionDirectionDiscontiguous
+};
+
+enum AXTextSelectionGranularity {
+ AXTextSelectionGranularityUnknown,
+ AXTextSelectionGranularityCharacter,
+ AXTextSelectionGranularityWord,
+ AXTextSelectionGranularityLine,
+ AXTextSelectionGranularitySentence,
+ AXTextSelectionGranularityParagraph,
+ AXTextSelectionGranularityPage,
+ AXTextSelectionGranularityDocument,
+ AXTextSelectionGranularityAll
+};
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/mac/MOXTextMarkerDelegate.mm b/accessible/mac/MOXTextMarkerDelegate.mm
new file mode 100644
index 0000000000..3e1e451ddd
--- /dev/null
+++ b/accessible/mac/MOXTextMarkerDelegate.mm
@@ -0,0 +1,527 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+#include "DocAccessible.h"
+
+#import "MOXTextMarkerDelegate.h"
+
+#include "mozAccessible.h"
+#include "mozilla/Preferences.h"
+#include "nsISelectionListener.h"
+
+using namespace mozilla::a11y;
+
+#define PREF_ACCESSIBILITY_MAC_DEBUG "accessibility.mac.debug"
+
+static nsTHashMap<nsPtrHashKey<mozilla::a11y::Accessible>,
+ MOXTextMarkerDelegate*>
+ sDelegates;
+
+@implementation MOXTextMarkerDelegate
+
++ (id)getOrCreateForDoc:(mozilla::a11y::Accessible*)aDoc {
+ MOZ_ASSERT(aDoc);
+
+ MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc);
+ if (!delegate) {
+ delegate = [[MOXTextMarkerDelegate alloc] initWithDoc:aDoc];
+ sDelegates.InsertOrUpdate(aDoc, delegate);
+ [delegate retain];
+ }
+
+ return delegate;
+}
+
++ (void)destroyForDoc:(mozilla::a11y::Accessible*)aDoc {
+ MOZ_ASSERT(aDoc);
+
+ MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc);
+ if (delegate) {
+ sDelegates.Remove(aDoc);
+ [delegate release];
+ }
+}
+
+- (id)initWithDoc:(Accessible*)aDoc {
+ MOZ_ASSERT(aDoc, "Cannot init MOXTextDelegate with null");
+ if ((self = [super init])) {
+ mGeckoDocAccessible = aDoc;
+ }
+
+ mCaretMoveGranularity = nsISelectionListener::NO_AMOUNT;
+
+ return self;
+}
+
+- (void)dealloc {
+ [self invalidateSelection];
+ [super dealloc];
+}
+
+- (void)setSelectionFrom:(Accessible*)startContainer
+ at:(int32_t)startOffset
+ to:(Accessible*)endContainer
+ at:(int32_t)endOffset {
+ GeckoTextMarkerRange selection(GeckoTextMarker(startContainer, startOffset),
+ GeckoTextMarker(endContainer, endOffset));
+
+ // We store it as an AXTextMarkerRange because it is a safe
+ // way to keep a weak reference - when we need to use the
+ // range we can convert it back to a GeckoTextMarkerRange
+ // and check that it's valid.
+ mSelection = selection.CreateAXTextMarkerRange();
+ CFRetain(mSelection);
+}
+
+- (void)setCaretOffset:(mozilla::a11y::Accessible*)container
+ at:(int32_t)offset
+ moveGranularity:(int32_t)granularity {
+ GeckoTextMarker caretMarker(container, offset);
+
+ mPrevCaret = mCaret;
+ mCaret = caretMarker.CreateAXTextMarker();
+ mCaretMoveGranularity = granularity;
+
+ CFRetain(mCaret);
+}
+
+mozAccessible* GetEditableNativeFromGeckoAccessible(Accessible* aAcc) {
+ // The gecko accessible may not have a native accessible so we need
+ // to walk up the parent chain to find the nearest one.
+ // This happens when caching is enabled and the text marker's accessible
+ // may be a text leaf that is pruned from the platform.
+ for (Accessible* acc = aAcc; acc; acc = acc->Parent()) {
+ if (mozAccessible* mozAcc = GetNativeFromGeckoAccessible(acc)) {
+ return [mozAcc moxEditableAncestor];
+ }
+ }
+
+ return nil;
+}
+
+// This returns an info object to pass with AX SelectedTextChanged events.
+// It uses the current and previous caret position to make decisions
+// regarding which attributes to add to the info object.
+- (NSDictionary*)selectionChangeInfo {
+ GeckoTextMarkerRange selectedGeckoRange =
+ GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
+ mGeckoDocAccessible, mSelection);
+
+ int32_t stateChangeType =
+ selectedGeckoRange.Start() == selectedGeckoRange.End()
+ ? AXTextStateChangeTypeSelectionMove
+ : AXTextStateChangeTypeSelectionExtend;
+
+ // This is the base info object, includes the selected marker range and
+ // the change type depending on the collapsed state of the selection.
+ NSMutableDictionary* info = [[@{
+ @"AXSelectedTextMarkerRange" : selectedGeckoRange.IsValid()
+ ? (__bridge id)mSelection
+ : [NSNull null],
+ @"AXTextStateChangeType" : @(stateChangeType),
+ } mutableCopy] autorelease];
+
+ GeckoTextMarker caretMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, mCaret);
+ GeckoTextMarker prevCaretMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, mPrevCaret);
+
+ if (!caretMarker.IsValid()) {
+ // If the current caret is invalid, stop here and return base info.
+ return info;
+ }
+
+ mozAccessible* caretEditable =
+ GetEditableNativeFromGeckoAccessible(caretMarker.Acc());
+
+ if (!caretEditable && stateChangeType == AXTextStateChangeTypeSelectionMove) {
+ // If we are not in an editable, VO expects AXTextStateSync to be present
+ // and true.
+ info[@"AXTextStateSync"] = @YES;
+ }
+
+ if (!prevCaretMarker.IsValid() || caretMarker == prevCaretMarker) {
+ // If we have no stored previous marker, stop here.
+ return info;
+ }
+
+ mozAccessible* prevCaretEditable =
+ GetEditableNativeFromGeckoAccessible(prevCaretMarker.Acc());
+
+ if (prevCaretEditable != caretEditable) {
+ // If the caret goes in or out of an editable, consider the
+ // move direction "discontiguous".
+ info[@"AXTextSelectionDirection"] =
+ @(AXTextSelectionDirectionDiscontiguous);
+ if ([[caretEditable moxFocused] boolValue]) {
+ // If the caret is in a new focused editable, VO expects this attribute to
+ // be present and to be true.
+ info[@"AXTextSelectionChangedFocus"] = @YES;
+ }
+
+ return info;
+ }
+
+ bool isForward = prevCaretMarker < caretMarker;
+ int direction = isForward ? AXTextSelectionDirectionNext
+ : AXTextSelectionDirectionPrevious;
+
+ int32_t granularity = AXTextSelectionGranularityUnknown;
+ switch (mCaretMoveGranularity) {
+ case nsISelectionListener::CHARACTER_AMOUNT:
+ case nsISelectionListener::CLUSTER_AMOUNT:
+ granularity = AXTextSelectionGranularityCharacter;
+ break;
+ case nsISelectionListener::WORD_AMOUNT:
+ case nsISelectionListener::WORDNOSPACE_AMOUNT:
+ granularity = AXTextSelectionGranularityWord;
+ break;
+ case nsISelectionListener::LINE_AMOUNT:
+ granularity = AXTextSelectionGranularityLine;
+ break;
+ case nsISelectionListener::BEGINLINE_AMOUNT:
+ direction = AXTextSelectionDirectionBeginning;
+ granularity = AXTextSelectionGranularityLine;
+ break;
+ case nsISelectionListener::ENDLINE_AMOUNT:
+ direction = AXTextSelectionDirectionEnd;
+ granularity = AXTextSelectionGranularityLine;
+ break;
+ case nsISelectionListener::PARAGRAPH_AMOUNT:
+ granularity = AXTextSelectionGranularityParagraph;
+ break;
+ default:
+ break;
+ }
+
+ // Determine selection direction with marker comparison.
+ // If the delta between the two markers is more than one, consider it
+ // a word. Not accurate, but good enough for VO.
+ [info addEntriesFromDictionary:@{
+ @"AXTextSelectionDirection" : @(direction),
+ @"AXTextSelectionGranularity" : @(granularity)
+ }];
+
+ return info;
+}
+
+- (void)invalidateSelection {
+ CFRelease(mSelection);
+ CFRelease(mCaret);
+ CFRelease(mPrevCaret);
+ mSelection = nil;
+}
+
+- (mozilla::a11y::GeckoTextMarkerRange)selection {
+ return mozilla::a11y::GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
+ mGeckoDocAccessible, mSelection);
+}
+
+- (AXTextMarkerRef)moxStartTextMarker {
+ GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, 0);
+ return geckoTextPoint.CreateAXTextMarker();
+}
+
+- (AXTextMarkerRef)moxEndTextMarker {
+ GeckoTextMarker geckoTextPoint(mGeckoDocAccessible,
+ nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
+ return geckoTextPoint.CreateAXTextMarker();
+}
+
+- (AXTextMarkerRangeRef)moxSelectedTextMarkerRange {
+ return mSelection && GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
+ mGeckoDocAccessible, mSelection)
+ .IsValid()
+ ? mSelection
+ : nil;
+}
+
+- (NSString*)moxStringForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
+ mozilla::a11y::GeckoTextMarkerRange range =
+ GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
+ mGeckoDocAccessible, textMarkerRange);
+ if (!range.IsValid()) {
+ return @"";
+ }
+
+ return range.Text();
+}
+
+- (NSNumber*)moxLengthForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
+ mozilla::a11y::GeckoTextMarkerRange range =
+ GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
+ mGeckoDocAccessible, textMarkerRange);
+ if (!range.IsValid()) {
+ return @0;
+ }
+
+ return @(range.Length());
+}
+
+- (AXTextMarkerRangeRef)moxTextMarkerRangeForUnorderedTextMarkers:
+ (NSArray*)textMarkers {
+ if ([textMarkers count] != 2) {
+ // Don't allow anything but a two member array.
+ return nil;
+ }
+
+ GeckoTextMarker p1 = GeckoTextMarker::MarkerFromAXTextMarker(
+ mGeckoDocAccessible, (__bridge AXTextMarkerRef)textMarkers[0]);
+ GeckoTextMarker p2 = GeckoTextMarker::MarkerFromAXTextMarker(
+ mGeckoDocAccessible, (__bridge AXTextMarkerRef)textMarkers[1]);
+
+ if (!p1.IsValid() || !p2.IsValid()) {
+ // If either marker is invalid, return nil.
+ return nil;
+ }
+
+ bool ordered = p1 < p2;
+ GeckoTextMarkerRange range(ordered ? p1 : p2, ordered ? p2 : p1);
+
+ return range.CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRef)moxStartTextMarkerForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange {
+ mozilla::a11y::GeckoTextMarkerRange range =
+ GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
+ mGeckoDocAccessible, textMarkerRange);
+
+ return range.IsValid() ? range.Start().CreateAXTextMarker() : nil;
+}
+
+- (AXTextMarkerRef)moxEndTextMarkerForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange {
+ mozilla::a11y::GeckoTextMarkerRange range =
+ GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
+ mGeckoDocAccessible, textMarkerRange);
+
+ return range.IsValid() ? range.End().CreateAXTextMarker() : nil;
+}
+
+- (AXTextMarkerRangeRef)moxLeftWordTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.LeftWordRange().CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRangeRef)moxRightWordTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.RightWordRange().CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRangeRef)moxLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.LineRange().CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRangeRef)moxLeftLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.LeftLineRange().CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRangeRef)moxRightLineTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.RightLineRange().CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRangeRef)moxParagraphTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.ParagraphRange().CreateAXTextMarkerRange();
+}
+
+// override
+- (AXTextMarkerRangeRef)moxStyleTextMarkerRangeForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.StyleRange().CreateAXTextMarkerRange();
+}
+
+- (AXTextMarkerRef)moxNextTextMarkerForTextMarker:(AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ if (!geckoTextMarker.Next()) {
+ return nil;
+ }
+
+ return geckoTextMarker.CreateAXTextMarker();
+}
+
+- (AXTextMarkerRef)moxPreviousTextMarkerForTextMarker:
+ (AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ if (!geckoTextMarker.Previous()) {
+ return nil;
+ }
+
+ return geckoTextMarker.CreateAXTextMarker();
+}
+
+- (NSAttributedString*)moxAttributedStringForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange {
+ mozilla::a11y::GeckoTextMarkerRange range =
+ GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
+ mGeckoDocAccessible, textMarkerRange);
+ if (!range.IsValid()) {
+ return nil;
+ }
+
+ return range.AttributedText();
+}
+
+- (NSValue*)moxBoundsForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
+ mozilla::a11y::GeckoTextMarkerRange range =
+ GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
+ mGeckoDocAccessible, textMarkerRange);
+ if (!range.IsValid()) {
+ return nil;
+ }
+
+ return range.Bounds();
+}
+
+- (NSNumber*)moxIndexForTextMarker:(AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ GeckoTextMarkerRange range(GeckoTextMarker(mGeckoDocAccessible, 0),
+ geckoTextMarker);
+
+ return @(range.Length());
+}
+
+- (AXTextMarkerRef)moxTextMarkerForIndex:(NSNumber*)index {
+ GeckoTextMarker geckoTextMarker = GeckoTextMarker::MarkerFromIndex(
+ mGeckoDocAccessible, [index integerValue]);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ return geckoTextMarker.CreateAXTextMarker();
+}
+
+- (id)moxUIElementForTextMarker:(AXTextMarkerRef)textMarker {
+ GeckoTextMarker geckoTextMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return nil;
+ }
+
+ Accessible* leaf = geckoTextMarker.Leaf();
+ if (!leaf) {
+ return nil;
+ }
+
+ return GetNativeFromGeckoAccessible(leaf);
+}
+
+- (AXTextMarkerRangeRef)moxTextMarkerRangeForUIElement:(id)element {
+ if (![element isKindOfClass:[mozAccessible class]]) {
+ return nil;
+ }
+
+ GeckoTextMarkerRange range((Accessible*)[element geckoAccessible]);
+ return range.CreateAXTextMarkerRange();
+}
+
+- (NSString*)moxMozDebugDescriptionForTextMarker:(AXTextMarkerRef)textMarker {
+ if (!mozilla::Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) {
+ return nil;
+ }
+
+ GeckoTextMarker geckoTextMarker =
+ GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
+ if (!geckoTextMarker.IsValid()) {
+ return @"<GeckoTextMarker 0x0 [0]>";
+ }
+
+ return [NSString stringWithFormat:@"<GeckoTextMarker %p [%d]>",
+ geckoTextMarker.Acc(),
+ geckoTextMarker.Offset()];
+}
+
+- (NSString*)moxMozDebugDescriptionForTextMarkerRange:
+ (AXTextMarkerRangeRef)textMarkerRange {
+ if (!mozilla::Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) {
+ return nil;
+ }
+
+ mozilla::a11y::GeckoTextMarkerRange range =
+ GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
+ mGeckoDocAccessible, textMarkerRange);
+ if (!range.IsValid()) {
+ return @"<GeckoTextMarkerRange 0x0 [0] - 0x0 [0]>";
+ }
+
+ return [NSString stringWithFormat:@"<GeckoTextMarkerRange %p [%d] - %p [%d]>",
+ range.Start().Acc(), range.Start().Offset(),
+ range.End().Acc(), range.End().Offset()];
+}
+
+- (void)moxSetSelectedTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
+ mozilla::a11y::GeckoTextMarkerRange range =
+ GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
+ mGeckoDocAccessible, textMarkerRange);
+ if (range.IsValid()) {
+ range.Select();
+ }
+}
+
+@end
diff --git a/accessible/mac/MOXWebAreaAccessible.h b/accessible/mac/MOXWebAreaAccessible.h
new file mode 100644
index 0000000000..1ef11af50c
--- /dev/null
+++ b/accessible/mac/MOXWebAreaAccessible.h
@@ -0,0 +1,105 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+
+using namespace mozilla::a11y;
+
+@class MOXRootGroup;
+
+@interface MOXWebAreaAccessible : mozAccessible {
+ MOXRootGroup* mRootGroup;
+}
+// overrides
+- (NSString*)moxRole;
+
+// overrides
+- (NSString*)moxRoleDescription;
+
+// overrides
+- (NSURL*)moxURL;
+
+// override
+- (NSNumber*)moxLoaded;
+
+// override
+- (NSNumber*)moxLoadingProgress;
+
+// override
+- (NSArray*)moxLinkUIElements;
+
+// override
+- (NSArray*)moxUnignoredChildren;
+
+// override
+- (BOOL)moxBlockSelector:(SEL)selector;
+
+// override
+- (void)moxPostNotification:(NSString*)notification;
+
+// override
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+// override
+- (void)dealloc;
+
+- (NSArray*)rootGroupChildren;
+
+- (id)rootGroup;
+
+@end
+
+@interface MOXRootGroup : MOXAccessibleBase {
+ MOXWebAreaAccessible* mParent;
+}
+
+// override
+- (id)initWithParent:(MOXWebAreaAccessible*)parent;
+
+// override
+- (NSString*)moxRole;
+
+// override
+- (NSString*)moxRoleDescription;
+
+// override
+- (id<mozAccessible>)moxParent;
+
+// override
+- (NSArray*)moxChildren;
+
+// override
+- (NSString*)moxIdentifier;
+
+// override
+- (NSString*)moxSubrole;
+
+// override
+- (id)moxHitTest:(NSPoint)point;
+
+// override
+- (NSValue*)moxPosition;
+
+// override
+- (NSValue*)moxSize;
+
+// override
+- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate;
+
+// override
+- (NSNumber*)moxUIElementCountForSearchPredicate:(NSDictionary*)searchPredicate;
+
+// override
+- (BOOL)disableChild:(id)child;
+
+// override
+- (void)expire;
+
+// override
+- (BOOL)isExpired;
+
+@end
diff --git a/accessible/mac/MOXWebAreaAccessible.mm b/accessible/mac/MOXWebAreaAccessible.mm
new file mode 100644
index 0000000000..c1ae585fa1
--- /dev/null
+++ b/accessible/mac/MOXWebAreaAccessible.mm
@@ -0,0 +1,276 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "MOXWebAreaAccessible.h"
+
+#import "MOXSearchInfo.h"
+#import "MacUtils.h"
+
+#include "nsAccUtils.h"
+#include "nsCocoaUtils.h"
+#include "DocAccessible.h"
+#include "DocAccessibleParent.h"
+
+using namespace mozilla::a11y;
+
+@implementation MOXRootGroup
+
+- (id)initWithParent:(MOXWebAreaAccessible*)parent {
+ // The parent is always a MOXWebAreaAccessible
+ mParent = parent;
+ return [super init];
+}
+
+- (NSString*)moxRole {
+ return NSAccessibilityGroupRole;
+}
+
+- (NSString*)moxRoleDescription {
+ if ([[self moxSubrole] isEqualToString:@"AXLandmarkApplication"]) {
+ return utils::LocalizedString(u"application"_ns);
+ }
+
+ return NSAccessibilityRoleDescription(NSAccessibilityGroupRole, nil);
+}
+
+- (id<mozAccessible>)moxParent {
+ return mParent;
+}
+
+- (NSArray*)moxChildren {
+ // Reparent the children of the web area here.
+ return [mParent rootGroupChildren];
+}
+
+- (NSString*)moxIdentifier {
+ // This is mostly for testing purposes to assert that this is the generated
+ // root group.
+ return @"root-group";
+}
+
+- (NSString*)moxSubrole {
+ // Steal the subrole internally mapped to the web area.
+ return [mParent moxSubrole];
+}
+
+- (id)moxHitTest:(NSPoint)point {
+ return [mParent moxHitTest:point];
+}
+
+- (NSValue*)moxPosition {
+ return [mParent moxPosition];
+}
+
+- (NSValue*)moxSize {
+ return [mParent moxSize];
+}
+
+- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate {
+ MOXSearchInfo* search =
+ [[[MOXSearchInfo alloc] initWithParameters:searchPredicate
+ andRoot:self] autorelease];
+
+ return [search performSearch];
+}
+
+- (NSNumber*)moxUIElementCountForSearchPredicate:
+ (NSDictionary*)searchPredicate {
+ return [NSNumber
+ numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate]
+ count]];
+}
+
+- (BOOL)disableChild:(id)child {
+ return NO;
+}
+
+- (void)expire {
+ mParent = nil;
+ [super expire];
+}
+
+- (BOOL)isExpired {
+ MOZ_ASSERT((mParent == nil) == mIsExpired);
+
+ return [super isExpired];
+}
+
+@end
+
+@implementation MOXWebAreaAccessible
+
+- (NSString*)moxRole {
+ // The OS role is AXWebArea regardless of the gecko role
+ // (APPLICATION or DOCUMENT).
+ // If the web area has a role of APPLICATION, its root group will
+ // reflect that in a subrole/description.
+ return @"AXWebArea";
+}
+
+- (NSString*)moxRoleDescription {
+ // The role description is "HTML Content" regardless of the gecko role
+ // (APPLICATION or DOCUMENT)
+ return utils::LocalizedString(u"htmlContent"_ns);
+}
+
+- (NSURL*)moxURL {
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ nsAutoString url;
+ MOZ_ASSERT(mGeckoAccessible->IsDoc());
+ nsAccUtils::DocumentURL(mGeckoAccessible, url);
+
+ if (url.IsEmpty()) {
+ return nil;
+ }
+
+ return [NSURL URLWithString:nsCocoaUtils::ToNSString(url)];
+}
+
+- (NSNumber*)moxLoaded {
+ if ([self isExpired]) {
+ return nil;
+ }
+ // We are loaded if we aren't busy or stale
+ return @([self stateWithMask:(states::BUSY & states::STALE)] == 0);
+}
+
+// overrides
+- (NSNumber*)moxLoadingProgress {
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ if ([self stateWithMask:states::STALE] != 0) {
+ // We expose stale state until the document is ready (DOM is loaded and tree
+ // is constructed) so we indicate load hasn't started while this state is
+ // present.
+ return @0.0;
+ }
+
+ if ([self stateWithMask:states::BUSY] != 0) {
+ // We expose state busy until the document and all its subdocuments are
+ // completely loaded, so we indicate partial loading here
+ return @0.5;
+ }
+
+ // if we are not busy and not stale, we are loaded
+ return @1.0;
+}
+
+- (NSArray*)moxLinkUIElements {
+ NSDictionary* searchPredicate = @{
+ @"AXSearchKey" : @"AXLinkSearchKey",
+ @"AXImmediateDescendantsOnly" : @NO,
+ @"AXResultsLimit" : @(-1),
+ @"AXDirection" : @"AXDirectionNext",
+ };
+
+ return [self moxUIElementsForSearchPredicate:searchPredicate];
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ switch (eventType) {
+ case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE:
+ [self moxPostNotification:
+ NSAccessibilityFocusedUIElementChangedNotification];
+ MOZ_ASSERT(mGeckoAccessible->IsRemote() ||
+ mGeckoAccessible->AsLocal()->IsRoot() ||
+ mGeckoAccessible->AsLocal()->AsDoc()->ParentDocument(),
+ "Non-root doc without a parent!");
+ if ((mGeckoAccessible->IsRemote() &&
+ mGeckoAccessible->AsRemote()->IsDoc() &&
+ mGeckoAccessible->AsRemote()->AsDoc()->IsTopLevel()) ||
+ (mGeckoAccessible->IsLocal() &&
+ !mGeckoAccessible->AsLocal()->IsRoot() &&
+ mGeckoAccessible->AsLocal()->AsDoc()->ParentDocument() &&
+ mGeckoAccessible->AsLocal()->AsDoc()->ParentDocument()->IsRoot())) {
+ // we fire an AXLoadComplete event on top-level documents only
+ [self moxPostNotification:@"AXLoadComplete"];
+ } else {
+ // otherwise the doc belongs to an iframe (IsTopLevelInContentProcess)
+ // and we fire AXLayoutComplete instead
+ [self moxPostNotification:@"AXLayoutComplete"];
+ }
+ break;
+ }
+
+ [super handleAccessibleEvent:eventType];
+}
+
+- (NSArray*)rootGroupChildren {
+ // This method is meant to expose the doc's children to the root group.
+ return [super moxChildren];
+}
+
+- (NSArray*)moxUnignoredChildren {
+ if (id rootGroup = [self rootGroup]) {
+ return @[ [self rootGroup] ];
+ }
+
+ // There is no root group, expose the children here directly.
+ return [super moxUnignoredChildren];
+}
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ if (selector == @selector(moxSubrole)) {
+ // Never expose a subrole for a web area.
+ return YES;
+ }
+
+ if (selector == @selector(moxElementBusy)) {
+ // Don't confuse aria-busy with a document's busy state.
+ return YES;
+ }
+
+ return [super moxBlockSelector:selector];
+}
+
+- (void)moxPostNotification:(NSString*)notification {
+ if (![notification isEqualToString:@"AXElementBusyChanged"]) {
+ // Suppress AXElementBusyChanged since it uses gecko's BUSY state
+ // to tell VoiceOver about aria-busy changes. We use that state
+ // differently in documents.
+ [super moxPostNotification:notification];
+ }
+}
+
+- (id)rootGroup {
+ NSArray* children = [super moxUnignoredChildren];
+ if (mRole == roles::DOCUMENT && [children count] == 1 &&
+ [[[children firstObject] moxUnignoredChildren] count] != 0) {
+ // We only need a root group if our document:
+ // (1) has multiple children, or
+ // (2) a one child that is a leaf, or
+ // (3) has a role other than the default document role
+ return nil;
+ }
+
+ if (!mRootGroup) {
+ mRootGroup = [[MOXRootGroup alloc] initWithParent:self];
+ }
+
+ return mRootGroup;
+}
+
+- (void)expire {
+ [mRootGroup expire];
+ [super expire];
+}
+
+- (void)dealloc {
+ // This object can only be dealoced after the gecko accessible wrapper
+ // reference is released, and that happens after expire is called.
+ MOZ_ASSERT([self isExpired]);
+ [mRootGroup release];
+
+ [super dealloc];
+}
+
+@end
diff --git a/accessible/mac/MacUtils.h b/accessible/mac/MacUtils.h
new file mode 100644
index 0000000000..33ee5d0a19
--- /dev/null
+++ b/accessible/mac/MacUtils.h
@@ -0,0 +1,62 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 _MacUtils_H_
+#define _MacUtils_H_
+
+#include "nsStringFwd.h"
+#include "mozAccessible.h"
+#include "MOXAccessibleBase.h"
+
+@class NSString;
+@class mozAccessible;
+
+namespace mozilla {
+namespace a11y {
+namespace utils {
+
+// convert an array of Gecko accessibles to an NSArray of native accessibles
+template <typename AccArray>
+NSArray<mozAccessible*>* ConvertToNSArray(AccArray& aArray) {
+ NSMutableArray* nativeArray = [[[NSMutableArray alloc] init] autorelease];
+
+ // iterate through the list, and get each native accessible.
+ for (Accessible* curAccessible : aArray) {
+ mozAccessible* curNative = GetNativeFromGeckoAccessible(curAccessible);
+ if (curNative)
+ [nativeArray addObject:GetObjectOrRepresentedView(curNative)];
+ }
+
+ return nativeArray;
+}
+
+/**
+ * Get a localized string from the string bundle.
+ * Return nil if not found.
+ */
+NSString* LocalizedString(const nsString& aString);
+
+/**
+ * Gets an accessible atttribute from the mozAccessible's associated
+ * accessible wrapper or proxy, and returns the value as an NSString.
+ * nil if no attribute is found.
+ */
+NSString* GetAccAttr(mozAccessible* aNativeAccessible, nsAtom* aAttrName);
+
+/**
+ * Return true if the passed raw pointer is a live document accessible. Uses
+ * the provided root doc accessible to check for current documents.
+ */
+bool DocumentExists(Accessible* aDoc, uintptr_t aDocPtr);
+
+NSDictionary* StringAttributesFromAccAttributes(AccAttributes* aAttributes,
+ Accessible* aContainer);
+} // namespace utils
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/mac/MacUtils.mm b/accessible/mac/MacUtils.mm
new file mode 100644
index 0000000000..5534e8fcc8
--- /dev/null
+++ b/accessible/mac/MacUtils.mm
@@ -0,0 +1,169 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "MacUtils.h"
+#include "mozAccessible.h"
+
+#include "LocalAccessible.h"
+#include "DocAccessible.h"
+#include "DocAccessibleParent.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/a11y/PDocAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+namespace utils {
+
+/**
+ * Get a localized string from the a11y string bundle.
+ * Return nil if not found.
+ */
+NSString* LocalizedString(const nsString& aString) {
+ nsString text;
+
+ Accessible::TranslateString(aString, text);
+
+ return text.IsEmpty() ? nil : nsCocoaUtils::ToNSString(text);
+}
+
+NSString* GetAccAttr(mozAccessible* aNativeAccessible, nsAtom* aAttrName) {
+ nsAutoString result;
+ Accessible* acc = [aNativeAccessible geckoAccessible];
+ RefPtr<AccAttributes> attributes = acc->Attributes();
+
+ if (!attributes) {
+ return nil;
+ }
+
+ attributes->GetAttribute(aAttrName, result);
+
+ if (!result.IsEmpty()) {
+ return nsCocoaUtils::ToNSString(result);
+ }
+
+ return nil;
+}
+
+bool DocumentExists(Accessible* aDoc, uintptr_t aDocPtr) {
+ if (reinterpret_cast<uintptr_t>(aDoc) == aDocPtr) {
+ return true;
+ }
+
+ if (aDoc->IsLocal()) {
+ DocAccessible* docAcc = aDoc->AsLocal()->AsDoc();
+ uint32_t docCount = docAcc->ChildDocumentCount();
+ for (uint32_t i = 0; i < docCount; i++) {
+ if (DocumentExists(docAcc->GetChildDocumentAt(i), aDocPtr)) {
+ return true;
+ }
+ }
+ } else {
+ DocAccessibleParent* docProxy = aDoc->AsRemote()->AsDoc();
+ size_t docCount = docProxy->ChildDocCount();
+ for (uint32_t i = 0; i < docCount; i++) {
+ if (DocumentExists(docProxy->ChildDocAt(i), aDocPtr)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+static NSColor* ColorFromColor(const Color& aColor) {
+ return [NSColor colorWithCalibratedRed:NS_GET_R(aColor.mValue) / 255.0
+ green:NS_GET_G(aColor.mValue) / 255.0
+ blue:NS_GET_B(aColor.mValue) / 255.0
+ alpha:1.0];
+}
+
+NSDictionary* StringAttributesFromAccAttributes(AccAttributes* aAttributes,
+ Accessible* aContainer) {
+ if (!aAttributes) {
+ if (mozAccessible* mozAcc = GetNativeFromGeckoAccessible(aContainer)) {
+ // If we don't have attributes provided this is probably a control like
+ // a button or empty entry. Just provide the accessible as an
+ // AXAttachment.
+ return @{@"AXAttachment" : mozAcc};
+ }
+ return @{};
+ }
+
+ NSMutableDictionary* attrDict =
+ [NSMutableDictionary dictionaryWithCapacity:aAttributes->Count()];
+ NSMutableDictionary* fontAttrDict = [[NSMutableDictionary alloc] init];
+ [attrDict setObject:fontAttrDict forKey:@"AXFont"];
+ for (auto iter : *aAttributes) {
+ if (iter.Name() == nsGkAtoms::backgroundColor) {
+ if (Maybe<Color> value = iter.Value<Color>()) {
+ NSColor* color = ColorFromColor(*value);
+ [attrDict setObject:(__bridge id)color.CGColor
+ forKey:@"AXBackgroundColor"];
+ }
+ } else if (iter.Name() == nsGkAtoms::color) {
+ if (Maybe<Color> value = iter.Value<Color>()) {
+ NSColor* color = ColorFromColor(*value);
+ [attrDict setObject:(__bridge id)color.CGColor
+ forKey:@"AXForegroundColor"];
+ }
+ } else if (iter.Name() == nsGkAtoms::font_size) {
+ if (Maybe<FontSize> pointSize = iter.Value<FontSize>()) {
+ int32_t fontPixelSize = static_cast<int32_t>(pointSize->mValue * 4 / 3);
+ [fontAttrDict setObject:@(fontPixelSize) forKey:@"AXFontSize"];
+ }
+ } else if (iter.Name() == nsGkAtoms::font_family) {
+ nsAutoString fontFamily;
+ iter.ValueAsString(fontFamily);
+ [fontAttrDict setObject:nsCocoaUtils::ToNSString(fontFamily)
+ forKey:@"AXFontFamily"];
+ } else if (iter.Name() == nsGkAtoms::textUnderlineColor) {
+ [attrDict setObject:@1 forKey:@"AXUnderline"];
+ if (Maybe<Color> value = iter.Value<Color>()) {
+ NSColor* color = ColorFromColor(*value);
+ [attrDict setObject:(__bridge id)color.CGColor
+ forKey:@"AXUnderlineColor"];
+ }
+ } else if (iter.Name() == nsGkAtoms::invalid) {
+ // XXX: There is currently no attribute for grammar
+ if (auto value = iter.Value<RefPtr<nsAtom>>()) {
+ if (*value == nsGkAtoms::spelling) {
+ [attrDict setObject:@YES
+ forKey:NSAccessibilityMarkedMisspelledTextAttribute];
+ }
+ }
+ } else {
+ nsAutoString valueStr;
+ iter.ValueAsString(valueStr);
+ nsAutoString keyStr;
+ iter.NameAsString(keyStr);
+ [attrDict setObject:nsCocoaUtils::ToNSString(valueStr)
+ forKey:nsCocoaUtils::ToNSString(keyStr)];
+ }
+ }
+
+ mozAccessible* container = GetNativeFromGeckoAccessible(aContainer);
+ id<MOXAccessible> link =
+ [container moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
+ return [[moxAcc moxRole] isEqualToString:NSAccessibilityLinkRole];
+ }];
+ if (link) {
+ [attrDict setObject:link forKey:@"AXLink"];
+ }
+
+ id<MOXAccessible> heading =
+ [container moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
+ return [[moxAcc moxRole] isEqualToString:@"AXHeading"];
+ }];
+ if (heading) {
+ [attrDict setObject:[heading moxValue] forKey:@"AXHeadingLevel"];
+ }
+
+ return attrDict;
+}
+} // namespace utils
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/mac/Platform.mm b/accessible/mac/Platform.mm
new file mode 100644
index 0000000000..eb507adefb
--- /dev/null
+++ b/accessible/mac/Platform.mm
@@ -0,0 +1,268 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+
+#import "MOXTextMarkerDelegate.h"
+
+#include "Platform.h"
+#include "RemoteAccessible.h"
+#include "DocAccessibleParent.h"
+#include "mozTableAccessible.h"
+#include "mozTextAccessible.h"
+#include "MOXWebAreaAccessible.h"
+#include "nsAccUtils.h"
+#include "TextRange.h"
+
+#include "nsAppShell.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/Telemetry.h"
+
+// Available from 10.13 onwards; test availability at runtime before using
+@interface NSWorkspace (AvailableSinceHighSierra)
+@property(readonly) BOOL isVoiceOverEnabled;
+@property(readonly) BOOL isSwitchControlEnabled;
+@end
+
+namespace mozilla {
+namespace a11y {
+
+// Mac a11y whitelisting
+static bool sA11yShouldBeEnabled = false;
+
+bool ShouldA11yBeEnabled() {
+ EPlatformDisabledState disabledState = PlatformDisabledState();
+ return (disabledState == ePlatformIsForceEnabled) ||
+ ((disabledState == ePlatformIsEnabled) && sA11yShouldBeEnabled);
+}
+
+void PlatformInit() {}
+
+void PlatformShutdown() {}
+
+void ProxyCreated(RemoteAccessible* aProxy) {
+ if (aProxy->Role() == roles::WHITESPACE) {
+ // We don't create a native object if we're child of a "flat" accessible;
+ // for example, on OS X buttons shouldn't have any children, because that
+ // makes the OS confused. We also don't create accessibles for <br>
+ // (whitespace) elements.
+ return;
+ }
+
+ // Pass in dummy state for now as retrieving proxy state requires IPC.
+ // Note that we can use RemoteAccessible::IsTable* functions here because they
+ // do not use IPC calls but that might change after bug 1210477.
+ Class type;
+ if (aProxy->IsTable()) {
+ type = [mozTableAccessible class];
+ } else if (aProxy->IsTableRow()) {
+ type = [mozTableRowAccessible class];
+ } else if (aProxy->IsTableCell()) {
+ type = [mozTableCellAccessible class];
+ } else if (aProxy->IsDoc()) {
+ type = [MOXWebAreaAccessible class];
+ } else {
+ type = GetTypeFromRole(aProxy->Role());
+ }
+
+ mozAccessible* mozWrapper = [[type alloc] initWithAccessible:aProxy];
+ aProxy->SetWrapper(reinterpret_cast<uintptr_t>(mozWrapper));
+}
+
+void ProxyDestroyed(RemoteAccessible* aProxy) {
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy);
+ [wrapper expire];
+ [wrapper release];
+ aProxy->SetWrapper(0);
+
+ if (aProxy->IsDoc()) {
+ [MOXTextMarkerDelegate destroyForDoc:aProxy];
+ }
+}
+
+void PlatformEvent(Accessible* aTarget, uint32_t aEventType) {
+ // Ignore event that we don't escape below, they aren't yet supported.
+ if (aEventType != nsIAccessibleEvent::EVENT_ALERT &&
+ aEventType != nsIAccessibleEvent::EVENT_VALUE_CHANGE &&
+ aEventType != nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE &&
+ aEventType != nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE &&
+ aEventType != nsIAccessibleEvent::EVENT_REORDER &&
+ aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED &&
+ aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED &&
+ aEventType != nsIAccessibleEvent::EVENT_NAME_CHANGE &&
+ aEventType != nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) {
+ return;
+ }
+
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
+ if (wrapper) {
+ [wrapper handleAccessibleEvent:aEventType];
+ }
+}
+
+void PlatformStateChangeEvent(Accessible* aTarget, uint64_t aState,
+ bool aEnabled) {
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
+ if (wrapper) {
+ [wrapper stateChanged:aState isEnabled:aEnabled];
+ }
+}
+
+void PlatformFocusEvent(Accessible* aTarget,
+ const LayoutDeviceIntRect& aCaretRect) {
+ if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) {
+ [wrapper handleAccessibleEvent:nsIAccessibleEvent::EVENT_FOCUS];
+ }
+}
+
+void PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset,
+ bool aIsSelectionCollapsed, int32_t aGranularity,
+ const LayoutDeviceIntRect& aCaretRect,
+ bool aFromUser) {
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
+ MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate
+ getOrCreateForDoc:nsAccUtils::DocumentFor(aTarget)];
+ [delegate setCaretOffset:aTarget at:aOffset moveGranularity:aGranularity];
+ if (aIsSelectionCollapsed) {
+ // If selection is collapsed, invalidate selection.
+ [delegate setSelectionFrom:aTarget at:aOffset to:aTarget at:aOffset];
+ }
+
+ if (wrapper) {
+ if (mozTextAccessible* textAcc =
+ static_cast<mozTextAccessible*>([wrapper moxEditableAncestor])) {
+ [textAcc
+ handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED];
+ } else {
+ [wrapper
+ handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED];
+ }
+ }
+}
+
+void PlatformTextChangeEvent(Accessible* aTarget, const nsAString& aStr,
+ int32_t aStart, uint32_t aLen, bool aIsInsert,
+ bool aFromUser) {
+ Accessible* acc = aTarget;
+ // If there is a text input ancestor, use it as the event source.
+ while (acc && GetTypeFromRole(acc->Role()) != [mozTextAccessible class]) {
+ acc = acc->Parent();
+ }
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(acc ? acc : aTarget);
+ [wrapper handleAccessibleTextChangeEvent:nsCocoaUtils::ToNSString(aStr)
+ inserted:aIsInsert
+ inContainer:aTarget
+ at:aStart];
+}
+
+void PlatformShowHideEvent(Accessible*, Accessible*, bool, bool) {}
+
+void PlatformSelectionEvent(Accessible* aTarget, Accessible* aWidget,
+ uint32_t aEventType) {
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(aWidget);
+ if (wrapper) {
+ [wrapper handleAccessibleEvent:aEventType];
+ }
+}
+
+void PlatformTextSelectionChangeEvent(Accessible* aTarget,
+ const nsTArray<TextRange>& aSelection) {
+ if (aSelection.Length()) {
+ MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate
+ getOrCreateForDoc:nsAccUtils::DocumentFor(aTarget)];
+ // Cache the selection.
+ [delegate setSelectionFrom:aSelection[0].StartContainer()
+ at:aSelection[0].StartOffset()
+ to:aSelection[0].EndContainer()
+ at:aSelection[0].EndOffset()];
+ }
+
+ mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
+ if (wrapper) {
+ [wrapper
+ handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED];
+ }
+}
+
+void PlatformRoleChangedEvent(Accessible* aTarget, const a11y::role& aRole,
+ uint8_t aRoleMapEntryIndex) {
+ if (mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget)) {
+ [wrapper handleRoleChanged:aRole];
+ }
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+@interface GeckoNSApplication (a11y)
+- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute;
+@end
+
+@implementation GeckoNSApplication (a11y)
+
+- (NSAccessibilityRole)accessibilityRole {
+ // For ATs that don't request `AXEnhancedUserInterface` we need to enable
+ // accessibility when a role is fetched. Not ideal, but this is needed
+ // for such services as Voice Control.
+ if (!mozilla::a11y::sA11yShouldBeEnabled) {
+ [self accessibilitySetValue:@YES forAttribute:@"AXEnhancedUserInterface"];
+ }
+ return [super accessibilityRole];
+}
+
+- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
+ if ([attribute isEqualToString:@"AXEnhancedUserInterface"]) {
+ mozilla::a11y::sA11yShouldBeEnabled = ([value intValue] == 1);
+ if (sA11yShouldBeEnabled) {
+ // If accessibility should be enabled, log the appropriate client
+ nsAutoString client;
+ if ([[NSWorkspace sharedWorkspace]
+ respondsToSelector:@selector(isVoiceOverEnabled)] &&
+ [[NSWorkspace sharedWorkspace] isVoiceOverEnabled]) {
+ client.Assign(u"VoiceOver"_ns);
+ } else if ([[NSWorkspace sharedWorkspace]
+ respondsToSelector:@selector(isSwitchControlEnabled)] &&
+ [[NSWorkspace sharedWorkspace] isSwitchControlEnabled]) {
+ client.Assign(u"SwitchControl"_ns);
+ } else {
+ // This is more complicated than the NSWorkspace queries above
+ // because (a) there is no "full keyboard access" query for NSWorkspace
+ // and (b) the [NSApplication fullKeyboardAccessEnabled] query checks
+ // the pre-Monterey version of full keyboard access, which is not what
+ // we're looking for here. For more info, see bug 1772375 comment 7.
+ Boolean exists;
+ int val = CFPreferencesGetAppIntegerValue(
+ CFSTR("FullKeyboardAccessEnabled"),
+ CFSTR("com.apple.Accessibility"), &exists);
+ if (exists && val == 1) {
+ client.Assign(u"FullKeyboardAccess"_ns);
+ } else {
+ val = CFPreferencesGetAppIntegerValue(
+ CFSTR("CommandAndControlEnabled"),
+ CFSTR("com.apple.Accessibility"), &exists);
+ if (exists && val == 1) {
+ client.Assign(u"VoiceControl"_ns);
+ } else {
+ client.Assign(u"Unknown"_ns);
+ }
+ }
+ }
+
+#if defined(MOZ_TELEMETRY_REPORTING)
+ mozilla::Telemetry::ScalarSet(
+ mozilla::Telemetry::ScalarID::A11Y_INSTANTIATORS, client);
+#endif // defined(MOZ_TELEMETRY_REPORTING)
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::AccessibilityClient,
+ NS_ConvertUTF16toUTF8(client));
+ }
+ }
+
+ return [super accessibilitySetValue:value forAttribute:attribute];
+}
+
+@end
diff --git a/accessible/mac/PlatformExtTypes.h b/accessible/mac/PlatformExtTypes.h
new file mode 100644
index 0000000000..8d861ed12e
--- /dev/null
+++ b/accessible/mac/PlatformExtTypes.h
@@ -0,0 +1,25 @@
+/* -*- 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_PlatformExtTypes_h__
+#define mozilla_a11y_PlatformExtTypes_h__
+
+namespace mozilla {
+namespace a11y {
+
+enum class EWhichRange {
+ eLeftWord,
+ eRightWord,
+ eLine,
+ eLeftLine,
+ eRightLine,
+ eParagraph,
+ eStyle
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_PlatformExtTypes_h__
diff --git a/accessible/mac/RootAccessibleWrap.h b/accessible/mac/RootAccessibleWrap.h
new file mode 100644
index 0000000000..632233cfcf
--- /dev/null
+++ b/accessible/mac/RootAccessibleWrap.h
@@ -0,0 +1,40 @@
+/* -*- 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/. */
+
+/* For documentation of the accessibility architecture,
+ * see http://lxr.mozilla.org/seamonkey/source/accessible/accessible-docs.html
+ */
+
+#ifndef mozilla_a11y_RootAccessibleWrap_h__
+#define mozilla_a11y_RootAccessibleWrap_h__
+
+#include "RootAccessible.h"
+
+namespace mozilla {
+
+class PresShell;
+
+namespace a11y {
+
+/**
+ * Mac 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();
+
+ Class GetNativeType();
+
+ // let's our native accessible get in touch with the
+ // native cocoa view that is our accessible parent.
+ void GetNativeWidget(void** aOutView);
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/mac/RootAccessibleWrap.mm b/accessible/mac/RootAccessibleWrap.mm
new file mode 100644
index 0000000000..e3d3da9224
--- /dev/null
+++ b/accessible/mac/RootAccessibleWrap.mm
@@ -0,0 +1,51 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "mozRootAccessible.h"
+
+#include "gfxPlatform.h"
+#include "nsCOMPtr.h"
+#include "nsObjCExceptions.h"
+#include "nsIFrame.h"
+#include "nsView.h"
+#include "nsIWidget.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+RootAccessibleWrap::RootAccessibleWrap(dom::Document* aDocument,
+ PresShell* aPresShell)
+ : RootAccessible(aDocument, aPresShell) {}
+
+RootAccessibleWrap::~RootAccessibleWrap() {}
+
+Class RootAccessibleWrap::GetNativeType() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [mozRootAccessible class];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+void RootAccessibleWrap::GetNativeWidget(void** aOutView) {
+ nsIFrame* frame = GetFrame();
+ if (frame) {
+ nsView* view = frame->GetView();
+ if (view) {
+ nsIWidget* widget = view->GetWidget();
+ if (widget) {
+ *aOutView = (void**)widget->GetNativeData(NS_NATIVE_WIDGET);
+ MOZ_ASSERT(
+ *aOutView || gfxPlatform::IsHeadless(),
+ "Couldn't get the native NSView parent we need to connect the "
+ "accessibility hierarchy!");
+ }
+ }
+ }
+}
diff --git a/accessible/mac/RotorRules.h b/accessible/mac/RotorRules.h
new file mode 100644
index 0000000000..7ccb191cb1
--- /dev/null
+++ b/accessible/mac/RotorRules.h
@@ -0,0 +1,144 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+#include "Pivot.h"
+
+using namespace mozilla::a11y;
+
+/**
+ * This rule matches all accessibles that satisfy the "boilerplate"
+ * pivot conditions and have a corresponding native accessible.
+ */
+class RotorRule : public PivotRule {
+ public:
+ explicit RotorRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText);
+ explicit RotorRule(const nsString& aSearchText);
+ uint16_t Match(Accessible* aAcc) override;
+
+ private:
+ Accessible* mDirectDescendantsFrom;
+ const nsString& mSearchText;
+};
+
+/**
+ * This rule matches all accessibles of a given role.
+ */
+class RotorRoleRule : public RotorRule {
+ public:
+ explicit RotorRoleRule(role aRole, Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText);
+ explicit RotorRoleRule(role aRole, const nsString& aSearchText);
+ uint16_t Match(Accessible* aAcc) override;
+
+ private:
+ role mRole;
+};
+
+class RotorMacRoleRule : public RotorRule {
+ public:
+ explicit RotorMacRoleRule(NSString* aRole, const nsString& aSearchText);
+ explicit RotorMacRoleRule(NSString* aRole, Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText);
+ ~RotorMacRoleRule();
+ virtual uint16_t Match(Accessible* aAcc) override;
+
+ protected:
+ NSString* mMacRole;
+};
+
+class RotorControlRule final : public RotorRule {
+ public:
+ explicit RotorControlRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText);
+ explicit RotorControlRule(const nsString& aSearchText);
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+};
+
+class RotorTextEntryRule final : public RotorRule {
+ public:
+ explicit RotorTextEntryRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText);
+ explicit RotorTextEntryRule(const nsString& aSearchText);
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+};
+
+class RotorLinkRule : public RotorRule {
+ public:
+ explicit RotorLinkRule(const nsString& aSearchText);
+ explicit RotorLinkRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText);
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+};
+
+class RotorVisitedLinkRule final : public RotorLinkRule {
+ public:
+ explicit RotorVisitedLinkRule(const nsString& aSearchText);
+ explicit RotorVisitedLinkRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText);
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+};
+
+class RotorUnvisitedLinkRule final : public RotorLinkRule {
+ public:
+ explicit RotorUnvisitedLinkRule(const nsString& aSearchText);
+ explicit RotorUnvisitedLinkRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText);
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+};
+
+/**
+ * This rule matches all accessibles that satisfy the "boilerplate"
+ * pivot conditions and have a corresponding native accessible.
+ */
+class RotorNotMacRoleRule : public RotorMacRoleRule {
+ public:
+ explicit RotorNotMacRoleRule(NSString* aMacRole,
+ Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText);
+ explicit RotorNotMacRoleRule(NSString* aMacRole, const nsString& aSearchText);
+ uint16_t Match(Accessible* aAcc) override;
+};
+
+class RotorStaticTextRule : public RotorRule {
+ public:
+ explicit RotorStaticTextRule(const nsString& aSearchText);
+ explicit RotorStaticTextRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText);
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+};
+
+class RotorHeadingLevelRule : public RotorRoleRule {
+ public:
+ explicit RotorHeadingLevelRule(int32_t aLevel, const nsString& aSearchText);
+ explicit RotorHeadingLevelRule(int32_t aLevel,
+ Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText);
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+
+ private:
+ int32_t mLevel;
+};
+
+class RotorLiveRegionRule : public RotorRule {
+ public:
+ explicit RotorLiveRegionRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText)
+ : RotorRule(aDirectDescendantsFrom, aSearchText) {}
+ explicit RotorLiveRegionRule(const nsString& aSearchText)
+ : RotorRule(aSearchText) {}
+
+ uint16_t Match(Accessible* aAcc) override;
+};
diff --git a/accessible/mac/RotorRules.mm b/accessible/mac/RotorRules.mm
new file mode 100644
index 0000000000..07f8479161
--- /dev/null
+++ b/accessible/mac/RotorRules.mm
@@ -0,0 +1,390 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "RotorRules.h"
+
+#include "nsCocoaUtils.h"
+#include "DocAccessibleParent.h"
+#include "nsIAccessiblePivot.h"
+#include "nsAccUtils.h"
+
+#include "nsAccessibilityService.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+// Generic Rotor Rule
+
+RotorRule::RotorRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText)
+ : mDirectDescendantsFrom(aDirectDescendantsFrom),
+ mSearchText(aSearchText) {}
+
+RotorRule::RotorRule(const nsString& aSearchText)
+ : mDirectDescendantsFrom(nullptr), mSearchText(aSearchText) {}
+
+uint16_t RotorRule::Match(Accessible* aAcc) {
+ uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;
+
+ if (nsAccUtils::MustPrune(aAcc)) {
+ result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+
+ if (mDirectDescendantsFrom && (aAcc != mDirectDescendantsFrom)) {
+ result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+
+ if ([GetNativeFromGeckoAccessible(aAcc) isAccessibilityElement]) {
+ result |= nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH) &&
+ !mSearchText.IsEmpty()) {
+ // If we have a non-empty search text, there are some roles
+ // we can safely ignore.
+ switch (aAcc->Role()) {
+ case roles::LANDMARK:
+ case roles::COMBOBOX:
+ case roles::LISTITEM:
+ case roles::COMBOBOX_LIST:
+ case roles::MENUBAR:
+ case roles::MENUPOPUP:
+ case roles::DOCUMENT:
+ case roles::APPLICATION:
+ // XXX: These roles either have AXTitle/AXDescription overridden as
+ // empty, or should never be returned in search text results. This
+ // should be better mapped somewhere.
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ break;
+ default:
+ nsAutoString name;
+ aAcc->Name(name);
+ if (!CaseInsensitiveFindInReadable(mSearchText, name)) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ break;
+ }
+ }
+
+ return result;
+}
+
+// Rotor Role Rule
+
+RotorRoleRule::RotorRoleRule(role aRole, Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText)
+ : RotorRule(aDirectDescendantsFrom, aSearchText), mRole(aRole){};
+
+RotorRoleRule::RotorRoleRule(role aRole, const nsString& aSearchText)
+ : RotorRule(aSearchText), mRole(aRole){};
+
+uint16_t RotorRoleRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired role, we flip the match bit to "unmatch"
+ // otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH) &&
+ aAcc->Role() != mRole) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+
+ return result;
+}
+
+// Rotor Mac Role Rule
+
+RotorMacRoleRule::RotorMacRoleRule(NSString* aMacRole,
+ Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText)
+ : RotorRule(aDirectDescendantsFrom, aSearchText), mMacRole(aMacRole) {
+ [mMacRole retain];
+};
+
+RotorMacRoleRule::RotorMacRoleRule(NSString* aMacRole,
+ const nsString& aSearchText)
+ : RotorRule(aSearchText), mMacRole(aMacRole) {
+ [mMacRole retain];
+};
+
+RotorMacRoleRule::~RotorMacRoleRule() { [mMacRole release]; }
+
+uint16_t RotorMacRoleRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired role, we flip the match bit to "unmatch"
+ // otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if (![[nativeMatch moxRole] isEqualToString:mMacRole]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+// Rotor Control Rule
+
+RotorControlRule::RotorControlRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText)
+ : RotorRule(aDirectDescendantsFrom, aSearchText){};
+
+RotorControlRule::RotorControlRule(const nsString& aSearchText)
+ : RotorRule(aSearchText){};
+
+uint16_t RotorControlRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired role, we flip the match bit to "unmatch"
+ // otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ switch (aAcc->Role()) {
+ case roles::PUSHBUTTON:
+ case roles::SPINBUTTON:
+ case roles::DETAILS:
+ case roles::CHECKBUTTON:
+ case roles::LISTBOX:
+ case roles::COMBOBOX:
+ case roles::EDITCOMBOBOX:
+ case roles::RADIOBUTTON:
+ case roles::RADIO_GROUP:
+ case roles::PAGETAB:
+ case roles::SLIDER:
+ case roles::SWITCH:
+ case roles::ENTRY:
+ case roles::OUTLINE:
+ case roles::PASSWORD_TEXT:
+ case roles::BUTTONMENU:
+ return result;
+
+ case roles::DATE_EDITOR:
+ case roles::TIME_EDITOR:
+ result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ return result;
+
+ case roles::GROUPING: {
+ // Groupings are sometimes used (like radio groups) to denote
+ // sets of controls. If that's the case, we want to surface
+ // them. We also want to surface grouped time and date controls.
+ for (unsigned int i = 0; i < aAcc->ChildCount(); i++) {
+ Accessible* currChild = aAcc->ChildAt(i);
+ if (currChild->Role() == roles::CHECKBUTTON ||
+ currChild->Role() == roles::SWITCH ||
+ currChild->Role() == roles::SPINBUTTON ||
+ currChild->Role() == roles::RADIOBUTTON) {
+ return result;
+ }
+ }
+
+ // if we iterated through the groups children and didn't
+ // find a control with one of the roles above, we should
+ // ignore this grouping
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ return result;
+ }
+
+ default:
+ // if we did not match on any above role, we should
+ // ignore this accessible.
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+// Rotor TextEntry Rule
+
+RotorTextEntryRule::RotorTextEntryRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText)
+ : RotorRule(aDirectDescendantsFrom, aSearchText){};
+
+RotorTextEntryRule::RotorTextEntryRule(const nsString& aSearchText)
+ : RotorRule(aSearchText){};
+
+uint16_t RotorTextEntryRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired role, we flip the match bit to "unmatch"
+ // otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ if (aAcc->Role() != roles::PASSWORD_TEXT && aAcc->Role() != roles::ENTRY) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+// Rotor Link Rule
+
+RotorLinkRule::RotorLinkRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText)
+ : RotorRule(aDirectDescendantsFrom, aSearchText){};
+
+RotorLinkRule::RotorLinkRule(const nsString& aSearchText)
+ : RotorRule(aSearchText){};
+
+uint16_t RotorLinkRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired role, we flip the match bit to "unmatch"
+ // otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if (![[nativeMatch moxRole] isEqualToString:@"AXLink"]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+RotorVisitedLinkRule::RotorVisitedLinkRule(const nsString& aSearchText)
+ : RotorLinkRule(aSearchText) {}
+
+RotorVisitedLinkRule::RotorVisitedLinkRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText)
+ : RotorLinkRule(aDirectDescendantsFrom, aSearchText) {}
+
+uint16_t RotorVisitedLinkRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorLinkRule::Match(aAcc);
+
+ if (result & nsIAccessibleTraversalRule::FILTER_MATCH) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if (![[nativeMatch moxVisited] boolValue]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+RotorUnvisitedLinkRule::RotorUnvisitedLinkRule(const nsString& aSearchText)
+ : RotorLinkRule(aSearchText) {}
+
+RotorUnvisitedLinkRule::RotorUnvisitedLinkRule(
+ Accessible* aDirectDescendantsFrom, const nsString& aSearchText)
+ : RotorLinkRule(aDirectDescendantsFrom, aSearchText) {}
+
+uint16_t RotorUnvisitedLinkRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorLinkRule::Match(aAcc);
+
+ if (result & nsIAccessibleTraversalRule::FILTER_MATCH) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if ([[nativeMatch moxVisited] boolValue]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+// Match Not Rule
+
+RotorNotMacRoleRule::RotorNotMacRoleRule(NSString* aMacRole,
+ Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText)
+ : RotorMacRoleRule(aMacRole, aDirectDescendantsFrom, aSearchText) {}
+
+RotorNotMacRoleRule::RotorNotMacRoleRule(NSString* aMacRole,
+ const nsString& aSearchText)
+ : RotorMacRoleRule(aMacRole, aSearchText) {}
+
+uint16_t RotorNotMacRoleRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not different from the desired role, we flip the
+ // match bit to "unmatch" otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if ([[nativeMatch moxRole] isEqualToString:mMacRole]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+ return result;
+}
+
+// Rotor Static Text Rule
+
+RotorStaticTextRule::RotorStaticTextRule(Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText)
+ : RotorRule(aDirectDescendantsFrom, aSearchText){};
+
+RotorStaticTextRule::RotorStaticTextRule(const nsString& aSearchText)
+ : RotorRule(aSearchText){};
+
+uint16_t RotorStaticTextRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired role, we flip the match bit to "unmatch"
+ // otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if (![[nativeMatch moxRole] isEqualToString:@"AXStaticText"]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+// Rotor Heading Level Rule
+
+RotorHeadingLevelRule::RotorHeadingLevelRule(int32_t aLevel,
+ Accessible* aDirectDescendantsFrom,
+ const nsString& aSearchText)
+ : RotorRoleRule(roles::HEADING, aDirectDescendantsFrom, aSearchText),
+ mLevel(aLevel){};
+
+RotorHeadingLevelRule::RotorHeadingLevelRule(int32_t aLevel,
+ const nsString& aSearchText)
+ : RotorRoleRule(roles::HEADING, aSearchText), mLevel(aLevel){};
+
+uint16_t RotorHeadingLevelRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRoleRule::Match(aAcc);
+
+ // if a match was found in the base-class's Match function,
+ // it is valid to consider that match again here. if it is
+ // not of the desired heading level, we flip the match bit to
+ // "unmatch" otherwise, the match persists.
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ int32_t currLevel = aAcc->GroupPosition().level;
+
+ if (currLevel != mLevel) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+
+ return result;
+}
+
+uint16_t RotorLiveRegionRule::Match(Accessible* aAcc) {
+ uint16_t result = RotorRule::Match(aAcc);
+
+ if ((result & nsIAccessibleTraversalRule::FILTER_MATCH)) {
+ mozAccessible* nativeMatch = GetNativeFromGeckoAccessible(aAcc);
+ if (![nativeMatch moxIsLiveRegion]) {
+ result &= ~nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+ }
+ return result;
+}
diff --git a/accessible/mac/SelectorMapGen.py b/accessible/mac/SelectorMapGen.py
new file mode 100755
index 0000000000..1e406ade1f
--- /dev/null
+++ b/accessible/mac/SelectorMapGen.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env 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/.
+
+import re
+
+
+def write_map(fd, name, text):
+ matches = re.findall(r"^//\s(AX\w+)\n-\s?\(.*?\)([\w:]+)", text, re.MULTILINE)
+ entries = [' @"%s" : @"%s"' % (a, s) for [a, s] in matches]
+
+ fd.write("NSDictionary* %s() {\n" % name)
+ fd.write(" // Create an autoreleased NSDictionary object once, and leak it.\n")
+ fd.write(" static NSDictionary* s%s = [@{\n" % name)
+ fd.write(",\n".join(entries))
+ fd.write("\n } retain];\n\n")
+ fd.write(" return s%s;\n" % name)
+ fd.write("}\n\n")
+
+
+def gen_mm(fd, protocol_file):
+ protocol = open(protocol_file).read()
+ fd.write("/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n\n")
+ fd.write("#import <Foundation/Foundation.h>\n\n")
+ fd.write("namespace mozilla {\nnamespace a11y {\nnamespace mac {\n\n")
+
+ sections = re.findall(
+ r"#pragma mark - (\w+)\n(.*?)(?=(?:#pragma mark|@end))", protocol, re.DOTALL
+ )
+ for name, text in sections:
+ write_map(fd, name, text)
+
+ fd.write("}\n}\n}\n")
+
+
+def gen_h(fd, protocol_file):
+ protocol = open(protocol_file).read()
+ sections = re.findall(
+ r"#pragma mark - (\w+)\n(.*?)(?=(?:#pragma mark|@end))", protocol, re.DOTALL
+ )
+
+ fd.write("/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n\n")
+ fd.write("#ifndef _MacSelectorMap_H_\n")
+ fd.write("#define _MacSelectorMap_H_\n")
+ fd.write("\n@class NSDictionary;\n")
+ fd.write("\nnamespace mozilla {\nnamespace a11y {\nnamespace mac {\n\n")
+ for name, _ in sections:
+ fd.write("NSDictionary* %s();\n\n" % name)
+ fd.write("}\n}\n}\n")
+ fd.write("\n#endif\n")
+
+
+# For debugging
+if __name__ == "__main__":
+ import sys
+
+ gen_mm(sys.stdout, "accessible/mac/MOXAccessibleProtocol.h")
+
+ gen_h(sys.stdout, "accessible/mac/MOXAccessibleProtocol.h")
diff --git a/accessible/mac/moz.build b/accessible/mac/moz.build
new file mode 100644
index 0000000000..acf6c4443b
--- /dev/null
+++ b/accessible/mac/moz.build
@@ -0,0 +1,70 @@
+# -*- 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 += [
+ "mozAccessibleProtocol.h",
+]
+
+EXPORTS.mozilla.a11y += [
+ "AccessibleWrap.h",
+ "PlatformExtTypes.h",
+]
+
+UNIFIED_SOURCES += [
+ "AccessibleWrap.mm",
+ "DocAccessibleWrap.mm",
+ "GeckoTextMarker.mm",
+ "MacUtils.mm",
+ "MOXAccessibleBase.mm",
+ "MOXLandmarkAccessibles.mm",
+ "MOXMathAccessibles.mm",
+ "MOXSearchInfo.mm",
+ "MOXTextMarkerDelegate.mm",
+ "MOXWebAreaAccessible.mm",
+ "mozAccessible.mm",
+ "mozActionElements.mm",
+ "mozHTMLAccessible.mm",
+ "mozRootAccessible.mm",
+ "mozSelectableElements.mm",
+ "mozTableAccessible.mm",
+ "mozTextAccessible.mm",
+ "Platform.mm",
+ "RootAccessibleWrap.mm",
+ "RotorRules.mm",
+]
+
+SOURCES += [
+ "!MacSelectorMap.mm",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/generic",
+ "/accessible/html",
+ "/accessible/ipc",
+ "/accessible/xul",
+ "/layout/generic",
+ "/layout/xul",
+ "/widget",
+ "/widget/cocoa",
+]
+
+GeneratedFile(
+ "MacSelectorMap.h",
+ script="/accessible/mac/SelectorMapGen.py",
+ entry_point="gen_h",
+ inputs=["MOXAccessibleProtocol.h"],
+)
+GeneratedFile(
+ "MacSelectorMap.mm",
+ script="/accessible/mac/SelectorMapGen.py",
+ entry_point="gen_mm",
+ inputs=["MOXAccessibleProtocol.h"],
+)
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/accessible/mac/mozAccessible.h b/accessible/mac/mozAccessible.h
new file mode 100644
index 0000000000..175e25d508
--- /dev/null
+++ b/accessible/mac/mozAccessible.h
@@ -0,0 +1,285 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 _MozAccessible_H_
+#define _MozAccessible_H_
+
+#include "AccessibleWrap.h"
+#include "RemoteAccessible.h"
+
+#import <Cocoa/Cocoa.h>
+
+#import "MOXAccessibleBase.h"
+
+@class mozRootAccessible;
+
+/**
+ * All mozAccessibles are either abstract objects (that correspond to XUL
+ * widgets, HTML frames, etc) or are attached to a certain view; for example
+ * a document view. When we hand an object off to an AT, we always want
+ * to give it the represented view, in the latter case.
+ */
+
+namespace mozilla {
+namespace a11y {
+
+inline mozAccessible* GetNativeFromGeckoAccessible(
+ mozilla::a11y::Accessible* aAcc) {
+ if (!aAcc) {
+ return nil;
+ }
+ if (LocalAccessible* acc = aAcc->AsLocal()) {
+ mozAccessible* native = nil;
+ acc->GetNativeInterface((void**)&native);
+ return native;
+ }
+
+ RemoteAccessible* proxy = aAcc->AsRemote();
+ return reinterpret_cast<mozAccessible*>(proxy->GetWrapper());
+}
+
+// Checked state values some accessibles return as AXValue.
+enum CheckedState {
+ kUncheckable = -1,
+ kUnchecked = 0,
+ kChecked = 1,
+ kMixed = 2
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+@interface mozAccessible : MOXAccessibleBase {
+ /**
+ * Reference to the accessible we were created with;
+ * either a proxy accessible or an accessible wrap.
+ */
+ mozilla::a11y::Accessible* mGeckoAccessible;
+
+ /**
+ * The role of our gecko accessible.
+ */
+ mozilla::a11y::role mRole;
+
+ nsStaticAtom* mARIARole;
+
+ bool mIsLiveRegion;
+}
+
+// inits with the given wrap or proxy accessible
+- (id)initWithAccessible:(mozilla::a11y::Accessible*)aAcc;
+
+// allows for gecko accessible access outside of the class
+- (mozilla::a11y::Accessible*)geckoAccessible;
+
+// override
+- (void)dealloc;
+
+// should a child be disabled
+- (BOOL)disableChild:(mozAccessible*)child;
+
+// Given a gecko accessibility event type, post the relevant
+// system accessibility notification.
+// Note: when overriding or adding new events, make sure your events aren't
+// filtered out in Platform::PlatformEvent or AccessibleWrap::HandleAccEvent!
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+- (void)handleAccessibleTextChangeEvent:(NSString*)change
+ inserted:(BOOL)isInserted
+ inContainer:(mozilla::a11y::Accessible*)container
+ at:(int32_t)start;
+
+// internal method to retrieve a child at a given index.
+- (id)childAt:(uint32_t)i;
+
+// Get gecko accessible's state.
+- (uint64_t)state;
+
+// Get gecko accessible's state filtered through given mask.
+- (uint64_t)stateWithMask:(uint64_t)mask;
+
+// Notify of a state change, so notifications can be fired.
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled;
+
+// Get top level (tab) web area.
+- (mozAccessible*)topWebArea;
+
+// Handle a role change
+- (void)handleRoleChanged:(mozilla::a11y::role)newRole;
+
+// Get ARIA role
+- (nsStaticAtom*)ARIARole;
+
+// Get array of related mozAccessibles
+- (NSArray<mozAccessible*>*)getRelationsByType:
+ (mozilla::a11y::RelationType)relationType;
+
+#pragma mark - mozAccessible protocol / widget
+
+// override
+- (BOOL)hasRepresentedView;
+
+// override
+- (id)representedView;
+
+// override
+- (BOOL)isRoot;
+
+#pragma mark - MOXAccessible protocol
+
+// override
+- (BOOL)moxBlockSelector:(SEL)selector;
+
+// override
+- (id)moxHitTest:(NSPoint)point;
+
+// override
+- (id)moxFocusedUIElement;
+
+- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate;
+
+- (BOOL)moxIsLiveRegion;
+
+// Attribute getters
+
+// override
+- (id<mozAccessible>)moxParent;
+
+// override
+- (NSArray*)moxChildren;
+
+// override
+- (NSValue*)moxSize;
+
+// override
+- (NSValue*)moxPosition;
+
+// override
+- (NSString*)moxRole;
+
+// override
+- (NSString*)moxSubrole;
+
+// override
+- (NSString*)moxRoleDescription;
+
+// override
+- (NSWindow*)moxWindow;
+
+// override
+- (id)moxValue;
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (NSString*)moxLabel;
+
+// override
+- (NSString*)moxHelp;
+
+// override
+- (NSNumber*)moxEnabled;
+
+// override
+- (NSNumber*)moxFocused;
+
+// override
+- (NSNumber*)moxSelected;
+
+// override
+- (NSNumber*)moxExpanded;
+
+// override
+- (NSValue*)moxFrame;
+
+// override
+- (NSString*)moxARIACurrent;
+
+// override
+- (NSNumber*)moxARIAAtomic;
+
+// override
+- (NSString*)moxARIALive;
+
+// override
+- (NSNumber*)moxARIAPosInSet;
+
+// override
+- (NSNumber*)moxARIASetSize;
+
+// override
+- (NSString*)moxARIARelevant;
+
+// override
+- (id)moxTitleUIElement;
+
+// override
+- (NSString*)moxDOMIdentifier;
+
+// override
+- (NSNumber*)moxRequired;
+
+// override
+- (NSNumber*)moxElementBusy;
+
+// override
+- (NSArray*)moxLinkedUIElements;
+
+// override
+- (NSArray*)moxARIAControls;
+
+// override
+- (id)moxEditableAncestor;
+
+// override
+- (id)moxHighestEditableAncestor;
+
+// override
+- (id)moxFocusableAncestor;
+
+#ifndef RELEASE_OR_BETA
+// override
+- (NSString*)moxMozDebugDescription;
+#endif
+
+// override
+- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate;
+
+// override
+- (NSNumber*)moxUIElementCountForSearchPredicate:(NSDictionary*)searchPredicate;
+
+// override
+- (void)moxSetFocused:(NSNumber*)focused;
+
+// override
+- (void)moxPerformScrollToVisible;
+
+// override
+- (void)moxPerformShowMenu;
+
+// override
+- (void)moxPerformPress;
+
+// override
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent;
+
+// override
+- (BOOL)moxIgnoreChild:(mozAccessible*)child;
+
+#pragma mark -
+
+// makes ourselves "expired". after this point, we might be around if someone
+// has retained us (e.g., a third-party), but we really contain no information.
+// override
+- (void)expire;
+// override
+- (BOOL)isExpired;
+
+@end
+
+#endif
diff --git a/accessible/mac/mozAccessible.mm b/accessible/mac/mozAccessible.mm
new file mode 100644
index 0000000000..2d8e140343
--- /dev/null
+++ b/accessible/mac/mozAccessible.mm
@@ -0,0 +1,1003 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+#include "MOXAccessibleBase.h"
+
+#import "MacUtils.h"
+#import "mozView.h"
+#import "MOXSearchInfo.h"
+#import "MOXTextMarkerDelegate.h"
+#import "MOXWebAreaAccessible.h"
+#import "mozTextAccessible.h"
+#import "mozRootAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "nsAccUtils.h"
+#include "DocAccessibleParent.h"
+#include "Relation.h"
+#include "mozilla/a11y/Role.h"
+#include "RootAccessible.h"
+#include "mozilla/a11y/PDocAccessible.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "OuterDocAccessible.h"
+#include "nsChildView.h"
+#include "xpcAccessibleMacInterface.h"
+
+#include "nsRect.h"
+#include "nsCocoaUtils.h"
+#include "nsCoord.h"
+#include "nsObjCExceptions.h"
+#include "nsWhitespaceTokenizer.h"
+#include <prdtoa.h>
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+#pragma mark -
+
+@interface mozAccessible ()
+- (BOOL)providesLabelNotTitle;
+
+- (void)maybePostLiveRegionChanged;
+- (void)maybePostA11yUtilNotification;
+@end
+
+@implementation mozAccessible
+
+- (id)initWithAccessible:(Accessible*)aAcc {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ MOZ_ASSERT(aAcc, "Cannot init mozAccessible with null");
+ if ((self = [super init])) {
+ mGeckoAccessible = aAcc;
+ mRole = aAcc->Role();
+ }
+
+ return self;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+#pragma mark - mozAccessible widget
+
+- (BOOL)hasRepresentedView {
+ return NO;
+}
+
+- (id)representedView {
+ return nil;
+}
+
+- (BOOL)isRoot {
+ return NO;
+}
+
+#pragma mark -
+
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ if (acc->IsContent() && acc->GetContent()->IsXULElement()) {
+ if (acc->VisibilityState() & states::INVISIBLE) {
+ return YES;
+ }
+ }
+ }
+
+ return [parent moxIgnoreChild:self];
+}
+
+- (BOOL)moxIgnoreChild:(mozAccessible*)child {
+ return nsAccUtils::MustPrune(mGeckoAccessible);
+}
+
+- (id)childAt:(uint32_t)i {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ Accessible* child = mGeckoAccessible->ChildAt(i);
+ return child ? GetNativeFromGeckoAccessible(child) : nil;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (uint64_t)state {
+ return mGeckoAccessible->State();
+}
+
+- (uint64_t)stateWithMask:(uint64_t)mask {
+ return [self state] & mask;
+}
+
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
+ if (state == states::BUSY) {
+ [self moxPostNotification:@"AXElementBusyChanged"];
+ }
+}
+
+- (BOOL)providesLabelNotTitle {
+ // These accessible types are the exception to the rule of label vs. title:
+ // They may be named explicitly, but they still provide a label not a title.
+ return mRole == roles::GROUPING || mRole == roles::RADIO_GROUP ||
+ mRole == roles::FIGURE || mRole == roles::GRAPHIC ||
+ mRole == roles::DOCUMENT || mRole == roles::OUTLINE ||
+ mRole == roles::ARTICLE || mRole == roles::ENTRY ||
+ mRole == roles::SPINBUTTON;
+}
+
+- (mozilla::a11y::Accessible*)geckoAccessible {
+ return mGeckoAccessible;
+}
+
+#pragma mark - MOXAccessible protocol
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ if (selector == @selector(moxPerformPress)) {
+ uint8_t actionCount = mGeckoAccessible->ActionCount();
+
+ // If we have no action, we don't support press, so return YES.
+ return actionCount == 0;
+ }
+
+ if (selector == @selector(moxSetFocused:)) {
+ return [self stateWithMask:states::FOCUSABLE] == 0;
+ }
+
+ if (selector == @selector(moxARIALive) ||
+ selector == @selector(moxARIAAtomic) ||
+ selector == @selector(moxARIARelevant)) {
+ return ![self moxIsLiveRegion];
+ }
+
+ if (selector == @selector(moxARIAPosInSet) || selector == @selector
+ (moxARIASetSize)) {
+ GroupPos groupPos = mGeckoAccessible->GroupPosition();
+ return groupPos.setSize == 0;
+ }
+
+ if (selector == @selector(moxExpanded)) {
+ return [self stateWithMask:states::EXPANDABLE] == 0;
+ }
+
+ return [super moxBlockSelector:selector];
+}
+
+- (id)moxFocusedUIElement {
+ MOZ_ASSERT(mGeckoAccessible);
+ // This only gets queried on the web area or the root group
+ // so just use the doc's focused child instead of trying to get
+ // the focused child of mGeckoAccessible.
+ Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible);
+ mozAccessible* focusedChild =
+ GetNativeFromGeckoAccessible(doc->FocusedChild());
+
+ if ([focusedChild isAccessibilityElement]) {
+ return focusedChild;
+ }
+
+ // return ourself if we can't get a native focused child.
+ return self;
+}
+
+- (id<MOXTextMarkerSupport>)moxTextMarkerDelegate {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ return [MOXTextMarkerDelegate
+ getOrCreateForDoc:nsAccUtils::DocumentFor(mGeckoAccessible)];
+}
+
+- (BOOL)moxIsLiveRegion {
+ return mIsLiveRegion;
+}
+
+- (id)moxHitTest:(NSPoint)point {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ // Convert the given screen-global point in the cocoa coordinate system (with
+ // origin in the bottom-left corner of the screen) into point in the Gecko
+ // coordinate system (with origin in a top-left screen point).
+ NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
+ NSPoint tmpPoint =
+ NSMakePoint(point.x, [mainView frame].size.height - point.y);
+ LayoutDeviceIntPoint geckoPoint = nsCocoaUtils::CocoaPointsToDevPixels(
+ tmpPoint, nsCocoaUtils::GetBackingScaleFactor(mainView));
+
+ Accessible* child = mGeckoAccessible->ChildAtPoint(
+ geckoPoint.x, geckoPoint.y, Accessible::EWhichChildAtPoint::DeepestChild);
+
+ if (child) {
+ mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
+ return [nativeChild isAccessibilityElement]
+ ? nativeChild
+ : [nativeChild moxUnignoredParent];
+ }
+
+ // if we didn't find anything, return ourself or child view.
+ return self;
+}
+
+- (id<mozAccessible>)moxParent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ Accessible* parent = mGeckoAccessible->Parent();
+
+ if (!parent) {
+ return nil;
+ }
+
+ id nativeParent = GetNativeFromGeckoAccessible(parent);
+ if ([nativeParent isKindOfClass:[MOXWebAreaAccessible class]]) {
+ // Before returning a WebArea as parent, check to see if
+ // there is a generated root group that is an intermediate container.
+ if (id<mozAccessible> rootGroup = [nativeParent rootGroup]) {
+ nativeParent = rootGroup;
+ }
+ }
+
+ if (!nativeParent && mGeckoAccessible->IsLocal()) {
+ // Return native of root accessible if we have no direct parent.
+ // XXX: need to return a sensible fallback in proxy case as well
+ nativeParent = GetNativeFromGeckoAccessible(
+ mGeckoAccessible->AsLocal()->RootAccessible());
+ }
+
+ return GetObjectOrRepresentedView(nativeParent);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+// gets all our native children lazily, including those that are ignored.
+- (NSArray*)moxChildren {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ NSMutableArray* children = [[[NSMutableArray alloc]
+ initWithCapacity:mGeckoAccessible->ChildCount()] autorelease];
+
+ for (uint32_t childIdx = 0; childIdx < mGeckoAccessible->ChildCount();
+ childIdx++) {
+ Accessible* child = mGeckoAccessible->ChildAt(childIdx);
+ mozAccessible* nativeChild = GetNativeFromGeckoAccessible(child);
+ if (!nativeChild) {
+ continue;
+ }
+
+ [children addObject:nativeChild];
+ }
+
+ return children;
+}
+
+- (NSValue*)moxPosition {
+ CGRect frame = [[self moxFrame] rectValue];
+
+ return [NSValue valueWithPoint:NSMakePoint(frame.origin.x, frame.origin.y)];
+}
+
+- (NSValue*)moxSize {
+ CGRect frame = [[self moxFrame] rectValue];
+
+ return
+ [NSValue valueWithSize:NSMakeSize(frame.size.width, frame.size.height)];
+}
+
+- (NSString*)moxRole {
+#define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
+ msaaRole, ia2Role, androidClass, nameRule) \
+ case roles::geckoRole: \
+ return macRole;
+
+ switch (mRole) {
+#include "RoleMap.h"
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown role.");
+ return NSAccessibilityUnknownRole;
+ }
+
+#undef ROLE
+}
+
+- (nsStaticAtom*)ARIARole {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (mGeckoAccessible->HasARIARole()) {
+ const nsRoleMapEntry* roleMap = mGeckoAccessible->ARIARoleMap();
+ return roleMap->roleAtom;
+ }
+
+ return nsGkAtoms::_empty;
+}
+
+- (NSString*)moxSubrole {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ // Deal with landmarks first
+ // macOS groups the specific landmark types of DPub ARIA into two broad
+ // categories with corresponding subroles: Navigation and region/container.
+ if (mRole == roles::LANDMARK) {
+ nsAtom* landmark = mGeckoAccessible->LandmarkRole();
+ // HTML Elements treated as landmarks, and ARIA landmarks.
+ if (landmark) {
+ if (landmark == nsGkAtoms::banner) return @"AXLandmarkBanner";
+ if (landmark == nsGkAtoms::complementary)
+ return @"AXLandmarkComplementary";
+ if (landmark == nsGkAtoms::contentinfo) return @"AXLandmarkContentInfo";
+ if (landmark == nsGkAtoms::main) return @"AXLandmarkMain";
+ if (landmark == nsGkAtoms::navigation) return @"AXLandmarkNavigation";
+ if (landmark == nsGkAtoms::search) return @"AXLandmarkSearch";
+ }
+
+ // None of the above, so assume DPub ARIA.
+ return @"AXLandmarkRegion";
+ }
+
+ // Now, deal with widget roles
+ nsStaticAtom* roleAtom = nullptr;
+
+ if (mRole == roles::DIALOG) {
+ roleAtom = [self ARIARole];
+
+ if (roleAtom == nsGkAtoms::alertdialog) {
+ return @"AXApplicationAlertDialog";
+ }
+ if (roleAtom == nsGkAtoms::dialog) {
+ return @"AXApplicationDialog";
+ }
+ }
+
+ if (mRole == roles::FORM) {
+ roleAtom = [self ARIARole];
+
+ if (roleAtom == nsGkAtoms::form) {
+ return @"AXLandmarkForm";
+ }
+ }
+
+#define ROLE(geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
+ msaaRole, ia2Role, androidClass, nameRule) \
+ case roles::geckoRole: \
+ if (![macSubrole isEqualToString:NSAccessibilityUnknownSubrole]) { \
+ return macSubrole; \
+ } else { \
+ break; \
+ }
+
+ switch (mRole) {
+#include "RoleMap.h"
+ }
+
+ // These are special. They map to roles::NOTHING
+ // and are instructed by the ARIA map to use the native host role.
+ roleAtom = [self ARIARole];
+
+ if (roleAtom == nsGkAtoms::log_) {
+ return @"AXApplicationLog";
+ }
+
+ if (roleAtom == nsGkAtoms::timer) {
+ return @"AXApplicationTimer";
+ }
+ // macOS added an AXSubrole value to distinguish generic AXGroup objects
+ // from those which are AXGroups as a result of an explicit ARIA role,
+ // such as the non-landmark, non-listitem text containers in DPub ARIA.
+ if (mRole == roles::FOOTNOTE || mRole == roles::SECTION) {
+ return @"AXApplicationGroup";
+ }
+
+ return NSAccessibilityUnknownSubrole;
+
+#undef ROLE
+}
+
+struct RoleDescrMap {
+ NSString* role;
+ const nsString description;
+};
+
+static const RoleDescrMap sRoleDescrMap[] = {
+ {@"AXApplicationAlert", u"alert"_ns},
+ {@"AXApplicationAlertDialog", u"alertDialog"_ns},
+ {@"AXApplicationDialog", u"dialog"_ns},
+ {@"AXApplicationLog", u"log"_ns},
+ {@"AXApplicationMarquee", u"marquee"_ns},
+ {@"AXApplicationStatus", u"status"_ns},
+ {@"AXApplicationTimer", u"timer"_ns},
+ {@"AXContentSeparator", u"separator"_ns},
+ {@"AXDefinition", u"definition"_ns},
+ {@"AXDetails", u"details"_ns},
+ {@"AXDocument", u"document"_ns},
+ {@"AXDocumentArticle", u"article"_ns},
+ {@"AXDocumentMath", u"math"_ns},
+ {@"AXDocumentNote", u"note"_ns},
+ {@"AXLandmarkApplication", u"application"_ns},
+ {@"AXLandmarkBanner", u"banner"_ns},
+ {@"AXLandmarkComplementary", u"complementary"_ns},
+ {@"AXLandmarkContentInfo", u"content"_ns},
+ {@"AXLandmarkMain", u"main"_ns},
+ {@"AXLandmarkNavigation", u"navigation"_ns},
+ {@"AXLandmarkRegion", u"region"_ns},
+ {@"AXLandmarkSearch", u"search"_ns},
+ {@"AXSearchField", u"searchTextField"_ns},
+ {@"AXSummary", u"summary"_ns},
+ {@"AXTabPanel", u"tabPanel"_ns},
+ {@"AXTerm", u"term"_ns},
+ {@"AXUserInterfaceTooltip", u"tooltip"_ns}};
+
+struct RoleDescrComparator {
+ const NSString* mRole;
+ explicit RoleDescrComparator(const NSString* aRole) : mRole(aRole) {}
+ int operator()(const RoleDescrMap& aEntry) const {
+ return [mRole compare:aEntry.role];
+ }
+};
+
+- (NSString*)moxRoleDescription {
+ if (NSString* ariaRoleDescription =
+ utils::GetAccAttr(self, nsGkAtoms::aria_roledescription)) {
+ if ([ariaRoleDescription length]) {
+ return ariaRoleDescription;
+ }
+ }
+
+ if (mRole == roles::FIGURE) return utils::LocalizedString(u"figure"_ns);
+
+ if (mRole == roles::HEADING) return utils::LocalizedString(u"heading"_ns);
+
+ if (mRole == roles::MARK) {
+ return utils::LocalizedString(u"highlight"_ns);
+ }
+
+ NSString* subrole = [self moxSubrole];
+
+ if (subrole) {
+ size_t idx = 0;
+ if (BinarySearchIf(sRoleDescrMap, 0, ArrayLength(sRoleDescrMap),
+ RoleDescrComparator(subrole), &idx)) {
+ return utils::LocalizedString(sRoleDescrMap[idx].description);
+ }
+ }
+
+ return NSAccessibilityRoleDescription([self moxRole], subrole);
+}
+
+- (NSString*)moxLabel {
+ if ([self isExpired]) {
+ return nil;
+ }
+
+ nsAutoString name;
+
+ /* If our accessible is:
+ * 1. Named by invisible text, or
+ * 2. Has more than one labeling relation, or
+ * 3. Is a special role defined in providesLabelNotTitle
+ * ... return its name as a label (AXDescription).
+ */
+ ENameValueFlag flag = mGeckoAccessible->Name(name);
+ if (flag == eNameFromSubtree) {
+ return nil;
+ }
+
+ if (![self providesLabelNotTitle]) {
+ NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
+ if ([relations count] == 1) {
+ return nil;
+ }
+ }
+
+ return nsCocoaUtils::ToNSString(name);
+}
+
+- (NSString*)moxTitle {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // In some special cases we provide the name in the label (AXDescription).
+ if ([self providesLabelNotTitle]) {
+ return nil;
+ }
+
+ nsAutoString title;
+ mGeckoAccessible->Name(title);
+ if (nsCoreUtils::IsWhitespaceString(title)) {
+ return @"";
+ }
+
+ return nsCocoaUtils::ToNSString(title);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (id)moxValue {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsAutoString value;
+ mGeckoAccessible->Value(value);
+
+ return nsCocoaUtils::ToNSString(value);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSString*)moxHelp {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // What needs to go here is actually the accDescription of an item.
+ // The MSAA acc_help method has nothing to do with this one.
+ nsAutoString helpText;
+ mGeckoAccessible->Description(helpText);
+
+ return nsCocoaUtils::ToNSString(helpText);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSWindow*)moxWindow {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // Get a pointer to the native window (NSWindow) we reside in.
+ NSWindow* nativeWindow = nil;
+ DocAccessible* docAcc = nullptr;
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ docAcc = acc->Document();
+ } else {
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+ LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser();
+ if (outerDoc) docAcc = outerDoc->Document();
+ }
+
+ if (docAcc) nativeWindow = static_cast<NSWindow*>(docAcc->GetNativeWindow());
+
+ MOZ_ASSERT(nativeWindow || gfxPlatform::IsHeadless(),
+ "Couldn't get native window");
+ return nativeWindow;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSNumber*)moxEnabled {
+ if ([self stateWithMask:states::UNAVAILABLE]) {
+ return @NO;
+ }
+
+ if (![self isRoot]) {
+ mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
+ if (![parent isRoot]) {
+ return @(![parent disableChild:self]);
+ }
+ }
+
+ return @YES;
+}
+
+- (NSNumber*)moxFocused {
+ return @([self stateWithMask:states::FOCUSED] != 0);
+}
+
+- (NSNumber*)moxSelected {
+ return @NO;
+}
+
+- (NSNumber*)moxExpanded {
+ return @([self stateWithMask:states::EXPANDED] != 0);
+}
+
+- (NSValue*)moxFrame {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ LayoutDeviceIntRect rect = mGeckoAccessible->Bounds();
+ NSScreen* mainView = [[NSScreen screens] objectAtIndex:0];
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mainView);
+
+ return [NSValue
+ valueWithRect:NSMakeRect(
+ static_cast<CGFloat>(rect.x) / scaleFactor,
+ [mainView frame].size.height -
+ static_cast<CGFloat>(rect.y + rect.height) /
+ scaleFactor,
+ static_cast<CGFloat>(rect.width) / scaleFactor,
+ static_cast<CGFloat>(rect.height) / scaleFactor)];
+}
+
+- (NSString*)moxARIACurrent {
+ if (![self stateWithMask:states::CURRENT]) {
+ return nil;
+ }
+
+ return utils::GetAccAttr(self, nsGkAtoms::aria_current);
+}
+
+- (NSNumber*)moxARIAAtomic {
+ return @(utils::GetAccAttr(self, nsGkAtoms::aria_atomic) != nil);
+}
+
+- (NSString*)moxARIALive {
+ return utils::GetAccAttr(self, nsGkAtoms::aria_live);
+}
+
+- (NSNumber*)moxARIAPosInSet {
+ GroupPos groupPos = mGeckoAccessible->GroupPosition();
+ return @(groupPos.posInSet);
+}
+
+- (NSNumber*)moxARIASetSize {
+ GroupPos groupPos = mGeckoAccessible->GroupPosition();
+ return @(groupPos.setSize);
+}
+
+- (NSString*)moxARIARelevant {
+ if (NSString* relevant =
+ utils::GetAccAttr(self, nsGkAtoms::containerRelevant)) {
+ return relevant;
+ }
+
+ // Default aria-relevant value
+ return @"additions text";
+}
+
+- (id)moxTitleUIElement {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ NSArray* relations = [self getRelationsByType:RelationType::LABELLED_BY];
+ if ([relations count] == 1) {
+ return [relations firstObject];
+ }
+
+ return nil;
+}
+
+- (NSString*)moxDOMIdentifier {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ nsAutoString id;
+ mGeckoAccessible->DOMNodeID(id);
+
+ return nsCocoaUtils::ToNSString(id);
+}
+
+- (NSNumber*)moxRequired {
+ return @([self stateWithMask:states::REQUIRED] != 0);
+}
+
+- (NSNumber*)moxElementBusy {
+ return @([self stateWithMask:states::BUSY] != 0);
+}
+
+- (NSArray*)moxLinkedUIElements {
+ return [self getRelationsByType:RelationType::FLOWS_TO];
+}
+
+- (NSArray*)moxARIAControls {
+ return [self getRelationsByType:RelationType::CONTROLLER_FOR];
+}
+
+- (mozAccessible*)topWebArea {
+ Accessible* doc = nsAccUtils::DocumentFor(mGeckoAccessible);
+ while (doc) {
+ if (doc->IsLocal()) {
+ DocAccessible* docAcc = doc->AsLocal()->AsDoc();
+ if (docAcc->DocumentNode()->GetBrowsingContext()->IsTopContent()) {
+ return GetNativeFromGeckoAccessible(docAcc);
+ }
+
+ doc = docAcc->ParentDocument();
+ } else {
+ DocAccessibleParent* docProxy = doc->AsRemote()->AsDoc();
+ if (docProxy->IsTopLevel()) {
+ return GetNativeFromGeckoAccessible(docProxy);
+ }
+ doc = docProxy->ParentDoc();
+ }
+ }
+
+ return nil;
+}
+
+- (void)handleRoleChanged:(mozilla::a11y::role)newRole {
+ mRole = newRole;
+ mARIARole = nullptr;
+
+ // For testing purposes
+ [self moxPostNotification:@"AXMozRoleChanged"];
+}
+
+- (id)moxEditableAncestor {
+ return [self moxFindAncestor:^BOOL(id moxAcc, BOOL* stop) {
+ return [moxAcc isKindOfClass:[mozTextAccessible class]];
+ }];
+}
+
+- (id)moxHighestEditableAncestor {
+ id highestAncestor = [self moxEditableAncestor];
+ while ([highestAncestor conformsToProtocol:@protocol(MOXAccessible)]) {
+ id ancestorParent = [highestAncestor moxUnignoredParent];
+ if (![ancestorParent conformsToProtocol:@protocol(MOXAccessible)]) {
+ break;
+ }
+
+ id higherAncestor = [ancestorParent moxEditableAncestor];
+
+ if (!higherAncestor) {
+ break;
+ }
+
+ highestAncestor = higherAncestor;
+ }
+
+ return highestAncestor;
+}
+
+- (id)moxFocusableAncestor {
+ // XXX: Checking focusable state up the chain can be expensive. For now,
+ // we can just return AXEditableAncestor since the main use case for this
+ // is rich text editing with links.
+ return [self moxEditableAncestor];
+}
+
+#ifndef RELEASE_OR_BETA
+- (NSString*)moxMozDebugDescription {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!mGeckoAccessible) {
+ return [NSString stringWithFormat:@"<%@: %p mGeckoAccessible=null>",
+ NSStringFromClass([self class]), self];
+ }
+
+ NSMutableString* domInfo = [NSMutableString string];
+ if (NSString* tagName = utils::GetAccAttr(self, nsGkAtoms::tag)) {
+ [domInfo appendFormat:@" %@", tagName];
+ NSString* domID = [self moxDOMIdentifier];
+ if ([domID length]) {
+ [domInfo appendFormat:@"#%@", domID];
+ }
+ if (NSString* className = utils::GetAccAttr(self, nsGkAtoms::_class)) {
+ [domInfo
+ appendFormat:@".%@",
+ [className stringByReplacingOccurrencesOfString:@" "
+ withString:@"."]];
+ }
+ }
+
+ return [NSString stringWithFormat:@"<%@: %p %@%@>",
+ NSStringFromClass([self class]), self,
+ [self moxRole], domInfo];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+#endif
+
+- (NSArray*)moxUIElementsForSearchPredicate:(NSDictionary*)searchPredicate {
+ // Create our search object and set it up with the searchPredicate
+ // params. The init function does additional parsing. We pass a
+ // reference to the web area to use as a start element if one is not
+ // specified.
+ MOXSearchInfo* search =
+ [[[MOXSearchInfo alloc] initWithParameters:searchPredicate
+ andRoot:self] autorelease];
+
+ return [search performSearch];
+}
+
+- (NSNumber*)moxUIElementCountForSearchPredicate:
+ (NSDictionary*)searchPredicate {
+ return [NSNumber
+ numberWithDouble:[[self moxUIElementsForSearchPredicate:searchPredicate]
+ count]];
+}
+
+- (void)moxSetFocused:(NSNumber*)focused {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if ([focused boolValue]) {
+ mGeckoAccessible->TakeFocus();
+ }
+}
+
+- (void)moxPerformScrollToVisible {
+ MOZ_ASSERT(mGeckoAccessible);
+ mGeckoAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE);
+}
+
+- (void)moxPerformShowMenu {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ // We don't need to convert this rect into mac coordinates because the
+ // mouse event synthesizer expects layout (gecko) coordinates.
+ LayoutDeviceIntRect bounds = mGeckoAccessible->Bounds();
+
+ LocalAccessible* rootAcc = mGeckoAccessible->IsLocal()
+ ? mGeckoAccessible->AsLocal()->RootAccessible()
+ : mGeckoAccessible->AsRemote()
+ ->OuterDocOfRemoteBrowser()
+ ->RootAccessible();
+ id objOrView =
+ GetObjectOrRepresentedView(GetNativeFromGeckoAccessible(rootAcc));
+
+ LayoutDeviceIntPoint p = LayoutDeviceIntPoint(
+ bounds.X() + (bounds.Width() / 2), bounds.Y() + (bounds.Height() / 2));
+ nsIWidget* widget = [objOrView widget];
+ widget->SynthesizeNativeMouseEvent(
+ p, nsIWidget::NativeMouseMessage::ButtonDown, MouseButton::eSecondary,
+ nsIWidget::Modifiers::NO_MODIFIERS, nullptr);
+}
+
+- (void)moxPerformPress {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ mGeckoAccessible->DoAction(0);
+}
+
+#pragma mark -
+
+- (BOOL)disableChild:(mozAccessible*)child {
+ return NO;
+}
+
+- (void)maybePostLiveRegionChanged {
+ id<MOXAccessible> liveRegion =
+ [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
+ return [moxAcc moxIsLiveRegion];
+ }];
+
+ if (liveRegion) {
+ [liveRegion moxPostNotification:@"AXLiveRegionChanged"];
+ }
+}
+
+- (void)maybePostA11yUtilNotification {
+ MOZ_ASSERT(mGeckoAccessible);
+ // Sometimes we use a special live region to make announcements to the user.
+ // This region is a child of the root document, but doesn't contain any
+ // content. If we try to fire regular AXLiveRegion changed events through it,
+ // VoiceOver clips the notifications because it (rightfully) doesn't detect
+ // focus within the region. We get around this by firing an
+ // AXAnnouncementRequested notification here instead.
+ // Verify we're trying to send a notification for the a11yUtils alert (and not
+ // a random acc with the same ID) by checking:
+ // - The gecko acc is local, our a11y-announcement lives in browser.xhtml
+ // - The ID of the gecko acc is "a11y-announcement"
+ // - The native acc is a direct descendent of the root
+ if (mGeckoAccessible->IsLocal() &&
+ [[self moxDOMIdentifier] isEqualToString:@"a11y-announcement"] &&
+ [[self moxParent] isKindOfClass:[mozRootAccessible class]]) {
+ // Our actual announcement should be stored as a child of the alert,
+ // so we verify a child exists, and then query that child below.
+ NSArray* children = [self moxChildren];
+ MOZ_ASSERT([children count] == 1 && children[0],
+ "A11yUtil event received, but no announcement found?");
+
+ mozAccessible* announcement = children[0];
+ NSString* key;
+ if ([announcement providesLabelNotTitle]) {
+ key = [announcement moxLabel];
+ } else {
+ key = [announcement moxTitle];
+ }
+
+ NSDictionary* info = @{
+ NSAccessibilityAnnouncementKey : key ? key : @(""),
+ NSAccessibilityPriorityKey : @(NSAccessibilityPriorityMedium)
+ };
+
+ id window = [self moxWindow];
+
+ // This sends events via nsIObserverService to be consumed by our
+ // mochitests. Normally we'd fire these events through moxPostNotification
+ // which takes care of this, but because the window we fetch above isn't
+ // derrived from MOXAccessibleBase, we do this (and post the notification)
+ // manually.
+ xpcAccessibleMacEvent::FireEvent(
+ window, NSAccessibilityAnnouncementRequestedNotification, info);
+ NSAccessibilityPostNotificationWithUserInfo(
+ window, NSAccessibilityAnnouncementRequestedNotification, info);
+ }
+}
+
+- (NSArray<mozAccessible*>*)getRelationsByType:(RelationType)relationType {
+ NSMutableArray<mozAccessible*>* relations =
+ [[[NSMutableArray alloc] init] autorelease];
+ Relation rel = mGeckoAccessible->RelationByType(relationType);
+ while (Accessible* relAcc = rel.Next()) {
+ if (mozAccessible* relNative = GetNativeFromGeckoAccessible(relAcc)) {
+ [relations addObject:relNative];
+ }
+ }
+
+ return relations;
+}
+
+- (void)handleAccessibleTextChangeEvent:(NSString*)change
+ inserted:(BOOL)isInserted
+ inContainer:(Accessible*)container
+ at:(int32_t)start {
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ switch (eventType) {
+ case nsIAccessibleEvent::EVENT_ALERT:
+ [self maybePostA11yUtilNotification];
+ break;
+ case nsIAccessibleEvent::EVENT_FOCUS:
+ [self moxPostNotification:
+ NSAccessibilityFocusedUIElementChangedNotification];
+ break;
+ case nsIAccessibleEvent::EVENT_MENUPOPUP_START:
+ [self moxPostNotification:@"AXMenuOpened"];
+ break;
+ case nsIAccessibleEvent::EVENT_MENUPOPUP_END:
+ [self moxPostNotification:@"AXMenuClosed"];
+ break;
+ case nsIAccessibleEvent::EVENT_SELECTION:
+ case nsIAccessibleEvent::EVENT_SELECTION_ADD:
+ case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
+ case nsIAccessibleEvent::EVENT_SELECTION_WITHIN:
+ [self moxPostNotification:
+ NSAccessibilitySelectedChildrenChangedNotification];
+ break;
+ case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
+ if (![self stateWithMask:states::SELECTABLE_TEXT]) {
+ break;
+ }
+ // We consider any caret move event to be a selected text change event.
+ // So dispatching an event for EVENT_TEXT_SELECTION_CHANGED would be
+ // reduntant.
+ MOXTextMarkerDelegate* delegate =
+ static_cast<MOXTextMarkerDelegate*>([self moxTextMarkerDelegate]);
+ NSMutableDictionary* userInfo =
+ [[[delegate selectionChangeInfo] mutableCopy] autorelease];
+ userInfo[@"AXTextChangeElement"] = self;
+
+ mozAccessible* webArea = [self topWebArea];
+ [webArea
+ moxPostNotification:NSAccessibilitySelectedTextChangedNotification
+ withUserInfo:userInfo];
+ [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification
+ withUserInfo:userInfo];
+ break;
+ }
+ case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED:
+ mIsLiveRegion = true;
+ [self moxPostNotification:@"AXLiveRegionCreated"];
+ break;
+ case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED:
+ mIsLiveRegion = false;
+ break;
+ case nsIAccessibleEvent::EVENT_REORDER:
+ [self maybePostLiveRegionChanged];
+ break;
+ case nsIAccessibleEvent::EVENT_NAME_CHANGE: {
+ if (![self providesLabelNotTitle]) {
+ [self moxPostNotification:NSAccessibilityTitleChangedNotification];
+ }
+ [self maybePostLiveRegionChanged];
+ break;
+ }
+ }
+}
+
+- (void)expire {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ mGeckoAccessible = nullptr;
+
+ [self moxPostNotification:NSAccessibilityUIElementDestroyedNotification];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (BOOL)isExpired {
+ return !mGeckoAccessible;
+}
+
+@end
diff --git a/accessible/mac/mozAccessibleProtocol.h b/accessible/mac/mozAccessibleProtocol.h
new file mode 100644
index 0000000000..bc418fa4f5
--- /dev/null
+++ b/accessible/mac/mozAccessibleProtocol.h
@@ -0,0 +1,65 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+
+#import "mozView.h"
+
+/* This protocol's primary use is so widget/cocoa can talk back to us
+ properly.
+
+ ChildView owns the topmost mozRootAccessible, and needs to take care of
+ setting up that parent/child relationship.
+
+ This protocol is thus used to make sure it knows it's talking to us, and not
+ just some random |id|.
+*/
+
+@protocol mozAccessible <NSObject>
+
+// returns whether this accessible is the root accessible. there is one
+// root accessible per window.
+- (BOOL)isRoot;
+
+// some mozAccessibles implement accessibility support in place of another
+// object. for example, ChildView gets its support from us.
+//
+// instead of returning a mozAccessible to the OS when it wants an object, we
+// need to pass the view we represent, so the OS doesn't get confused and think
+// we return some random object.
+- (BOOL)hasRepresentedView;
+- (id)representedView;
+
+/*** general ***/
+
+// returns the accessible at the specified point.
+- (id)accessibilityHitTest:(NSPoint)point;
+
+// whether this element should be exposed to platform.
+- (BOOL)isAccessibilityElement;
+
+// currently focused UI element (possibly a child accessible)
+- (id)accessibilityFocusedUIElement;
+
+/*** attributes ***/
+
+// all supported attributes
+- (NSArray*)accessibilityAttributeNames;
+
+// value for given attribute.
+- (id)accessibilityAttributeValue:(NSString*)attribute;
+
+// whether a particular attribute can be modified
+- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute;
+
+/*** actions ***/
+
+- (NSArray*)accessibilityActionNames;
+- (NSString*)accessibilityActionDescription:(NSString*)action;
+- (void)accessibilityPerformAction:(NSString*)action;
+
+@end
diff --git a/accessible/mac/mozActionElements.h b/accessible/mac/mozActionElements.h
new file mode 100644
index 0000000000..f9940c793a
--- /dev/null
+++ b/accessible/mac/mozActionElements.h
@@ -0,0 +1,108 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+#import "mozAccessible.h"
+
+/* Simple subclasses for things like checkboxes, buttons, etc. */
+
+@interface mozButtonAccessible : mozAccessible
+
+// override
+- (NSNumber*)moxHasPopup;
+
+// override
+- (NSString*)moxPopupValue;
+
+@end
+
+@interface mozPopupButtonAccessible : mozButtonAccessible
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (BOOL)moxBlockSelector:(SEL)selector;
+
+// override
+- (NSArray*)moxChildren;
+
+// override
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled;
+
+@end
+
+@interface mozCheckboxAccessible : mozButtonAccessible
+
+// override
+- (id)moxValue;
+
+// override
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled;
+
+@end
+
+// LocalAccessible for a radio button
+@interface mozRadioButtonAccessible : mozCheckboxAccessible
+
+// override
+- (NSArray*)moxLinkedUIElements;
+
+@end
+
+/**
+ * Accessible for a PANE
+ */
+@interface mozPaneAccessible : mozAccessible
+
+// override
+- (NSArray*)moxChildren;
+
+@end
+
+/**
+ * Base accessible for an incrementable
+ */
+@interface mozIncrementableAccessible : mozAccessible
+
+// override
+- (id)moxValue;
+
+// override
+- (NSString*)moxValueDescription;
+
+// override
+- (id)moxMinValue;
+
+// override
+- (id)moxMaxValue;
+
+// override
+- (void)moxSetValue:(id)value;
+
+// override
+- (void)moxPerformIncrement;
+
+// override
+- (void)moxPerformDecrement;
+
+// override
+- (NSString*)moxOrientation;
+
+// override
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+- (void)changeValueBySteps:(int)factor;
+
+@end
+
+@interface mozDatePickerAccessible : mozAccessible
+
+// override
+- (NSString*)moxTitle;
+
+@end
diff --git a/accessible/mac/mozActionElements.mm b/accessible/mac/mozActionElements.mm
new file mode 100644
index 0000000000..f39f2c8ad5
--- /dev/null
+++ b/accessible/mac/mozActionElements.mm
@@ -0,0 +1,228 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozActionElements.h"
+
+#import "MacUtils.h"
+#include "LocalAccessible-inl.h"
+#include "DocAccessible.h"
+#include "XULTabAccessible.h"
+#include "HTMLFormControlAccessible.h"
+
+#include "nsCocoaUtils.h"
+#include "mozilla/FloatingPoint.h"
+
+using namespace mozilla::a11y;
+
+@implementation mozButtonAccessible
+
+- (NSNumber*)moxHasPopup {
+ return @([self stateWithMask:states::HASPOPUP] != 0);
+}
+
+- (NSString*)moxPopupValue {
+ if ([self stateWithMask:states::HASPOPUP] != 0) {
+ return utils::GetAccAttr(self, nsGkAtoms::aria_haspopup);
+ }
+
+ return nil;
+}
+
+@end
+
+@implementation mozPopupButtonAccessible
+
+- (NSString*)moxTitle {
+ // Popup buttons don't have titles.
+ return @"";
+}
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ if (selector == @selector(moxHasPopup)) {
+ return YES;
+ }
+
+ return [super moxBlockSelector:selector];
+}
+
+- (NSArray*)moxChildren {
+ if ([self stateWithMask:states::EXPANDED] == 0) {
+ // If the popup button is collapsed don't return its children.
+ return @[];
+ }
+
+ return [super moxChildren];
+}
+
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
+ [super stateChanged:state isEnabled:enabled];
+
+ if (state == states::EXPANDED) {
+ // If the EXPANDED state is updated, fire AXMenu events on the
+ // popups child which is the actual menu.
+ if (mozAccessible* popup = (mozAccessible*)[self childAt:0]) {
+ [popup moxPostNotification:(enabled ? @"AXMenuOpened" : @"AXMenuClosed")];
+ }
+ }
+}
+
+@end
+
+@implementation mozRadioButtonAccessible
+
+- (NSArray*)moxLinkedUIElements {
+ return [[self getRelationsByType:RelationType::MEMBER_OF]
+ arrayByAddingObjectsFromArray:[super moxLinkedUIElements]];
+}
+
+@end
+
+@implementation mozCheckboxAccessible
+
+- (int)isChecked {
+ // check if we're checked or in a mixed state
+ uint64_t state =
+ [self stateWithMask:(states::CHECKED | states::PRESSED | states::MIXED)];
+ if (state & (states::CHECKED | states::PRESSED)) {
+ return kChecked;
+ }
+
+ if (state & states::MIXED) {
+ return kMixed;
+ }
+
+ return kUnchecked;
+}
+
+- (id)moxValue {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ return [NSNumber numberWithInt:[self isChecked]];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
+ [super stateChanged:state isEnabled:enabled];
+
+ if (state & (states::CHECKED | states::PRESSED | states::MIXED)) {
+ [self moxPostNotification:NSAccessibilityValueChangedNotification];
+ }
+}
+
+@end
+
+@implementation mozPaneAccessible
+
+- (NSArray*)moxChildren {
+ // By default, all tab panels are exposed in the a11y tree
+ // even if the tab they represent isn't the active tab. To
+ // prevent VoiceOver from navigating background tab content,
+ // only expose the tab panel that is currently on screen.
+ for (mozAccessible* child in [super moxChildren]) {
+ if (!([child state] & states::OFFSCREEN)) {
+ return [NSArray arrayWithObject:GetObjectOrRepresentedView(child)];
+ }
+ }
+ MOZ_ASSERT_UNREACHABLE("We have no on screen tab content?");
+ return @[];
+}
+
+@end
+
+@implementation mozIncrementableAccessible
+
+- (id)moxValue {
+ return [NSNumber numberWithDouble:mGeckoAccessible->CurValue()];
+}
+
+- (NSString*)moxValueDescription {
+ nsAutoString valueDesc;
+ mGeckoAccessible->Value(valueDesc);
+ return nsCocoaUtils::ToNSString(valueDesc);
+}
+- (id)moxMinValue {
+ return [NSNumber numberWithDouble:mGeckoAccessible->MinValue()];
+}
+
+- (id)moxMaxValue {
+ return [NSNumber numberWithDouble:mGeckoAccessible->MaxValue()];
+}
+
+- (void)moxSetValue:(id)value {
+ [self setValue:([value doubleValue])];
+}
+
+- (void)moxPerformIncrement {
+ [self changeValueBySteps:1];
+}
+
+- (void)moxPerformDecrement {
+ [self changeValueBySteps:-1];
+}
+
+- (NSString*)moxOrientation {
+ RefPtr<AccAttributes> attributes = mGeckoAccessible->Attributes();
+ if (attributes) {
+ nsAutoString result;
+ attributes->GetAttribute(nsGkAtoms::aria_orientation, result);
+ if (result.Equals(u"horizontal"_ns)) {
+ return NSAccessibilityHorizontalOrientationValue;
+ } else if (result.Equals(u"vertical"_ns)) {
+ return NSAccessibilityVerticalOrientationValue;
+ }
+ }
+
+ return NSAccessibilityUnknownOrientationValue;
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ switch (eventType) {
+ case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE:
+ case nsIAccessibleEvent::EVENT_VALUE_CHANGE:
+ [self moxPostNotification:NSAccessibilityValueChangedNotification];
+ break;
+ default:
+ [super handleAccessibleEvent:eventType];
+ break;
+ }
+}
+
+/*
+ * Updates the accessible's current value by factor and step.
+ *
+ * factor: A signed integer representing the number of times to
+ * apply step to the current value. A positive value will increment,
+ * while a negative one will decrement.
+ * step: An unsigned integer specified by the webauthor and indicating the
+ * amount by which to increment/decrement the current value.
+ */
+- (void)changeValueBySteps:(int)factor {
+ MOZ_ASSERT(mGeckoAccessible, "mGeckoAccessible is null");
+
+ double newValue =
+ mGeckoAccessible->CurValue() + (mGeckoAccessible->Step() * factor);
+ [self setValue:(newValue)];
+}
+
+/*
+ * Updates the accessible's current value to the specified value
+ */
+- (void)setValue:(double)value {
+ MOZ_ASSERT(mGeckoAccessible, "mGeckoAccessible is null");
+ mGeckoAccessible->SetCurValue(value);
+}
+
+@end
+
+@implementation mozDatePickerAccessible
+
+- (NSString*)moxTitle {
+ return utils::LocalizedString(u"dateField"_ns);
+}
+
+@end
diff --git a/accessible/mac/mozHTMLAccessible.h b/accessible/mac/mozHTMLAccessible.h
new file mode 100644
index 0000000000..48fd4b0bdc
--- /dev/null
+++ b/accessible/mac/mozHTMLAccessible.h
@@ -0,0 +1,44 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+
+@interface mozHeadingAccessible : mozAccessible
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (id)moxValue;
+
+@end
+
+@interface mozLinkAccessible : mozAccessible
+
+// override
+- (id)moxValue;
+
+// override
+- (NSString*)moxRole;
+
+// override
+- (NSURL*)moxURL;
+
+// override
+- (NSNumber*)moxVisited;
+
+// override
+- (NSArray*)moxLinkedUIElements;
+
+@end
+
+@interface MOXListItemAccessible : mozAccessible
+
+// override
+- (NSString*)moxTitle;
+
+@end
diff --git a/accessible/mac/mozHTMLAccessible.mm b/accessible/mac/mozHTMLAccessible.mm
new file mode 100644
index 0000000000..0968003341
--- /dev/null
+++ b/accessible/mac/mozHTMLAccessible.mm
@@ -0,0 +1,83 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozHTMLAccessible.h"
+
+#import "LocalAccessible-inl.h"
+#import "HyperTextAccessible.h"
+
+#import "nsCocoaUtils.h"
+
+using namespace mozilla::a11y;
+
+@implementation mozHeadingAccessible
+
+- (NSString*)moxTitle {
+ nsAutoString title;
+
+ ENameValueFlag flag = mGeckoAccessible->Name(title);
+ if (flag != eNameFromSubtree) {
+ // If this is a name via relation or attribute (eg. aria-label)
+ // it will be provided via AXDescription.
+ return nil;
+ }
+
+ return nsCocoaUtils::ToNSString(title);
+}
+
+- (id)moxValue {
+ GroupPos groupPos = mGeckoAccessible->GroupPosition();
+
+ return [NSNumber numberWithInt:groupPos.level];
+}
+
+@end
+
+@implementation mozLinkAccessible
+
+- (NSString*)moxValue {
+ return @"";
+}
+
+- (NSURL*)moxURL {
+ nsAutoString value;
+ mGeckoAccessible->Value(value);
+
+ NSString* urlString = value.IsEmpty() ? nil : nsCocoaUtils::ToNSString(value);
+ if (!urlString) return nil;
+
+ return [NSURL URLWithString:urlString];
+}
+
+- (NSNumber*)moxVisited {
+ return @([self stateWithMask:states::TRAVERSED] != 0);
+}
+
+- (NSString*)moxRole {
+ // If this is not LINKED, just expose this as a generic group accessible.
+ // Chrome and Safari expose this as a childless AXStaticText, but
+ // the HTML Accessibility API Mappings spec says this should be an AXGroup.
+ if (![self stateWithMask:states::LINKED]) {
+ return NSAccessibilityGroupRole;
+ }
+
+ return [super moxRole];
+}
+
+- (NSArray*)moxLinkedUIElements {
+ return [self getRelationsByType:RelationType::LINKS_TO];
+}
+
+@end
+
+@implementation MOXListItemAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+@end
diff --git a/accessible/mac/mozRootAccessible.h b/accessible/mac/mozRootAccessible.h
new file mode 100644
index 0000000000..929eca01dd
--- /dev/null
+++ b/accessible/mac/mozRootAccessible.h
@@ -0,0 +1,58 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+#import "mozAccessible.h"
+
+// our protocol that we implement (so cocoa widgets can talk to us)
+#import "mozAccessibleProtocol.h"
+
+/*
+ The root accessible. There is one per window.
+ Created by the RootAccessibleWrap.
+*/
+@interface mozRootAccessible : mozAccessible {
+ // the mozView that we're representing.
+ // all outside communication goes through the mozView.
+ // in reality, it's just piping all calls to us, and we're
+ // doing its dirty work!
+ //
+ // whenever someone asks who we are (e.g., a child asking
+ // for its parent, or our parent asking for its child), we'll
+ // respond the mozView. it is absolutely necessary for third-
+ // party tools that we do this!
+ //
+ // /hwaara
+ id<mozView, mozAccessible> mParallelView; // weak ref
+}
+
+// override
+- (id)initWithAccessible:(mozilla::a11y::Accessible*)aAcc;
+
+#pragma mark - MOXAccessible
+
+// override
+- (NSNumber*)moxMain;
+
+// override
+- (NSNumber*)moxMinimized;
+
+// override
+- (id)moxUnignoredParent;
+
+#pragma mark - mozAccessible/widget
+
+// override
+- (BOOL)hasRepresentedView;
+
+// override
+- (id)representedView;
+
+// override
+- (BOOL)isRoot;
+
+@end
diff --git a/accessible/mac/mozRootAccessible.mm b/accessible/mac/mozRootAccessible.mm
new file mode 100644
index 0000000000..3f171ada8c
--- /dev/null
+++ b/accessible/mac/mozRootAccessible.mm
@@ -0,0 +1,84 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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"
+
+#import "mozRootAccessible.h"
+
+#import "mozView.h"
+
+#include "gfxPlatform.h"
+// This must be included last:
+#include "nsObjCExceptions.h"
+
+using namespace mozilla::a11y;
+
+static id<mozAccessible, mozView> getNativeViewFromRootAccessible(
+ LocalAccessible* aAccessible) {
+ RootAccessibleWrap* root =
+ static_cast<RootAccessibleWrap*>(aAccessible->AsRoot());
+ id<mozAccessible, mozView> nativeView = nil;
+ root->GetNativeWidget((void**)&nativeView);
+ return nativeView;
+}
+
+#pragma mark -
+
+@implementation mozRootAccessible
+
+- (id)initWithAccessible:(mozilla::a11y::Accessible*)aAcc {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(!aAcc->IsRemote(), "mozRootAccessible is never a proxy");
+
+ mParallelView = getNativeViewFromRootAccessible(aAcc->AsLocal());
+
+ return [super initWithAccessible:aAcc];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (NSNumber*)moxMain {
+ return @([[self moxWindow] isMainWindow]);
+}
+
+- (NSNumber*)moxMinimized {
+ return @([[self moxWindow] isMiniaturized]);
+}
+
+// return the AXParent that our parallell NSView tells us about.
+- (id)moxUnignoredParent {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ // If there is no represented view (eg. headless), this will return nil.
+ return [[self representedView]
+ accessibilityAttributeValue:NSAccessibilityParentAttribute];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)hasRepresentedView {
+ return YES;
+}
+
+// this will return our parallell NSView. see mozDocAccessible.h
+- (id)representedView {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(mParallelView || gfxPlatform::IsHeadless(),
+ "root accessible does not have a native parallel view.");
+
+ return mParallelView;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+- (BOOL)isRoot {
+ return YES;
+}
+
+@end
diff --git a/accessible/mac/mozSelectableElements.h b/accessible/mac/mozSelectableElements.h
new file mode 100644
index 0000000000..77c8c30aed
--- /dev/null
+++ b/accessible/mac/mozSelectableElements.h
@@ -0,0 +1,128 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+#import "mozAccessible.h"
+
+@interface mozSelectableAccessible : mozAccessible
+
+- (NSArray*)selectableChildren;
+
+// override
+- (void)moxSetSelectedChildren:(NSArray*)selectedChildren;
+
+// override
+- (NSArray*)moxSelectedChildren;
+
+@end
+
+@interface mozSelectableChildAccessible : mozAccessible
+
+// override
+- (NSNumber*)moxSelected;
+
+// override
+- (void)moxSetSelected:(NSNumber*)selected;
+
+@end
+
+@interface mozTabGroupAccessible : mozSelectableAccessible
+
+// override
+- (NSArray*)moxTabs;
+
+// override
+- (NSArray*)moxContents;
+
+// override
+- (id)moxValue;
+
+@end
+
+@interface mozTabAccessible : mozSelectableChildAccessible
+
+// override
+- (NSString*)moxRoleDescription;
+
+// override
+- (id)moxValue;
+
+@end
+
+@interface mozListboxAccessible : mozSelectableAccessible
+
+// override
+- (BOOL)moxIgnoreChild:(mozAccessible*)child;
+
+// override
+- (BOOL)disableChild:(mozAccessible*)child;
+
+// override
+- (NSString*)moxOrientation;
+
+@end
+
+@interface mozOptionAccessible : mozSelectableChildAccessible
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (id)moxValue;
+
+@end
+
+@interface mozMenuAccessible : mozSelectableAccessible {
+ BOOL mIsOpened;
+}
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (NSString*)moxLabel;
+
+// override
+- (NSArray*)moxVisibleChildren;
+
+// override
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent;
+
+// override
+- (id)moxTitleUIElement;
+
+// override
+- (void)moxPostNotification:(NSString*)notification;
+
+// override
+- (void)expire;
+
+- (BOOL)isOpened;
+
+@end
+
+@interface mozMenuItemAccessible : mozSelectableChildAccessible
+
+// override
+- (NSString*)moxLabel;
+
+// override
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent;
+
+// override
+- (NSString*)moxMenuItemMarkChar;
+
+// override
+- (NSNumber*)moxSelected;
+
+// override
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+// override
+- (void)moxPerformPress;
+
+@end
diff --git a/accessible/mac/mozSelectableElements.mm b/accessible/mac/mozSelectableElements.mm
new file mode 100644
index 0000000000..348221ef1d
--- /dev/null
+++ b/accessible/mac/mozSelectableElements.mm
@@ -0,0 +1,330 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozSelectableElements.h"
+#import "MOXWebAreaAccessible.h"
+#import "MacUtils.h"
+#include "LocalAccessible-inl.h"
+#include "nsCocoaUtils.h"
+
+using namespace mozilla::a11y;
+
+@implementation mozSelectableAccessible
+
+/**
+ * Return the mozAccessibles that are selectable.
+ */
+- (NSArray*)selectableChildren {
+ NSArray* toFilter;
+ if ([self isKindOfClass:[mozMenuAccessible class]]) {
+ // If we are a menu, our children are only selectable if they are visible
+ // so we filter this array instead of our unignored children list, which may
+ // contain invisible items.
+ toFilter = [static_cast<mozMenuAccessible*>(self) moxVisibleChildren];
+ } else {
+ toFilter = [self moxUnignoredChildren];
+ }
+ return [toFilter
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
+ mozAccessible* child,
+ NSDictionary* bindings) {
+ return [child isKindOfClass:[mozSelectableChildAccessible class]];
+ }]];
+}
+
+- (void)moxSetSelectedChildren:(NSArray*)selectedChildren {
+ for (id child in [self selectableChildren]) {
+ BOOL selected =
+ [selectedChildren indexOfObjectIdenticalTo:child] != NSNotFound;
+ [child moxSetSelected:@(selected)];
+ }
+}
+
+/**
+ * Return the mozAccessibles that are actually selected.
+ */
+- (NSArray*)moxSelectedChildren {
+ return [[self selectableChildren]
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
+ mozAccessible* child,
+ NSDictionary* bindings) {
+ // Return mozSelectableChildAccessibles that have are selected (truthy
+ // value).
+ return [[(mozSelectableChildAccessible*)child moxSelected] boolValue];
+ }]];
+}
+
+@end
+
+@implementation mozSelectableChildAccessible
+
+- (NSNumber*)moxSelected {
+ return @([self stateWithMask:states::SELECTED] != 0);
+}
+
+- (void)moxSetSelected:(NSNumber*)selected {
+ // Get SELECTABLE and UNAVAILABLE state.
+ uint64_t state =
+ [self stateWithMask:(states::SELECTABLE | states::UNAVAILABLE)];
+ if ((state & states::SELECTABLE) == 0 || (state & states::UNAVAILABLE) != 0) {
+ // The object is either not selectable or is unavailable. Don't do anything.
+ return;
+ }
+
+ mGeckoAccessible->SetSelected([selected boolValue]);
+}
+
+@end
+
+@implementation mozTabGroupAccessible
+
+- (NSArray*)moxTabs {
+ return [self selectableChildren];
+}
+
+- (NSArray*)moxContents {
+ return [self moxUnignoredChildren];
+}
+
+- (id)moxValue {
+ // The value of a tab group is its selected child. In the case
+ // of multiple selections this will return the first one.
+ return [[self moxSelectedChildren] firstObject];
+}
+
+@end
+
+@implementation mozTabAccessible
+
+- (NSString*)moxRoleDescription {
+ return utils::LocalizedString(u"tab"_ns);
+}
+
+- (id)moxValue {
+ // Retuens 1 if item is selected, 0 if not.
+ return [self moxSelected];
+}
+
+@end
+
+@implementation mozListboxAccessible
+
+- (BOOL)moxIgnoreChild:(mozAccessible*)child {
+ if (!child || child->mRole == roles::GROUPING) {
+ return YES;
+ }
+
+ return [super moxIgnoreChild:child];
+}
+
+- (BOOL)disableChild:(mozAccessible*)child {
+ return ![child isKindOfClass:[mozSelectableChildAccessible class]];
+}
+
+- (NSString*)moxOrientation {
+ return NSAccessibilityUnknownOrientationValue;
+}
+
+@end
+
+@implementation mozOptionAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+- (id)moxValue {
+ // Swap title and value of option so it behaves more like a AXStaticText.
+ return [super moxTitle];
+}
+
+@end
+
+@implementation mozMenuAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+- (NSString*)moxLabel {
+ return @"";
+}
+
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
+ // This helps us generate the correct moxChildren array for
+ // a sub menu -- that returned array should contain all
+ // menu items, regardless of if they are visible or not.
+ // Because moxChildren does ignore filtering, and because
+ // our base ignore method filters out invisible accessibles,
+ // we override this method.
+ if ([parent isKindOfClass:[MOXWebAreaAccessible class]] ||
+ [parent isKindOfClass:[MOXRootGroup class]]) {
+ // We are a top level menu. Check our visibility the normal way
+ return [super moxIgnoreWithParent:parent];
+ }
+
+ if ([parent isKindOfClass:[mozMenuItemAccessible class]] &&
+ [parent geckoAccessible]->Role() == roles::PARENT_MENUITEM) {
+ // We are a submenu. If our parent menu item is in an open menu
+ // we should not be ignored
+ id grandparent = [parent moxParent];
+ if ([grandparent isKindOfClass:[mozMenuAccessible class]]) {
+ mozMenuAccessible* parentMenu =
+ static_cast<mozMenuAccessible*>(grandparent);
+ return ![parentMenu isOpened];
+ }
+ }
+
+ // Otherwise, we call into our superclass's ignore method
+ // to handle menus that are not submenus
+ return [super moxIgnoreWithParent:parent];
+}
+
+- (NSArray*)moxVisibleChildren {
+ // VO expects us to expose two lists of children on menus: all children
+ // (done in moxUnignoredChildren), and children which are visible (here).
+ // We implement ignoreWithParent for both menus and menu items
+ // to ensure moxUnignoredChildren returns a complete list of children
+ // regardless of visibility, see comments in those methods for additional
+ // info.
+ return [[self moxChildren]
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
+ mozAccessible* child,
+ NSDictionary* bindings) {
+ if (LocalAccessible* acc = [child geckoAccessible]->AsLocal()) {
+ if (acc->IsContent() && acc->GetContent()->IsXULElement()) {
+ return ((acc->VisibilityState() & states::INVISIBLE) == 0);
+ }
+ }
+ return true;
+ }]];
+}
+
+- (id)moxTitleUIElement {
+ id parent = [self moxUnignoredParent];
+ if (parent && [parent isKindOfClass:[mozAccessible class]]) {
+ return parent;
+ }
+
+ return nil;
+}
+
+- (void)moxPostNotification:(NSString*)notification {
+ if ([notification isEqualToString:@"AXMenuOpened"]) {
+ mIsOpened = YES;
+ } else if ([notification isEqualToString:@"AXMenuClosed"]) {
+ mIsOpened = NO;
+ }
+
+ [super moxPostNotification:notification];
+}
+
+- (void)expire {
+ if (mIsOpened) {
+ // VO needs to receive a menu closed event when the menu goes away.
+ // If the menu is being destroyed, send a menu closed event first.
+ [self moxPostNotification:@"AXMenuClosed"];
+ }
+
+ [super expire];
+}
+
+- (BOOL)isOpened {
+ return mIsOpened;
+}
+
+@end
+
+@implementation mozMenuItemAccessible
+
+- (NSString*)moxLabel {
+ return @"";
+}
+
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
+ // This helps us generate the correct moxChildren array for
+ // a mozMenuAccessible; the returned array should contain all
+ // menu items, regardless of if they are visible or not.
+ // Because moxChildren does ignore filtering, and because
+ // our base ignore method filters out invisible accessibles,
+ // we override this method.
+ Accessible* parentAcc = [parent geckoAccessible];
+ if (parentAcc) {
+ Accessible* grandparentAcc = parentAcc->Parent();
+ if (mozAccessible* directGrandparent =
+ GetNativeFromGeckoAccessible(grandparentAcc)) {
+ if ([directGrandparent isKindOfClass:[MOXWebAreaAccessible class]]) {
+ return [parent moxIgnoreWithParent:directGrandparent];
+ }
+ }
+ }
+
+ id grandparent = [parent moxParent];
+ if ([grandparent isKindOfClass:[mozMenuItemAccessible class]]) {
+ mozMenuItemAccessible* acc =
+ static_cast<mozMenuItemAccessible*>(grandparent);
+ if ([acc geckoAccessible]->Role() == roles::PARENT_MENUITEM) {
+ mozMenuAccessible* parentMenu = static_cast<mozMenuAccessible*>(parent);
+ // if we are a menu item in a submenu, display only when
+ // parent menu item is open
+ return ![parentMenu isOpened];
+ }
+ }
+
+ // Otherwise, we call into our superclass's method to handle
+ // menuitems that are not within submenus
+ return [super moxIgnoreWithParent:parent];
+}
+
+- (NSString*)moxMenuItemMarkChar {
+ LocalAccessible* acc = mGeckoAccessible->AsLocal();
+ if (acc && acc->IsContent() &&
+ acc->GetContent()->IsXULElement(nsGkAtoms::menuitem)) {
+ // We need to provide a marker character. This is the visible "√" you see
+ // on dropdown menus. In our a11y tree this is a single child text node
+ // of the menu item.
+ // We do this only with XUL menuitems that conform to the native theme, and
+ // not with aria menu items that might have a pseudo element or something.
+ if (acc->ChildCount() == 1 &&
+ acc->LocalFirstChild()->Role() == roles::STATICTEXT) {
+ nsAutoString marker;
+ acc->LocalFirstChild()->Name(marker);
+ if (marker.Length() == 1) {
+ return nsCocoaUtils::ToNSString(marker);
+ }
+ }
+ }
+
+ return nil;
+}
+
+- (NSNumber*)moxSelected {
+ // Our focused state is equivelent to native selected states for menus.
+ return @([self stateWithMask:states::FOCUSED] != 0);
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ switch (eventType) {
+ case nsIAccessibleEvent::EVENT_FOCUS:
+ // Our focused state is equivelent to native selected states for menus.
+ mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
+ [parent moxPostNotification:
+ NSAccessibilitySelectedChildrenChangedNotification];
+ break;
+ }
+
+ [super handleAccessibleEvent:eventType];
+}
+
+- (void)moxPerformPress {
+ [super moxPerformPress];
+ // when a menu item is pressed (chosen), we need to tell
+ // VoiceOver about it, so we send this notification
+ [self moxPostNotification:@"AXMenuItemSelected"];
+}
+
+@end
diff --git a/accessible/mac/mozTableAccessible.h b/accessible/mac/mozTableAccessible.h
new file mode 100644
index 0000000000..09a0c1d5ea
--- /dev/null
+++ b/accessible/mac/mozTableAccessible.h
@@ -0,0 +1,177 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+
+@interface mozColumnContainer : MOXAccessibleBase {
+ uint32_t mIndex;
+ mozAccessible* mParent;
+ NSMutableArray* mChildren;
+}
+
+// override
+- (id)initWithIndex:(uint32_t)aIndex andParent:(mozAccessible*)aParent;
+
+// override
+- (NSString*)moxRole;
+
+// override
+- (NSString*)moxRoleDescription;
+
+// override
+- (mozAccessible*)moxParent;
+
+// override
+- (NSArray*)moxUnignoredChildren;
+
+// override
+- (void)dealloc;
+
+// override
+- (void)expire;
+
+// override
+- (BOOL)isExpired;
+
+- (void)invalidateChildren;
+
+@end
+
+@interface mozTablePartAccessible : mozAccessible
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (NSString*)moxRole;
+
+- (BOOL)isLayoutTablePart;
+
+@end
+
+@interface mozTableAccessible : mozTablePartAccessible {
+ NSMutableArray* mColContainers;
+}
+
+// local override
+- (BOOL)isLayoutTablePart;
+
+- (void)invalidateColumns;
+
+// override
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+// override
+- (void)dealloc;
+
+// override
+- (void)expire;
+
+// override
+- (NSNumber*)moxRowCount;
+
+// override
+- (NSNumber*)moxColumnCount;
+
+// override
+- (NSArray*)moxRows;
+
+// override
+- (NSArray*)moxColumns;
+
+// override
+- (NSArray*)moxUnignoredChildren;
+
+// override
+- (NSArray*)moxColumnHeaderUIElements;
+
+// override
+- (id)moxCellForColumnAndRow:(NSArray*)columnAndRow;
+
+@end
+
+@interface mozTableRowAccessible : mozTablePartAccessible
+
+// override
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+// override
+- (NSNumber*)moxIndex;
+
+@end
+
+@interface mozTableCellAccessible : mozTablePartAccessible
+
+// override
+- (NSValue*)moxRowIndexRange;
+
+// override
+- (NSValue*)moxColumnIndexRange;
+
+// override
+- (NSArray*)moxRowHeaderUIElements;
+
+// override
+- (NSArray*)moxColumnHeaderUIElements;
+
+@end
+
+@interface mozOutlineAccessible : mozAccessible
+
+// local override
+- (BOOL)isLayoutTablePart;
+
+// override
+- (NSArray*)moxRows;
+
+// override
+- (NSArray*)moxColumns;
+
+// override
+- (NSArray*)moxSelectedRows;
+
+// override
+- (NSString*)moxOrientation;
+
+@end
+
+@interface mozOutlineRowAccessible : mozTableRowAccessible
+
+// override
+- (BOOL)isLayoutTablePart;
+
+// override
+- (NSNumber*)moxDisclosing;
+
+// override
+- (void)moxSetDisclosing:(NSNumber*)disclosing;
+
+// override
+- (NSNumber*)moxExpanded;
+
+// override
+- (id)moxDisclosedByRow;
+
+// override
+- (NSNumber*)moxDisclosureLevel;
+
+// override
+- (NSArray*)moxDisclosedRows;
+
+// override
+- (NSNumber*)moxIndex;
+
+// override
+- (NSString*)moxLabel;
+
+// override
+- (id)moxValue;
+
+// override
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled;
+
+@end
diff --git a/accessible/mac/mozTableAccessible.mm b/accessible/mac/mozTableAccessible.mm
new file mode 100644
index 0000000000..a179780a81
--- /dev/null
+++ b/accessible/mac/mozTableAccessible.mm
@@ -0,0 +1,630 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozTableAccessible.h"
+#import "nsCocoaUtils.h"
+#import "MacUtils.h"
+
+#include "AccIterator.h"
+#include "LocalAccessible.h"
+#include "mozilla/a11y/TableAccessible.h"
+#include "mozilla/a11y/TableCellAccessible.h"
+#include "nsAccessibilityService.h"
+#include "nsIAccessiblePivot.h"
+#include "XULTreeAccessible.h"
+#include "Pivot.h"
+#include "nsAccUtils.h"
+#include "Relation.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+@implementation mozColumnContainer
+
+- (id)initWithIndex:(uint32_t)aIndex andParent:(mozAccessible*)aParent {
+ self = [super init];
+ mIndex = aIndex;
+ mParent = aParent;
+ return self;
+}
+
+- (NSString*)moxRole {
+ return NSAccessibilityColumnRole;
+}
+
+- (NSString*)moxRoleDescription {
+ return NSAccessibilityRoleDescription(NSAccessibilityColumnRole, nil);
+}
+
+- (mozAccessible*)moxParent {
+ return mParent;
+}
+
+- (NSArray*)moxUnignoredChildren {
+ if (mChildren) return mChildren;
+
+ mChildren = [[NSMutableArray alloc] init];
+
+ TableAccessible* table = [mParent geckoAccessible]->AsTable();
+ MOZ_ASSERT(table, "Got null table when fetching column children!");
+ uint32_t numRows = table->RowCount();
+
+ for (uint32_t j = 0; j < numRows; j++) {
+ Accessible* cell = table->CellAt(j, mIndex);
+ mozAccessible* nativeCell = cell ? GetNativeFromGeckoAccessible(cell) : nil;
+ if ([nativeCell isAccessibilityElement]) {
+ [mChildren addObject:nativeCell];
+ }
+ }
+
+ return mChildren;
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [self invalidateChildren];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)expire {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [self invalidateChildren];
+
+ mParent = nil;
+
+ [super expire];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (BOOL)isExpired {
+ MOZ_ASSERT((mChildren == nil && mParent == nil) == mIsExpired);
+
+ return [super isExpired];
+}
+
+- (void)invalidateChildren {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ // make room for new children
+ if (mChildren) {
+ [mChildren release];
+ mChildren = nil;
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@end
+
+@implementation mozTablePartAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+- (NSString*)moxRole {
+ return [self isLayoutTablePart] ? NSAccessibilityGroupRole : [super moxRole];
+}
+
+- (BOOL)isLayoutTablePart {
+ mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
+ if ([parent isKindOfClass:[mozTablePartAccessible class]]) {
+ return [(mozTablePartAccessible*)parent isLayoutTablePart];
+ } else if ([parent isKindOfClass:[mozOutlineAccessible class]]) {
+ return [(mozOutlineAccessible*)parent isLayoutTablePart];
+ }
+
+ return NO;
+}
+@end
+
+@implementation mozTableAccessible
+
+- (BOOL)isLayoutTablePart {
+ if (mGeckoAccessible->Role() == roles::TREE_TABLE) {
+ // tree tables are never layout tables, and we shouldn't
+ // query IsProbablyLayoutTable() on them, so we short
+ // circuit here
+ return false;
+ }
+
+ // For LocalAccessible and cached RemoteAccessible, we could use
+ // AsTable()->IsProbablyLayoutTable(). However, if the cache is enabled,
+ // that would build the table cache, which is pointless for layout tables on
+ // Mac because layout tables are AXGroups and do not expose table properties
+ // like AXRows, AXColumns, etc.
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ return acc->AsTable()->IsProbablyLayoutTable();
+ }
+ RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
+ return proxy->TableIsProbablyForLayout();
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ if (eventType == nsIAccessibleEvent::EVENT_REORDER ||
+ eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) {
+ [self invalidateColumns];
+ }
+
+ [super handleAccessibleEvent:eventType];
+}
+
+- (void)dealloc {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+
+ [self invalidateColumns];
+ [super dealloc];
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+- (void)expire {
+ [self invalidateColumns];
+ [super expire];
+}
+
+- (NSNumber*)moxRowCount {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ return @(mGeckoAccessible->AsTable()->RowCount());
+}
+
+- (NSNumber*)moxColumnCount {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ return @(mGeckoAccessible->AsTable()->ColCount());
+}
+
+- (NSArray*)moxRows {
+ // Create a new array with the list of table rows.
+ NSArray* children = [self moxChildren];
+ NSMutableArray* rows = [[[NSMutableArray alloc] init] autorelease];
+ for (mozAccessible* curr : children) {
+ if ([curr isKindOfClass:[mozTableRowAccessible class]]) {
+ [rows addObject:curr];
+ } else if ([[curr moxRole] isEqualToString:@"AXGroup"]) {
+ // Plain thead/tbody elements are removed from the core a11y tree and
+ // replaced with their subtree, but thead/tbody elements with click
+ // handlers are not -- they remain as groups. We need to expose any
+ // rows they contain as rows of the parent table.
+ [rows
+ addObjectsFromArray:[[curr moxChildren]
+ filteredArrayUsingPredicate:
+ [NSPredicate predicateWithBlock:^BOOL(
+ mozAccessible* child,
+ NSDictionary* bindings) {
+ return [child
+ isKindOfClass:[mozTableRowAccessible
+ class]];
+ }]]];
+ }
+ }
+
+ return rows;
+}
+
+- (NSArray*)moxColumns {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (mColContainers) {
+ return mColContainers;
+ }
+
+ mColContainers = [[NSMutableArray alloc] init];
+ uint32_t numCols = 0;
+
+ numCols = mGeckoAccessible->AsTable()->ColCount();
+ for (uint32_t i = 0; i < numCols; i++) {
+ mozColumnContainer* container =
+ [[mozColumnContainer alloc] initWithIndex:i andParent:self];
+ [mColContainers addObject:container];
+ }
+
+ return mColContainers;
+}
+
+- (NSArray*)moxUnignoredChildren {
+ if (![self isLayoutTablePart]) {
+ return [[super moxUnignoredChildren]
+ arrayByAddingObjectsFromArray:[self moxColumns]];
+ }
+
+ return [super moxUnignoredChildren];
+}
+
+- (NSArray*)moxColumnHeaderUIElements {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ uint32_t numCols = 0;
+ TableAccessible* table = nullptr;
+
+ table = mGeckoAccessible->AsTable();
+ numCols = table->ColCount();
+ NSMutableArray* colHeaders =
+ [[[NSMutableArray alloc] initWithCapacity:numCols] autorelease];
+
+ for (uint32_t i = 0; i < numCols; i++) {
+ Accessible* cell = table->CellAt(0, i);
+ if (cell && cell->Role() == roles::COLUMNHEADER) {
+ mozAccessible* colHeader = GetNativeFromGeckoAccessible(cell);
+ [colHeaders addObject:colHeader];
+ }
+ }
+
+ return colHeaders;
+}
+
+- (id)moxCellForColumnAndRow:(NSArray*)columnAndRow {
+ if (columnAndRow == nil || [columnAndRow count] != 2) {
+ return nil;
+ }
+
+ uint32_t col = [[columnAndRow objectAtIndex:0] unsignedIntValue];
+ uint32_t row = [[columnAndRow objectAtIndex:1] unsignedIntValue];
+
+ MOZ_ASSERT(mGeckoAccessible);
+
+ Accessible* cell = mGeckoAccessible->AsTable()->CellAt(row, col);
+ if (!cell) {
+ return nil;
+ }
+
+ return GetNativeFromGeckoAccessible(cell);
+}
+
+- (void)invalidateColumns {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
+ if (mColContainers) {
+ for (mozColumnContainer* col in mColContainers) {
+ [col expire];
+ }
+ [mColContainers release];
+ mColContainers = nil;
+ }
+ NS_OBJC_END_TRY_IGNORE_BLOCK;
+}
+
+@end
+
+@interface mozTableRowAccessible ()
+- (mozTableAccessible*)getTableParent;
+@end
+
+@implementation mozTableRowAccessible
+
+- (mozTableAccessible*)getTableParent {
+ id tableParent = static_cast<mozTableAccessible*>(
+ [self moxFindAncestor:^BOOL(id curr, BOOL* stop) {
+ if ([curr isKindOfClass:[mozOutlineAccessible class]]) {
+ // Outline rows are a kind of table row, so it's possible
+ // we're trying to call getTableParent on an outline row here.
+ // Stop searching.
+ *stop = YES;
+ }
+ return [curr isKindOfClass:[mozTableAccessible class]];
+ }]);
+
+ return [tableParent isKindOfClass:[mozTableAccessible class]] ? tableParent
+ : nil;
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ if (eventType == nsIAccessibleEvent::EVENT_REORDER) {
+ // It is possible for getTableParent to return nil if we're
+ // handling a reorder on an outilne row. Outlines don't have
+ // columns, so there's nothing to do here and this will no-op.
+ [[self getTableParent] invalidateColumns];
+ }
+
+ [super handleAccessibleEvent:eventType];
+}
+
+- (NSNumber*)moxIndex {
+ return @([[[self getTableParent] moxRows] indexOfObjectIdenticalTo:self]);
+}
+
+@end
+
+@implementation mozTableCellAccessible
+
+- (NSValue*)moxRowIndexRange {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ TableCellAccessible* cell = mGeckoAccessible->AsTableCell();
+ return
+ [NSValue valueWithRange:NSMakeRange(cell->RowIdx(), cell->RowExtent())];
+}
+
+- (NSValue*)moxColumnIndexRange {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ TableCellAccessible* cell = mGeckoAccessible->AsTableCell();
+ return
+ [NSValue valueWithRange:NSMakeRange(cell->ColIdx(), cell->ColExtent())];
+}
+
+- (NSArray*)moxRowHeaderUIElements {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ TableCellAccessible* cell = mGeckoAccessible->AsTableCell();
+ AutoTArray<Accessible*, 10> headerCells;
+ if (cell) {
+ cell->RowHeaderCells(&headerCells);
+ }
+ return utils::ConvertToNSArray(headerCells);
+}
+
+- (NSArray*)moxColumnHeaderUIElements {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ TableCellAccessible* cell = mGeckoAccessible->AsTableCell();
+ AutoTArray<Accessible*, 10> headerCells;
+ if (cell) {
+ cell->ColHeaderCells(&headerCells);
+ }
+ return utils::ConvertToNSArray(headerCells);
+}
+
+@end
+
+/**
+ * This rule matches all accessibles with roles::OUTLINEITEM. If
+ * outlines are nested, it ignores the nested subtree and returns
+ * only items which are descendants of the primary outline.
+ */
+class OutlineRule : public PivotRule {
+ public:
+ uint16_t Match(Accessible* aAcc) override {
+ uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;
+
+ if (nsAccUtils::MustPrune(aAcc)) {
+ result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ }
+
+ if (![GetNativeFromGeckoAccessible(aAcc) isAccessibilityElement]) {
+ return result;
+ }
+
+ if (aAcc->Role() == roles::OUTLINE) {
+ // if the accessible is an outline, we ignore all children
+ result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
+ } else if (aAcc->Role() == roles::OUTLINEITEM) {
+ // if the accessible is not an outline item, we match here
+ result |= nsIAccessibleTraversalRule::FILTER_MATCH;
+ }
+
+ return result;
+ }
+};
+
+@implementation mozOutlineAccessible
+
+- (BOOL)isLayoutTablePart {
+ return NO;
+}
+
+- (NSArray*)moxRows {
+ // Create a new array with the list of outline rows. We
+ // use pivot here to do a deep traversal of all rows nested
+ // in this outline, not just those which are direct
+ // children, since that's what VO expects.
+ NSMutableArray* allRows = [[[NSMutableArray alloc] init] autorelease];
+ Pivot p = Pivot(mGeckoAccessible);
+ OutlineRule rule = OutlineRule();
+ Accessible* firstChild = mGeckoAccessible->FirstChild();
+ Accessible* match = p.Next(firstChild, rule, true);
+ while (match) {
+ [allRows addObject:GetNativeFromGeckoAccessible(match)];
+ match = p.Next(match, rule);
+ }
+ return allRows;
+}
+
+- (NSArray*)moxColumns {
+ if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
+ if (acc->IsContent() && acc->GetContent()->IsXULElement(nsGkAtoms::tree)) {
+ XULTreeAccessible* treeAcc = (XULTreeAccessible*)acc;
+ NSMutableArray* cols = [[[NSMutableArray alloc] init] autorelease];
+ // XUL trees store their columns in a group at the tree's first
+ // child. Here, we iterate over that group to get each column's
+ // native accessible and add it to our col array.
+ LocalAccessible* treeColumns = treeAcc->LocalChildAt(0);
+ if (treeColumns) {
+ uint32_t colCount = treeColumns->ChildCount();
+ for (uint32_t i = 0; i < colCount; i++) {
+ LocalAccessible* treeColumnItem = treeColumns->LocalChildAt(i);
+ [cols addObject:GetNativeFromGeckoAccessible(treeColumnItem)];
+ }
+ return cols;
+ }
+ }
+ }
+ // Webkit says we shouldn't expose any cols for aria-tree
+ // so we return an empty array here
+ return @[];
+}
+
+- (NSArray*)moxSelectedRows {
+ NSMutableArray* selectedRows = [[[NSMutableArray alloc] init] autorelease];
+ NSArray* allRows = [self moxRows];
+ for (mozAccessible* row in allRows) {
+ if ([row stateWithMask:states::SELECTED] != 0) {
+ [selectedRows addObject:row];
+ }
+ }
+
+ return selectedRows;
+}
+
+- (NSString*)moxOrientation {
+ return NSAccessibilityVerticalOrientationValue;
+}
+
+@end
+
+@implementation mozOutlineRowAccessible
+
+- (BOOL)isLayoutTablePart {
+ return NO;
+}
+
+- (NSNumber*)moxDisclosing {
+ return @([self stateWithMask:states::EXPANDED] != 0);
+}
+
+- (void)moxSetDisclosing:(NSNumber*)disclosing {
+ // VoiceOver requires this to be settable, but doesn't
+ // require it actually affect our disclosing state.
+ // We expose the attr as settable with this method
+ // but do nothing to actually set it.
+ return;
+}
+
+- (NSNumber*)moxExpanded {
+ return @([self stateWithMask:states::EXPANDED] != 0);
+}
+
+- (id)moxDisclosedByRow {
+ // According to webkit: this attr corresponds to the row
+ // that contains this row. It should be the same as the
+ // first parent that is a treeitem. If the parent is the tree
+ // itself, this should be nil. This is tricky for xul trees because
+ // all rows are direct children of the outline; they use
+ // relations to expose their heirarchy structure.
+
+ // first we check the relations to see if we're in a xul tree
+ // with weird row semantics
+ NSArray<mozAccessible*>* disclosingRows =
+ [self getRelationsByType:RelationType::NODE_CHILD_OF];
+ mozAccessible* disclosingRow = [disclosingRows firstObject];
+
+ if (disclosingRow) {
+ // if we find a row from our relation check,
+ // verify it isn't the outline itself and return
+ // appropriately
+ if ([[disclosingRow moxRole] isEqualToString:@"AXOutline"]) {
+ return nil;
+ }
+
+ return disclosingRow;
+ }
+
+ mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
+ // otherwise, its likely we're in an aria tree, so we can use
+ // these role and subrole checks
+ if ([[parent moxRole] isEqualToString:@"AXOutline"]) {
+ return nil;
+ }
+
+ if ([[parent moxSubrole] isEqualToString:@"AXOutlineRow"]) {
+ disclosingRow = parent;
+ }
+
+ return nil;
+}
+
+- (NSNumber*)moxDisclosureLevel {
+ GroupPos groupPos = mGeckoAccessible->GroupPosition();
+
+ // mac expects 0-indexed levels, but groupPos.level is 1-indexed
+ // so we subtract 1 here for levels above 0
+ return groupPos.level > 0 ? @(groupPos.level - 1) : @(groupPos.level);
+}
+
+- (NSArray*)moxDisclosedRows {
+ // According to webkit: this attr corresponds to the rows
+ // that are considered inside this row. Again, this is weird for
+ // xul trees so we have to use relations first and then fall-back
+ // to the children filter for non-xul outlines.
+
+ // first we check the relations to see if we're in a xul tree
+ // with weird row semantics
+ if (NSArray* disclosedRows =
+ [self getRelationsByType:RelationType::NODE_PARENT_OF]) {
+ // if we find rows from our relation check, return them here
+ return disclosedRows;
+ }
+
+ // otherwise, filter our children for outline rows
+ return [[self moxChildren]
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
+ mozAccessible* child,
+ NSDictionary* bindings) {
+ return [child isKindOfClass:[mozOutlineRowAccessible class]];
+ }]];
+}
+
+- (NSNumber*)moxIndex {
+ id<MOXAccessible> outline =
+ [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
+ return [[moxAcc moxRole] isEqualToString:@"AXOutline"];
+ }];
+
+ NSUInteger index = [[outline moxRows] indexOfObjectIdenticalTo:self];
+ return index == NSNotFound ? nil : @(index);
+}
+
+- (NSString*)moxLabel {
+ nsAutoString title;
+ mGeckoAccessible->Name(title);
+
+ // XXX: When parsing outlines built with ul/lu's, we
+ // include the bullet in this description even
+ // though webkit doesn't. Not all outlines are built with
+ // ul/lu's so we can't strip the first character here.
+
+ return nsCocoaUtils::ToNSString(title);
+}
+
+- (int)checkedValue {
+ uint64_t state = [self
+ stateWithMask:(states::CHECKABLE | states::CHECKED | states::MIXED)];
+
+ if (state & states::CHECKABLE) {
+ if (state & states::CHECKED) {
+ return kChecked;
+ }
+
+ if (state & states::MIXED) {
+ return kMixed;
+ }
+
+ return kUnchecked;
+ }
+
+ return kUncheckable;
+}
+
+- (id)moxValue {
+ int checkedValue = [self checkedValue];
+ return checkedValue >= 0 ? @(checkedValue) : nil;
+}
+
+- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
+ [super stateChanged:state isEnabled:enabled];
+
+ if (state & states::EXPANDED) {
+ // If the EXPANDED state is updated, fire appropriate events on the
+ // outline row.
+ [self moxPostNotification:(enabled
+ ? NSAccessibilityRowExpandedNotification
+ : NSAccessibilityRowCollapsedNotification)];
+ }
+
+ if (state & (states::CHECKED | states::CHECKABLE | states::MIXED)) {
+ // If the MIXED, CHECKED or CHECKABLE state changes, update the value we
+ // expose for the row, which communicates checked status.
+ [self moxPostNotification:NSAccessibilityValueChangedNotification];
+ }
+}
+
+@end
diff --git a/accessible/mac/mozTextAccessible.h b/accessible/mac/mozTextAccessible.h
new file mode 100644
index 0000000000..b242a2da32
--- /dev/null
+++ b/accessible/mac/mozTextAccessible.h
@@ -0,0 +1,114 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import "mozAccessible.h"
+
+@interface mozTextAccessible : mozAccessible
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (id)moxValue;
+
+// override
+- (id)moxRequired;
+
+// override
+- (NSNumber*)moxInvalid;
+
+// override
+- (NSNumber*)moxInsertionPointLineNumber;
+
+// override
+- (NSString*)moxRole;
+
+// override
+- (NSString*)moxSubrole;
+
+// override
+- (NSNumber*)moxNumberOfCharacters;
+
+// override
+- (NSString*)moxSelectedText;
+
+// override
+- (NSValue*)moxSelectedTextRange;
+
+// override
+- (NSValue*)moxVisibleCharacterRange;
+
+// override
+- (BOOL)moxBlockSelector:(SEL)selector;
+
+// override
+- (void)moxSetValue:(id)value;
+
+// override
+- (void)moxSetSelectedText:(NSString*)text;
+
+// override
+- (void)moxSetSelectedTextRange:(NSValue*)range;
+
+// override
+- (void)moxSetVisibleCharacterRange:(NSValue*)range;
+
+// override
+- (NSString*)moxStringForRange:(NSValue*)range;
+
+// override
+- (NSAttributedString*)moxAttributedStringForRange:(NSValue*)range;
+
+// override
+- (NSValue*)moxRangeForLine:(NSNumber*)line;
+
+// override
+- (NSNumber*)moxLineForIndex:(NSNumber*)index;
+
+// override
+- (NSValue*)moxBoundsForRange:(NSValue*)range;
+
+#pragma mark - mozAccessible
+
+// override
+- (void)handleAccessibleTextChangeEvent:(NSString*)change
+ inserted:(BOOL)isInserted
+ inContainer:(mozilla::a11y::Accessible*)container
+ at:(int32_t)start;
+
+// override
+- (void)handleAccessibleEvent:(uint32_t)eventType;
+
+@end
+
+@interface mozTextLeafAccessible : mozAccessible
+
+// override
+- (BOOL)moxBlockSelector:(SEL)selector;
+
+// override
+- (NSString*)moxValue;
+
+// override
+- (NSString*)moxTitle;
+
+// override
+- (NSString*)moxLabel;
+
+// override
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent;
+
+// override
+- (NSString*)moxStringForRange:(NSValue*)range;
+
+// override
+- (NSAttributedString*)moxAttributedStringForRange:(NSValue*)range;
+
+// override
+- (NSValue*)moxBoundsForRange:(NSValue*)range;
+
+@end
diff --git a/accessible/mac/mozTextAccessible.mm b/accessible/mac/mozTextAccessible.mm
new file mode 100644
index 0000000000..4993e220d2
--- /dev/null
+++ b/accessible/mac/mozTextAccessible.mm
@@ -0,0 +1,423 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "HyperTextAccessible-inl.h"
+#include "LocalAccessible-inl.h"
+#include "mozilla/a11y/PDocAccessible.h"
+#include "nsCocoaUtils.h"
+#include "nsObjCExceptions.h"
+#include "TextLeafAccessible.h"
+
+#import "mozTextAccessible.h"
+#import "GeckoTextMarker.h"
+#import "MOXTextMarkerDelegate.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+inline bool ToNSRange(id aValue, NSRange* aRange) {
+ MOZ_ASSERT(aRange, "aRange is nil");
+
+ if ([aValue isKindOfClass:[NSValue class]] &&
+ strcmp([(NSValue*)aValue objCType], @encode(NSRange)) == 0) {
+ *aRange = [aValue rangeValue];
+ return true;
+ }
+
+ return false;
+}
+
+inline NSString* ToNSString(id aValue) {
+ if ([aValue isKindOfClass:[NSString class]]) {
+ return aValue;
+ }
+
+ return nil;
+}
+
+@interface mozTextAccessible ()
+- (long)textLength;
+- (BOOL)isReadOnly;
+- (NSString*)text;
+- (GeckoTextMarkerRange)selection;
+- (GeckoTextMarkerRange)textMarkerRangeFromRange:(NSValue*)range;
+@end
+
+@implementation mozTextAccessible
+
+- (NSString*)moxTitle {
+ return @"";
+}
+
+- (id)moxValue {
+ // Apple's SpeechSynthesisServer expects AXValue to return an AXStaticText
+ // object's AXSelectedText attribute. See bug 674612 for details.
+ // Also if there is no selected text, we return the full text.
+ // See bug 369710 for details.
+ if ([[self moxRole] isEqualToString:NSAccessibilityStaticTextRole]) {
+ NSString* selectedText = [self moxSelectedText];
+ return (selectedText && [selectedText length]) ? selectedText : [self text];
+ }
+
+ return [self text];
+}
+
+- (id)moxRequired {
+ return @([self stateWithMask:states::REQUIRED] != 0);
+}
+
+- (NSString*)moxInvalid {
+ if ([self stateWithMask:states::INVALID] != 0) {
+ // If the attribute exists, it has one of four values: true, false,
+ // grammar, or spelling. We query the attribute value here in order
+ // to find the correct string to return.
+ RefPtr<AccAttributes> attributes;
+ HyperTextAccessibleBase* text = mGeckoAccessible->AsHyperTextBase();
+ if (text && mGeckoAccessible->IsTextRole()) {
+ attributes = text->DefaultTextAttributes();
+ }
+
+ nsAutoString invalidStr;
+ if (!attributes ||
+ !attributes->GetAttribute(nsGkAtoms::invalid, invalidStr)) {
+ return @"true";
+ }
+ return nsCocoaUtils::ToNSString(invalidStr);
+ }
+
+ // If the flag is not set, we return false.
+ return @"false";
+}
+
+- (NSNumber*)moxInsertionPointLineNumber {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ int32_t lineNumber = -1;
+ if (HyperTextAccessibleBase* textAcc = mGeckoAccessible->AsHyperTextBase()) {
+ lineNumber = textAcc->CaretLineNumber() - 1;
+ }
+
+ return (lineNumber >= 0) ? [NSNumber numberWithInt:lineNumber] : nil;
+}
+
+- (NSString*)moxRole {
+ if ([self stateWithMask:states::MULTI_LINE]) {
+ return NSAccessibilityTextAreaRole;
+ }
+
+ return [super moxRole];
+}
+
+- (NSString*)moxSubrole {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ if (mRole == roles::PASSWORD_TEXT) {
+ return NSAccessibilitySecureTextFieldSubrole;
+ }
+
+ if (mRole == roles::ENTRY && mGeckoAccessible->IsSearchbox()) {
+ return @"AXSearchField";
+ }
+
+ return nil;
+}
+
+- (NSNumber*)moxNumberOfCharacters {
+ return @([self textLength]);
+}
+
+- (NSString*)moxSelectedText {
+ GeckoTextMarkerRange selection = [self selection];
+ if (!selection.IsValid()) {
+ return nil;
+ }
+
+ return selection.Text();
+}
+
+- (NSValue*)moxSelectedTextRange {
+ GeckoTextMarkerRange selection = [self selection];
+ if (!selection.IsValid()) {
+ return nil;
+ }
+
+ GeckoTextMarkerRange fromStartToSelection(
+ GeckoTextMarker(mGeckoAccessible, 0), selection.Start());
+
+ return [NSValue valueWithRange:NSMakeRange(fromStartToSelection.Length(),
+ selection.Length())];
+}
+
+- (NSValue*)moxVisibleCharacterRange {
+ // XXX this won't work with Textarea and such as we actually don't give
+ // the visible character range.
+ return [NSValue valueWithRange:NSMakeRange(0, [self textLength])];
+}
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ if (selector == @selector(moxSetValue:) && [self isReadOnly]) {
+ return YES;
+ }
+
+ return [super moxBlockSelector:selector];
+}
+
+- (void)moxSetValue:(id)value {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ nsString text;
+ nsCocoaUtils::GetStringForNSString(value, text);
+ if (HyperTextAccessibleBase* textAcc = mGeckoAccessible->AsHyperTextBase()) {
+ textAcc->ReplaceText(text);
+ }
+}
+
+- (void)moxSetSelectedText:(NSString*)selectedText {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ NSString* stringValue = ToNSString(selectedText);
+ if (!stringValue) {
+ return;
+ }
+
+ HyperTextAccessibleBase* textAcc = mGeckoAccessible->AsHyperTextBase();
+ if (!textAcc) {
+ return;
+ }
+ int32_t start = 0, end = 0;
+ textAcc->SelectionBoundsAt(0, &start, &end);
+ nsString text;
+ nsCocoaUtils::GetStringForNSString(stringValue, text);
+ textAcc->SelectionBoundsAt(0, &start, &end);
+ textAcc->DeleteText(start, end - start);
+ textAcc->InsertText(text, start);
+}
+
+- (void)moxSetSelectedTextRange:(NSValue*)selectedTextRange {
+ GeckoTextMarkerRange markerRange =
+ [self textMarkerRangeFromRange:selectedTextRange];
+
+ if (markerRange.IsValid()) {
+ markerRange.Select();
+ }
+}
+
+- (void)moxSetVisibleCharacterRange:(NSValue*)visibleCharacterRange {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ NSRange range;
+ if (!ToNSRange(visibleCharacterRange, &range)) {
+ return;
+ }
+
+ if (HyperTextAccessibleBase* textAcc = mGeckoAccessible->AsHyperTextBase()) {
+ textAcc->ScrollSubstringTo(range.location, range.location + range.length,
+ nsIAccessibleScrollType::SCROLL_TYPE_TOP_EDGE);
+ }
+}
+
+- (NSString*)moxStringForRange:(NSValue*)range {
+ GeckoTextMarkerRange markerRange = [self textMarkerRangeFromRange:range];
+
+ if (!markerRange.IsValid()) {
+ return nil;
+ }
+
+ return markerRange.Text();
+}
+
+- (NSAttributedString*)moxAttributedStringForRange:(NSValue*)range {
+ GeckoTextMarkerRange markerRange = [self textMarkerRangeFromRange:range];
+
+ if (!markerRange.IsValid()) {
+ return nil;
+ }
+
+ return markerRange.AttributedText();
+}
+
+- (NSValue*)moxRangeForLine:(NSNumber*)line {
+ // XXX: actually get the integer value for the line #
+ return [NSValue valueWithRange:NSMakeRange(0, [self textLength])];
+}
+
+- (NSNumber*)moxLineForIndex:(NSNumber*)index {
+ // XXX: actually return the line #
+ return @0;
+}
+
+- (NSValue*)moxBoundsForRange:(NSValue*)range {
+ GeckoTextMarkerRange markerRange = [self textMarkerRangeFromRange:range];
+
+ if (!markerRange.IsValid()) {
+ return nil;
+ }
+
+ return markerRange.Bounds();
+}
+
+#pragma mark - mozAccessible
+
+- (void)handleAccessibleTextChangeEvent:(NSString*)change
+ inserted:(BOOL)isInserted
+ inContainer:(Accessible*)container
+ at:(int32_t)start {
+ GeckoTextMarker startMarker(container, start);
+ NSDictionary* userInfo = @{
+ @"AXTextChangeElement" : self,
+ @"AXTextStateChangeType" : @(AXTextStateChangeTypeEdit),
+ @"AXTextChangeValues" : @[ @{
+ @"AXTextChangeValue" : (change ? change : @""),
+ @"AXTextChangeValueStartMarker" :
+ (__bridge id)startMarker.CreateAXTextMarker(),
+ @"AXTextEditType" : isInserted ? @(AXTextEditTypeTyping)
+ : @(AXTextEditTypeDelete)
+ } ]
+ };
+
+ mozAccessible* webArea = [self topWebArea];
+ [webArea moxPostNotification:NSAccessibilityValueChangedNotification
+ withUserInfo:userInfo];
+ [self moxPostNotification:NSAccessibilityValueChangedNotification
+ withUserInfo:userInfo];
+
+ [self moxPostNotification:NSAccessibilityValueChangedNotification];
+}
+
+- (void)handleAccessibleEvent:(uint32_t)eventType {
+ switch (eventType) {
+ default:
+ [super handleAccessibleEvent:eventType];
+ break;
+ }
+}
+
+#pragma mark -
+
+- (long)textLength {
+ return [[self text] length];
+}
+
+- (BOOL)isReadOnly {
+ return [self stateWithMask:states::EDITABLE] == 0;
+}
+
+- (NSString*)text {
+ // A password text field returns an empty value
+ if (mRole == roles::PASSWORD_TEXT) {
+ return @"";
+ }
+
+ id<MOXTextMarkerSupport> delegate = [self moxTextMarkerDelegate];
+ return [delegate
+ moxStringForTextMarkerRange:[delegate
+ moxTextMarkerRangeForUIElement:self]];
+}
+
+- (GeckoTextMarkerRange)selection {
+ MOZ_ASSERT(mGeckoAccessible);
+
+ id<MOXTextMarkerSupport> delegate = [self moxTextMarkerDelegate];
+ GeckoTextMarkerRange selection =
+ [static_cast<MOXTextMarkerDelegate*>(delegate) selection];
+
+ if (!selection.IsValid() || !selection.Crop(mGeckoAccessible)) {
+ // The selection is not in this accessible. Return invalid range.
+ return GeckoTextMarkerRange();
+ }
+
+ return selection;
+}
+
+- (GeckoTextMarkerRange)textMarkerRangeFromRange:(NSValue*)range {
+ NSRange r = [range rangeValue];
+
+ GeckoTextMarker startMarker =
+ GeckoTextMarker::MarkerFromIndex(mGeckoAccessible, r.location);
+
+ GeckoTextMarker endMarker =
+ GeckoTextMarker::MarkerFromIndex(mGeckoAccessible, r.location + r.length);
+
+ return GeckoTextMarkerRange(startMarker, endMarker);
+}
+
+@end
+
+@implementation mozTextLeafAccessible
+
+- (BOOL)moxBlockSelector:(SEL)selector {
+ if (selector == @selector(moxChildren) || selector == @selector
+ (moxTitleUIElement)) {
+ return YES;
+ }
+
+ return [super moxBlockSelector:selector];
+}
+
+- (NSString*)moxValue {
+ NSString* val = [super moxTitle];
+ return [val length] ? val : nil;
+}
+
+- (NSString*)moxTitle {
+ return nil;
+}
+
+- (NSString*)moxLabel {
+ return nil;
+}
+
+- (BOOL)moxIgnoreWithParent:(mozAccessible*)parent {
+ // Don't render text nodes that are completely empty
+ // or those that should be ignored based on our
+ // standard ignore rules
+ return [self moxValue] == nil || [super moxIgnoreWithParent:parent];
+}
+
+static GeckoTextMarkerRange TextMarkerSubrange(Accessible* aAccessible,
+ NSValue* aRange) {
+ GeckoTextMarkerRange textMarkerRange(aAccessible);
+ GeckoTextMarker start = textMarkerRange.Start();
+ GeckoTextMarker end = textMarkerRange.End();
+
+ NSRange r = [aRange rangeValue];
+ start.Offset() += r.location;
+ end.Offset() = start.Offset() + r.length;
+
+ textMarkerRange = GeckoTextMarkerRange(start, end);
+ // Crop range to accessible
+ textMarkerRange.Crop(aAccessible);
+
+ return textMarkerRange;
+}
+
+- (NSString*)moxStringForRange:(NSValue*)range {
+ MOZ_ASSERT(mGeckoAccessible);
+ GeckoTextMarkerRange textMarkerRange =
+ TextMarkerSubrange(mGeckoAccessible, range);
+
+ return textMarkerRange.Text();
+}
+
+- (NSAttributedString*)moxAttributedStringForRange:(NSValue*)range {
+ MOZ_ASSERT(mGeckoAccessible);
+ GeckoTextMarkerRange textMarkerRange =
+ TextMarkerSubrange(mGeckoAccessible, range);
+
+ return textMarkerRange.AttributedText();
+}
+
+- (NSValue*)moxBoundsForRange:(NSValue*)range {
+ MOZ_ASSERT(mGeckoAccessible);
+ GeckoTextMarkerRange textMarkerRange =
+ TextMarkerSubrange(mGeckoAccessible, range);
+
+ return textMarkerRange.Bounds();
+}
+
+@end
diff --git a/accessible/moz.build b/accessible/moz.build
new file mode 100644
index 0000000000..ed05b5b50e
--- /dev/null
+++ b/accessible/moz.build
@@ -0,0 +1,62 @@
+# -*- 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/.
+
+toolkit = CONFIG["MOZ_WIDGET_TOOLKIT"]
+
+if toolkit == "gtk":
+ DIRS += ["atk"]
+elif toolkit == "windows":
+ DIRS += ["windows"]
+elif toolkit == "cocoa":
+ DIRS += ["mac"]
+elif toolkit == "android":
+ DIRS += ["android"]
+else:
+ DIRS += ["other"]
+
+DIRS += [
+ "aom",
+ "base",
+ "basetypes",
+ "generic",
+ "html",
+ "interfaces",
+ "ipc",
+ "xpcom",
+ "xul",
+]
+
+TEST_DIRS += ["tests/mochitest"]
+
+BROWSER_CHROME_MANIFESTS += [
+ "tests/browser/atk/browser.toml",
+ "tests/browser/bounds/browser.toml",
+ "tests/browser/browser.toml",
+ "tests/browser/e10s/browser.toml",
+ "tests/browser/events/browser.toml",
+ "tests/browser/fission/browser.toml",
+ "tests/browser/general/browser.toml",
+ "tests/browser/hittest/browser.toml",
+ "tests/browser/mac/browser.toml",
+ "tests/browser/pivot/browser.toml",
+ "tests/browser/role/browser.toml",
+ "tests/browser/scroll/browser.toml",
+ "tests/browser/selectable/browser.toml",
+ "tests/browser/states/browser.toml",
+ "tests/browser/telemetry/browser.toml",
+ "tests/browser/text/browser.toml",
+ "tests/browser/tree/browser.toml",
+ "tests/browser/windows/ia2/browser.toml",
+ "tests/browser/windows/uia/browser.toml",
+]
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Disability Access APIs")
+
+SPHINX_TREES["/accessible"] = "docs"
+
+with Files("docs/**"):
+ SCHEDULES.exclusive = ["docs"]
diff --git a/accessible/other/AccessibleWrap.cpp b/accessible/other/AccessibleWrap.cpp
new file mode 100644
index 0000000000..81cd520de7
--- /dev/null
+++ b/accessible/other/AccessibleWrap.cpp
@@ -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/. */
+
+#include "AccessibleWrap.h"
+
+using namespace mozilla::a11y;
+
+//-----------------------------------------------------
+// construction
+//-----------------------------------------------------
+AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
+ : LocalAccessible(aContent, aDoc) {}
+
+//-----------------------------------------------------
+// destruction
+//-----------------------------------------------------
+AccessibleWrap::~AccessibleWrap() {}
diff --git a/accessible/other/AccessibleWrap.h b/accessible/other/AccessibleWrap.h
new file mode 100644
index 0000000000..c5a097c0c1
--- /dev/null
+++ b/accessible/other/AccessibleWrap.h
@@ -0,0 +1,28 @@
+/* -*- 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/. */
+
+/* For documentation of the accessibility architecture,
+ * see http://lxr.mozilla.org/seamonkey/source/accessible/accessible-docs.html
+ */
+
+#ifndef mozilla_a11y_AccessibleWrap_h_
+#define mozilla_a11y_AccessibleWrap_h_
+
+#include "nsCOMPtr.h"
+#include "LocalAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+class AccessibleWrap : public LocalAccessible {
+ public: // construction, destruction
+ AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc);
+ virtual ~AccessibleWrap();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/other/ApplicationAccessibleWrap.h b/accessible/other/ApplicationAccessibleWrap.h
new file mode 100644
index 0000000000..89b07916c9
--- /dev/null
+++ b/accessible/other/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/other/DocAccessibleWrap.h b/accessible/other/DocAccessibleWrap.h
new file mode 100644
index 0000000000..609cc8de18
--- /dev/null
+++ b/accessible/other/DocAccessibleWrap.h
@@ -0,0 +1,23 @@
+/* -*- 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/. */
+
+/* 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 {
+namespace a11y {
+
+typedef DocAccessible DocAccessibleWrap;
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/other/Platform.cpp b/accessible/other/Platform.cpp
new file mode 100644
index 0000000000..038a7a6a8c
--- /dev/null
+++ b/accessible/other/Platform.cpp
@@ -0,0 +1,38 @@
+/* -*- 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"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+void a11y::PlatformInit() {}
+
+void a11y::PlatformShutdown() {}
+
+void a11y::ProxyCreated(RemoteAccessible*) {}
+
+void a11y::ProxyDestroyed(RemoteAccessible*) {}
+
+void a11y::PlatformEvent(Accessible*, uint32_t) {}
+
+void a11y::PlatformStateChangeEvent(Accessible*, uint64_t, bool) {}
+
+void a11y::PlatformFocusEvent(Accessible* aTarget,
+ const LayoutDeviceIntRect& aCaretRect) {}
+
+void a11y::PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset,
+ bool aIsSelectionCollapsed,
+ int32_t aGranularity,
+ const LayoutDeviceIntRect& aCaretRect,
+ bool aFromUser) {}
+
+void a11y::PlatformTextChangeEvent(Accessible*, const nsAString&, int32_t,
+ uint32_t, bool, bool) {}
+
+void a11y::PlatformShowHideEvent(Accessible*, Accessible*, bool, bool) {}
+
+void a11y::PlatformSelectionEvent(Accessible*, Accessible*, uint32_t) {}
diff --git a/accessible/other/RootAccessibleWrap.h b/accessible/other/RootAccessibleWrap.h
new file mode 100644
index 0000000000..395e9aa212
--- /dev/null
+++ b/accessible/other/RootAccessibleWrap.h
@@ -0,0 +1,23 @@
+/* -*- 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/. */
+
+/* For documentation of the accessibility architecture,
+ * see http://lxr.mozilla.org/seamonkey/source/accessible/accessible-docs.html
+ */
+
+#ifndef mozilla_a11y_RootAccessibleWrap_h__
+#define mozilla_a11y_RootAccessibleWrap_h__
+
+#include "RootAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+typedef RootAccessible RootAccessibleWrap;
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/other/moz.build b/accessible/other/moz.build
new file mode 100644
index 0000000000..728fff5783
--- /dev/null
+++ b/accessible/other/moz.build
@@ -0,0 +1,23 @@
+# -*- 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",
+]
+
+SOURCES += [
+ "AccessibleWrap.cpp",
+ "Platform.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/generic",
+ "/accessible/html",
+ "/accessible/xul",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/accessible/tests/browser/.eslintrc.js b/accessible/tests/browser/.eslintrc.js
new file mode 100644
index 0000000000..528797cb91
--- /dev/null
+++ b/accessible/tests/browser/.eslintrc.js
@@ -0,0 +1,28 @@
+"use strict";
+
+module.exports = {
+ rules: {
+ "mozilla/no-aArgs": "error",
+ "mozilla/reject-importGlobalProperties": ["error", "everything"],
+ "mozilla/var-only-at-top-level": "error",
+
+ "block-scoped-var": "error",
+ camelcase: ["error", { properties: "never" }],
+ complexity: ["error", 20],
+
+ "handle-callback-err": ["error", "er"],
+ "max-nested-callbacks": ["error", 4],
+ "new-cap": ["error", { capIsNew: false }],
+ "no-fallthrough": "error",
+ "no-multi-str": "error",
+ "no-proto": "error",
+ "no-return-assign": "error",
+ "no-shadow": "error",
+ "no-unused-vars": ["error", { vars: "all", args: "none" }],
+ "one-var": ["error", "never"],
+ radix: "error",
+ strict: ["error", "global"],
+ yoda: "error",
+ "no-undef-init": "error",
+ },
+};
diff --git a/accessible/tests/browser/Common.sys.mjs b/accessible/tests/browser/Common.sys.mjs
new file mode 100644
index 0000000000..466a0d2b99
--- /dev/null
+++ b/accessible/tests/browser/Common.sys.mjs
@@ -0,0 +1,451 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { Assert } from "resource://testing-common/Assert.sys.mjs";
+
+const MAX_TRIM_LENGTH = 100;
+
+export const CommonUtils = {
+ /**
+ * Constant passed to getAccessible to indicate that it shouldn't fail if
+ * there is no accessible.
+ */
+ DONOTFAIL_IF_NO_ACC: 1,
+
+ /**
+ * Constant passed to getAccessible to indicate that it shouldn't fail if it
+ * does not support an interface.
+ */
+ DONOTFAIL_IF_NO_INTERFACE: 2,
+
+ /**
+ * nsIAccessibilityService service.
+ */
+ get accService() {
+ if (!this._accService) {
+ this._accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ }
+
+ return this._accService;
+ },
+
+ clearAccService() {
+ this._accService = null;
+ Cu.forceGC();
+ },
+
+ /**
+ * Adds an observer for an 'a11y-consumers-changed' event.
+ */
+ addAccConsumersChangedObserver() {
+ const deferred = {};
+ this._accConsumersChanged = new Promise(resolve => {
+ deferred.resolve = resolve;
+ });
+ const observe = (subject, topic, data) => {
+ Services.obs.removeObserver(observe, "a11y-consumers-changed");
+ deferred.resolve(JSON.parse(data));
+ };
+ Services.obs.addObserver(observe, "a11y-consumers-changed");
+ },
+
+ /**
+ * Returns a promise that resolves when 'a11y-consumers-changed' event is
+ * fired.
+ *
+ * @return {Promise}
+ * event promise evaluating to event's data
+ */
+ observeAccConsumersChanged() {
+ return this._accConsumersChanged;
+ },
+
+ /**
+ * Adds an observer for an 'a11y-init-or-shutdown' event with a value of "1"
+ * which indicates that an accessibility service is initialized in the current
+ * process.
+ */
+ addAccServiceInitializedObserver() {
+ const deferred = {};
+ this._accServiceInitialized = new Promise((resolve, reject) => {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+ const observe = (subject, topic, data) => {
+ if (data === "1") {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ deferred.resolve();
+ } else {
+ deferred.reject("Accessibility service is shutdown unexpectedly.");
+ }
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ },
+
+ /**
+ * Returns a promise that resolves when an accessibility service is
+ * initialized in the current process. Otherwise (if the service is shutdown)
+ * the promise is rejected.
+ */
+ observeAccServiceInitialized() {
+ return this._accServiceInitialized;
+ },
+
+ /**
+ * Adds an observer for an 'a11y-init-or-shutdown' event with a value of "0"
+ * which indicates that an accessibility service is shutdown in the current
+ * process.
+ */
+ addAccServiceShutdownObserver() {
+ const deferred = {};
+ this._accServiceShutdown = new Promise((resolve, reject) => {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+ const observe = (subject, topic, data) => {
+ if (data === "0") {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ deferred.resolve();
+ } else {
+ deferred.reject("Accessibility service is initialized unexpectedly.");
+ }
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ },
+
+ /**
+ * Returns a promise that resolves when an accessibility service is shutdown
+ * in the current process. Otherwise (if the service is initialized) the
+ * promise is rejected.
+ */
+ observeAccServiceShutdown() {
+ return this._accServiceShutdown;
+ },
+
+ /**
+ * Extract DOMNode id from an accessible. If the accessible is in the remote
+ * process, DOMNode is not present in parent process. However, if specified by
+ * the author, DOMNode id will be attached to an accessible object.
+ *
+ * @param {nsIAccessible} accessible accessible
+ * @return {String?} DOMNode id if available
+ */
+ getAccessibleDOMNodeID(accessible) {
+ if (accessible instanceof Ci.nsIAccessibleDocument) {
+ // If accessible is a document, trying to find its document body id.
+ try {
+ return accessible.DOMNode.body.id;
+ } catch (e) {
+ /* This only works if accessible is not a proxy. */
+ }
+ }
+ try {
+ return accessible.DOMNode.id;
+ } catch (e) {
+ /* This will fail if DOMNode is in different process. */
+ }
+ try {
+ // When e10s is enabled, accessible will have an "id" property if its
+ // corresponding DOMNode has an id. If accessible is a document, its "id"
+ // property corresponds to the "id" of its body element.
+ return accessible.id;
+ } catch (e) {
+ /* This will fail if accessible is not a proxy. */
+ }
+
+ return null;
+ },
+
+ getObjAddress(obj) {
+ const exp = /native\s*@\s*(0x[a-f0-9]+)/g;
+ const match = exp.exec(obj.toString());
+ if (match) {
+ return match[1];
+ }
+
+ return obj.toString();
+ },
+
+ getNodePrettyName(node) {
+ try {
+ let tag = "";
+ if (node.nodeType == Node.DOCUMENT_NODE) {
+ tag = "document";
+ } else {
+ tag = node.localName;
+ if (node.nodeType == Node.ELEMENT_NODE && node.hasAttribute("id")) {
+ tag += `@id="${node.getAttribute("id")}"`;
+ }
+ }
+
+ return `"${tag} node", address: ${this.getObjAddress(node)}`;
+ } catch (e) {
+ return `" no node info "`;
+ }
+ },
+
+ /**
+ * Convert role to human readable string.
+ */
+ roleToString(role) {
+ return this.accService.getStringRole(role);
+ },
+
+ /**
+ * Shorten a long string if it exceeds MAX_TRIM_LENGTH.
+ *
+ * @param aString the string to shorten.
+ *
+ * @returns the shortened string.
+ */
+ shortenString(str) {
+ if (str.length <= MAX_TRIM_LENGTH) {
+ return str;
+ }
+
+ // Trim the string if its length is > MAX_TRIM_LENGTH characters.
+ const trimOffset = MAX_TRIM_LENGTH / 2;
+
+ return `${str.substring(0, trimOffset - 1)}…${str.substring(
+ str.length - trimOffset,
+ str.length
+ )}`;
+ },
+
+ normalizeAccTreeObj(obj) {
+ const key = Object.keys(obj)[0];
+ const roleName = `ROLE_${key}`;
+ if (roleName in Ci.nsIAccessibleRole) {
+ return {
+ role: Ci.nsIAccessibleRole[roleName],
+ children: obj[key],
+ };
+ }
+
+ return obj;
+ },
+
+ stringifyTree(obj) {
+ let text = this.roleToString(obj.role) + ": [ ";
+ if ("children" in obj) {
+ for (let i = 0; i < obj.children.length; i++) {
+ const c = this.normalizeAccTreeObj(obj.children[i]);
+ text += this.stringifyTree(c);
+ if (i < obj.children.length - 1) {
+ text += ", ";
+ }
+ }
+ }
+
+ return `${text}] `;
+ },
+
+ /**
+ * Return pretty name for identifier, it may be ID, DOM node or accessible.
+ */
+ prettyName(identifier) {
+ if (identifier instanceof Array) {
+ let msg = "";
+ for (let idx = 0; idx < identifier.length; idx++) {
+ if (msg != "") {
+ msg += ", ";
+ }
+
+ msg += this.prettyName(identifier[idx]);
+ }
+ return msg;
+ }
+
+ if (identifier instanceof Ci.nsIAccessible) {
+ const acc = this.getAccessible(identifier);
+ const domID = this.getAccessibleDOMNodeID(acc);
+ let msg = "[";
+ try {
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ if (domID) {
+ msg += `DOM node id: ${domID}, `;
+ }
+ } else {
+ msg += `${this.getNodePrettyName(acc.DOMNode)}, `;
+ }
+ msg += `role: ${this.roleToString(acc.role)}`;
+ if (acc.name) {
+ msg += `, name: "${this.shortenString(acc.name)}"`;
+ }
+ } catch (e) {
+ msg += "defunct";
+ }
+
+ if (acc) {
+ msg += `, address: ${this.getObjAddress(acc)}`;
+ }
+ msg += "]";
+
+ return msg;
+ }
+
+ if (Node.isInstance(identifier)) {
+ return `[ ${this.getNodePrettyName(identifier)} ]`;
+ }
+
+ if (identifier && typeof identifier === "object") {
+ const treeObj = this.normalizeAccTreeObj(identifier);
+ if ("role" in treeObj) {
+ return `{ ${this.stringifyTree(treeObj)} }`;
+ }
+
+ return JSON.stringify(identifier);
+ }
+
+ return ` "${identifier}" `;
+ },
+
+ /**
+ * Return accessible for the given identifier (may be ID attribute or DOM
+ * element or accessible object) or null.
+ *
+ * @param accOrElmOrID
+ * identifier to get an accessible implementing the given interfaces
+ * @param aInterfaces
+ * [optional] the interface or an array interfaces to query it/them
+ * from obtained accessible
+ * @param elmObj
+ * [optional] object to store DOM element which accessible is obtained
+ * for
+ * @param doNotFailIf
+ * [optional] no error for special cases (see DONOTFAIL_IF_NO_ACC,
+ * DONOTFAIL_IF_NO_INTERFACE)
+ * @param doc
+ * [optional] document for when accOrElmOrID is an ID.
+ */
+ getAccessible(accOrElmOrID, interfaces, elmObj, doNotFailIf, doc) {
+ if (!accOrElmOrID) {
+ return null;
+ }
+
+ let elm = null;
+ if (accOrElmOrID instanceof Ci.nsIAccessible) {
+ try {
+ elm = accOrElmOrID.DOMNode;
+ } catch (e) {}
+ } else if (Node.isInstance(accOrElmOrID)) {
+ elm = accOrElmOrID;
+ } else {
+ elm = doc.getElementById(accOrElmOrID);
+ if (!elm) {
+ Assert.ok(false, `Can't get DOM element for ${accOrElmOrID}`);
+ return null;
+ }
+ }
+
+ if (elmObj && typeof elmObj == "object") {
+ elmObj.value = elm;
+ }
+
+ let acc = accOrElmOrID instanceof Ci.nsIAccessible ? accOrElmOrID : null;
+ if (!acc) {
+ try {
+ acc = this.accService.getAccessibleFor(elm);
+ } catch (e) {}
+
+ if (!acc) {
+ if (!(doNotFailIf & this.DONOTFAIL_IF_NO_ACC)) {
+ Assert.ok(
+ false,
+ `Can't get accessible for ${this.prettyName(accOrElmOrID)}`
+ );
+ }
+
+ return null;
+ }
+ }
+
+ if (!interfaces) {
+ return acc;
+ }
+
+ if (!(interfaces instanceof Array)) {
+ interfaces = [interfaces];
+ }
+
+ for (let index = 0; index < interfaces.length; index++) {
+ if (acc instanceof interfaces[index]) {
+ continue;
+ }
+
+ try {
+ acc.QueryInterface(interfaces[index]);
+ } catch (e) {
+ if (!(doNotFailIf & this.DONOTFAIL_IF_NO_INTERFACE)) {
+ Assert.ok(
+ false,
+ `Can't query ${interfaces[index]} for ${accOrElmOrID}`
+ );
+ }
+
+ return null;
+ }
+ }
+
+ return acc;
+ },
+
+ /**
+ * Return the DOM node by identifier (may be accessible, DOM node or ID).
+ */
+ getNode(accOrNodeOrID, doc) {
+ if (!accOrNodeOrID) {
+ return null;
+ }
+
+ if (Node.isInstance(accOrNodeOrID)) {
+ return accOrNodeOrID;
+ }
+
+ if (accOrNodeOrID instanceof Ci.nsIAccessible) {
+ return accOrNodeOrID.DOMNode;
+ }
+
+ const node = doc.getElementById(accOrNodeOrID);
+ if (!node) {
+ Assert.ok(false, `Can't get DOM element for ${accOrNodeOrID}`);
+ return null;
+ }
+
+ return node;
+ },
+
+ /**
+ * Return root accessible.
+ *
+ * @param {DOMNode} doc
+ * Chrome document.
+ *
+ * @return {nsIAccessible}
+ * Accessible object for chrome window.
+ */
+ getRootAccessible(doc) {
+ const acc = this.getAccessible(doc);
+ return acc ? acc.rootDocument.QueryInterface(Ci.nsIAccessible) : null;
+ },
+
+ /**
+ * Analogy of SimpleTest.is function used to compare objects.
+ */
+ isObject(obj, expectedObj, msg) {
+ if (obj == expectedObj) {
+ Assert.ok(true, msg);
+ return;
+ }
+
+ Assert.ok(
+ false,
+ `${msg} - got "${this.prettyName(obj)}", expected "${this.prettyName(
+ expectedObj
+ )}"`
+ );
+ },
+};
diff --git a/accessible/tests/browser/Layout.sys.mjs b/accessible/tests/browser/Layout.sys.mjs
new file mode 100644
index 0000000000..15b0060717
--- /dev/null
+++ b/accessible/tests/browser/Layout.sys.mjs
@@ -0,0 +1,178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { Assert } from "resource://testing-common/Assert.sys.mjs";
+
+import { CommonUtils } from "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs";
+
+export const Layout = {
+ /**
+ * Zoom the given document.
+ */
+ zoomDocument(doc, zoom) {
+ const bc = BrowsingContext.getFromWindow(doc.defaultView);
+ // To mirror the behaviour of the UI, we set the zoom
+ // value on the top level browsing context. This value automatically
+ // propagates down to iframes.
+ bc.top.fullZoom = zoom;
+ },
+
+ /**
+ * Set the relative resolution of this document. This is what apz does.
+ * On non-mobile platforms you won't see a visible change.
+ */
+ setResolution(doc, zoom) {
+ const windowUtils = doc.defaultView.windowUtils;
+ windowUtils.setResolutionAndScaleTo(zoom);
+ },
+
+ /**
+ * Assert.is() function checking the expected value is within the range.
+ */
+ isWithin(expected, got, within, msg) {
+ if (Math.abs(got - expected) <= within) {
+ Assert.ok(true, `${msg} - Got ${got}`);
+ } else {
+ Assert.ok(
+ false,
+ `${msg} - Got ${got}, expected ${expected} with error of ${within}`
+ );
+ }
+ },
+
+ /**
+ * Return the accessible coordinates relative to the screen in device pixels.
+ */
+ getPos(id) {
+ const accessible = CommonUtils.getAccessible(id);
+ const x = {};
+ const y = {};
+ accessible.getBounds(x, y, {}, {});
+
+ return [x.value, y.value];
+ },
+
+ /**
+ * Return the accessible coordinates and size relative to the screen in device
+ * pixels. This methods also retrieves coordinates in CSS pixels and ensures that they
+ * match Dev pixels with a given device pixel ratio.
+ */
+ getBounds(id, dpr) {
+ const accessible = CommonUtils.getAccessible(id);
+ const x = {};
+ const y = {};
+ const width = {};
+ const height = {};
+ const xInCSS = {};
+ const yInCSS = {};
+ const widthInCSS = {};
+ const heightInCSS = {};
+ accessible.getBounds(x, y, width, height);
+ accessible.getBoundsInCSSPixels(xInCSS, yInCSS, widthInCSS, heightInCSS);
+
+ this.isWithin(
+ x.value / dpr,
+ xInCSS.value,
+ 1,
+ "X in CSS pixels is calculated correctly"
+ );
+ this.isWithin(
+ y.value / dpr,
+ yInCSS.value,
+ 1,
+ "Y in CSS pixels is calculated correctly"
+ );
+ this.isWithin(
+ width.value / dpr,
+ widthInCSS.value,
+ 1,
+ "Width in CSS pixels is calculated correctly"
+ );
+ this.isWithin(
+ height.value / dpr,
+ heightInCSS.value,
+ 1,
+ "Height in CSS pixels is calculated correctly"
+ );
+
+ return [x.value, y.value, width.value, height.value];
+ },
+
+ getRangeExtents(id, startOffset, endOffset, coordOrigin) {
+ const hyperText = CommonUtils.getAccessible(id, [Ci.nsIAccessibleText]);
+ const x = {};
+ const y = {};
+ const width = {};
+ const height = {};
+ hyperText.getRangeExtents(
+ startOffset,
+ endOffset,
+ x,
+ y,
+ width,
+ height,
+ coordOrigin
+ );
+
+ return [x.value, y.value, width.value, height.value];
+ },
+
+ CSSToDevicePixels(win, x, y, width, height) {
+ const ratio = win.devicePixelRatio;
+
+ // CSS pixels and ratio can be not integer. Device pixels are always integer.
+ // Do our best and hope it works.
+ return [
+ Math.round(x * ratio),
+ Math.round(y * ratio),
+ Math.round(width * ratio),
+ Math.round(height * ratio),
+ ];
+ },
+
+ /**
+ * Return DOM node coordinates relative the screen and its size in device
+ * pixels.
+ */
+ getBoundsForDOMElm(id, doc) {
+ let x = 0;
+ let y = 0;
+ let width = 0;
+ let height = 0;
+
+ const elm = CommonUtils.getNode(id, doc);
+ const elmWindow = elm.ownerGlobal;
+ if (elm.localName == "area") {
+ const mapName = elm.parentNode.getAttribute("name");
+ const selector = `[usemap="#${mapName}"]`;
+ const img = elm.ownerDocument.querySelector(selector);
+
+ const areaCoords = elm.coords.split(",");
+ const areaX = parseInt(areaCoords[0], 10);
+ const areaY = parseInt(areaCoords[1], 10);
+ const areaWidth = parseInt(areaCoords[2], 10) - areaX;
+ const areaHeight = parseInt(areaCoords[3], 10) - areaY;
+
+ const rect = img.getBoundingClientRect();
+ x = rect.left + areaX;
+ y = rect.top + areaY;
+ width = areaWidth;
+ height = areaHeight;
+ } else {
+ const rect = elm.getBoundingClientRect();
+ x = rect.left;
+ y = rect.top;
+ width = rect.width;
+ height = rect.height;
+ }
+
+ return this.CSSToDevicePixels(
+ elmWindow,
+ x + elmWindow.mozInnerScreenX,
+ y + elmWindow.mozInnerScreenY,
+ width,
+ height
+ );
+ },
+};
diff --git a/accessible/tests/browser/atk/a11y_setup.py b/accessible/tests/browser/atk/a11y_setup.py
new file mode 100644
index 0000000000..c5fe3481ff
--- /dev/null
+++ b/accessible/tests/browser/atk/a11y_setup.py
@@ -0,0 +1,64 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""Python environment for ATK a11y browser tests.
+"""
+
+import os
+import subprocess
+import sys
+
+import psutil
+
+# pyatspi can't be installed using pip. Rely on the system installation.
+# Get the path to the system installation of pyatspi.
+pyatspiFile = subprocess.check_output(
+ (
+ os.path.join(sys.base_prefix, "bin", "python3"),
+ "-c",
+ "import pyatspi; print(pyatspi.__file__)",
+ ),
+ encoding="utf-8",
+).rstrip()
+sys.path.append(os.path.dirname(os.path.dirname(pyatspiFile)))
+import pyatspi
+
+sys.path.pop()
+del pyatspiFile
+
+
+def getDoc():
+ """Get the Accessible for the document being tested."""
+ # We can compare the parent process ids to find the Firefox started by the
+ # test harness.
+ commonPid = psutil.Process().ppid()
+ for app in pyatspi.Registry.getDesktop(0):
+ if (
+ app.name == "Firefox"
+ and psutil.Process(app.get_process_id()).ppid() == commonPid
+ ):
+ break
+ else:
+ raise LookupError("Couldn't find Firefox application Accessible")
+ root = app[0]
+ for embeds in root.getRelationSet():
+ if embeds.getRelationType() == pyatspi.RELATION_EMBEDS:
+ break
+ else:
+ raise LookupError("Firefox root doesn't have RELATION_EMBEDS")
+ doc = embeds.getTarget(0)
+ child = doc[0]
+ if child.get_attributes().get("id") == "default-iframe-id":
+ # This is an iframe or remoteIframe test.
+ doc = child[0]
+ return doc
+
+
+def findByDomId(root, id):
+ for child in root:
+ if child.get_attributes().get("id") == id:
+ return child
+ descendant = findByDomId(child, id)
+ if descendant:
+ return descendant
diff --git a/accessible/tests/browser/atk/browser.toml b/accessible/tests/browser/atk/browser.toml
new file mode 100644
index 0000000000..99cdba4ca2
--- /dev/null
+++ b/accessible/tests/browser/atk/browser.toml
@@ -0,0 +1,16 @@
+[DEFAULT]
+subsuite = "a11y"
+skip-if = [
+ "os != 'linux'",
+ "headless",
+]
+support-files = ["head.js"]
+prefs = [
+ # Enabling the a11y service from XPCOM doesn't seem to be enough to get ATK
+ # working correctly. Force enable it before the test starts.
+ "accessibility.force_disabled=-1",
+ "javascript.options.asyncstack_capture_debuggee_only=false",
+]
+
+["browser_role.js"]
+["browser_table.js"]
diff --git a/accessible/tests/browser/atk/browser_role.js b/accessible/tests/browser/atk/browser_role.js
new file mode 100644
index 0000000000..7b870b3337
--- /dev/null
+++ b/accessible/tests/browser/atk/browser_role.js
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const ATSPI_ROLE_DOCUMENT_WEB = 95;
+const ATSPI_ROLE_PARAGRAPH = 73;
+
+addAccessibleTask(
+ `
+<p id="p">p</p>
+ `,
+ async function (browser, docAcc) {
+ let role = await runPython(`
+ global doc
+ doc = getDoc()
+ return doc.getRole()
+ `);
+ is(role, ATSPI_ROLE_DOCUMENT_WEB, "doc has correct ATSPI role");
+ ok(
+ await runPython(`
+ global p
+ p = findByDomId(doc, "p")
+ return p == doc[0]
+ `),
+ "doc's first child is p"
+ );
+ role = await runPython(`p.getRole()`);
+ is(role, ATSPI_ROLE_PARAGRAPH, "p has correct ATSPI role");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/atk/browser_table.js b/accessible/tests/browser/atk/browser_table.js
new file mode 100644
index 0000000000..98b3270465
--- /dev/null
+++ b/accessible/tests/browser/atk/browser_table.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test getRowColumnSpan.
+ */
+addAccessibleTask(
+ `
+<table>
+ <tr>
+ <th id="ab" colspan="2">ab</th>
+ <td id="cf" rowspan="2">cf</td>
+ </tr>
+ <tr>
+ <td id="d">d</td>
+ <td>e</td>
+ </tr>
+</table>
+ `,
+ async function (browser, docAcc) {
+ let result = await runPython(`
+ global doc
+ doc = getDoc()
+ ab = findByDomId(doc, "ab")
+ return str(ab.queryTableCell().getRowColumnSpan())
+ `);
+ is(
+ result,
+ "(row=0, column=0, row_span=1, column_span=2)",
+ "ab getColumnRowSpan correct"
+ );
+ result = await runPython(`
+ cf = findByDomId(doc, "cf")
+ return str(cf.queryTableCell().getRowColumnSpan())
+ `);
+ is(
+ result,
+ "(row=0, column=2, row_span=2, column_span=1)",
+ "cf getColumnRowSpan correct"
+ );
+ result = await runPython(`
+ d = findByDomId(doc, "d")
+ return str(d.queryTableCell().getRowColumnSpan())
+ `);
+ is(
+ result,
+ "(row=1, column=0, row_span=1, column_span=1)",
+ "d getColumnRowSpan correct"
+ );
+ }
+);
diff --git a/accessible/tests/browser/atk/head.js b/accessible/tests/browser/atk/head.js
new file mode 100644
index 0000000000..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/atk/head.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
diff --git a/accessible/tests/browser/bounds/browser.toml b/accessible/tests/browser/bounds/browser.toml
new file mode 100644
index 0000000000..da1fe37a1e
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser.toml
@@ -0,0 +1,34 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/letters.gif",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_accessible_moved.js"]
+
+["browser_caret_rect.js"]
+
+["browser_position.js"]
+
+["browser_test_display_contents.js"]
+
+["browser_test_iframe_transform.js"]
+
+["browser_test_resolution.js"]
+skip-if = ["os == 'win'"] # bug 1372296
+
+["browser_test_simple_transform.js"]
+
+["browser_test_zoom.js"]
+skip-if = ["true"] # Bug 1734271
+
+["browser_test_zoom_text.js"]
+https_first_disabled = true
+skip-if = ["os == 'win'"] # bug 1372296
+
+["browser_zero_area.js"]
diff --git a/accessible/tests/browser/bounds/browser_accessible_moved.js b/accessible/tests/browser/bounds/browser_accessible_moved.js
new file mode 100644
index 0000000000..307c680000
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_accessible_moved.js
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function assertBoundsNonZero(acc) {
+ // XXX We don't use getBounds because it uses BoundsInCSSPixels(), but that
+ // isn't implemented for the cache yet.
+ let x = {};
+ let y = {};
+ let width = {};
+ let height = {};
+ acc.getBounds(x, y, width, height);
+ ok(x.value > 0, "x is non-0");
+ ok(y.value > 0, "y is non-0");
+ ok(width.value > 0, "width is non-0");
+ ok(height.value > 0, "height is non-0");
+}
+
+/**
+ * Test that bounds aren't 0 after an Accessible is moved (but not re-created).
+ */
+addAccessibleTask(
+ `
+<div id="root" role="group"><div id="scrollable" role="presentation" style="height: 1px;"><button id="button">test</button></div></div>
+ `,
+ async function (browser, docAcc) {
+ let button = findAccessibleChildByID(docAcc, "button");
+ assertBoundsNonZero(button);
+
+ const root = findAccessibleChildByID(docAcc, "root");
+ let reordered = waitForEvent(EVENT_REORDER, root);
+ // scrollable wasn't in the a11y tree, but this will force it to be created.
+ // button will be moved inside it.
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("scrollable").style.overflow = "scroll";
+ });
+ await reordered;
+
+ const scrollable = findAccessibleChildByID(docAcc, "scrollable");
+ assertBoundsNonZero(scrollable);
+ // XXX button's RemoteAccessible was recreated, so we have to fetch it
+ // again. This shouldn't be necessary once bug 1739050 is fixed.
+ button = findAccessibleChildByID(docAcc, "button");
+ assertBoundsNonZero(button);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/bounds/browser_caret_rect.js b/accessible/tests/browser/bounds/browser_caret_rect.js
new file mode 100644
index 0000000000..ac0ee3aa50
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_caret_rect.js
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+async function getCaretRect(browser, id) {
+ // The caret rect can only be queried on LocalAccessible. On Windows, we do
+ // send it across processes with caret events, but this currently can't be
+ // queried outside of the event, nor with XPCOM.
+ const [x, y, w, h] = await invokeContentTask(browser, [id], contentId => {
+ const node = content.document.getElementById(contentId);
+ const contentAcc = content.CommonUtils.accService.getAccessibleFor(node);
+ contentAcc.QueryInterface(Ci.nsIAccessibleText);
+ const caretX = {};
+ const caretY = {};
+ const caretW = {};
+ const caretH = {};
+ contentAcc.getCaretRect(caretX, caretY, caretW, caretH);
+ return [caretX.value, caretY.value, caretW.value, caretH.value];
+ });
+ info(`Caret bounds: ${x}, ${y}, ${w}, ${h}`);
+ return [x, y, w, h];
+}
+
+async function testCaretRect(browser, docAcc, id, offset) {
+ const acc = findAccessibleChildByID(docAcc, id, [nsIAccessibleText]);
+ is(acc.caretOffset, offset, `Caret at offset ${offset}`);
+ const charX = {};
+ const charY = {};
+ const charW = {};
+ const charH = {};
+ const atEnd = offset == acc.characterCount;
+ const empty = offset == 0 && atEnd;
+ const queryOffset = atEnd && !empty ? offset - 1 : offset;
+ acc.getCharacterExtents(
+ queryOffset,
+ charX,
+ charY,
+ charW,
+ charH,
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ info(
+ `Character ${queryOffset} bounds: ${charX.value}, ${charY.value}, ${charW.value}, ${charH.value}`
+ );
+ const [caretX, caretY, caretW, caretH] = await getCaretRect(browser, id);
+ if (atEnd) {
+ ok(caretX > charX.value, "Caret x after last character x");
+ } else {
+ is(caretX, charX.value, "Caret x same as character x");
+ }
+ is(caretY, charY.value, "Caret y same as character y");
+ is(caretW, 1, "Caret width is 1");
+ if (!empty) {
+ is(caretH, charH.value, "Caret height same as character height");
+ }
+}
+
+function getAccBounds(acc) {
+ const x = {};
+ const y = {};
+ const w = {};
+ const h = {};
+ acc.getBounds(x, y, w, h);
+ return [x.value, y.value, w.value, h.value];
+}
+
+/**
+ * Test the caret rect in content documents.
+ */
+addAccessibleTask(
+ `
+<input id="input" value="ab">
+<input id="emptyInput">
+ `,
+ async function (browser, docAcc) {
+ async function runTests() {
+ const input = findAccessibleChildByID(docAcc, "input", [
+ nsIAccessibleText,
+ ]);
+ info("Focusing input");
+ let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
+ input.takeFocus();
+ await caretMoved;
+ await testCaretRect(browser, docAcc, "input", 0);
+ info("Setting caretOffset to 1");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
+ input.caretOffset = 1;
+ await caretMoved;
+ await testCaretRect(browser, docAcc, "input", 1);
+ info("Setting caretOffset to 2");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
+ input.caretOffset = 2;
+ await caretMoved;
+ await testCaretRect(browser, docAcc, "input", 2);
+ info("Resetting caretOffset to 0");
+ input.caretOffset = 0;
+
+ const emptyInput = findAccessibleChildByID(docAcc, "emptyInput", [
+ nsIAccessibleText,
+ ]);
+ info("Focusing emptyInput");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, emptyInput);
+ emptyInput.takeFocus();
+ await caretMoved;
+ await testCaretRect(browser, docAcc, "emptyInput", 0);
+ }
+
+ await runTests();
+
+ // Check that the caret rect is correct when the title bar is shown.
+ if (LINUX || Services.env.get("MOZ_HEADLESS")) {
+ // Disabling tabs in title bar doesn't change the bounds on Linux or in
+ // headless mode.
+ info("Skipping title bar tests");
+ return;
+ }
+ const [, origDocY] = getAccBounds(docAcc);
+ info("Showing title bar");
+ let titleBarChanged = BrowserTestUtils.waitForMutationCondition(
+ document.documentElement,
+ { attributes: true, attributeFilter: ["tabsintitlebar"] },
+ () => !document.documentElement.hasAttribute("tabsintitlebar")
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.inTitlebar", false]],
+ });
+ await titleBarChanged;
+ const [, newDocY] = getAccBounds(docAcc);
+ Assert.greater(
+ newDocY,
+ origDocY,
+ "Doc has larger y after title bar change"
+ );
+ await runTests();
+ await SpecialPowers.popPrefEnv();
+ },
+ { chrome: true, topLevel: true }
+);
diff --git a/accessible/tests/browser/bounds/browser_position.js b/accessible/tests/browser/bounds/browser_position.js
new file mode 100644
index 0000000000..bfba58be87
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_position.js
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test changing the left/top CSS properties.
+ */
+addAccessibleTask(
+ `
+<div id="div" style="position: relative; left: 0px; top: 0px; width: fit-content;">
+ test
+</div>
+ `,
+ async function (browser, docAcc) {
+ await testBoundsWithContent(docAcc, "div", browser);
+ info("Changing left");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").style.left = "200px";
+ });
+ await waitForContentPaint(browser);
+ await testBoundsWithContent(docAcc, "div", browser);
+ info("Changing top");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").style.top = "200px";
+ });
+ await waitForContentPaint(browser);
+ await testBoundsWithContent(docAcc, "div", browser);
+ },
+ { chrome: true, topLevel: true, iframe: true }
+);
+
+/**
+ * Test moving one element by inserting a second element before it such that the
+ * second element doesn't reflow.
+ */
+addAccessibleTask(
+ `
+<div id="reflowContainer">
+ <p>1</p>
+ <p id="reflow2" hidden>2</p>
+ <p id="reflow3">3</p>
+</div>
+<p id="noReflow">noReflow</p>
+ `,
+ async function (browser, docAcc) {
+ for (const id of ["reflowContainer", "reflow3", "noReflow"]) {
+ await testBoundsWithContent(docAcc, id, browser);
+ }
+ // Show p2, which will reflow everything inside "reflowContainer", but just
+ // move "noReflow" down without reflowing it.
+ info("Showing p2");
+ let shown = waitForEvent(EVENT_SHOW, "reflow2");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("reflow2").hidden = false;
+ });
+ await waitForContentPaint(browser);
+ await shown;
+ for (const id of ["reflowContainer", "reflow2", "reflow3", "noReflow"]) {
+ await testBoundsWithContent(docAcc, id, browser);
+ }
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
+
+/**
+ * Test bounds when an Accessible is re-parented.
+ */
+addAccessibleTask(
+ `
+<div id="container">
+ <p style="height: 300px;">1</p>
+ <div class="pParent">
+ <p id="p2">2</p>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const paraTree = { PARAGRAPH: [{ TEXT_LEAF: [] }] };
+ const container = findAccessibleChildByID(docAcc, "container");
+ testAccessibleTree(container, { SECTION: [paraTree, paraTree] });
+ await testBoundsWithContent(docAcc, "p2", browser);
+ // Add a click listener to the div containing p2. This will cause an
+ // Accessible to be created for that div, which didn't have one before.
+ info("Adding click listener to pParent");
+ let reordered = waitForEvent(EVENT_REORDER, container);
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .querySelector(".pParent")
+ .addEventListener("click", function () {});
+ });
+ await reordered;
+ testAccessibleTree(container, {
+ SECTION: [paraTree, { SECTION: [paraTree] }],
+ });
+ await testBoundsWithContent(docAcc, "p2", browser);
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
+
+/**
+ * Test the bounds of items in an inline list with content that offsets the
+ * origin of the list's bounding box (creating an IB split within the UL frame).
+ */
+addAccessibleTask(
+ `
+ <style>
+ ul,li {
+ display:inline;
+ list-style-type:none;
+ list-style-position:inside;
+ margin:0;
+ padding:0;
+ }
+ </style>
+ <div id="container" style="background:green; max-width: 400px;">List of information: <ul id="list"><li id="one">item one</li> | <li id="two">item two</li> | <li id="three">item three</li> | <li id="four">item four</li> | <li id="five">item five</li></ul></div>
+ `,
+ async function (browser, docAcc) {
+ await testBoundsWithContent(docAcc, "list", browser);
+ await testBoundsWithContent(docAcc, "one", browser);
+ await testBoundsWithContent(docAcc, "two", browser);
+ await testBoundsWithContent(docAcc, "three", browser);
+ await testBoundsWithContent(docAcc, "four", browser);
+ await testBoundsWithContent(docAcc, "five", browser);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
diff --git a/accessible/tests/browser/bounds/browser_test_display_contents.js b/accessible/tests/browser/bounds/browser_test_display_contents.js
new file mode 100644
index 0000000000..db1bfce178
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_display_contents.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/layout.js */
+
+async function testContentBounds(browser, acc) {
+ let [expectedX, expectedY, expectedWidth, expectedHeight] =
+ await getContentBoundsForDOMElm(browser, getAccessibleDOMNodeID(acc));
+
+ let contentDPR = await getContentDPR(browser);
+ let [x, y, width, height] = getBounds(acc, contentDPR);
+ let prettyAccName = prettyName(acc);
+ is(x, expectedX, "Wrong x coordinate of " + prettyAccName);
+ is(y, expectedY, "Wrong y coordinate of " + prettyAccName);
+ is(width, expectedWidth, "Wrong width of " + prettyAccName);
+ ok(height >= expectedHeight, "Wrong height of " + prettyAccName);
+}
+
+async function runTests(browser, accDoc) {
+ let p = findAccessibleChildByID(accDoc, "div");
+ let p2 = findAccessibleChildByID(accDoc, "p");
+
+ await testContentBounds(browser, p);
+ await testContentBounds(browser, p2);
+}
+
+/**
+ * Test accessible bounds for accs with display:contents
+ */
+addAccessibleTask(
+ `
+ <div id="div">before
+ <ul id="ul" style="display: contents;">
+ <li id="li" style="display: contents;">
+ <p id="p">item</p>
+ </li>
+ </ul>
+ </div>`,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/bounds/browser_test_iframe_transform.js b/accessible/tests/browser/bounds/browser_test_iframe_transform.js
new file mode 100644
index 0000000000..a44ab75faf
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_iframe_transform.js
@@ -0,0 +1,209 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TRANSLATION_OFFSET = 50;
+const ELEM_ID = "test-elem-id";
+
+// Modify the style of an iframe within the content process. This is different
+// from, e.g., invokeSetStyle, because this function doesn't rely on
+// invokeContentTask, which runs in the context of the iframe itself.
+async function invokeSetStyleIframe(browser, id, style, value) {
+ if (value) {
+ Logger.log(`Setting ${style} style to ${value} for iframe with id: ${id}`);
+ } else {
+ Logger.log(`Removing ${style} style from iframe with id: ${id}`);
+ }
+
+ // Translate the iframe itself (not content within it).
+ await SpecialPowers.spawn(
+ browser,
+ [id, style, value],
+ (iframeId, iframeStyle, iframeValue) => {
+ const elm = content.document.getElementById(iframeId);
+ if (iframeValue) {
+ elm.style[iframeStyle] = iframeValue;
+ } else {
+ delete elm.style[iframeStyle];
+ }
+ }
+ );
+}
+
+// Test the accessible's bounds, comparing them to the content bounds from DOM.
+// This function also accepts an offset, which is necessary in some cases where
+// DOM doesn't know about cross-process offsets.
+function testBoundsWithOffset(browser, iframeDocAcc, id, domElmBounds, offset) {
+ // Get the bounds as reported by the accessible.
+ const acc = findAccessibleChildByID(iframeDocAcc, id);
+ const accX = {};
+ const accY = {};
+ const accWidth = {};
+ const accHeight = {};
+ acc.getBounds(accX, accY, accWidth, accHeight);
+
+ // getContentBoundsForDOMElm's result doesn't include iframe translation
+ // for in-process iframes, but does for out-of-process iframes. To account
+ // for that here, manually add in the translation offset when examining an
+ // in-process iframe.
+ const addTranslationOffset = !gIsRemoteIframe;
+ const expectedX = addTranslationOffset
+ ? domElmBounds[0] + offset
+ : domElmBounds[0];
+ const expectedY = addTranslationOffset
+ ? domElmBounds[1] + offset
+ : domElmBounds[1];
+ const expectedWidth = domElmBounds[2];
+ const expectedHeight = domElmBounds[3];
+
+ let boundsAreEquivalent = true;
+ boundsAreEquivalent &&= accX.value == expectedX;
+ boundsAreEquivalent &&= accY.value == expectedY;
+ boundsAreEquivalent &&= accWidth.value == expectedWidth;
+ boundsAreEquivalent &&= accHeight.value == expectedHeight;
+ return boundsAreEquivalent;
+}
+
+addAccessibleTask(
+ `<div id='${ELEM_ID}'>hello world</div>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ ok(iframeDocAcc, "IFRAME document accessible is present");
+
+ await testBoundsWithContent(iframeDocAcc, ELEM_ID, browser);
+
+ // Translate the iframe, which should modify cross-process offset.
+ await invokeSetStyleIframe(
+ browser,
+ DEFAULT_IFRAME_ID,
+ "transform",
+ `translate(${TRANSLATION_OFFSET}px, ${TRANSLATION_OFFSET}px)`
+ );
+
+ // Allow content to advance to update DOM, then capture the DOM bounds.
+ await waitForContentPaint(browser);
+ const domElmBoundsAfterTranslate = await getContentBoundsForDOMElm(
+ browser,
+ ELEM_ID
+ );
+
+ // Ensure that there's enough time for the cache to update.
+ await untilCacheOk(() => {
+ return testBoundsWithOffset(
+ browser,
+ iframeDocAcc,
+ ELEM_ID,
+ domElmBoundsAfterTranslate,
+ TRANSLATION_OFFSET
+ );
+ }, "Accessible bounds have changed in the cache and match DOM bounds.");
+
+ // Adjust padding of the iframe, then verify bounds adjust properly.
+ // iframes already have a border by default, so we check padding here.
+ const PADDING_OFFSET = 100;
+ await invokeSetStyleIframe(
+ browser,
+ DEFAULT_IFRAME_ID,
+ "padding",
+ `${PADDING_OFFSET}px`
+ );
+
+ // Allow content to advance to update DOM, then capture the DOM bounds.
+ await waitForContentPaint(browser);
+ const domElmBoundsAfterAddingPadding = await getContentBoundsForDOMElm(
+ browser,
+ ELEM_ID
+ );
+
+ await untilCacheOk(() => {
+ return testBoundsWithOffset(
+ browser,
+ iframeDocAcc,
+ ELEM_ID,
+ domElmBoundsAfterAddingPadding,
+ TRANSLATION_OFFSET
+ );
+ }, "Accessible bounds have changed in the cache and match DOM bounds.");
+ },
+ {
+ topLevel: false,
+ iframe: true,
+ remoteIframe: true,
+ iframeAttrs: {
+ style: `height: 100px; width: 100px;`,
+ },
+ }
+);
+
+/**
+ * Test document bounds change notifications.
+ * Note: This uses iframes to change the doc container size in order
+ * to have the doc accessible's bounds change.
+ */
+addAccessibleTask(
+ `<div id="div" style="width: 30px; height: 30px"></div>`,
+ async function (browser, accDoc, foo) {
+ const docWidth = () => {
+ let width = {};
+ accDoc.getBounds({}, {}, width, {});
+ return width.value;
+ };
+
+ await untilCacheIs(docWidth, 0, "Doc width is 0");
+ await invokeSetStyleIframe(browser, DEFAULT_IFRAME_ID, "width", `300px`);
+ await untilCacheIs(docWidth, 300, "Doc width is 300");
+ },
+ {
+ chrome: false,
+ topLevel: false,
+ iframe: true,
+ remoteIframe: true,
+ iframeAttrs: { style: "width: 0;" },
+ }
+);
+
+/**
+ * Test document bounds after re-creating an iframe.
+ */
+addAccessibleTask(
+ `
+<ol id="ol">
+ <iframe id="iframe" src="data:text/html,"></iframe>
+</ol>
+ `,
+ async function (browser, docAcc) {
+ let iframeDoc = findAccessibleChildByID(docAcc, "iframe").firstChild;
+ ok(iframeDoc, "Got the iframe document");
+ const origX = {};
+ const origY = {};
+ iframeDoc.getBounds(origX, origY, {}, {});
+ let reordered = waitForEvent(EVENT_REORDER, docAcc);
+ await invokeContentTask(browser, [], () => {
+ // This will cause a bounds cache update to be queued for the iframe doc.
+ content.document.getElementById("iframe").width = "600";
+ // This will recreate the ol a11y subtree, including the iframe. The
+ // iframe document will be unbound briefly while this happens. We want to
+ // be sure processing the bounds cache update queued above doesn't assert
+ // while the document is unbound. The setTimeout is necessary to get the
+ // cache update to happen at the right time.
+ content.setTimeout(
+ () => (content.document.getElementById("ol").type = "i"),
+ 0
+ );
+ });
+ await reordered;
+ const iframe = findAccessibleChildByID(docAcc, "iframe");
+ // We don't currently fire an event when a DocAccessible is re-bound to a new OuterDoc.
+ await BrowserTestUtils.waitForCondition(() => iframe.firstChild);
+ iframeDoc = iframe.firstChild;
+ ok(iframeDoc, "Got the iframe document after re-creation");
+ const newX = {};
+ const newY = {};
+ iframeDoc.getBounds(newX, newY, {}, {});
+ ok(
+ origX.value == newX.value && origY.value == newY.value,
+ "Iframe document x and y are same after iframe re-creation"
+ );
+ }
+);
diff --git a/accessible/tests/browser/bounds/browser_test_resolution.js b/accessible/tests/browser/bounds/browser_test_resolution.js
new file mode 100644
index 0000000000..0b0b47418d
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_resolution.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/layout.js */
+
+async function testScaledBounds(browser, accDoc, scale, id, type = "object") {
+ let acc = findAccessibleChildByID(accDoc, id);
+
+ // Get document offset
+ let [docX, docY] = getBounds(accDoc);
+
+ // Get the unscaled bounds of the accessible
+ let [x, y, width, height] =
+ type == "text"
+ ? getRangeExtents(acc, 0, -1, COORDTYPE_SCREEN_RELATIVE)
+ : getBounds(acc);
+
+ await invokeContentTask(browser, [scale], _scale => {
+ const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+ Layout.setResolution(content.document, _scale);
+ });
+
+ let [scaledX, scaledY, scaledWidth, scaledHeight] =
+ type == "text"
+ ? getRangeExtents(acc, 0, -1, COORDTYPE_SCREEN_RELATIVE)
+ : getBounds(acc);
+
+ let name = prettyName(acc);
+ isWithin(scaledWidth, width * scale, 2, "Wrong scaled width of " + name);
+ isWithin(scaledHeight, height * scale, 2, "Wrong scaled height of " + name);
+ isWithin(scaledX - docX, (x - docX) * scale, 2, "Wrong scaled x of " + name);
+ isWithin(scaledY - docY, (y - docY) * scale, 2, "Wrong scaled y of " + name);
+
+ await invokeContentTask(browser, [], () => {
+ const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+ Layout.setResolution(content.document, 1.0);
+ });
+}
+
+async function runTests(browser, accDoc) {
+ // The scrollbars get in the way of container bounds calculation.
+ await SpecialPowers.pushPrefEnv({
+ set: [["ui.useOverlayScrollbars", 1]],
+ });
+
+ await testScaledBounds(browser, accDoc, 2.0, "p1");
+ await testScaledBounds(browser, accDoc, 0.5, "p2");
+ await testScaledBounds(browser, accDoc, 3.5, "b1");
+
+ await testScaledBounds(browser, accDoc, 2.0, "p1", "text");
+ await testScaledBounds(browser, accDoc, 0.75, "p2", "text");
+}
+
+/**
+ * Test accessible boundaries when page is zoomed
+ */
+addAccessibleTask(
+ `
+<p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p>
+<p id="p2">para 2</p>
+<button id="b1">Hello</button>
+`,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/bounds/browser_test_simple_transform.js b/accessible/tests/browser/bounds/browser_test_simple_transform.js
new file mode 100644
index 0000000000..7197968b40
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_simple_transform.js
@@ -0,0 +1,225 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+// test basic translation
+addAccessibleTask(
+ `<p id="translate">hello world</p>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ ok(iframeDocAcc, "IFRAME document accessible is present");
+ await testBoundsWithContent(iframeDocAcc, "translate", browser);
+
+ await invokeContentTask(browser, [], () => {
+ let p = content.document.getElementById("translate");
+ p.style = "transform: translate(100px, 100px);";
+ });
+
+ await waitForContentPaint(browser);
+ await testBoundsWithContent(iframeDocAcc, "translate", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Test translation with two children.
+addAccessibleTask(
+ `
+<div role="main" style="translate: 0 300px;">
+ <p id="p1">hello</p>
+ <p id="p2">world</p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ await testBoundsWithContent(docAcc, "p1", browser);
+ await testBoundsWithContent(docAcc, "p2", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// test basic rotation
+addAccessibleTask(
+ `<p id="rotate">hello world</p>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ ok(iframeDocAcc, "IFRAME document accessible is present");
+ await testBoundsWithContent(iframeDocAcc, "rotate", browser);
+
+ await invokeContentTask(browser, [], () => {
+ let p = content.document.getElementById("rotate");
+ p.style = "transform: rotate(-40deg);";
+ });
+
+ await waitForContentPaint(browser);
+ await testBoundsWithContent(iframeDocAcc, "rotate", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// test basic scale
+addAccessibleTask(
+ `<p id="scale">hello world</p>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ ok(iframeDocAcc, "IFRAME document accessible is present");
+ await testBoundsWithContent(iframeDocAcc, "scale", browser);
+
+ await invokeContentTask(browser, [], () => {
+ let p = content.document.getElementById("scale");
+ p.style = "transform: scale(2);";
+ });
+
+ await waitForContentPaint(browser);
+ await testBoundsWithContent(iframeDocAcc, "scale", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Test will-change: transform with no transform.
+addAccessibleTask(
+ `
+<div id="willChangeTop" style="will-change: transform;">
+ <p>hello</p>
+ <p id="willChangeTopP2">world</p>
+</div>
+<div role="group">
+ <div id="willChangeInner" style="will-change: transform;">
+ <p>hello</p>
+ <p id="willChangeInnerP2">world</p>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ // Even though willChangeTop has no transform, it has
+ // will-change: transform, which means nsIFrame::IsTransformed returns
+ // true. We don't cache identity matrices, but because there is an offset
+ // to the root frame, layout includes this in the returned transform
+ // matrix. That means we get a non-identity matrix and thus we cache it.
+ // This is why we only test the identity matrix cache optimization for
+ // willChangeInner.
+ let hasTransform;
+ try {
+ const willChangeInner = findAccessibleChildByID(
+ docAcc,
+ "willChangeInner"
+ );
+ willChangeInner.cache.getStringProperty("transform");
+ hasTransform = true;
+ } catch (e) {
+ hasTransform = false;
+ }
+ ok(!hasTransform, "willChangeInner has no cached transform");
+ await testBoundsWithContent(docAcc, "willChangeTopP2", browser);
+ await testBoundsWithContent(docAcc, "willChangeInnerP2", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that a transform forces creation of an accessible.
+addAccessibleTask(
+ `
+<div id="container">
+ <div style="transform:translate(100px,100px);">
+ <p>test</p>
+ </div>
+</div>
+
+<div id="div-presentational" role="presentation" style="transform:translate(100px,100px);">
+ <p>test</p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const tree = { TEXT_CONTAINER: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }] };
+
+ const divWithTransform = findAccessibleChildByID(
+ docAcc,
+ "container"
+ ).firstChild;
+ testAccessibleTree(divWithTransform, tree);
+ // testBoundsWithContent takes an id, but divWithTransform doesn't have one,
+ // so we can't test the bounds for it.
+
+ // An accessible should still be created, even if the role is "presentation."
+ const divPresentational = findAccessibleChildByID(
+ docAcc,
+ "div-presentational"
+ );
+ testAccessibleTree(divPresentational, tree);
+ await testBoundsWithContent(docAcc, "div-presentational", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that adding a transform on the fly forces creation of an accessible.
+addAccessibleTask(
+ `
+<div id="div-to-transform" role="none" style="position: absolute; width: 300px; height: 300px;">
+ <p>test</p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ let divToTransform = findAccessibleChildByID(docAcc, "div-to-transform");
+ ok(!divToTransform, "There should not be a div accessible.");
+
+ // Translate the div.
+ await invokeContentTask(browser, [], () => {
+ let div = content.document.getElementById("div-to-transform");
+ div.style.transform = "translate(100%, 100%)";
+ });
+ await waitForContentPaint(browser);
+
+ // Verify that the SECTION accessible appeared after we gave it a transform.
+ divToTransform = findAccessibleChildByID(docAcc, "div-to-transform");
+ const tree = {
+ TEXT_CONTAINER: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }],
+ };
+ testAccessibleTree(divToTransform, tree);
+
+ // Verify that the bounds of the div are correctly modified.
+ await testBoundsWithContent(docAcc, "div-to-transform", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Test translated, position: absolute Accessible in a container.
+addAccessibleTask(
+ `
+<div id="container">
+ <div id="transform" style="position: absolute; transform: translate(100px, 100px);">
+ <p id="p">test</p>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ await testBoundsWithContent(docAcc, "transform", browser);
+ await testBoundsWithContent(docAcc, "p", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Test bounds of a rotated element after scroll.
+addAccessibleTask(
+ `
+<div id="scrollable" style="transform: rotate(180deg); overflow: scroll; height: 500px;">
+ <p id="test">hello world</p><hr style="height: 100vh;">
+</div>
+ `,
+ async function (browser, docAcc) {
+ info(
+ "Testing that the unscrolled bounds of a transformed element are correct."
+ );
+ await testBoundsWithContent(docAcc, "test", browser);
+
+ await invokeContentTask(browser, [], () => {
+ // Scroll the scrollable region down (scrolls up due to the transform).
+ let elem = content.document.getElementById("scrollable");
+ elem.scrollTo(0, elem.scrollHeight);
+ });
+
+ info(
+ "Testing that the scrolled bounds of a transformed element are correct."
+ );
+ await testBoundsWithContent(docAcc, "test", browser);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/bounds/browser_test_zoom.js b/accessible/tests/browser/bounds/browser_test_zoom.js
new file mode 100644
index 0000000000..ac84e485a4
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_zoom.js
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/layout.js */
+
+async function testContentBounds(browser, acc) {
+ let [expectedX, expectedY, expectedWidth, expectedHeight] =
+ await getContentBoundsForDOMElm(browser, getAccessibleDOMNodeID(acc));
+
+ let contentDPR = await getContentDPR(browser);
+ let [x, y, width, height] = getBounds(acc, contentDPR);
+ let prettyAccName = prettyName(acc);
+ is(x, expectedX, "Wrong x coordinate of " + prettyAccName);
+ is(y, expectedY, "Wrong y coordinate of " + prettyAccName);
+ is(width, expectedWidth, "Wrong width of " + prettyAccName);
+ ok(height >= expectedHeight, "Wrong height of " + prettyAccName);
+}
+
+async function runTests(browser, accDoc) {
+ let p1 = findAccessibleChildByID(accDoc, "p1");
+ let p2 = findAccessibleChildByID(accDoc, "p2");
+ let imgmap = findAccessibleChildByID(accDoc, "imgmap");
+ if (!imgmap.childCount) {
+ // An image map may not be available even after the doc and image load
+ // is complete. We don't recieve any DOM events for this change either,
+ // so we need to wait for a REORDER.
+ await waitForEvent(EVENT_REORDER, "imgmap");
+ }
+ let area = imgmap.firstChild;
+
+ await testContentBounds(browser, p1);
+ await testContentBounds(browser, p2);
+ await testContentBounds(browser, area);
+
+ await SpecialPowers.spawn(browser, [], () => {
+ const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+ Layout.zoomDocument(content.document, 2.0);
+ });
+
+ await testContentBounds(browser, p1);
+ await testContentBounds(browser, p2);
+ await testContentBounds(browser, area);
+}
+
+/**
+ * Test accessible boundaries when page is zoomed
+ */
+addAccessibleTask(
+ `
+<p id="p1">para 1</p><p id="p2">para 2</p>
+<map name="atoz_map" id="map">
+ <area id="area1" href="http://mozilla.org"
+ coords=17,0,30,14" alt="mozilla.org" shape="rect">
+</map>
+<img id="imgmap" width="447" height="15"
+ usemap="#atoz_map"
+ src="http://example.com/a11y/accessible/tests/mochitest/letters.gif">`,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/bounds/browser_test_zoom_text.js b/accessible/tests/browser/bounds/browser_test_zoom_text.js
new file mode 100644
index 0000000000..3f40b698bf
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_test_zoom_text.js
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/layout.js */
+
+async function runTests(browser, accDoc) {
+ async function testTextNode(id) {
+ let hyperTextNode = findAccessibleChildByID(accDoc, id);
+ let textNode = hyperTextNode.firstChild;
+
+ let contentDPR = await getContentDPR(browser);
+ let [x, y, width, height] = getBounds(textNode, contentDPR);
+ testTextBounds(
+ hyperTextNode,
+ 0,
+ -1,
+ [x, y, width, height],
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ // A 0 range should return an empty rect.
+ testTextBounds(
+ hyperTextNode,
+ 0,
+ 0,
+ [0, 0, 0, 0],
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ }
+
+ async function testEmptyInputNode(id) {
+ let inputNode = findAccessibleChildByID(accDoc, id);
+
+ let [x, y, width, height] = getBounds(inputNode);
+ testTextBounds(
+ inputNode,
+ 0,
+ -1,
+ [x, y, width, height],
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ // A 0 range in an empty input should still return
+ // rect of input node.
+ testTextBounds(
+ inputNode,
+ 0,
+ 0,
+ [x, y, width, height],
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ }
+
+ await testTextNode("p1");
+ await testTextNode("p2");
+ await testEmptyInputNode("i1");
+
+ await SpecialPowers.spawn(browser, [], () => {
+ const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+ Layout.zoomDocument(content.document, 2.0);
+ });
+
+ await testTextNode("p1");
+
+ await SpecialPowers.spawn(browser, [], () => {
+ const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+ Layout.zoomDocument(content.document, 1.0);
+ });
+}
+
+/**
+ * Test the text range boundary when page is zoomed
+ */
+addAccessibleTask(
+ `
+ <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p>
+ <p id='p2'>ل</p>
+ <form><input id='i1' /></form>`,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/bounds/browser_zero_area.js b/accessible/tests/browser/bounds/browser_zero_area.js
new file mode 100644
index 0000000000..c0f9db2673
--- /dev/null
+++ b/accessible/tests/browser/bounds/browser_zero_area.js
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/layout.js */
+
+async function testContentBounds(browser, acc, expectedWidth, expectedHeight) {
+ let [expectedX, expectedY] = await getContentBoundsForDOMElm(
+ browser,
+ getAccessibleDOMNodeID(acc)
+ );
+
+ let contentDPR = await getContentDPR(browser);
+ let [x, y, width, height] = getBounds(acc, contentDPR);
+ let prettyAccName = prettyName(acc);
+ is(x, expectedX, "Wrong x coordinate of " + prettyAccName);
+ is(y, expectedY, "Wrong y coordinate of " + prettyAccName);
+ is(width, expectedWidth, "Wrong width of " + prettyAccName);
+ is(height, expectedHeight, "Wrong height of " + prettyAccName);
+}
+/**
+ * Test accessible bounds with different combinations of overflow and
+ * non-zero frame area.
+ */
+addAccessibleTask(
+ `
+ <div id="a1" style="height:100px; width:100px; background:green;"></div>
+ <div id="a2" style="height:100px; width:100px; background:green;"><div style="height:300px; max-width: 300px; background:blue;"></div></div>
+ <div id="a3" style="height:0; width:0;"><div style="height:200px; width:200px; background:green;"></div></div>
+ `,
+ async function (browser, accDoc) {
+ const a1 = findAccessibleChildByID(accDoc, "a1");
+ const a2 = findAccessibleChildByID(accDoc, "a2");
+ const a3 = findAccessibleChildByID(accDoc, "a3");
+ await testContentBounds(browser, a1, 100, 100);
+ await testContentBounds(browser, a2, 100, 100);
+ await testContentBounds(browser, a3, 200, 200);
+ }
+);
+
+/**
+ * Ensure frames with zero area have their x, y coordinates correctly reported
+ * in bounds()
+ */
+addAccessibleTask(
+ `
+<br>
+<div id="a" style="height:0; width:0;"></div>
+`,
+ async function (browser, accDoc) {
+ const a = findAccessibleChildByID(accDoc, "a");
+ await testContentBounds(browser, a, 0, 0);
+ }
+);
+
+/**
+ * Ensure accessibles have accurately signed dimensions and position when
+ * offscreen.
+ */
+addAccessibleTask(
+ `
+<input type="radio" id="radio" style="left: -671091em; position: absolute;">
+`,
+ async function (browser, accDoc) {
+ const radio = findAccessibleChildByID(accDoc, "radio");
+ const contentDPR = await getContentDPR(browser);
+ const [x, y, width, height] = getBounds(radio, contentDPR);
+ ok(x < 0, "X coordinate should be negative");
+ ok(y > 0, "Y coordinate should be positive");
+ ok(width > 0, "Width should be positive");
+ ok(height > 0, "Height should be positive");
+ // Note: the exact values of x, y, width, and height
+ // are inconsistent with the DOM element values of those
+ // fields, so we don't check our bounds against them with
+ // `testContentBounds` here. DOM reports a negative width,
+ // positive height, and a slightly different (+/- 20)
+ // x and y.
+ }
+);
+
+/**
+ * Test height: 0 with align-items: flex-end. This causes the content to
+ * overflow above the frame's main rect.
+ */
+addAccessibleTask(
+ `
+<aside style="height: 0; display: flex; align-items: flex-end;">
+ <div id="inner0">testing</div>
+</aside>
+<aside style="height: 1; display: flex; align-items: flex-end;">
+ <div id="inner1">testing</div>
+</aside>
+ `,
+ async function (browser, docAcc) {
+ await testBoundsWithContent(docAcc, "inner0", browser);
+ await testBoundsWithContent(docAcc, "inner1", browser);
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
+
+/**
+ * Test a div (block) inside a span (inline). This causes the span's primary
+ * frame to have an empty rect offset from its visible content.
+ */
+addAccessibleTask(
+ `
+<span id="span" tabindex="-1">
+ <div id="div">Testing</div>
+</span>
+ `,
+ async function (browser, docAcc) {
+ await testBoundsWithContent(docAcc, "span", browser);
+ await testBoundsWithContent(docAcc, "div", browser);
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/bounds/head.js b/accessible/tests/browser/bounds/head.js
new file mode 100644
index 0000000000..c1882b9495
--- /dev/null
+++ b/accessible/tests/browser/bounds/head.js
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "layout.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
diff --git a/accessible/tests/browser/browser.toml b/accessible/tests/browser/browser.toml
new file mode 100644
index 0000000000..a6264f837b
--- /dev/null
+++ b/accessible/tests/browser/browser.toml
@@ -0,0 +1,52 @@
+[DEFAULT]
+skip-if = ["a11y_checks"] # 1534855
+subsuite = "a11y"
+support-files = [
+ "!/accessible/tests/mochitest/*.js",
+ "*.sys.mjs",
+ "atk/a11y_setup.py",
+ "head.js",
+ "python_runner_wsh.py",
+ "shared-head.js",
+ "windows/a11y_setup.py",
+ "windows/a11y_setup_requirements.txt",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_shutdown_acc_reference.js"]
+
+["browser_shutdown_doc_acc_reference.js"]
+
+["browser_shutdown_multi_acc_reference_doc.js"]
+
+["browser_shutdown_multi_acc_reference_obj.js"]
+
+["browser_shutdown_multi_proxy_acc_reference_doc.js"]
+skip-if = ["os == 'linux' && verify && debug"]
+
+["browser_shutdown_multi_proxy_acc_reference_obj.js"]
+skip-if = ["os == 'linux' && verify && debug"]
+
+["browser_shutdown_multi_reference.js"]
+
+["browser_shutdown_parent_own_reference.js"]
+skip-if = ["os == 'win' && verify && debug"]
+
+["browser_shutdown_pref.js"]
+
+["browser_shutdown_proxy_acc_reference.js"]
+
+["browser_shutdown_proxy_doc_acc_reference.js"]
+skip-if = ["os == 'win' && verify && debug"]
+
+["browser_shutdown_remote_no_reference.js"]
+skip-if = ["os == 'win' && verify && debug"]
+
+["browser_shutdown_remote_only.js"]
+
+["browser_shutdown_remote_own_reference.js"]
+
+["browser_shutdown_scope_lifecycle.js"]
+
+["browser_shutdown_start_restart.js"]
+skip-if = ["verify && debug"]
diff --git a/accessible/tests/browser/browser_shutdown_acc_reference.js b/accessible/tests/browser/browser_shutdown_acc_reference.js
new file mode 100644
index 0000000000..1768095f94
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_acc_reference.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ // Create a11y service.
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+
+ await a11yInit;
+ ok(accService, "Service initialized");
+
+ // Accessible object reference will live longer than the scope of this
+ // function.
+ let acc = await new Promise(resolve => {
+ let intervalId = setInterval(() => {
+ let tabAcc = accService.getAccessibleFor(gBrowser.selectedTab);
+ if (tabAcc) {
+ clearInterval(intervalId);
+ resolve(tabAcc);
+ }
+ }, 10);
+ });
+ ok(acc, "Accessible object is created");
+
+ let canShutdown = false;
+ // This promise will resolve only if canShutdown flag is set to true. If
+ // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut
+ // down, the promise will reject.
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+
+ // Force garbage collection that should not trigger shutdown because there is
+ // a reference to an accessible object.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a reference to an accessible object.
+ acc = null;
+ ok(!acc, "Accessible object is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+});
diff --git a/accessible/tests/browser/browser_shutdown_doc_acc_reference.js b/accessible/tests/browser/browser_shutdown_doc_acc_reference.js
new file mode 100644
index 0000000000..8f7bf6d423
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_doc_acc_reference.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ // Create a11y service.
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+
+ await a11yInit;
+ ok(accService, "Service initialized");
+
+ // Accessible document reference will live longer than the scope of this
+ // function.
+ let docAcc = accService.getAccessibleFor(document);
+ ok(docAcc, "Accessible document is created");
+
+ let canShutdown = false;
+ // This promise will resolve only if canShutdown flag is set to true. If
+ // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut
+ // down, the promise will reject.
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+
+ // Force garbage collection that should not trigger shutdown because there is
+ // a reference to an accessible document.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a reference to an accessible document.
+ docAcc = null;
+ ok(!docAcc, "Accessible document is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+});
diff --git a/accessible/tests/browser/browser_shutdown_multi_acc_reference_doc.js b/accessible/tests/browser/browser_shutdown_multi_acc_reference_doc.js
new file mode 100644
index 0000000000..273fc7175d
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_multi_acc_reference_doc.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ // Create a11y service.
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+
+ await a11yInit;
+ ok(accService, "Service initialized");
+
+ let docAcc = accService.getAccessibleFor(document);
+ ok(docAcc, "Accessible document is created");
+
+ // Accessible object reference will live longer than the scope of this
+ // function.
+ let acc = await new Promise(resolve => {
+ let intervalId = setInterval(() => {
+ let tabAcc = accService.getAccessibleFor(gBrowser.selectedTab);
+ if (tabAcc) {
+ clearInterval(intervalId);
+ resolve(tabAcc);
+ }
+ }, 10);
+ });
+ ok(acc, "Accessible object is created");
+
+ let canShutdown = false;
+ // This promise will resolve only if canShutdown flag is set to true. If
+ // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut
+ // down, the promise will reject.
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+
+ // Force garbage collection that should not trigger shutdown because there are
+ // references to accessible objects.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Remove a reference to an accessible object.
+ acc = null;
+ ok(!acc, "Accessible object is removed");
+ // Force garbage collection that should not trigger shutdown because there is
+ // a reference to an accessible document.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a reference to an accessible document.
+ docAcc = null;
+ ok(!docAcc, "Accessible document is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+});
diff --git a/accessible/tests/browser/browser_shutdown_multi_acc_reference_obj.js b/accessible/tests/browser/browser_shutdown_multi_acc_reference_obj.js
new file mode 100644
index 0000000000..af21b3dc4c
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_multi_acc_reference_obj.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ // Create a11y service.
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+
+ await a11yInit;
+ ok(accService, "Service initialized");
+
+ let docAcc = accService.getAccessibleFor(document);
+ ok(docAcc, "Accessible document is created");
+
+ // Accessible object reference will live longer than the scope of this
+ // function.
+ let acc = await new Promise(resolve => {
+ let intervalId = setInterval(() => {
+ let tabAcc = accService.getAccessibleFor(gBrowser.selectedTab);
+ if (tabAcc) {
+ clearInterval(intervalId);
+ resolve(tabAcc);
+ }
+ }, 10);
+ });
+ ok(acc, "Accessible object is created");
+
+ let canShutdown = false;
+ // This promise will resolve only if canShutdown flag is set to true. If
+ // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut
+ // down, the promise will reject.
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+
+ // Force garbage collection that should not trigger shutdown because there are
+ // references to accessible objects.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Remove a reference to an accessible document.
+ docAcc = null;
+ ok(!docAcc, "Accessible document is removed");
+ // Force garbage collection that should not trigger shutdown because there is
+ // a reference to an accessible object.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a reference to an accessible object.
+ acc = null;
+ ok(!acc, "Accessible object is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+});
diff --git a/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.js b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.js
new file mode 100644
index 0000000000..e4091c5216
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.js
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ let docLoaded = waitForEvent(
+ Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE,
+ "body"
+ );
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized");
+ await a11yInit;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body id="body"><div id="div"></div></body>
+ </html>`,
+ },
+ async function (browser) {
+ let docLoadedEvent = await docLoaded;
+ let docAcc = docLoadedEvent.accessibleDocument;
+ ok(docAcc, "Accessible document proxy is created");
+ // Remove unnecessary dangling references
+ docLoaded = null;
+ docLoadedEvent = null;
+ forceGC();
+
+ let acc = docAcc.getChildAt(0);
+ ok(acc, "Accessible proxy is created");
+
+ let canShutdown = false;
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+ // Force garbage collection that should not trigger shutdown because there
+ // is a reference to an accessible proxy.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Remove a reference to an accessible proxy.
+ acc = null;
+ ok(!acc, "Accessible proxy is removed");
+ // Force garbage collection that should not trigger shutdown because there is
+ // a reference to an accessible document proxy.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a last reference to an accessible document proxy.
+ docAcc = null;
+ ok(!docAcc, "Accessible document proxy is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+ }
+ );
+});
diff --git a/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js
new file mode 100644
index 0000000000..f6eca362b0
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ let docLoaded = waitForEvent(
+ Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE,
+ "body"
+ );
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized");
+ await a11yInit;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body id="body"><div id="div"></div></body>
+ </html>`,
+ },
+ async function (browser) {
+ let docLoadedEvent = await docLoaded;
+ let docAcc = docLoadedEvent.accessibleDocument;
+ ok(docAcc, "Accessible document proxy is created");
+ // Remove unnecessary dangling references
+ docLoaded = null;
+ docLoadedEvent = null;
+ forceGC();
+
+ let acc = docAcc.getChildAt(0);
+ ok(acc, "Accessible proxy is created");
+
+ let canShutdown = false;
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+ // Force garbage collection that should not trigger shutdown because there
+ // is a reference to an accessible proxy.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Remove a reference to an accessible document proxy.
+ docAcc = null;
+ ok(!docAcc, "Accessible document proxy is removed");
+ // Force garbage collection that should not trigger shutdown because there is
+ // a reference to an accessible proxy.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a last reference to an accessible proxy.
+ acc = null;
+ ok(!acc, "Accessible proxy is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+ }
+ );
+});
diff --git a/accessible/tests/browser/browser_shutdown_multi_reference.js b/accessible/tests/browser/browser_shutdown_multi_reference.js
new file mode 100644
index 0000000000..a92f6faf61
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_multi_reference.js
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ info("Creating a service");
+ // Create a11y service.
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService1 = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ await a11yInit;
+ ok(accService1, "Service initialized");
+
+ // Add another reference to a11y service. This will not trigger
+ // 'a11y-init-or-shutdown' event
+ let accService2 = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService2, "Service initialized");
+
+ info("Removing all service references");
+ let canShutdown = false;
+ // This promise will resolve only if canShutdown flag is set to true. If
+ // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut
+ // down, the promise will reject.
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+ // Remove first a11y service reference.
+ accService1 = null;
+ ok(!accService1, "Service is removed");
+ // Force garbage collection that should not trigger shutdown because there is
+ // another reference.
+ forceGC();
+
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove last a11y service reference.
+ accService2 = null;
+ ok(!accService2, "Service is removed");
+ // Force garbage collection that should trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+});
diff --git a/accessible/tests/browser/browser_shutdown_parent_own_reference.js b/accessible/tests/browser/browser_shutdown_parent_own_reference.js
new file mode 100644
index 0000000000..472e977626
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_parent_own_reference.js
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body></body>
+ </html>`,
+ },
+ async function (browser) {
+ info(
+ "Creating a service in parent and waiting for service to be created " +
+ "in content"
+ );
+ await loadContentScripts(browser, {
+ script: "Common.sys.mjs",
+ symbol: "CommonUtils",
+ });
+ // Create a11y service in the main process. This will trigger creating of
+ // the a11y service in parent as well.
+ const [parentA11yInitObserver, parentA11yInit] = initAccService();
+ const [contentA11yInitObserver, contentA11yInit] =
+ initAccService(browser);
+
+ await Promise.all([parentA11yInitObserver, contentA11yInitObserver]);
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized in parent");
+ await Promise.all([parentA11yInit, contentA11yInit]);
+
+ info(
+ "Adding additional reference to accessibility service in content " +
+ "process"
+ );
+ // Add a new reference to the a11y service inside the content process.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.CommonUtils.accService;
+ });
+
+ info(
+ "Trying to shut down a service in content and making sure it stays " +
+ "alive as it was started by parent"
+ );
+ let contentCanShutdown = false;
+ // This promise will resolve only if contentCanShutdown flag is set to true.
+ // If 'a11y-init-or-shutdown' event with '0' flag (in content) comes before
+ // it can be shut down, the promise will reject.
+ const [contentA11yShutdownObserver, contentA11yShutdownPromise] =
+ shutdownAccService(browser);
+ await contentA11yShutdownObserver;
+ const contentA11yShutdown = new Promise((resolve, reject) =>
+ contentA11yShutdownPromise.then(flag =>
+ contentCanShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+ // Remove a11y service reference in content and force garbage collection.
+ // This should not trigger shutdown since a11y was originally initialized by
+ // the main process.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.CommonUtils.clearAccService();
+ });
+
+ // Have some breathing room between a11y service shutdowns.
+ await TestUtils.waitForTick();
+
+ info("Removing a service in parent");
+ // Now allow a11y service to shutdown in content.
+ contentCanShutdown = true;
+ // Remove the a11y service reference in the main process.
+ const [parentA11yShutdownObserver, parentA11yShutdown] =
+ shutdownAccService();
+ await parentA11yShutdownObserver;
+
+ accService = null;
+ ok(!accService, "Service is removed in parent");
+ // Force garbage collection that should trigger shutdown in both parent and
+ // content.
+ forceGC();
+ await Promise.all([parentA11yShutdown, contentA11yShutdown]);
+ }
+ );
+});
diff --git a/accessible/tests/browser/browser_shutdown_pref.js b/accessible/tests/browser/browser_shutdown_pref.js
new file mode 100644
index 0000000000..74cef28b03
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_pref.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PREF_ACCESSIBILITY_FORCE_DISABLED = "accessibility.force_disabled";
+
+add_task(async function testForceDisable() {
+ ok(
+ !Services.appinfo.accessibilityEnabled,
+ "Accessibility is disabled by default"
+ );
+
+ info("Reset force disabled preference");
+ Services.prefs.clearUserPref(PREF_ACCESSIBILITY_FORCE_DISABLED);
+
+ info("Enable accessibility service via XPCOM");
+ let [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ await a11yInit;
+ ok(Services.appinfo.accessibilityEnabled, "Accessibility is enabled");
+
+ info("Force disable a11y service via preference");
+ let [a11yShutdownObserver, a11yShutdown] = shutdownAccService();
+ await a11yShutdownObserver;
+
+ Services.prefs.setIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED, 1);
+ await a11yShutdown;
+ ok(!Services.appinfo.accessibilityEnabled, "Accessibility is disabled");
+
+ info("Attempt to get an instance of a11y service and call its method.");
+ accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ try {
+ accService.getAccesssibleFor(document);
+ ok(false, "getAccesssibleFor should've triggered an exception.");
+ } catch (e) {
+ ok(
+ true,
+ "getAccesssibleFor triggers an exception as a11y service is shutdown."
+ );
+ }
+ ok(!Services.appinfo.accessibilityEnabled, "Accessibility is disabled");
+
+ info("Reset force disabled preference");
+ Services.prefs.clearUserPref(PREF_ACCESSIBILITY_FORCE_DISABLED);
+
+ info("Create a11y service again");
+ [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ await a11yInit;
+ ok(Services.appinfo.accessibilityEnabled, "Accessibility is enabled");
+
+ info("Remove all references to a11y service");
+ [a11yShutdownObserver, a11yShutdown] = shutdownAccService();
+ await a11yShutdownObserver;
+
+ accService = null;
+ forceGC();
+ await a11yShutdown;
+ ok(!Services.appinfo.accessibilityEnabled, "Accessibility is disabled");
+});
diff --git a/accessible/tests/browser/browser_shutdown_proxy_acc_reference.js b/accessible/tests/browser/browser_shutdown_proxy_acc_reference.js
new file mode 100644
index 0000000000..7144cff019
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_proxy_acc_reference.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized");
+ await a11yInit;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body><div id="div" style="visibility: hidden;"></div></body>
+ </html>`,
+ },
+ async function (browser) {
+ let onShow = waitForEvent(Ci.nsIAccessibleEvent.EVENT_SHOW, "div");
+ await invokeSetStyle(browser, "div", "visibility", "visible");
+ let showEvent = await onShow;
+ let divAcc = showEvent.accessible;
+ ok(divAcc, "Accessible proxy is created");
+ // Remove unnecessary dangling references
+ onShow = null;
+ showEvent = null;
+ forceGC();
+
+ let canShutdown = false;
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+ // Force garbage collection that should not trigger shutdown because there
+ // is a reference to an accessible proxy.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a last reference to an accessible proxy.
+ divAcc = null;
+ ok(!divAcc, "Accessible proxy is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+ }
+ );
+});
diff --git a/accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js b/accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js
new file mode 100644
index 0000000000..6d4ad71f1e
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ let docLoaded = waitForEvent(
+ Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE,
+ "body"
+ );
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized");
+ await a11yInit;
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body id="body"></body>
+ </html>`,
+ },
+ async function (browser) {
+ let docLoadedEvent = await docLoaded;
+ let docAcc = docLoadedEvent.accessibleDocument;
+ ok(docAcc, "Accessible document proxy is created");
+ // Remove unnecessary dangling references
+ docLoaded = null;
+ docLoadedEvent = null;
+ forceGC();
+
+ let canShutdown = false;
+ const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService();
+ await a11yShutdownObserver;
+ const a11yShutdown = new Promise((resolve, reject) =>
+ a11yShutdownPromise.then(flag =>
+ canShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ accService = null;
+ ok(!accService, "Service is removed");
+ // Force garbage collection that should not trigger shutdown because there
+ // is a reference to an accessible proxy.
+ forceGC();
+ // Have some breathing room when removing a11y service references.
+ await TestUtils.waitForTick();
+
+ // Now allow a11y service to shutdown.
+ canShutdown = true;
+ // Remove a last reference to an accessible document proxy.
+ docAcc = null;
+ ok(!docAcc, "Accessible document proxy is removed");
+
+ // Force garbage collection that should now trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+ }
+ );
+});
diff --git a/accessible/tests/browser/browser_shutdown_remote_no_reference.js b/accessible/tests/browser/browser_shutdown_remote_no_reference.js
new file mode 100644
index 0000000000..6a38ff15b8
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_remote_no_reference.js
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body></body>
+ </html>`,
+ },
+ async function (browser) {
+ info(
+ "Creating a service in parent and waiting for service to be created " +
+ "in content"
+ );
+ await loadContentScripts(browser, {
+ script: "Common.sys.mjs",
+ symbol: "CommonUtils",
+ });
+ // Create a11y service in the main process. This will trigger creating of
+ // the a11y service in parent as well.
+ const [parentA11yInitObserver, parentA11yInit] = initAccService();
+ const [contentA11yInitObserver, contentA11yInit] =
+ initAccService(browser);
+ let [parentConsumersChangedObserver, parentConsumersChanged] =
+ accConsumersChanged();
+ let [contentConsumersChangedObserver, contentConsumersChanged] =
+ accConsumersChanged(browser);
+
+ await Promise.all([
+ parentA11yInitObserver,
+ contentA11yInitObserver,
+ parentConsumersChangedObserver,
+ contentConsumersChangedObserver,
+ ]);
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized in parent");
+ await Promise.all([parentA11yInit, contentA11yInit]);
+ await parentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: true,
+ MainProcess: false,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in parent are correct."
+ )
+ );
+ await contentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: false,
+ MainProcess: true,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in content are correct."
+ )
+ );
+
+ Assert.deepEqual(
+ JSON.parse(accService.getConsumers()),
+ {
+ XPCOM: true,
+ MainProcess: false,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in parent are correct."
+ );
+
+ info(
+ "Removing a service in parent and waiting for service to be shut " +
+ "down in content"
+ );
+ // Remove a11y service reference in the main process.
+ const [parentA11yShutdownObserver, parentA11yShutdown] =
+ shutdownAccService();
+ const [contentA11yShutdownObserver, contentA11yShutdown] =
+ shutdownAccService(browser);
+ [parentConsumersChangedObserver, parentConsumersChanged] =
+ accConsumersChanged();
+ [contentConsumersChangedObserver, contentConsumersChanged] =
+ accConsumersChanged(browser);
+
+ await Promise.all([
+ parentA11yShutdownObserver,
+ contentA11yShutdownObserver,
+ parentConsumersChangedObserver,
+ contentConsumersChangedObserver,
+ ]);
+
+ accService = null;
+ ok(!accService, "Service is removed in parent");
+ // Force garbage collection that should trigger shutdown in both main and
+ // content process.
+ forceGC();
+ await Promise.all([parentA11yShutdown, contentA11yShutdown]);
+ await parentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: false,
+ MainProcess: false,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers are correct."
+ )
+ );
+ await contentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: false,
+ MainProcess: false,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers are correct."
+ )
+ );
+ }
+ );
+});
diff --git a/accessible/tests/browser/browser_shutdown_remote_only.js b/accessible/tests/browser/browser_shutdown_remote_only.js
new file mode 100644
index 0000000000..95c3604400
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_remote_only.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body></body>
+ </html>`,
+ },
+ async function (browser) {
+ info("Creating a service in content");
+ await loadContentScripts(browser, {
+ script: "Common.sys.mjs",
+ symbol: "CommonUtils",
+ });
+ // Create a11y service in the content process.
+ const [a11yInitObserver, a11yInit] = initAccService(browser);
+ await a11yInitObserver;
+ await SpecialPowers.spawn(browser, [], () => {
+ content.CommonUtils.accService;
+ });
+ await a11yInit;
+ ok(
+ true,
+ "Accessibility service is started in content process correctly."
+ );
+
+ info("Removing a service in content");
+ // Remove a11y service reference from the content process.
+ const [a11yShutdownObserver, a11yShutdown] = shutdownAccService(browser);
+ await a11yShutdownObserver;
+ // Force garbage collection that should trigger shutdown.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.CommonUtils.clearAccService();
+ });
+ await a11yShutdown;
+ ok(
+ true,
+ "Accessibility service is shutdown in content process correctly."
+ );
+ }
+ );
+});
diff --git a/accessible/tests/browser/browser_shutdown_remote_own_reference.js b/accessible/tests/browser/browser_shutdown_remote_own_reference.js
new file mode 100644
index 0000000000..a30d191b53
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_remote_own_reference.js
@@ -0,0 +1,175 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: `data:text/html,
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body></body>
+ </html>`,
+ },
+ async function (browser) {
+ info(
+ "Creating a service in parent and waiting for service to be created " +
+ "in content"
+ );
+ await loadContentScripts(browser, {
+ script: "Common.sys.mjs",
+ symbol: "CommonUtils",
+ });
+ // Create a11y service in the main process. This will trigger creating of
+ // the a11y service in parent as well.
+ const [parentA11yInitObserver, parentA11yInit] = initAccService();
+ const [contentA11yInitObserver, contentA11yInit] =
+ initAccService(browser);
+ let [contentConsumersChangedObserver, contentConsumersChanged] =
+ accConsumersChanged(browser);
+
+ await Promise.all([
+ parentA11yInitObserver,
+ contentA11yInitObserver,
+ contentConsumersChangedObserver,
+ ]);
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized in parent");
+ await Promise.all([parentA11yInit, contentA11yInit]);
+ await contentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: false,
+ MainProcess: true,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in content are correct."
+ )
+ );
+
+ info(
+ "Adding additional reference to accessibility service in content " +
+ "process"
+ );
+ [contentConsumersChangedObserver, contentConsumersChanged] =
+ accConsumersChanged(browser);
+ await contentConsumersChangedObserver;
+ // Add a new reference to the a11y service inside the content process.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.CommonUtils.accService;
+ });
+ await contentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: true,
+ MainProcess: true,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in content are correct."
+ )
+ );
+
+ const contentConsumers = await SpecialPowers.spawn(browser, [], () =>
+ content.CommonUtils.accService.getConsumers()
+ );
+ Assert.deepEqual(
+ JSON.parse(contentConsumers),
+ {
+ XPCOM: true,
+ MainProcess: true,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in parent are correct."
+ );
+
+ info(
+ "Shutting down a service in parent and making sure the one in " +
+ "content stays alive"
+ );
+ let contentCanShutdown = false;
+ const [parentA11yShutdownObserver, parentA11yShutdown] =
+ shutdownAccService();
+ [contentConsumersChangedObserver, contentConsumersChanged] =
+ accConsumersChanged(browser);
+ // This promise will resolve only if contentCanShutdown flag is set to true.
+ // If 'a11y-init-or-shutdown' event with '0' flag (in content) comes before
+ // it can be shut down, the promise will reject.
+ const [contentA11yShutdownObserver, contentA11yShutdownPromise] =
+ shutdownAccService(browser);
+ const contentA11yShutdown = new Promise((resolve, reject) =>
+ contentA11yShutdownPromise.then(flag =>
+ contentCanShutdown
+ ? resolve()
+ : reject("Accessible service was shut down incorrectly")
+ )
+ );
+
+ await Promise.all([
+ parentA11yShutdownObserver,
+ contentA11yShutdownObserver,
+ contentConsumersChangedObserver,
+ ]);
+ // Remove a11y service reference in the main process and force garbage
+ // collection. This should not trigger shutdown in content since a11y
+ // service is used by XPCOM.
+ accService = null;
+ ok(!accService, "Service is removed in parent");
+ // Force garbage collection that should not trigger shutdown because there
+ // is a reference in a content process.
+ forceGC();
+ await SpecialPowers.spawn(browser, [], () => {
+ SpecialPowers.Cu.forceGC();
+ });
+ await parentA11yShutdown;
+ await contentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: true,
+ MainProcess: false,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in content are correct."
+ )
+ );
+
+ // Have some breathing room between a11y service shutdowns.
+ await TestUtils.waitForTick();
+
+ info("Removing a service in content");
+ // Now allow a11y service to shutdown in content.
+ contentCanShutdown = true;
+ [contentConsumersChangedObserver, contentConsumersChanged] =
+ accConsumersChanged(browser);
+ await contentConsumersChangedObserver;
+ // Remove last reference to a11y service in content and force garbage
+ // collection that should trigger shutdown.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.CommonUtils.clearAccService();
+ });
+ await contentA11yShutdown;
+ await contentConsumersChanged.then(data =>
+ Assert.deepEqual(
+ data,
+ {
+ XPCOM: false,
+ MainProcess: false,
+ PlatformAPI: false,
+ },
+ "Accessibility service consumers in content are correct."
+ )
+ );
+ }
+ );
+});
diff --git a/accessible/tests/browser/browser_shutdown_scope_lifecycle.js b/accessible/tests/browser/browser_shutdown_scope_lifecycle.js
new file mode 100644
index 0000000000..fafa59bb50
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_scope_lifecycle.js
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ // Create a11y service inside of the function scope. Its reference should be
+ // released once the anonimous function is called.
+ const [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+ const a11yInitThenShutdown = a11yInit.then(async () => {
+ const [a11yShutdownObserver, a11yShutdown] = shutdownAccService();
+ await a11yShutdownObserver;
+ return a11yShutdown;
+ });
+
+ (function () {
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ ok(accService, "Service initialized");
+ })();
+
+ // Force garbage collection that should trigger shutdown.
+ forceGC();
+ await a11yInitThenShutdown;
+});
diff --git a/accessible/tests/browser/browser_shutdown_start_restart.js b/accessible/tests/browser/browser_shutdown_start_restart.js
new file mode 100644
index 0000000000..92d2823388
--- /dev/null
+++ b/accessible/tests/browser/browser_shutdown_start_restart.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function () {
+ info("Creating a service");
+ // Create a11y service.
+ let [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ await a11yInit;
+ ok(accService, "Service initialized");
+
+ info("Removing a service");
+ // Remove the only reference to an a11y service.
+ let [a11yShutdownObserver, a11yShutdown] = shutdownAccService();
+ await a11yShutdownObserver;
+
+ accService = null;
+ ok(!accService, "Service is removed");
+ // Force garbage collection that should trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+
+ info("Recreating a service");
+ // Re-create a11y service.
+ [a11yInitObserver, a11yInit] = initAccService();
+ await a11yInitObserver;
+
+ accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+ await a11yInit;
+ ok(accService, "Service initialized again");
+
+ info("Removing a service again");
+ // Remove the only reference to an a11y service again.
+ [a11yShutdownObserver, a11yShutdown] = shutdownAccService();
+ await a11yShutdownObserver;
+
+ accService = null;
+ ok(!accService, "Service is removed again");
+ // Force garbage collection that should trigger shutdown.
+ forceGC();
+ await a11yShutdown;
+});
diff --git a/accessible/tests/browser/e10s/browser.toml b/accessible/tests/browser/e10s/browser.toml
new file mode 100644
index 0000000000..dfac6b5219
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser.toml
@@ -0,0 +1,125 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "doc_treeupdate_ariadialog.html",
+ "doc_treeupdate_ariaowns.html",
+ "doc_treeupdate_imagemap.html",
+ "doc_treeupdate_removal.xhtml",
+ "doc_treeupdate_visibility.html",
+ "doc_treeupdate_whitespace.html",
+ "fonts/Ahem.sjs",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/events/slow_image.sjs",
+ "!/accessible/tests/mochitest/letters.gif",
+ "!/accessible/tests/mochitest/moz.png",
+]
+prefs = [
+ "javascript.options.asyncstack_capture_debuggee_only=false",
+ "dom.element.popover.enabled=true"
+]
+
+# Caching tests
+["browser_caching_actions.js"]
+
+["browser_caching_attributes.js"]
+
+["browser_caching_description.js"]
+
+["browser_caching_document_props.js"]
+
+["browser_caching_domnodeid.js"]
+
+["browser_caching_hyperlink.js"]
+
+["browser_caching_innerHTML.js"]
+skip-if = ["os != 'win'"]
+
+["browser_caching_interfaces.js"]
+
+["browser_caching_large_update.js"]
+
+["browser_caching_name.js"]
+
+["browser_caching_position.js"]
+
+["browser_caching_relations.js"]
+
+["browser_caching_relations_002.js"]
+
+["browser_caching_states.js"]
+
+["browser_caching_table.js"]
+
+["browser_caching_text_bounds.js"]
+
+["browser_caching_uniqueid.js"]
+
+["browser_caching_value.js"]
+
+# Events tests
+["browser_events_announcement.js"]
+skip-if = ["os == 'win'"] # Bug 1288839
+
+["browser_events_caretmove.js"]
+
+["browser_events_hide.js"]
+
+["browser_events_show.js"]
+
+["browser_events_statechange.js"]
+
+["browser_events_textchange.js"]
+
+["browser_file_input.js"]
+
+["browser_language.js"]
+
+["browser_obj_group.js"]
+
+["browser_obj_group_002.js"]
+
+# Tree update tests
+["browser_treeupdate_ariadialog.js"]
+
+["browser_treeupdate_ariaowns.js"]
+
+["browser_treeupdate_canvas.js"]
+
+["browser_treeupdate_csscontentvisibility.js"]
+
+["browser_treeupdate_cssoverflow.js"]
+
+["browser_treeupdate_doc.js"]
+
+["browser_treeupdate_gencontent.js"]
+
+["browser_treeupdate_hidden.js"]
+
+["browser_treeupdate_image.js"]
+
+["browser_treeupdate_imagemap.js"]
+
+["browser_treeupdate_list.js"]
+
+["browser_treeupdate_list_editabledoc.js"]
+
+["browser_treeupdate_listener.js"]
+
+["browser_treeupdate_move.js"]
+
+["browser_treeupdate_optgroup.js"]
+
+["browser_treeupdate_removal.js"]
+
+["browser_treeupdate_select_dropdown.js"]
+
+["browser_treeupdate_table.js"]
+
+["browser_treeupdate_textleaf.js"]
+
+["browser_treeupdate_visibility.js"]
+
+["browser_treeupdate_whitespace.js"]
diff --git a/accessible/tests/browser/e10s/browser_caching_actions.js b/accessible/tests/browser/e10s/browser_caching_actions.js
new file mode 100644
index 0000000000..893c818b75
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_actions.js
@@ -0,0 +1,295 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const gClickEvents = ["mousedown", "mouseup", "click"];
+
+const gActionDescrMap = {
+ jump: "Jump",
+ press: "Press",
+ check: "Check",
+ uncheck: "Uncheck",
+ select: "Select",
+ open: "Open",
+ close: "Close",
+ switch: "Switch",
+ click: "Click",
+ collapse: "Collapse",
+ expand: "Expand",
+ activate: "Activate",
+ cycle: "Cycle",
+ "click ancestor": "Click ancestor",
+};
+
+async function testActions(browser, docAcc, id, expectedActions, domEvents) {
+ const acc = findAccessibleChildByID(docAcc, id);
+ is(acc.actionCount, expectedActions.length, "Correct action count");
+
+ let actionNames = [];
+ let actionDescriptions = [];
+ for (let i = 0; i < acc.actionCount; i++) {
+ actionNames.push(acc.getActionName(i));
+ actionDescriptions.push(acc.getActionDescription(i));
+ }
+
+ is(actionNames.join(","), expectedActions.join(","), "Correct action names");
+ is(
+ actionDescriptions.join(","),
+ expectedActions.map(a => gActionDescrMap[a]).join(","),
+ "Correct action descriptions"
+ );
+
+ if (!domEvents) {
+ return;
+ }
+
+ // We need to set up the listener, and wait for the promise in two separate
+ // content tasks.
+ await invokeContentTask(browser, [id, domEvents], (_id, _domEvents) => {
+ let promises = _domEvents.map(
+ evtName =>
+ new Promise(resolve => {
+ const listener = e => {
+ if (e.target.id == _id) {
+ content.removeEventListener(evtName, listener);
+ content.evtPromise = null;
+ resolve(42);
+ }
+ };
+ content.addEventListener(evtName, listener);
+ })
+ );
+ content.evtPromise = Promise.all(promises);
+ });
+
+ acc.doAction(0);
+
+ let eventFired = await invokeContentTask(browser, [], async () => {
+ await content.evtPromise;
+ return true;
+ });
+
+ ok(eventFired, `DOM events fired '${domEvents}'`);
+}
+
+addAccessibleTask(
+ `<ul>
+ <li id="li_clickable1" onclick="">Clickable list item</li>
+ <li id="li_clickable2" onmousedown="">Clickable list item</li>
+ <li id="li_clickable3" onmouseup="">Clickable list item</li>
+ </ul>
+
+ <img id="onclick_img" onclick=""
+ src="http://example.com/a11y/accessible/tests/mochitest/moz.png">
+
+ <a id="link1" href="#">linkable textleaf accessible</a>
+ <div id="link2" onclick="">linkable textleaf accessible</div>
+
+ <a id="link3" href="#">
+ <img id="link3img" alt="image in link"
+ src="http://example.com/a11y/accessible/tests/mochitest/moz.png">
+ </a>
+
+ <a href="about:mozilla" id="link4" target="_blank" rel="opener">
+ <img src="../moz.png" id="link4img">
+ </a>
+ <a id="link5" onmousedown="">
+ <img src="../moz.png" id="link5img">
+ </a>
+ <a id="link6" onclick="">
+ <img src="../moz.png" id="link6img">
+ </a>
+ <a id="link7" onmouseup="">
+ <img src="../moz.png" id="link7img">
+ </a>
+
+ <div>
+ <label for="TextBox_t2" id="label1">
+ <span>Explicit</span>
+ </label>
+ <input name="in2" id="TextBox_t2" type="text" maxlength="17">
+ </div>
+
+ <div onclick=""><p id="p_in_clickable_div">p in clickable div</p></div>
+ `,
+ async function (browser, docAcc) {
+ is(docAcc.actionCount, 0, "Doc should not have any actions");
+
+ const _testActions = async (id, expectedActions, domEvents) => {
+ await testActions(browser, docAcc, id, expectedActions, domEvents);
+ };
+
+ await _testActions("li_clickable1", ["click"], gClickEvents);
+ await _testActions("li_clickable2", ["click"], gClickEvents);
+ await _testActions("li_clickable3", ["click"], gClickEvents);
+
+ await _testActions("onclick_img", ["click"], gClickEvents);
+ await _testActions("link1", ["jump"], gClickEvents);
+ await _testActions("link2", ["click"], gClickEvents);
+ await _testActions("link3", ["jump"], gClickEvents);
+ await _testActions("link3img", ["click ancestor"], gClickEvents);
+ await _testActions("link4", ["jump"], gClickEvents);
+ await _testActions("link4img", ["click ancestor"], gClickEvents);
+ await _testActions("link5", ["click"], gClickEvents);
+ await _testActions("link5img", ["click ancestor"], gClickEvents);
+ await _testActions("link6", ["click"], gClickEvents);
+ await _testActions("link6img", ["click ancestor"], gClickEvents);
+ await _testActions("link7", ["click"], gClickEvents);
+ await _testActions("link7img", ["click ancestor"], gClickEvents);
+ await _testActions("label1", ["click"], gClickEvents);
+ await _testActions("p_in_clickable_div", ["click ancestor"], gClickEvents);
+
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("li_clickable1")
+ .removeAttribute("onclick");
+ });
+
+ let acc = findAccessibleChildByID(docAcc, "li_clickable1");
+ await untilCacheIs(() => acc.actionCount, 0, "li has no actions");
+ let thrown = false;
+ try {
+ acc.doAction(0);
+ } catch (e) {
+ thrown = true;
+ }
+ ok(thrown, "doAction should throw exception");
+
+ // Remove 'for' from label
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("label1").removeAttribute("for");
+ });
+ acc = findAccessibleChildByID(docAcc, "label1");
+ await untilCacheIs(() => acc.actionCount, 0, "label has no actions");
+ thrown = false;
+ try {
+ acc.doAction(0);
+ ok(false, "doAction should throw exception");
+ } catch (e) {
+ thrown = true;
+ }
+ ok(thrown, "doAction should throw exception");
+
+ // Add 'longdesc' to image
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("onclick_img")
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ .setAttribute("longdesc", "http://example.com");
+ });
+ acc = findAccessibleChildByID(docAcc, "onclick_img");
+ await untilCacheIs(() => acc.actionCount, 2, "img has 2 actions");
+ await _testActions("onclick_img", ["click", "showlongdesc"]);
+
+ // Remove 'onclick' from image with 'longdesc'
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("onclick_img").removeAttribute("onclick");
+ });
+ acc = findAccessibleChildByID(docAcc, "onclick_img");
+ await untilCacheIs(() => acc.actionCount, 1, "img has 1 actions");
+ await _testActions("onclick_img", ["showlongdesc"]);
+
+ // Remove 'href' from link and test linkable child
+ let link1Acc = findAccessibleChildByID(docAcc, "link1");
+ is(
+ link1Acc.firstChild.getActionName(0),
+ "click ancestor",
+ "linkable child has click ancestor action"
+ );
+ let onRecreation = waitForEvents({
+ expected: [
+ [EVENT_HIDE, link1Acc],
+ [EVENT_SHOW, "link1"],
+ ],
+ });
+ await invokeContentTask(browser, [], () => {
+ let link1 = content.document.getElementById("link1");
+ link1.removeAttribute("href");
+ });
+ await onRecreation;
+ link1Acc = findAccessibleChildByID(docAcc, "link1");
+ await untilCacheIs(() => link1Acc.actionCount, 0, "link has no actions");
+ is(link1Acc.firstChild.actionCount, 0, "linkable child's actions removed");
+
+ // Add a click handler to the body. Ensure it propagates to descendants.
+ await invokeContentTask(browser, [], () => {
+ content.document.body.onclick = () => {};
+ });
+ await untilCacheIs(() => docAcc.actionCount, 1, "Doc has 1 action");
+ await _testActions("link1", ["click ancestor"]);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.body.onclick = null;
+ });
+ await untilCacheIs(() => docAcc.actionCount, 0, "Doc has no actions");
+ is(link1Acc.actionCount, 0, "link has no actions");
+
+ // Add a click handler to the root element. Ensure it propagates to
+ // descendants.
+ await invokeContentTask(browser, [], () => {
+ content.document.documentElement.onclick = () => {};
+ });
+ await untilCacheIs(() => docAcc.actionCount, 1, "Doc has 1 action");
+ await _testActions("link1", ["click ancestor"]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test access key.
+ */
+addAccessibleTask(
+ `
+<button id="noKey">noKey</button>
+<button id="key" accesskey="a">key</button>
+ `,
+ async function (browser, docAcc) {
+ const noKey = findAccessibleChildByID(docAcc, "noKey");
+ is(noKey.accessKey, "", "noKey has no accesskey");
+ const key = findAccessibleChildByID(docAcc, "key");
+ is(key.accessKey, MAC ? "⌃⌥a" : "Alt+Shift+a", "key has correct accesskey");
+
+ info("Changing accesskey");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("key").accessKey = "b";
+ });
+ await untilCacheIs(
+ () => key.accessKey,
+ MAC ? "⌃⌥b" : "Alt+Shift+b",
+ "Correct accesskey after change"
+ );
+
+ info("Removing accesskey");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("key").removeAttribute("accesskey");
+ });
+ await untilCacheIs(
+ () => key.accessKey,
+ "",
+ "Empty accesskey after removal"
+ );
+
+ info("Adding accesskey");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("key").accessKey = "c";
+ });
+ await untilCacheIs(
+ () => key.accessKey,
+ MAC ? "⌃⌥c" : "Alt+Shift+c",
+ "Correct accesskey after addition"
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: false, // Bug 1796846
+ remoteIframe: false, // Bug 1796846
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_attributes.js b/accessible/tests/browser/e10s/browser_caching_attributes.js
new file mode 100644
index 0000000000..139015061f
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_attributes.js
@@ -0,0 +1,763 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Default textbox accessible attributes.
+ */
+const defaultAttributes = {
+ "margin-top": "0px",
+ "margin-right": "0px",
+ "margin-bottom": "0px",
+ "margin-left": "0px",
+ "text-align": "start",
+ "text-indent": "0px",
+ id: "textbox",
+ tag: "input",
+ display: "inline-block",
+};
+
+/**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * expected {Object} expected attributes for given accessibles
+ * unexpected {Object} unexpected attributes for given accessibles
+ *
+ * action {?AsyncFunction} an optional action that awaits a change in
+ * attributes
+ * attrs {?Array} an optional list of attributes to update
+ * waitFor {?Number} an optional event to wait for
+ * }
+ */
+const attributesTests = [
+ {
+ desc: "Initiall accessible attributes",
+ expected: defaultAttributes,
+ unexpected: {
+ "line-number": "1",
+ "explicit-name": "true",
+ "container-live": "polite",
+ live: "polite",
+ },
+ },
+ {
+ desc: "@line-number attribute is present when textbox is focused",
+ async action(browser) {
+ await invokeFocus(browser, "textbox");
+ },
+ waitFor: EVENT_FOCUS,
+ expected: Object.assign({}, defaultAttributes, { "line-number": "1" }),
+ unexpected: {
+ "explicit-name": "true",
+ "container-live": "polite",
+ live: "polite",
+ },
+ },
+ {
+ desc: "@aria-live sets container-live and live attributes",
+ attrs: [
+ {
+ attr: "aria-live",
+ value: "polite",
+ },
+ ],
+ expected: Object.assign({}, defaultAttributes, {
+ "line-number": "1",
+ "container-live": "polite",
+ live: "polite",
+ }),
+ unexpected: {
+ "explicit-name": "true",
+ },
+ },
+ {
+ desc: "@title attribute sets explicit-name attribute to true",
+ attrs: [
+ {
+ attr: "title",
+ value: "textbox",
+ },
+ ],
+ expected: Object.assign({}, defaultAttributes, {
+ "line-number": "1",
+ "explicit-name": "true",
+ "container-live": "polite",
+ live: "polite",
+ }),
+ unexpected: {},
+ },
+];
+
+/**
+ * Test caching of accessible object attributes
+ */
+addAccessibleTask(
+ `
+ <input id="textbox" value="hello">`,
+ async function (browser, accDoc) {
+ let textbox = findAccessibleChildByID(accDoc, "textbox");
+ for (let {
+ desc,
+ action,
+ attrs,
+ expected,
+ waitFor,
+ unexpected,
+ } of attributesTests) {
+ info(desc);
+ let onUpdate;
+
+ if (waitFor) {
+ onUpdate = waitForEvent(waitFor, "textbox");
+ }
+
+ if (action) {
+ await action(browser);
+ } else if (attrs) {
+ for (let { attr, value } of attrs) {
+ await invokeSetAttribute(browser, "textbox", attr, value);
+ }
+ }
+
+ await onUpdate;
+ testAttrs(textbox, expected);
+ testAbsentAttrs(textbox, unexpected);
+ }
+ },
+ {
+ // These tests don't work yet with the parent process cache.
+ topLevel: false,
+ iframe: false,
+ remoteIframe: false,
+ }
+);
+
+/**
+ * Test caching of the tag attribute.
+ */
+addAccessibleTask(
+ `
+<p id="p">text</p>
+<textarea id="textarea"></textarea>
+ `,
+ async function (browser, docAcc) {
+ testAttrs(docAcc, { tag: "body" }, true);
+ const p = findAccessibleChildByID(docAcc, "p");
+ testAttrs(p, { tag: "p" }, true);
+ const textLeaf = p.firstChild;
+ testAbsentAttrs(textLeaf, { tag: "" });
+ const textarea = findAccessibleChildByID(docAcc, "textarea");
+ testAttrs(textarea, { tag: "textarea" }, true);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of the text-input-type attribute.
+ */
+addAccessibleTask(
+ `
+ <input id="default">
+ <input id="email" type="email">
+ <input id="password" type="password">
+ <input id="text" type="text">
+ <input id="date" type="date">
+ <input id="time" type="time">
+ <input id="checkbox" type="checkbox">
+ <input id="radio" type="radio">
+ `,
+ async function (browser, docAcc) {
+ function testInputType(id, inputType) {
+ if (inputType == undefined) {
+ testAbsentAttrs(findAccessibleChildByID(docAcc, id), {
+ "text-input-type": "",
+ });
+ } else {
+ testAttrs(
+ findAccessibleChildByID(docAcc, id),
+ { "text-input-type": inputType },
+ true
+ );
+ }
+ }
+
+ testInputType("default");
+ testInputType("email", "email");
+ testInputType("password", "password");
+ testInputType("text", "text");
+ testInputType("date", "date");
+ testInputType("time", "time");
+ testInputType("checkbox");
+ testInputType("radio");
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+/**
+ * Test caching of the display attribute.
+ */
+addAccessibleTask(
+ `
+<div id="div">
+ <ins id="ins">a</ins>
+ <button id="button">b</button>
+</div>
+<p>
+ <span id="presentationalSpan" role="none"
+ style="display: block; position: absolute; top: 0; left: 0; translate: 1px;">
+ a
+ </span>
+</p>
+ `,
+ async function (browser, docAcc) {
+ const div = findAccessibleChildByID(docAcc, "div");
+ testAttrs(div, { display: "block" }, true);
+ const ins = findAccessibleChildByID(docAcc, "ins");
+ testAttrs(ins, { display: "inline" }, true);
+ const textLeaf = ins.firstChild;
+ testAbsentAttrs(textLeaf, { display: "" });
+ const button = findAccessibleChildByID(docAcc, "button");
+ testAttrs(button, { display: "inline-block" }, true);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("ins").style.display = "block";
+ content.document.body.offsetTop; // Flush layout.
+ });
+ await untilCacheIs(
+ () => ins.attributes.getStringProperty("display"),
+ "block",
+ "ins display attribute changed to block"
+ );
+
+ // This span has role="none", but we force a generic Accessible because it
+ // has a transform. role="none" might have been used to avoid exposing
+ // display: block, so ensure we don't expose that.
+ const presentationalSpan = findAccessibleChildByID(
+ docAcc,
+ "presentationalSpan"
+ );
+ testAbsentAttrs(presentationalSpan, { display: "" });
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test that there is no display attribute on image map areas.
+ */
+addAccessibleTask(
+ `
+<map name="normalMap">
+ <area id="normalArea" shape="default">
+</map>
+<img src="http://example.com/a11y/accessible/tests/mochitest/moz.png" usemap="#normalMap">
+<audio>
+ <map name="unslottedMap">
+ <area id="unslottedArea" shape="default">
+ </map>
+</audio>
+<img src="http://example.com/a11y/accessible/tests/mochitest/moz.png" usemap="#unslottedMap">
+ `,
+ async function (browser, docAcc) {
+ const normalArea = findAccessibleChildByID(docAcc, "normalArea");
+ testAbsentAttrs(normalArea, { display: "" });
+ const unslottedArea = findAccessibleChildByID(docAcc, "unslottedArea");
+ testAbsentAttrs(unslottedArea, { display: "" });
+ },
+ { topLevel: true }
+);
+
+/**
+ * Test caching of the explicit-name attribute.
+ */
+addAccessibleTask(
+ `
+<h1 id="h1">content</h1>
+<button id="buttonContent">content</button>
+<button id="buttonLabel" aria-label="label">content</button>
+<button id="buttonEmpty"></button>
+<button id="buttonSummary"><details><summary>test</summary></details></button>
+<div id="div"></div>
+ `,
+ async function (browser, docAcc) {
+ const h1 = findAccessibleChildByID(docAcc, "h1");
+ testAbsentAttrs(h1, { "explicit-name": "" });
+ const buttonContent = findAccessibleChildByID(docAcc, "buttonContent");
+ testAbsentAttrs(buttonContent, { "explicit-name": "" });
+ const buttonLabel = findAccessibleChildByID(docAcc, "buttonLabel");
+ testAttrs(buttonLabel, { "explicit-name": "true" }, true);
+ const buttonEmpty = findAccessibleChildByID(docAcc, "buttonEmpty");
+ testAbsentAttrs(buttonEmpty, { "explicit-name": "" });
+ const buttonSummary = findAccessibleChildByID(docAcc, "buttonSummary");
+ testAbsentAttrs(buttonSummary, { "explicit-name": "" });
+ const div = findAccessibleChildByID(docAcc, "div");
+ testAbsentAttrs(div, { "explicit-name": "" });
+
+ info("Setting aria-label on h1");
+ let nameChanged = waitForEvent(EVENT_NAME_CHANGE, h1);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("h1").setAttribute("aria-label", "label");
+ });
+ await nameChanged;
+ testAttrs(h1, { "explicit-name": "true" }, true);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of ARIA attributes that are exposed via object attributes.
+ */
+addAccessibleTask(
+ `
+<div id="currentTrue" aria-current="true">currentTrue</div>
+<div id="currentFalse" aria-current="false">currentFalse</div>
+<div id="currentPage" aria-current="page">currentPage</div>
+<div id="currentBlah" aria-current="blah">currentBlah</div>
+<div id="haspopupMenu" aria-haspopup="menu">haspopup</div>
+<div id="rowColCountPositive" role="table" aria-rowcount="1000" aria-colcount="1000">
+ <div role="row">
+ <div id="rowColIndexPositive" role="cell" aria-rowindex="100" aria-colindex="100">positive</div>
+ </div>
+</div>
+<div id="rowColCountNegative" role="table" aria-rowcount="-1" aria-colcount="-1">
+ <div role="row">
+ <div id="rowColIndexNegative" role="cell" aria-rowindex="-1" aria-colindex="-1">negative</div>
+ </div>
+</div>
+<div id="rowColCountInvalid" role="table" aria-rowcount="z" aria-colcount="z">
+ <div role="row">
+ <div id="rowColIndexInvalid" role="cell" aria-rowindex="z" aria-colindex="z">invalid</div>
+ </div>
+</div>
+<div id="foo" aria-foo="bar">foo</div>
+<div id="mutate" aria-current="true">mutate</div>
+ `,
+ async function (browser, docAcc) {
+ const currentTrue = findAccessibleChildByID(docAcc, "currentTrue");
+ testAttrs(currentTrue, { current: "true" }, true);
+ const currentFalse = findAccessibleChildByID(docAcc, "currentFalse");
+ testAbsentAttrs(currentFalse, { current: "" });
+ const currentPage = findAccessibleChildByID(docAcc, "currentPage");
+ testAttrs(currentPage, { current: "page" }, true);
+ // Test that token normalization works.
+ const currentBlah = findAccessibleChildByID(docAcc, "currentBlah");
+ testAttrs(currentBlah, { current: "true" }, true);
+ const haspopupMenu = findAccessibleChildByID(docAcc, "haspopupMenu");
+ testAttrs(haspopupMenu, { haspopup: "menu" }, true);
+
+ // Test normalization of integer values.
+ const rowColCountPositive = findAccessibleChildByID(
+ docAcc,
+ "rowColCountPositive"
+ );
+ testAttrs(
+ rowColCountPositive,
+ { rowcount: "1000", colcount: "1000" },
+ true
+ );
+ const rowColIndexPositive = findAccessibleChildByID(
+ docAcc,
+ "rowColIndexPositive"
+ );
+ testAttrs(rowColIndexPositive, { rowindex: "100", colindex: "100" }, true);
+ const rowColCountNegative = findAccessibleChildByID(
+ docAcc,
+ "rowColCountNegative"
+ );
+ testAttrs(rowColCountNegative, { rowcount: "-1", colcount: "-1" }, true);
+ const rowColIndexNegative = findAccessibleChildByID(
+ docAcc,
+ "rowColIndexNegative"
+ );
+ testAbsentAttrs(rowColIndexNegative, { rowindex: "", colindex: "" });
+ const rowColCountInvalid = findAccessibleChildByID(
+ docAcc,
+ "rowColCountInvalid"
+ );
+ testAbsentAttrs(rowColCountInvalid, { rowcount: "", colcount: "" });
+ const rowColIndexInvalid = findAccessibleChildByID(
+ docAcc,
+ "rowColIndexInvalid"
+ );
+ testAbsentAttrs(rowColIndexInvalid, { rowindex: "", colindex: "" });
+
+ // Test that unknown aria- attributes get exposed.
+ const foo = findAccessibleChildByID(docAcc, "foo");
+ testAttrs(foo, { foo: "bar" }, true);
+
+ const mutate = findAccessibleChildByID(docAcc, "mutate");
+ testAttrs(mutate, { current: "true" }, true);
+ info("mutate: Removing aria-current");
+ let changed = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, mutate);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").removeAttribute("aria-current");
+ });
+ await changed;
+ testAbsentAttrs(mutate, { current: "" });
+ info("mutate: Adding aria-current");
+ changed = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, mutate);
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("mutate")
+ .setAttribute("aria-current", "page");
+ });
+ await changed;
+ testAttrs(mutate, { current: "page" }, true);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test support for the xml-roles attribute.
+ */
+addAccessibleTask(
+ `
+<div id="knownRole" role="main">knownRole</div>
+<div id="emptyRole" role="">emptyRole</div>
+<div id="unknownRole" role="foo">unknownRole</div>
+<div id="multiRole" role="foo main">multiRole</div>
+<main id="landmarkMarkup">landmarkMarkup</main>
+<main id="landmarkMarkupWithRole" role="banner">landmarkMarkupWithRole</main>
+<main id="landmarkMarkupWithEmptyRole" role="">landmarkMarkupWithEmptyRole</main>
+<article id="markup">markup</article>
+<article id="markupWithRole" role="banner">markupWithRole</article>
+<article id="markupWithEmptyRole" role="">markupWithEmptyRole</article>
+ `,
+ async function (browser, docAcc) {
+ const knownRole = findAccessibleChildByID(docAcc, "knownRole");
+ testAttrs(knownRole, { "xml-roles": "main" }, true);
+ const emptyRole = findAccessibleChildByID(docAcc, "emptyRole");
+ testAbsentAttrs(emptyRole, { "xml-roles": "" });
+ const unknownRole = findAccessibleChildByID(docAcc, "unknownRole");
+ testAttrs(unknownRole, { "xml-roles": "foo" }, true);
+ const multiRole = findAccessibleChildByID(docAcc, "multiRole");
+ testAttrs(multiRole, { "xml-roles": "foo main" }, true);
+ const landmarkMarkup = findAccessibleChildByID(docAcc, "landmarkMarkup");
+ testAttrs(landmarkMarkup, { "xml-roles": "main" }, true);
+ const landmarkMarkupWithRole = findAccessibleChildByID(
+ docAcc,
+ "landmarkMarkupWithRole"
+ );
+ testAttrs(landmarkMarkupWithRole, { "xml-roles": "banner" }, true);
+ const landmarkMarkupWithEmptyRole = findAccessibleChildByID(
+ docAcc,
+ "landmarkMarkupWithEmptyRole"
+ );
+ testAttrs(landmarkMarkupWithEmptyRole, { "xml-roles": "main" }, true);
+ const markup = findAccessibleChildByID(docAcc, "markup");
+ testAttrs(markup, { "xml-roles": "article" }, true);
+ const markupWithRole = findAccessibleChildByID(docAcc, "markupWithRole");
+ testAttrs(markupWithRole, { "xml-roles": "banner" }, true);
+ const markupWithEmptyRole = findAccessibleChildByID(
+ docAcc,
+ "markupWithEmptyRole"
+ );
+ testAttrs(markupWithEmptyRole, { "xml-roles": "article" }, true);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test lie region attributes.
+ */
+addAccessibleTask(
+ `
+<div id="noLive"><p>noLive</p></div>
+<output id="liveMarkup"><p>liveMarkup</p></output>
+<div id="ariaLive" aria-live="polite"><p>ariaLive</p></div>
+<div id="liveRole" role="log"><p>liveRole</p></div>
+<div id="nonLiveRole" role="group"><p>nonLiveRole</p></div>
+<div id="other" aria-atomic="true" aria-busy="true" aria-relevant="additions"><p>other</p></div>
+ `,
+ async function (browser, docAcc) {
+ const noLive = findAccessibleChildByID(docAcc, "noLive");
+ for (const acc of [noLive, noLive.firstChild]) {
+ testAbsentAttrs(acc, {
+ live: "",
+ "container-live": "",
+ "container-live-role": "",
+ atomic: "",
+ "container-atomic": "",
+ busy: "",
+ "container-busy": "",
+ relevant: "",
+ "container-relevant": "",
+ });
+ }
+ const liveMarkup = findAccessibleChildByID(docAcc, "liveMarkup");
+ testAttrs(liveMarkup, { live: "polite" }, true);
+ testAttrs(liveMarkup.firstChild, { "container-live": "polite" }, true);
+ const ariaLive = findAccessibleChildByID(docAcc, "ariaLive");
+ testAttrs(ariaLive, { live: "polite" }, true);
+ testAttrs(ariaLive.firstChild, { "container-live": "polite" }, true);
+ const liveRole = findAccessibleChildByID(docAcc, "liveRole");
+ testAttrs(liveRole, { live: "polite" }, true);
+ testAttrs(
+ liveRole.firstChild,
+ { "container-live": "polite", "container-live-role": "log" },
+ true
+ );
+ const nonLiveRole = findAccessibleChildByID(docAcc, "nonLiveRole");
+ testAbsentAttrs(nonLiveRole, { live: "" });
+ testAbsentAttrs(nonLiveRole.firstChild, {
+ "container-live": "",
+ "container-live-role": "",
+ });
+ const other = findAccessibleChildByID(docAcc, "other");
+ testAttrs(
+ other,
+ { atomic: "true", busy: "true", relevant: "additions" },
+ true
+ );
+ testAttrs(
+ other.firstChild,
+ {
+ "container-atomic": "true",
+ "container-busy": "true",
+ "container-relevant": "additions",
+ },
+ true
+ );
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test the id attribute.
+ */
+addAccessibleTask(
+ `
+<p id="withId">withId</p>
+<div id="noIdParent"><p>noId</p></div>
+ `,
+ async function (browser, docAcc) {
+ const withId = findAccessibleChildByID(docAcc, "withId");
+ testAttrs(withId, { id: "withId" }, true);
+ const noId = findAccessibleChildByID(docAcc, "noIdParent").firstChild;
+ testAbsentAttrs(noId, { id: "" });
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test the valuetext attribute.
+ */
+addAccessibleTask(
+ `
+<div id="valuenow" role="slider" aria-valuenow="1"></div>
+<div id="valuetext" role="slider" aria-valuetext="text"></div>
+<div id="noValue" role="button"></div>
+ `,
+ async function (browser, docAcc) {
+ const valuenow = findAccessibleChildByID(docAcc, "valuenow");
+ testAttrs(valuenow, { valuetext: "1" }, true);
+ const valuetext = findAccessibleChildByID(docAcc, "valuetext");
+ testAttrs(valuetext, { valuetext: "text" }, true);
+ const noValue = findAccessibleChildByID(docAcc, "noValue");
+ testAbsentAttrs(noValue, { valuetext: "valuetext" });
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+function untilCacheAttrIs(acc, attr, val, msg) {
+ return untilCacheOk(() => {
+ try {
+ return acc.attributes.getStringProperty(attr) == val;
+ } catch (e) {
+ return false;
+ }
+ }, msg);
+}
+
+function untilCacheAttrAbsent(acc, attr, msg) {
+ return untilCacheOk(() => {
+ try {
+ acc.attributes.getStringProperty(attr);
+ } catch (e) {
+ return true;
+ }
+ return false;
+ }, msg);
+}
+
+/**
+ * Test the class attribute.
+ */
+addAccessibleTask(
+ `
+<div id="oneClass" class="c1">oneClass</div>
+<div id="multiClass" class="c1 c2">multiClass</div>
+<div id="noClass">noClass</div>
+<div id="mutate">mutate</div>
+ `,
+ async function (browser, docAcc) {
+ const oneClass = findAccessibleChildByID(docAcc, "oneClass");
+ testAttrs(oneClass, { class: "c1" }, true);
+ const multiClass = findAccessibleChildByID(docAcc, "multiClass");
+ testAttrs(multiClass, { class: "c1 c2" }, true);
+ const noClass = findAccessibleChildByID(docAcc, "noClass");
+ testAbsentAttrs(noClass, { class: "" });
+
+ const mutate = findAccessibleChildByID(docAcc, "mutate");
+ testAbsentAttrs(mutate, { class: "" });
+ info("Adding class to mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").className = "c1 c2";
+ });
+ await untilCacheAttrIs(mutate, "class", "c1 c2", "mutate class correct");
+ info("Removing class from mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").removeAttribute("class");
+ });
+ await untilCacheAttrAbsent(mutate, "class", "mutate class not present");
+ },
+ { chrome: true, topLevel: true }
+);
+
+/**
+ * Test the src attribute.
+ */
+const kImgUrl = "https://example.com/a11y/accessible/tests/mochitest/moz.png";
+addAccessibleTask(
+ `
+<img id="noAlt" src="${kImgUrl}">
+<img id="alt" alt="alt" src="${kImgUrl}">
+<img id="mutate">
+ `,
+ async function (browser, docAcc) {
+ const noAlt = findAccessibleChildByID(docAcc, "noAlt");
+ testAttrs(noAlt, { src: kImgUrl }, true);
+ const alt = findAccessibleChildByID(docAcc, "alt");
+ testAttrs(alt, { src: kImgUrl }, true);
+
+ const mutate = findAccessibleChildByID(docAcc, "mutate");
+ testAbsentAttrs(mutate, { src: "" });
+ info("Adding src to mutate");
+ await invokeContentTask(browser, [kImgUrl], url => {
+ content.document.getElementById("mutate").src = url;
+ });
+ await untilCacheAttrIs(mutate, "src", kImgUrl, "mutate src correct");
+ info("Removing src from mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").removeAttribute("src");
+ });
+ await untilCacheAttrAbsent(mutate, "src", "mutate src not present");
+ },
+ { chrome: true, topLevel: true }
+);
+
+/**
+ * Test the placeholder attribute.
+ */
+addAccessibleTask(
+ `
+<input id="htmlWithLabel" aria-label="label" placeholder="HTML">
+<input id="htmlNoLabel" placeholder="HTML">
+<input id="ariaWithLabel" aria-label="label" aria-placeholder="ARIA">
+<input id="ariaNoLabel" aria-placeholder="ARIA">
+<input id="both" aria-label="label" placeholder="HTML" aria-placeholder="ARIA">
+<input id="mutate" placeholder="HTML">
+ `,
+ async function (browser, docAcc) {
+ const htmlWithLabel = findAccessibleChildByID(docAcc, "htmlWithLabel");
+ testAttrs(htmlWithLabel, { placeholder: "HTML" }, true);
+ const htmlNoLabel = findAccessibleChildByID(docAcc, "htmlNoLabel");
+ // placeholder is used as name, so not exposed as attribute.
+ testAbsentAttrs(htmlNoLabel, { placeholder: "" });
+ const ariaWithLabel = findAccessibleChildByID(docAcc, "ariaWithLabel");
+ testAttrs(ariaWithLabel, { placeholder: "ARIA" }, true);
+ const ariaNoLabel = findAccessibleChildByID(docAcc, "ariaNoLabel");
+ // No label doesn't impact aria-placeholder.
+ testAttrs(ariaNoLabel, { placeholder: "ARIA" }, true);
+ const both = findAccessibleChildByID(docAcc, "both");
+ testAttrs(both, { placeholder: "HTML" }, true);
+
+ const mutate = findAccessibleChildByID(docAcc, "mutate");
+ testAbsentAttrs(mutate, { placeholder: "" });
+ info("Adding label to mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("mutate")
+ .setAttribute("aria-label", "label");
+ });
+ await untilCacheAttrIs(
+ mutate,
+ "placeholder",
+ "HTML",
+ "mutate placeholder correct"
+ );
+ info("Removing mutate placeholder");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").removeAttribute("placeholder");
+ });
+ await untilCacheAttrAbsent(
+ mutate,
+ "placeholder",
+ "mutate placeholder not present"
+ );
+ info("Setting mutate aria-placeholder");
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("mutate")
+ .setAttribute("aria-placeholder", "ARIA");
+ });
+ await untilCacheAttrIs(
+ mutate,
+ "placeholder",
+ "ARIA",
+ "mutate placeholder correct"
+ );
+ info("Setting mutate placeholder");
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("mutate")
+ .setAttribute("placeholder", "HTML");
+ });
+ await untilCacheAttrIs(
+ mutate,
+ "placeholder",
+ "HTML",
+ "mutate placeholder correct"
+ );
+ },
+ { chrome: true, topLevel: true }
+);
+
+/**
+ * Test the ispopup attribute.
+ */
+addAccessibleTask(
+ `<div id="popover" popover>popover</div>`,
+ async function testIspopup(browser, docAcc) {
+ info("Showing popover");
+ let shown = waitForEvent(EVENT_SHOW, "popover");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("popover").showPopover();
+ });
+ let popover = (await shown).accessible;
+ testAttrs(popover, { ispopup: "auto" }, true);
+ info("Setting popover to null");
+ // Setting popover causes the Accessible to be recreated.
+ shown = waitForEvent(EVENT_SHOW, "popover");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("popover").popover = null;
+ });
+ popover = (await shown).accessible;
+ testAbsentAttrs(popover, { ispopup: "" });
+ info("Setting popover to manual and showing");
+ shown = waitForEvent(EVENT_SHOW, "popover");
+ await invokeContentTask(browser, [], () => {
+ const popoverDom = content.document.getElementById("popover");
+ popoverDom.popover = "manual";
+ popoverDom.showPopover();
+ });
+ popover = (await shown).accessible;
+ testAttrs(popover, { ispopup: "manual" }, true);
+ },
+ { chrome: true, topLevel: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_description.js b/accessible/tests/browser/e10s/browser_caching_description.js
new file mode 100644
index 0000000000..d489620e16
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_description.js
@@ -0,0 +1,280 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/name.js */
+loadScripts({ name: "name.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * expected {String} expected description value for a given accessible
+ * attrs {?Array} an optional list of attributes to update
+ * waitFor {?Array} an optional list of accessible events to wait for when
+ * attributes are updated
+ * }
+ */
+const tests = [
+ {
+ desc: "No description when there are no @alt, @title and @aria-describedby",
+ expected: "",
+ },
+ {
+ desc: "Description from @aria-describedby attribute",
+ attrs: [
+ {
+ attr: "aria-describedby",
+ value: "description",
+ },
+ ],
+ waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]],
+ expected: "aria description",
+ },
+ {
+ desc:
+ "No description from @aria-describedby since it is the same as the " +
+ "@alt attribute which is used as the name",
+ attrs: [
+ {
+ attr: "alt",
+ value: "aria description",
+ },
+ ],
+ waitFor: [[EVENT_NAME_CHANGE, "image"]],
+ expected: "",
+ },
+ {
+ desc:
+ "Description from @aria-describedby attribute when @alt and " +
+ "@aria-describedby are not the same",
+ attrs: [
+ {
+ attr: "aria-describedby",
+ value: "description2",
+ },
+ ],
+ waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]],
+ expected: "another description",
+ },
+ {
+ desc: "No description change when @alt is dropped but @aria-describedby remains",
+ attrs: [
+ {
+ attr: "alt",
+ },
+ ],
+ waitFor: [[EVENT_NAME_CHANGE, "image"]],
+ expected: "another description",
+ },
+ {
+ desc:
+ "Description from @aria-describedby attribute when @title (used for " +
+ "name) and @aria-describedby are not the same",
+ attrs: [
+ {
+ attr: "title",
+ value: "title",
+ },
+ ],
+ waitFor: [[EVENT_NAME_CHANGE, "image"]],
+ expected: "another description",
+ },
+ {
+ desc:
+ "No description from @aria-describedby since it is the same as the " +
+ "@title attribute which is used as the name",
+ attrs: [
+ {
+ attr: "title",
+ value: "another description",
+ },
+ ],
+ waitFor: [[EVENT_NAME_CHANGE, "image"]],
+ expected: "",
+ },
+ {
+ desc: "No description with only @title attribute which is used as the name",
+ attrs: [
+ {
+ attr: "aria-describedby",
+ },
+ ],
+ waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]],
+ expected: "",
+ },
+ {
+ desc:
+ "Description from @title attribute when @alt and @atitle are not the " +
+ "same",
+ attrs: [
+ {
+ attr: "alt",
+ value: "aria description",
+ },
+ ],
+ waitFor: [[EVENT_NAME_CHANGE, "image"]],
+ expected: "another description",
+ },
+ {
+ desc:
+ "No description from @title since it is the same as the @alt " +
+ "attribute which is used as the name",
+ attrs: [
+ {
+ attr: "alt",
+ value: "another description",
+ },
+ ],
+ waitFor: [[EVENT_NAME_CHANGE, "image"]],
+ expected: "",
+ },
+ {
+ desc:
+ "No description from @aria-describedby since it is the same as the " +
+ "@alt (used for name) and @title attributes",
+ attrs: [
+ {
+ attr: "aria-describedby",
+ value: "description2",
+ },
+ ],
+ waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]],
+ expected: "",
+ },
+ {
+ desc:
+ "Description from @aria-describedby attribute when it is different " +
+ "from @alt (used for name) and @title attributes",
+ attrs: [
+ {
+ attr: "aria-describedby",
+ value: "description",
+ },
+ ],
+ waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]],
+ expected: "aria description",
+ },
+ {
+ desc:
+ "No description from @aria-describedby since it is the same as the " +
+ "@alt attribute (used for name) but different from title",
+ attrs: [
+ {
+ attr: "alt",
+ value: "aria description",
+ },
+ ],
+ waitFor: [[EVENT_NAME_CHANGE, "image"]],
+ expected: "",
+ },
+ {
+ desc:
+ "Description from @aria-describedby attribute when @alt (used for " +
+ "name) and @aria-describedby are not the same but @title and " +
+ "aria-describedby are",
+ attrs: [
+ {
+ attr: "aria-describedby",
+ value: "description2",
+ },
+ ],
+ waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]],
+ expected: "another description",
+ },
+];
+
+/**
+ * Test caching of accessible object description
+ */
+addAccessibleTask(
+ `
+ <p id="description">aria description</p>
+ <p id="description2">another description</p>
+ <img id="image" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" />`,
+ async function (browser, accDoc) {
+ let imgAcc = findAccessibleChildByID(accDoc, "image");
+
+ for (let { desc, waitFor, attrs, expected } of tests) {
+ info(desc);
+ let onUpdate;
+ if (waitFor) {
+ onUpdate = waitForOrderedEvents(waitFor);
+ }
+ if (attrs) {
+ for (let { attr, value } of attrs) {
+ await invokeSetAttribute(browser, "image", attr, value);
+ }
+ }
+ await onUpdate;
+ // When attribute change (alt) triggers reorder event, accessible will
+ // become defunct.
+ if (isDefunct(imgAcc)) {
+ imgAcc = findAccessibleChildByID(accDoc, "image");
+ }
+ testDescr(imgAcc, expected);
+ }
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test that the description is updated when the content of a hidden aria-describedby
+ * subtree changes.
+ */
+addAccessibleTask(
+ `
+<button id="button" aria-describedby="desc">
+<div id="desc" hidden>a</div>
+ `,
+ async function (browser, docAcc) {
+ const button = findAccessibleChildByID(docAcc, "button");
+ testDescr(button, "a");
+ info("Changing desc textContent");
+ let descChanged = waitForEvent(EVENT_DESCRIPTION_CHANGE, button);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("desc").textContent = "c";
+ });
+ await descChanged;
+ testDescr(button, "c");
+ info("Prepending text node to desc");
+ descChanged = waitForEvent(EVENT_DESCRIPTION_CHANGE, button);
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("desc")
+ .prepend(content.document.createTextNode("b"));
+ });
+ await descChanged;
+ testDescr(button, "bc");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test aria-description, including mutations.
+ */
+addAccessibleTask(
+ `<button id="button" aria-description="a">button</button>`,
+ async function (browser, docAcc) {
+ const button = findAccessibleChildByID(docAcc, "button");
+ testDescr(button, "a");
+ info("Changing aria-description");
+ let changed = waitForEvent(EVENT_DESCRIPTION_CHANGE, button);
+ await invokeSetAttribute(browser, "button", "aria-description", "b");
+ await changed;
+ testDescr(button, "b");
+ info("Removing aria-description");
+ changed = waitForEvent(EVENT_DESCRIPTION_CHANGE, button);
+ await invokeSetAttribute(browser, "button", "aria-description");
+ await changed;
+ testDescr(button, "");
+ info("Setting aria-description");
+ changed = waitForEvent(EVENT_DESCRIPTION_CHANGE, button);
+ await invokeSetAttribute(browser, "button", "aria-description", "c");
+ await changed;
+ testDescr(button, "c");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_document_props.js b/accessible/tests/browser/e10s/browser_caching_document_props.js
new file mode 100644
index 0000000000..cf3a4e5721
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_document_props.js
@@ -0,0 +1,80 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ "e10s/doc_treeupdate_whitespace.html",
+ async function (browser, docAcc) {
+ info("Testing top level doc");
+ queryInterfaces(docAcc, [nsIAccessibleDocument]);
+ const topUrl =
+ (browser.isRemoteBrowser ? CURRENT_CONTENT_DIR : CURRENT_DIR) +
+ "e10s/doc_treeupdate_whitespace.html";
+ is(docAcc.URL, topUrl, "Initial URL correct");
+ is(docAcc.mimeType, "text/html", "Mime type is correct");
+ info("Changing URL");
+ await invokeContentTask(browser, [], () => {
+ content.history.pushState(
+ null,
+ "",
+ content.document.location.href + "/after"
+ );
+ });
+ is(docAcc.URL, topUrl + "/after", "URL correct after change");
+
+ // We can't use the harness to manage iframes for us because it uses data
+ // URIs for in-process iframes, but data URIs don't support
+ // history.pushState.
+
+ async function testIframe() {
+ queryInterfaces(iframeDocAcc, [nsIAccessibleDocument]);
+ is(iframeDocAcc.URL, src, "Initial URL correct");
+ is(iframeDocAcc.mimeType, "text/html", "Mime type is correct");
+ info("Changing URL");
+ await invokeContentTask(browser, [], async () => {
+ await SpecialPowers.spawn(content.iframe, [], () => {
+ content.history.pushState(
+ null,
+ "",
+ content.document.location.href + "/after"
+ );
+ });
+ });
+ is(iframeDocAcc.URL, src + "/after", "URL correct after change");
+ }
+
+ info("Testing same origin (in-process) iframe");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let src = "https://example.com/initial.html";
+ let loaded = waitForEvent(
+ EVENT_DOCUMENT_LOAD_COMPLETE,
+ evt => evt.accessible.parent.parent == docAcc
+ );
+ await invokeContentTask(browser, [src], cSrc => {
+ content.iframe = content.document.createElement("iframe");
+ content.iframe.src = cSrc;
+ content.document.body.append(content.iframe);
+ });
+ let iframeDocAcc = (await loaded).accessible;
+ await testIframe();
+
+ info("Testing different origin (out-of-process) iframe");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ src = "https://example.net/initial.html";
+ loaded = waitForEvent(
+ EVENT_DOCUMENT_LOAD_COMPLETE,
+ evt => evt.accessible.parent.parent == docAcc
+ );
+ await invokeContentTask(browser, [src], cSrc => {
+ content.iframe.src = cSrc;
+ });
+ iframeDocAcc = (await await loaded).accessible;
+ await testIframe();
+ },
+ { chrome: true, topLevel: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_domnodeid.js b/accessible/tests/browser/e10s/browser_caching_domnodeid.js
new file mode 100644
index 0000000000..722cc9a970
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_domnodeid.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test DOM ID caching on remotes.
+ */
+addAccessibleTask(
+ '<div id="div"></div>',
+ async function (browser, accDoc) {
+ const div = findAccessibleChildByID(accDoc, "div");
+ ok(div, "Got accessible with 'div' ID.");
+
+ let contentPromise = invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").id = "foo";
+ });
+ // We don't await for content task to return because we want to exercise the
+ // untilCacheIs function and demonstrate that it can await for a passing
+ // `is` test.
+ await untilCacheIs(
+ () => div.id,
+ "foo",
+ "ID is correct and updated in cache"
+ );
+
+ // Don't leave test without the content task promise resolved.
+ await contentPromise;
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_hyperlink.js b/accessible/tests/browser/e10s/browser_caching_hyperlink.js
new file mode 100644
index 0000000000..4b3f8a1bda
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_hyperlink.js
@@ -0,0 +1,228 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function testLinkIndexAtOffset(id, offset, index) {
+ let htAcc = getAccessible(id, [nsIAccessibleHyperText]);
+ is(
+ htAcc.getLinkIndexAtOffset(offset),
+ index,
+ "Wrong link index at offset " + offset + " for ID " + id + "!"
+ );
+}
+
+function testThis(
+ paragraph,
+ docURI,
+ id,
+ charIndex,
+ expectedLinkIndex,
+ expectedAnchors,
+ expectedURIs,
+ valid = true
+) {
+ testLinkIndexAtOffset(paragraph, charIndex, expectedLinkIndex);
+
+ let linkAcc = paragraph.getLinkAt(expectedLinkIndex);
+ ok(linkAcc, "No accessible for link " + id + "!");
+
+ is(linkAcc.valid, valid, `${id} is valid.`);
+
+ let linkIndex = paragraph.getLinkIndex(linkAcc);
+ is(linkIndex, expectedLinkIndex, "Wrong link index for " + id + "!");
+
+ is(linkAcc.anchorCount, expectedAnchors.length, "Correct number of anchors");
+ for (let i = 0; i < expectedAnchors.length; i++) {
+ let uri = linkAcc.getURI(i);
+ is(
+ (uri ? uri.spec : "").replace(docURI, ""),
+ expectedURIs[i],
+ `Wrong anchor URI at ${i} for "${id}"`
+ );
+ is(
+ getAccessibleDOMNodeID(linkAcc.getAnchor(i)),
+ expectedAnchors[i],
+ `Wrong anchor at ${i} for "${id}"`
+ );
+ }
+}
+
+/**
+ * Test hyperlinks
+ */
+addAccessibleTask(
+ `
+ <p id="testParagraph"><br
+ >Simple link:<br
+ ><a id="NormalHyperlink" href="https://www.mozilla.org">Mozilla Foundation</a><br
+ >ARIA link:<br
+ ><span id="AriaHyperlink" role="link"
+ onclick="window.open('https://www.mozilla.org/');"
+ tabindex="0">Mozilla Foundation Home</span><br
+ >Invalid, non-focusable hyperlink:<br
+ ><span id="InvalidAriaHyperlink" role="link" aria-invalid="true"
+ onclick="window.open('https:/www.mozilla.org/');">Invalid link</span><br
+ >Image map:<br
+ ><map name="atoz_map"><area href="https://www.bbc.co.uk/radio4/atoz/index.shtml#b"
+ coords="17,0,30,14"
+ id="b"
+ shape="rect"></area
+ ><area href="https://www.bbc.co.uk/radio4/atoz/index.shtml#a"
+ coords="0,0,13,14"
+ id="a"
+ shape="rect"></area></map
+ ><img width="447" id="imgmap"
+ height="15"
+ usemap="#atoz_map"
+ src="../letters.gif"></img><br
+ >Empty link:<br
+ ><a id="emptyLink" href=""><img src=""></img></a><br
+ >Link with embedded span<br
+ ><a id="LinkWithSpan" href="https://www.heise.de/"><span lang="de">Heise Online</span></a><br
+ >Named anchor, must not have "linked" state for it to be exposed correctly:<br
+ ><a id="namedAnchor" name="named_anchor">This should never be of state_linked</a>
+ </p>
+ `,
+ function (browser, accDoc) {
+ const paragraph = findAccessibleChildByID(accDoc, "testParagraph", [
+ nsIAccessibleHyperText,
+ ]);
+ is(paragraph.linkCount, 7, "Wrong link count for paragraph!");
+
+ const docURI = accDoc.URL;
+ // normal hyperlink
+ testThis(
+ paragraph,
+ docURI,
+ "NormalHyperlink",
+ 14,
+ 0,
+ ["NormalHyperlink"],
+ ["https://www.mozilla.org/"]
+ );
+
+ // ARIA hyperlink
+ testThis(
+ paragraph,
+ docURI,
+ "AriaHyperlink",
+ 27,
+ 1,
+ ["AriaHyperlink"],
+ [""]
+ );
+
+ // ARIA hyperlink with status invalid
+ testThis(
+ paragraph,
+ docURI,
+ "InvalidAriaHyperlink",
+ 63,
+ 2,
+ ["InvalidAriaHyperlink"],
+ [""],
+ false
+ );
+
+ // image map, but not its link children. They are not part of hypertext.
+ testThis(
+ paragraph,
+ docURI,
+ "imgmap",
+ 76,
+ 3,
+ ["b", "a"],
+ [
+ "https://www.bbc.co.uk/radio4/atoz/index.shtml#b",
+ "https://www.bbc.co.uk/radio4/atoz/index.shtml#a",
+ ]
+ );
+
+ // empty hyperlink
+ testThis(paragraph, docURI, "emptyLink", 90, 4, ["emptyLink"], [""]);
+
+ // normal hyperlink with embedded span
+ testThis(
+ paragraph,
+ docURI,
+ "LinkWithSpan",
+ 116,
+ 5,
+ ["LinkWithSpan"],
+ ["https://www.heise.de/"]
+ );
+
+ // Named anchor
+ testThis(paragraph, docURI, "namedAnchor", 193, 6, ["namedAnchor"], [""]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test paragraph with link
+ */
+addAccessibleTask(
+ `
+ <p id="p"><a href="http://mozilla.org">mozilla.org</a></p>
+ `,
+ function (browser, accDoc) {
+ // Paragraph with link
+ const p = findAccessibleChildByID(accDoc, "p", [nsIAccessibleHyperText]);
+ const link = p.getLinkAt(0);
+ is(link, p.getChildAt(0), "Wrong link for p2");
+ is(p.linkCount, 1, "Wrong link count for p2");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test paragraph with link
+ */
+addAccessibleTask(
+ `
+ <p id="p"><a href="www">mozilla</a><a href="www">mozilla</a><span> te</span><span>xt </span><a href="www">mozilla</a></p>
+ `,
+ function (browser, accDoc) {
+ // Paragraph with link
+ const p = findAccessibleChildByID(accDoc, "p", [nsIAccessibleHyperText]);
+
+ // getLinkIndexAtOffset, causes the offsets to be cached;
+ testLinkIndexAtOffset(p, 0, 0); // 1st 'mozilla' link
+ testLinkIndexAtOffset(p, 1, 1); // 2nd 'mozilla' link
+ testLinkIndexAtOffset(p, 2, -1); // ' ' of ' te' text node
+ testLinkIndexAtOffset(p, 3, -1); // 't' of ' te' text node
+ testLinkIndexAtOffset(p, 5, -1); // 'x' of 'xt ' text node
+ testLinkIndexAtOffset(p, 7, -1); // ' ' of 'xt ' text node
+ testLinkIndexAtOffset(p, 8, 2); // 3d 'mozilla' link
+ testLinkIndexAtOffset(p, 9, 2); // the end, latest link
+
+ // the second pass to make sure link indexes are calculated propertly from
+ // cached offsets.
+ testLinkIndexAtOffset(p, 0, 0); // 1st 'mozilla' link
+ testLinkIndexAtOffset(p, 1, 1); // 2nd 'mozilla' link
+ testLinkIndexAtOffset(p, 2, -1); // ' ' of ' te' text node
+ testLinkIndexAtOffset(p, 3, -1); // 't' of ' te' text node
+ testLinkIndexAtOffset(p, 5, -1); // 'x' of 'xt ' text node
+ testLinkIndexAtOffset(p, 7, -1); // ' ' of 'xt ' text node
+ testLinkIndexAtOffset(p, 8, 2); // 3d 'mozilla' link
+ testLinkIndexAtOffset(p, 9, 2); // the end, latest link
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_innerHTML.js b/accessible/tests/browser/e10s/browser_caching_innerHTML.js
new file mode 100644
index 0000000000..7baee32e26
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_innerHTML.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test caching of innerHTML on math elements for Windows clients.
+ */
+addAccessibleTask(
+ `
+<p id="p">test</p>
+<math id="math"><mfrac><mi>x</mi><mi>y</mi></mfrac></math>
+ `,
+ async function (browser, docAcc) {
+ const p = findAccessibleChildByID(docAcc, "p");
+ let hasHtml;
+ try {
+ p.cache.getStringProperty("html");
+ hasHtml = true;
+ } catch (e) {
+ hasHtml = false;
+ }
+ ok(!hasHtml, "p doesn't have cached html");
+
+ const math = findAccessibleChildByID(docAcc, "math");
+ is(
+ math.cache.getStringProperty("html"),
+ "<mfrac><mi>x</mi><mi>y</mi></mfrac>",
+ "math cached html is correct"
+ );
+
+ info("Mutating math");
+ await invokeContentTask(browser, [], () => {
+ content.document.querySelectorAll("mi")[1].textContent = "z";
+ });
+ await untilCacheIs(
+ () => math.cache.getStringProperty("html"),
+ "<mfrac><mi>x</mi><mi>z</mi></mfrac>",
+ "math cached html is correct after mutation"
+ );
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_interfaces.js b/accessible/tests/browser/e10s/browser_caching_interfaces.js
new file mode 100644
index 0000000000..c83d486bc6
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_interfaces.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test caching of accessible interfaces
+ */
+addAccessibleTask(
+ `
+ <img id="img" src="http://example.com/a11y/accessible/tests/mochitest/moz.png">
+ <select id="select" multiple></select>
+ <input id="number-input" type="number">
+ <table id="table">
+ <tr><td id="cell"><a id="link" href="#">hello</a></td></tr>
+ </table>
+ `,
+ async function (browser, accDoc) {
+ ok(
+ accDoc instanceof nsIAccessibleDocument,
+ "Document has Document interface"
+ );
+ ok(
+ accDoc instanceof nsIAccessibleHyperText,
+ "Document has HyperText interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "img") instanceof nsIAccessibleImage,
+ "img has Image interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "select") instanceof
+ nsIAccessibleSelectable,
+ "select has Selectable interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "number-input") instanceof
+ nsIAccessibleValue,
+ "number-input has Value interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "table") instanceof nsIAccessibleTable,
+ "table has Table interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "cell") instanceof nsIAccessibleTableCell,
+ "cell has TableCell interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "link") instanceof nsIAccessibleHyperLink,
+ "link has HyperLink interface"
+ );
+ ok(
+ findAccessibleChildByID(accDoc, "link") instanceof nsIAccessibleHyperText,
+ "link has HyperText interface"
+ );
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_large_update.js b/accessible/tests/browser/e10s/browser_caching_large_update.js
new file mode 100644
index 0000000000..ccf8a86921
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_large_update.js
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test a large update which adds many thousands of Accessibles with a
+ * lot of content in each.
+ */
+addAccessibleTask(
+ `<main id="main" hidden></main>`,
+ async function (browser, docAcc) {
+ let shown = waitForEvent(EVENT_SHOW, "main");
+ await invokeContentTask(browser, [], () => {
+ // Make a long string.
+ let text = "";
+ for (let i = 0; i < 100; ++i) {
+ text += "a";
+ }
+ // Create lots of nodes which include the long string.
+ const contMain = content.document.getElementById("main");
+ // 15000 children of main.
+ for (let w = 0; w < 15000; ++w) {
+ // Each of those goes 9 deep.
+ let parent = contMain;
+ for (let d = 0; d < 10; ++d) {
+ const div = content.document.createElement("div");
+ div.setAttribute("aria-label", `${w} ${d} ${text}`);
+ parent.append(div);
+ parent = div;
+ }
+ }
+ contMain.hidden = false;
+ });
+ const main = (await shown).accessible;
+ is(main.childCount, 15000, "main has correct number of children");
+
+ // We don't want to output passes for every check, since that would output
+ // hundreds of thousands of lines, which slows the test to a crawl. Instead,
+ // output any failures and keep track of overall success/failure.
+ let treeOk = true;
+ function check(val, msg) {
+ if (!val) {
+ ok(false, msg);
+ treeOk = false;
+ }
+ }
+
+ info("Checking tree");
+ for (let w = 0; w < 15000; ++w) {
+ let acc = main.getChildAt(w);
+ let parent = main;
+ for (let d = 0; d < 10; ++d) {
+ check(acc, `Got child ${w} depth ${d}`);
+ const name = `${w} ${d}`;
+ check(acc.name.startsWith(name + " "), `${name}: correct name`);
+ check(acc.parent == parent, `${name}: correct parent`);
+ parent = acc;
+ acc = acc.firstChild;
+ }
+ }
+ // check() sets treeOk to false for any failure.
+ ok(treeOk, "Tree is correct");
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_name.js b/accessible/tests/browser/e10s/browser_caching_name.js
new file mode 100644
index 0000000000..55f506b85a
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_name.js
@@ -0,0 +1,542 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+requestLongerTimeout(2);
+
+/* import-globals-from ../../mochitest/name.js */
+loadScripts({ name: "name.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Rules for name tests that are inspired by
+ * accessible/tests/mochitest/name/markuprules.xul
+ *
+ * Each element in the list of rules represents a name calculation rule for a
+ * particular test case.
+ *
+ * The rules have the following format:
+ * { attr } - calculated from attribute
+ * { elm } - calculated from another element
+ * { fromsubtree } - calculated from element's subtree
+ *
+ */
+const ARIARule = [{ attr: "aria-labelledby" }, { attr: "aria-label" }];
+const HTMLControlHeadRule = [...ARIARule, { elm: "label" }];
+const rules = {
+ CSSContent: [{ elm: "style" }, { fromsubtree: true }],
+ HTMLARIAGridCell: [...ARIARule, { fromsubtree: true }, { attr: "title" }],
+ HTMLControl: [
+ ...HTMLControlHeadRule,
+ { fromsubtree: true },
+ { attr: "title" },
+ ],
+ HTMLElm: [...ARIARule, { attr: "title" }],
+ HTMLImg: [...ARIARule, { attr: "alt" }, { attr: "title" }],
+ HTMLImgEmptyAlt: [...ARIARule, { attr: "title" }, { attr: "alt" }],
+ HTMLInputButton: [
+ ...HTMLControlHeadRule,
+ { attr: "value" },
+ { attr: "title" },
+ ],
+ HTMLInputImage: [
+ ...HTMLControlHeadRule,
+ { attr: "alt" },
+ { attr: "value" },
+ { attr: "title" },
+ ],
+ HTMLInputImageNoValidSrc: [
+ ...HTMLControlHeadRule,
+ { attr: "alt" },
+ { attr: "value" },
+ ],
+ HTMLInputReset: [...HTMLControlHeadRule, { attr: "value" }],
+ HTMLInputSubmit: [...HTMLControlHeadRule, { attr: "value" }],
+ HTMLLink: [...ARIARule, { fromsubtree: true }, { attr: "title" }],
+ HTMLLinkImage: [...ARIARule, { fromsubtree: true }, { attr: "title" }],
+ HTMLOption: [
+ ...ARIARule,
+ { attr: "label" },
+ { fromsubtree: true },
+ { attr: "title" },
+ ],
+ HTMLTable: [
+ ...ARIARule,
+ { elm: "caption" },
+ { attr: "summary" },
+ { attr: "title" },
+ ],
+};
+
+const markupTests = [
+ {
+ id: "btn",
+ ruleset: "HTMLControl",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="btn">test4</label>
+ <button id="btn"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ title="test5">press me</button>`,
+ expected: ["test2 test3", "test1", "test4", "press me", "test5"],
+ },
+ {
+ id: "btn",
+ ruleset: "HTMLInputButton",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="btn">test4</label>
+ <input id="btn"
+ type="button"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ value="name from value"
+ alt="no name from al"
+ src="no name from src"
+ data="no name from data"
+ title="name from title"/>`,
+ expected: [
+ "test2 test3",
+ "test1",
+ "test4",
+ "name from value",
+ "name from title",
+ ],
+ },
+ {
+ id: "btn-submit",
+ ruleset: "HTMLInputSubmit",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="btn-submit">test4</label>
+ <input id="btn-submit"
+ type="submit"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ value="name from value"
+ alt="no name from atl"
+ src="no name from src"
+ data="no name from data"
+ title="no name from title"/>`,
+ expected: ["test2 test3", "test1", "test4", "name from value"],
+ },
+ {
+ id: "btn-reset",
+ ruleset: "HTMLInputReset",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="btn-reset">test4</label>
+ <input id="btn-reset"
+ type="reset"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ value="name from value"
+ alt="no name from alt"
+ src="no name from src"
+ data="no name from data"
+ title="no name from title"/>`,
+ expected: ["test2 test3", "test1", "test4", "name from value"],
+ },
+ {
+ id: "btn-image",
+ ruleset: "HTMLInputImage",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="btn-image">test4</label>
+ <input id="btn-image"
+ type="image"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ alt="name from alt"
+ value="name from value"
+ src="http://example.com/a11y/accessible/tests/mochitest/moz.png"
+ data="no name from data"
+ title="name from title"/>`,
+ expected: [
+ "test2 test3",
+ "test1",
+ "test4",
+ "name from alt",
+ "name from value",
+ "name from title",
+ ],
+ },
+ {
+ id: "btn-image",
+ ruleset: "HTMLInputImageNoValidSrc",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="btn-image">test4</label>
+ <input id="btn-image"
+ type="image"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ alt="name from alt"
+ value="name from value"
+ data="no name from data"
+ title="no name from title"/>`,
+ expected: [
+ "test2 test3",
+ "test1",
+ "test4",
+ "name from alt",
+ "name from value",
+ ],
+ },
+ {
+ id: "opt",
+ ruleset: "HTMLOption",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <select>
+ <option id="opt"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ label="test4"
+ title="test5">option1</option>
+ <option>option2</option>
+ </select>`,
+ expected: ["test2 test3", "test1", "test4", "option1", "test5"],
+ },
+ {
+ id: "img",
+ ruleset: "HTMLImg",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <img id="img"
+ aria-label="Logo of Mozilla"
+ aria-labelledby="l1 l2"
+ alt="Mozilla logo"
+ title="This is a logo"
+ src="http://example.com/a11y/accessible/tests/mochitest/moz.png"/>`,
+ expected: [
+ "test2 test3",
+ "Logo of Mozilla",
+ "Mozilla logo",
+ "This is a logo",
+ ],
+ },
+ {
+ id: "tc",
+ ruleset: "HTMLElm",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="tc">test4</label>
+ <table>
+ <tr>
+ <td id="tc"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ title="test5">
+ <p>This is a paragraph</p>
+ <a href="#">This is a link</a>
+ <ul>
+ <li>This is a list</li>
+ </ul>
+ </td>
+ </tr>
+ </table>`,
+ expected: ["test2 test3", "test1", "test5"],
+ },
+ {
+ id: "gc",
+ ruleset: "HTMLARIAGridCell",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <label for="gc">test4</label>
+ <table>
+ <tr>
+ <td id="gc"
+ role="gridcell"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ title="This is a paragraph This is a link This is a list">
+ <p>This is a paragraph</p>
+ <a href="#">This is a link</a>
+ <ul>
+ <li>Listitem1</li>
+ <li>Listitem2</li>
+ </ul>
+ </td>
+ </tr>
+ </table>`,
+ expected: [
+ "test2 test3",
+ "test1",
+ "This is a paragraph This is a link \u2022 Listitem1 \u2022 Listitem2",
+ "This is a paragraph This is a link This is a list",
+ ],
+ },
+ {
+ id: "t",
+ ruleset: "HTMLTable",
+ markup: `
+ <span id="l1">lby_tst6_1</span>
+ <span id="l2">lby_tst6_2</span>
+ <label for="t">label_tst6</label>
+ <table id="t"
+ aria-label="arialabel_tst6"
+ aria-labelledby="l1 l2"
+ summary="summary_tst6"
+ title="title_tst6">
+ <caption>caption_tst6</caption>
+ <tr>
+ <td>cell1</td>
+ <td>cell2</td>
+ </tr>
+ </table>`,
+ expected: [
+ "lby_tst6_1 lby_tst6_2",
+ "arialabel_tst6",
+ "caption_tst6",
+ "summary_tst6",
+ "title_tst6",
+ ],
+ },
+ {
+ id: "btn",
+ ruleset: "CSSContent",
+ markup: `
+ <div role="main">
+ <style>
+ button::before {
+ content: "do not ";
+ }
+ </style>
+ <button id="btn">press me</button>
+ </div>`,
+ expected: ["do not press me", "press me"],
+ },
+ {
+ // TODO: uncomment when Bug-1256382 is resoved.
+ // id: 'li',
+ // ruleset: 'CSSContent',
+ // markup: `
+ // <style>
+ // ul {
+ // list-style-type: decimal;
+ // }
+ // </style>
+ // <ul id="ul">
+ // <li id="li">Listitem</li>
+ // </ul>`,
+ // expected: ['1. Listitem', `${String.fromCharCode(0x2022)} Listitem`]
+ // }, {
+ id: "a",
+ ruleset: "HTMLLink",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <a id="a"
+ href=""
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ title="test4">test5</a>`,
+ expected: ["test2 test3", "test1", "test5", "test4"],
+ },
+ {
+ id: "a-img",
+ ruleset: "HTMLLinkImage",
+ markup: `
+ <span id="l1">test2</span>
+ <span id="l2">test3</span>
+ <a id="a-img"
+ href=""
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ title="test4"><img alt="test5"/></a>`,
+ expected: ["test2 test3", "test1", "test5", "test4"],
+ },
+];
+
+/**
+ * Test accessible name that is calculated from an attribute, remove the
+ * attribute before proceeding to the next name test. If attribute removal
+ * results in a reorder or text inserted event - wait for it. If accessible
+ * becomes defunct, update its reference using the one that is attached to one
+ * of the above events.
+ * @param {Object} browser current "tabbrowser" element
+ * @param {Object} target { acc, id } structure that contains an
+ * accessible and its content element
+ * id.
+ * @param {Object} rule current attr rule for name calculation
+ * @param {[type]} expected expected name value
+ */
+async function testAttrRule(browser, target, rule, expected) {
+ let { id, acc } = target;
+ let { attr } = rule;
+
+ testName(acc, expected);
+
+ let nameChange = waitForEvent(EVENT_NAME_CHANGE, id);
+ await invokeContentTask(browser, [id, attr], (contentId, contentAttr) => {
+ content.document.getElementById(contentId).removeAttribute(contentAttr);
+ });
+ let event = await nameChange;
+
+ // Update accessible just in case it is now defunct.
+ target.acc = findAccessibleChildByID(event.accessible, id);
+}
+
+/**
+ * Test accessible name that is calculated from an element name, remove the
+ * element before proceeding to the next name test. If element removal results
+ * in a reorder event - wait for it. If accessible becomes defunct, update its
+ * reference using the one that is attached to a possible reorder event.
+ * @param {Object} browser current "tabbrowser" element
+ * @param {Object} target { acc, id } structure that contains an
+ * accessible and its content element
+ * id.
+ * @param {Object} rule current elm rule for name calculation
+ * @param {[type]} expected expected name value
+ */
+async function testElmRule(browser, target, rule, expected) {
+ let { id, acc } = target;
+ let { elm } = rule;
+
+ testName(acc, expected);
+ let nameChange = waitForEvent(EVENT_NAME_CHANGE, id);
+
+ await invokeContentTask(browser, [elm], contentElm => {
+ content.document.querySelector(`${contentElm}`).remove();
+ });
+ let event = await nameChange;
+
+ // Update accessible just in case it is now defunct.
+ target.acc = findAccessibleChildByID(event.accessible, id);
+}
+
+/**
+ * Test accessible name that is calculated from its subtree, remove the subtree
+ * and wait for a reorder event before proceeding to the next name test. If
+ * accessible becomes defunct, update its reference using the one that is
+ * attached to a reorder event.
+ * @param {Object} browser current "tabbrowser" element
+ * @param {Object} target { acc, id } structure that contains an
+ * accessible and its content element
+ * id.
+ * @param {Object} rule current subtree rule for name calculation
+ * @param {[type]} expected expected name value
+ */
+async function testSubtreeRule(browser, target, rule, expected) {
+ let { id, acc } = target;
+
+ testName(acc, expected);
+ let nameChange = waitForEvent(EVENT_NAME_CHANGE, id);
+
+ await invokeContentTask(browser, [id], contentId => {
+ let elm = content.document.getElementById(contentId);
+ while (elm.firstChild) {
+ elm.firstChild.remove();
+ }
+ });
+ let event = await nameChange;
+
+ // Update accessible just in case it is now defunct.
+ target.acc = findAccessibleChildByID(event.accessible, id);
+}
+
+/**
+ * Iterate over a list of rules and test accessible names for each one of the
+ * rules.
+ * @param {Object} browser current "tabbrowser" element
+ * @param {Object} target { acc, id } structure that contains an
+ * accessible and its content element
+ * id.
+ * @param {Array} ruleset A list of rules to test a target with
+ * @param {Array} expected A list of expected name value for each rule
+ */
+async function testNameRule(browser, target, ruleset, expected) {
+ for (let i = 0; i < ruleset.length; ++i) {
+ let rule = ruleset[i];
+ let testFn;
+ if (rule.attr) {
+ testFn = testAttrRule;
+ } else if (rule.elm) {
+ testFn = testElmRule;
+ } else if (rule.fromsubtree) {
+ testFn = testSubtreeRule;
+ }
+ await testFn(browser, target, rule, expected[i]);
+ }
+}
+
+markupTests.forEach(({ id, ruleset, markup, expected }) =>
+ addAccessibleTask(
+ markup,
+ async function (browser, accDoc) {
+ const observer = {
+ observe(subject, topic, data) {
+ const event = subject.QueryInterface(nsIAccessibleEvent);
+ console.log(eventToString(event));
+ },
+ };
+ Services.obs.addObserver(observer, "accessible-event");
+ // Find a target accessible from an accessible subtree.
+ let acc = findAccessibleChildByID(accDoc, id);
+ let target = { id, acc };
+ await testNameRule(browser, target, rules[ruleset], expected);
+ Services.obs.removeObserver(observer, "accessible-event");
+ },
+ { iframe: true, remoteIframe: true }
+ )
+);
+
+/**
+ * Test caching of the document title.
+ */
+addAccessibleTask(
+ ``,
+ async function (browser, docAcc) {
+ let nameChanged = waitForEvent(EVENT_NAME_CHANGE, docAcc);
+ await invokeContentTask(browser, [], () => {
+ content.document.title = "new title";
+ });
+ await nameChanged;
+ testName(docAcc, "new title");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test that the name is updated when the content of a hidden aria-labelledby
+ * subtree changes.
+ */
+addAccessibleTask(
+ `
+<button id="button" aria-labelledby="label">
+<div id="label" hidden>a</div>
+ `,
+ async function (browser, docAcc) {
+ const button = findAccessibleChildByID(docAcc, "button");
+ testName(button, "a");
+ info("Changing label textContent");
+ let nameChanged = waitForEvent(EVENT_NAME_CHANGE, button);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("label").textContent = "c";
+ });
+ await nameChanged;
+ testName(button, "c");
+ info("Prepending text node to label");
+ nameChanged = waitForEvent(EVENT_NAME_CHANGE, button);
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("label")
+ .prepend(content.document.createTextNode("b"));
+ });
+ await nameChanged;
+ testName(button, "bc");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_position.js b/accessible/tests/browser/e10s/browser_caching_position.js
new file mode 100644
index 0000000000..5de246bb91
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_position.js
@@ -0,0 +1,194 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/layout.js */
+loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR });
+
+function getCachedBounds(acc) {
+ let cachedBounds = "";
+ try {
+ cachedBounds = acc.cache.getStringProperty("relative-bounds");
+ } catch (e) {
+ ok(false, "Unable to fetch cached bounds from cache!");
+ }
+ return cachedBounds;
+}
+
+async function testCoordinates(accDoc, id, expectedWidthPx, expectedHeightPx) {
+ let acc = findAccessibleChildByID(accDoc, id, [Ci.nsIAccessibleImage]);
+ if (!acc) {
+ return;
+ }
+
+ let screenX = {};
+ let screenY = {};
+ let windowX = {};
+ let windowY = {};
+ let parentX = {};
+ let parentY = {};
+
+ // get screen coordinates.
+ acc.getImagePosition(
+ nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE,
+ screenX,
+ screenY
+ );
+ // get window coordinates.
+ acc.getImagePosition(
+ nsIAccessibleCoordinateType.COORDTYPE_WINDOW_RELATIVE,
+ windowX,
+ windowY
+ );
+ // get parent related coordinates.
+ acc.getImagePosition(
+ nsIAccessibleCoordinateType.COORDTYPE_PARENT_RELATIVE,
+ parentX,
+ parentY
+ );
+ // XXX For linked images, a negative parentY value is returned, and the
+ // screenY coordinate is the link's screenY coordinate minus 1.
+ // Until this is fixed, set parentY to -1 if it's negative.
+ if (parentY.value < 0) {
+ parentY.value = -1;
+ }
+
+ // See if asking image for child at image's screen coordinates gives
+ // correct accessible. getChildAtPoint operates on screen coordinates.
+ let tempAcc = null;
+ try {
+ tempAcc = acc.getChildAtPoint(screenX.value, screenY.value);
+ } catch (e) {}
+ is(tempAcc, acc, "Wrong accessible returned for position of " + id + "!");
+
+ // get image's parent.
+ let imageParentAcc = null;
+ try {
+ imageParentAcc = acc.parent;
+ } catch (e) {}
+ ok(imageParentAcc, "no parent accessible for " + id + "!");
+
+ if (imageParentAcc) {
+ // See if parent's screen coordinates plus image's parent relative
+ // coordinates equal to image's screen coordinates.
+ let parentAccX = {};
+ let parentAccY = {};
+ let parentAccWidth = {};
+ let parentAccHeight = {};
+ imageParentAcc.getBounds(
+ parentAccX,
+ parentAccY,
+ parentAccWidth,
+ parentAccHeight
+ );
+ is(
+ parentAccX.value + parentX.value,
+ screenX.value,
+ "Wrong screen x coordinate for " + id + "!"
+ );
+ // XXX see bug 456344
+ // is(
+ // parentAccY.value + parentY.value,
+ // screenY.value,
+ // "Wrong screen y coordinate for " + id + "!"
+ // );
+ }
+
+ let [expectedW, expectedH] = CSSToDevicePixels(
+ window,
+ expectedWidthPx,
+ expectedHeightPx
+ );
+ let width = {};
+ let height = {};
+ acc.getImageSize(width, height);
+ is(width.value, expectedW, "Wrong width for " + id + "!");
+ is(height.value, expectedH, "wrong height for " + id + "!");
+}
+
+addAccessibleTask(
+ `
+ <br>Simple image:<br>
+ <img id="nonLinkedImage" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"/>
+ <br>Linked image:<br>
+ <a href="http://www.mozilla.org"><img id="linkedImage" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"></a>
+ <br>Image with longdesc:<br>
+ <img id="longdesc" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" longdesc="longdesc_src.html"
+ alt="Image of Mozilla logo"/>
+ <br>Image with invalid url in longdesc:<br>
+ <img id="invalidLongdesc" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" longdesc="longdesc src.html"
+ alt="Image of Mozilla logo"/>
+ <br>Image with click and longdesc:<br>
+ <img id="clickAndLongdesc" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" longdesc="longdesc_src.html"
+ alt="Another image of Mozilla logo" onclick="alert('Clicked!');"/>
+
+ <br>image described by a link to be treated as longdesc<br>
+ <img id="longdesc2" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" aria-describedby="describing_link"
+ alt="Second Image of Mozilla logo"/>
+ <a id="describing_link" href="longdesc_src.html">link to description of image</a>
+
+ <br>Image described by a link to be treated as longdesc with whitespaces<br>
+ <img id="longdesc3" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" aria-describedby="describing_link2"
+ alt="Second Image of Mozilla logo"/>
+ <a id="describing_link2" href="longdesc src.html">link to description of image</a>
+
+ <br>Image with click:<br>
+ <img id="click" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"
+ alt="A third image of Mozilla logo" onclick="alert('Clicked, too!');"/>
+ `,
+ async function (browser, docAcc) {
+ // Test non-linked image
+ await testCoordinates(docAcc, "nonLinkedImage", 89, 38);
+
+ // Test linked image
+ await testCoordinates(docAcc, "linkedImage", 89, 38);
+
+ // Image with long desc
+ await testCoordinates(docAcc, "longdesc", 89, 38);
+
+ // Image with invalid url in long desc
+ await testCoordinates(docAcc, "invalidLongdesc", 89, 38);
+
+ // Image with click and long desc
+ await testCoordinates(docAcc, "clickAndLongdesc", 89, 38);
+
+ // Image with click
+ await testCoordinates(docAcc, "click", 89, 38);
+
+ // Image with long desc
+ await testCoordinates(docAcc, "longdesc2", 89, 38);
+
+ // Image described by HTML:a@href with whitespaces
+ await testCoordinates(docAcc, "longdesc3", 89, 38);
+ }
+);
+
+addAccessibleTask(
+ `
+ <br>Linked image:<br>
+ <a href="http://www.mozilla.org"><img id="linkedImage" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"></a>
+ `,
+ async function (browser, docAcc) {
+ const imgAcc = findAccessibleChildByID(docAcc, "linkedImage", [
+ Ci.nsIAccessibleImage,
+ ]);
+ const origCachedBounds = getCachedBounds(imgAcc);
+
+ await invokeContentTask(browser, [], () => {
+ const imgNode = content.document.getElementById("linkedImage");
+ imgNode.style = "margin-left: 1000px; margin-top: 500px;";
+ });
+
+ await untilCacheOk(() => {
+ return origCachedBounds != getCachedBounds(imgAcc);
+ }, "Cached bounds update after mutation");
+ },
+ {
+ // We can only access the `cache` attribute of an accessible when
+ // the cache is enabled and we're in a remote browser.
+ topLevel: true,
+ iframe: true,
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_relations.js b/accessible/tests/browser/e10s/browser_caching_relations.js
new file mode 100644
index 0000000000..cc83930830
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_relations.js
@@ -0,0 +1,291 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+requestLongerTimeout(2);
+
+/**
+ * A test specification that has the following format:
+ * [
+ * attr relevant aria attribute
+ * hostRelation corresponding host relation type
+ * dependantRelation corresponding dependant relation type
+ * ]
+ */
+const attrRelationsSpec = [
+ ["aria-labelledby", RELATION_LABELLED_BY, RELATION_LABEL_FOR],
+ ["aria-describedby", RELATION_DESCRIBED_BY, RELATION_DESCRIPTION_FOR],
+ ["aria-controls", RELATION_CONTROLLER_FOR, RELATION_CONTROLLED_BY],
+ ["aria-flowto", RELATION_FLOWS_TO, RELATION_FLOWS_FROM],
+ ["aria-details", RELATION_DETAILS, RELATION_DETAILS_FOR],
+ ["aria-errormessage", RELATION_ERRORMSG, RELATION_ERRORMSG_FOR],
+];
+
+/**
+ * Test caching of relations between accessible objects.
+ */
+addAccessibleTask(
+ `
+ <div id="dependant1">label</div>
+ <div id="dependant2">label2</div>
+ <div role="checkbox" id="host"></div>`,
+ async function (browser, accDoc) {
+ for (let spec of attrRelationsSpec) {
+ await testRelated(browser, accDoc, ...spec);
+ }
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of relations with respect to label objects and their "for" attr.
+ */
+addAccessibleTask(
+ `
+ <input type="checkbox" id="dependant1">
+ <input type="checkbox" id="dependant2">
+ <label id="host">label</label>`,
+ async function (browser, accDoc) {
+ await testRelated(
+ browser,
+ accDoc,
+ "for",
+ RELATION_LABEL_FOR,
+ RELATION_LABELLED_BY
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test rel caching for element with existing relation attribute.
+ */
+addAccessibleTask(
+ `<div id="label">label</div><button id="button" aria-labelledby="label">`,
+ async function (browser, accDoc) {
+ const button = findAccessibleChildByID(accDoc, "button");
+ const label = findAccessibleChildByID(accDoc, "label");
+
+ await testCachedRelation(button, RELATION_LABELLED_BY, label);
+ await testCachedRelation(label, RELATION_LABEL_FOR, button);
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of relations with respect to output objects and their "for" attr.
+ */
+addAccessibleTask(
+ `
+ <form oninput="host.value=parseInt(dependant1.value)+parseInt(dependant2.value)">
+ <input type="number" id="dependant1" value="50"> +
+ <input type="number" id="dependant2" value="25"> =
+ <output name="host" id="host"></output>
+ </form>`,
+ async function (browser, accDoc) {
+ await testRelated(
+ browser,
+ accDoc,
+ "for",
+ RELATION_CONTROLLED_BY,
+ RELATION_CONTROLLER_FOR
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test rel caching for <label> element with existing "for" attribute.
+ */
+addAccessibleTask(
+ `data:text/html,<label id="label" for="input">label</label><input id="input">`,
+ async function (browser, accDoc) {
+ const input = findAccessibleChildByID(accDoc, "input");
+ const label = findAccessibleChildByID(accDoc, "label");
+ await testCachedRelation(input, RELATION_LABELLED_BY, label);
+ await testCachedRelation(label, RELATION_LABEL_FOR, input);
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/*
+ * Test caching of relations with respect to label objects that are ancestors of
+ * their target.
+ */
+addAccessibleTask(
+ `
+ <label id="host">
+ <input type="checkbox" id="dependant1">
+ </label>`,
+ async function (browser, accDoc) {
+ const input = findAccessibleChildByID(accDoc, "dependant1");
+ const label = findAccessibleChildByID(accDoc, "host");
+
+ await testCachedRelation(input, RELATION_LABELLED_BY, label);
+ await testCachedRelation(label, RELATION_LABEL_FOR, input);
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/*
+ * Test EMBEDS on root accessible.
+ */
+addAccessibleTask(
+ `hello world`,
+ async function (browser, primaryDocAcc, secondaryDocAcc) {
+ // The root accessible should EMBED the top level
+ // content document. If this test runs in an iframe,
+ // the test harness will pass in doc accs for both the
+ // iframe (primaryDocAcc) and the top level remote
+ // browser (secondaryDocAcc). We should use the second
+ // one.
+ // If this is not in an iframe, we'll only get
+ // a single docAcc (primaryDocAcc) which refers to
+ // the top level content doc.
+ const topLevelDoc = secondaryDocAcc ? secondaryDocAcc : primaryDocAcc;
+ await testRelation(
+ getRootAccessible(document),
+ RELATION_EMBEDS,
+ topLevelDoc
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test CONTAINING_TAB_PANE
+ */
+addAccessibleTask(
+ `<p id="p">hello world</p>`,
+ async function (browser, primaryDocAcc, secondaryDocAcc) {
+ // The CONTAINING_TAB_PANE of any acc should be the top level
+ // content document. If this test runs in an iframe,
+ // the test harness will pass in doc accs for both the
+ // iframe (primaryDocAcc) and the top level remote
+ // browser (secondaryDocAcc). We should use the second
+ // one.
+ // If this is not in an iframe, we'll only get
+ // a single docAcc (primaryDocAcc) which refers to
+ // the top level content doc.
+ const topLevelDoc = secondaryDocAcc ? secondaryDocAcc : primaryDocAcc;
+ await testCachedRelation(
+ findAccessibleChildByID(primaryDocAcc, "p"),
+ RELATION_CONTAINING_TAB_PANE,
+ topLevelDoc
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/*
+ * Test relation caching on link
+ */
+addAccessibleTask(
+ `
+ <a id="link" href="#item">a</a>
+ <div id="item">hello</div>
+ <div id="item2">world</div>
+ <a id="link2" href="#anchor">b</a>
+ <a id="namedLink" name="anchor">c</a>`,
+ async function (browser, accDoc) {
+ const link = findAccessibleChildByID(accDoc, "link");
+ const link2 = findAccessibleChildByID(accDoc, "link2");
+ const namedLink = findAccessibleChildByID(accDoc, "namedLink");
+ const item = findAccessibleChildByID(accDoc, "item");
+ const item2 = findAccessibleChildByID(accDoc, "item2");
+
+ await testCachedRelation(link, RELATION_LINKS_TO, item);
+ await testCachedRelation(link2, RELATION_LINKS_TO, namedLink);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("link").href = "";
+ content.document.getElementById("namedLink").name = "newName";
+ });
+
+ await testCachedRelation(link, RELATION_LINKS_TO, null);
+ await testCachedRelation(link2, RELATION_LINKS_TO, null);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("link").href = "#item2";
+ });
+
+ await testCachedRelation(link, RELATION_LINKS_TO, item2);
+ },
+ {
+ chrome: true,
+ // IA2 doesn't have a LINKS_TO relation and Windows non-cached
+ // RemoteAccessible uses IA2, so we can't run these tests in this case.
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/*
+ * Test relation caching for NODE_CHILD_OF and NODE_PARENT_OF with aria trees.
+ */
+addAccessibleTask(
+ `
+ <div role="tree" id="tree">
+ <div role="treeitem" id="treeitem">test</div>
+ <div role="treeitem" id="treeitem2">test</div>
+ </div>`,
+ async function (browser, accDoc) {
+ const tree = findAccessibleChildByID(accDoc, "tree");
+ const treeItem = findAccessibleChildByID(accDoc, "treeitem");
+ const treeItem2 = findAccessibleChildByID(accDoc, "treeitem2");
+
+ await testCachedRelation(tree, RELATION_NODE_PARENT_OF, [
+ treeItem,
+ treeItem2,
+ ]);
+ await testCachedRelation(treeItem, RELATION_NODE_CHILD_OF, tree);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/*
+ * Test relation caching for NODE_CHILD_OF and NODE_PARENT_OF with aria lists.
+ */
+addAccessibleTask(
+ `
+ <div id="l1" role="list">
+ <div id="l1i1" role="listitem" aria-level="1">a</div>
+ <div id="l1i2" role="listitem" aria-level="2">b</div>
+ <div id="l1i3" role="listitem" aria-level="1">c</div>
+ </div>`,
+ async function (browser, accDoc) {
+ const list = findAccessibleChildByID(accDoc, "l1");
+ const listItem1 = findAccessibleChildByID(accDoc, "l1i1");
+ const listItem2 = findAccessibleChildByID(accDoc, "l1i2");
+ const listItem3 = findAccessibleChildByID(accDoc, "l1i3");
+
+ await testCachedRelation(list, RELATION_NODE_PARENT_OF, [
+ listItem1,
+ listItem3,
+ ]);
+ await testCachedRelation(listItem1, RELATION_NODE_CHILD_OF, list);
+ await testCachedRelation(listItem3, RELATION_NODE_CHILD_OF, list);
+
+ await testCachedRelation(listItem1, RELATION_NODE_PARENT_OF, listItem2);
+ await testCachedRelation(listItem2, RELATION_NODE_CHILD_OF, listItem1);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/*
+ * Test NODE_CHILD_OF relation caching for JAWS window emulation special case.
+ */
+addAccessibleTask(
+ ``,
+ async function (browser, accDoc) {
+ await testCachedRelation(accDoc, RELATION_NODE_CHILD_OF, accDoc.parent);
+ },
+ { topLevel: true, chrome: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_relations_002.js b/accessible/tests/browser/e10s/browser_caching_relations_002.js
new file mode 100644
index 0000000000..072656eb5e
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_relations_002.js
@@ -0,0 +1,366 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+requestLongerTimeout(2);
+
+/**
+ * Test MEMBER_OF relation caching on HTML radio buttons
+ */
+addAccessibleTask(
+ `
+ <input type="radio" id="r1">I have no name<br>
+ <input type="radio" id="r2">I also have no name<br>
+ <input type="radio" id="r3" name="n">I have a name<br>
+ <input type="radio" id="r4" name="a">I have a different name<br>
+ <fieldset role="radiogroup">
+ <input type="radio" id="r5" name="n">I have an already used name
+ and am in a different part of the tree
+ <input type="radio" id="r6" name="r">I have a different name but am
+ in the same group
+ </fieldset>`,
+ async function (browser, accDoc) {
+ const r1 = findAccessibleChildByID(accDoc, "r1");
+ const r2 = findAccessibleChildByID(accDoc, "r2");
+ const r3 = findAccessibleChildByID(accDoc, "r3");
+ const r4 = findAccessibleChildByID(accDoc, "r4");
+ const r5 = findAccessibleChildByID(accDoc, "r5");
+ const r6 = findAccessibleChildByID(accDoc, "r6");
+
+ await testCachedRelation(r1, RELATION_MEMBER_OF, null);
+ await testCachedRelation(r2, RELATION_MEMBER_OF, null);
+ await testCachedRelation(r3, RELATION_MEMBER_OF, [r3, r5]);
+ await testCachedRelation(r4, RELATION_MEMBER_OF, r4);
+ await testCachedRelation(r5, RELATION_MEMBER_OF, [r3, r5]);
+ await testCachedRelation(r6, RELATION_MEMBER_OF, r6);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("r5").name = "a";
+ });
+
+ await testCachedRelation(r3, RELATION_MEMBER_OF, r3);
+ await testCachedRelation(r4, RELATION_MEMBER_OF, [r5, r4]);
+ await testCachedRelation(r5, RELATION_MEMBER_OF, [r5, r4]);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/*
+ * Test MEMBER_OF relation caching on aria radio buttons
+ */
+addAccessibleTask(
+ `
+ <div role="radio" id="r1">I have no radio group</div><br>
+ <fieldset role="radiogroup" id="fs">
+ <div role="radio" id="r2">hello</div><br>
+ <div role="radio" id="r3">world</div><br>
+ </fieldset>`,
+ async function (browser, accDoc) {
+ const r1 = findAccessibleChildByID(accDoc, "r1");
+ const r2 = findAccessibleChildByID(accDoc, "r2");
+ let r3 = findAccessibleChildByID(accDoc, "r3");
+
+ await testCachedRelation(r1, RELATION_MEMBER_OF, null);
+ await testCachedRelation(r2, RELATION_MEMBER_OF, [r2, r3]);
+ await testCachedRelation(r3, RELATION_MEMBER_OF, [r2, r3]);
+ const r = waitForEvent(EVENT_INNER_REORDER, "fs");
+ await invokeContentTask(browser, [], () => {
+ let innerRadio = content.document.getElementById("r3");
+ content.document.body.appendChild(innerRadio);
+ });
+ await r;
+
+ r3 = findAccessibleChildByID(accDoc, "r3");
+ await testCachedRelation(r1, RELATION_MEMBER_OF, null);
+ await testCachedRelation(r2, RELATION_MEMBER_OF, r2);
+ await testCachedRelation(r3, RELATION_MEMBER_OF, null);
+ },
+ {
+ chrome: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/*
+ * Test mutation of LABEL relations via accessible shutdown.
+ */
+addAccessibleTask(
+ `
+ <div id="d"></div>
+ <label id="l">
+ <select id="s">
+ `,
+ async function (browser, accDoc) {
+ const label = findAccessibleChildByID(accDoc, "l");
+ const select = findAccessibleChildByID(accDoc, "s");
+ const div = findAccessibleChildByID(accDoc, "d");
+
+ await testCachedRelation(label, RELATION_LABEL_FOR, select);
+ await testCachedRelation(select, RELATION_LABELLED_BY, label);
+ await testCachedRelation(div, RELATION_LABELLED_BY, null);
+
+ const r = waitForEvent(EVENT_REORDER, "l");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("s").remove();
+ });
+ await r;
+ await invokeContentTask(browser, [], () => {
+ const l = content.document.getElementById("l");
+ l.htmlFor = "d";
+ });
+ await testCachedRelation(label, RELATION_LABEL_FOR, div);
+ await testCachedRelation(div, RELATION_LABELLED_BY, label);
+ },
+ {
+ chrome: false,
+ iframe: true,
+ remoteIframe: true,
+ topLevel: true,
+ }
+);
+
+/*
+ * Test mutation of LABEL relations via DOM ID reuse.
+ */
+addAccessibleTask(
+ `
+ <div id="label">before</div><input id="input" aria-labelledby="label">
+ `,
+ async function (browser, accDoc) {
+ let label = findAccessibleChildByID(accDoc, "label");
+ const input = findAccessibleChildByID(accDoc, "input");
+
+ await testCachedRelation(label, RELATION_LABEL_FOR, input);
+ await testCachedRelation(input, RELATION_LABELLED_BY, label);
+
+ const r = waitForEvent(EVENT_REORDER, accDoc);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("label").remove();
+ let l = content.document.createElement("div");
+ l.id = "label";
+ l.textContent = "after";
+ content.document.body.insertBefore(
+ l,
+ content.document.getElementById("input")
+ );
+ });
+ await r;
+ label = findAccessibleChildByID(accDoc, "label");
+ await testCachedRelation(label, RELATION_LABEL_FOR, input);
+ await testCachedRelation(input, RELATION_LABELLED_BY, label);
+ },
+ {
+ chrome: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/*
+ * Test LINKS_TO relation caching an anchor with multiple hashes
+ */
+addAccessibleTask(
+ `
+ <a id="link" href="#foo#bar">Origin</a><br>
+ <a id="anchor" name="foo#bar">Destination`,
+ async function (browser, accDoc) {
+ const link = findAccessibleChildByID(accDoc, "link");
+ const anchor = findAccessibleChildByID(accDoc, "anchor");
+
+ await testCachedRelation(link, RELATION_LINKS_TO, anchor);
+ },
+ {
+ chrome: true,
+ // IA2 doesn't have a LINKS_TO relation and Windows non-cached
+ // RemoteAccessible uses IA2, so we can't run these tests in this case.
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/*
+ * Test mutation of LABEL relations via accessible shutdown.
+ */
+addAccessibleTask(
+ `
+ <div id="d"></div>
+ <label id="l">
+ <select id="s">
+ `,
+ async function (browser, accDoc) {
+ const label = findAccessibleChildByID(accDoc, "l");
+ const select = findAccessibleChildByID(accDoc, "s");
+ const div = findAccessibleChildByID(accDoc, "d");
+
+ await testCachedRelation(label, RELATION_LABEL_FOR, select);
+ await testCachedRelation(select, RELATION_LABELLED_BY, label);
+ await testCachedRelation(div, RELATION_LABELLED_BY, null);
+ await untilCacheOk(() => {
+ try {
+ // We should get an acc ID back from this, but we don't have a way of
+ // verifying its correctness -- it should be the ID of the select.
+ return label.cache.getStringProperty("for");
+ } catch (e) {
+ ok(false, "Exception thrown while trying to read from the cache");
+ return false;
+ }
+ }, "Label for relation exists");
+
+ const r = waitForEvent(EVENT_REORDER, "l");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("s").remove();
+ });
+ await r;
+ await untilCacheOk(() => {
+ try {
+ label.cache.getStringProperty("for");
+ } catch (e) {
+ // This property should no longer exist in the cache, so we should
+ // get an exception if we try to fetch it.
+ return true;
+ }
+ return false;
+ }, "Label for relation exists");
+
+ await invokeContentTask(browser, [], () => {
+ const l = content.document.getElementById("l");
+ l.htmlFor = "d";
+ });
+ await testCachedRelation(label, RELATION_LABEL_FOR, div);
+ await testCachedRelation(div, RELATION_LABELLED_BY, label);
+ },
+ {
+ /**
+ * This functionality is broken in our LocalAcccessible implementation,
+ * so we avoid running this test in chrome or when the cache is off.
+ */
+ chrome: false,
+ iframe: true,
+ remoteIframe: true,
+ topLevel: true,
+ }
+);
+
+/**
+ * Test label relations on HTML figure/figcaption.
+ */
+addAccessibleTask(
+ `
+<figure id="figure1">
+ before
+ <figcaption id="caption1">caption1</figcaption>
+ after
+</figure>
+<figure id="figure2" aria-labelledby="label">
+ <figcaption id="caption2">caption2</figure>
+</figure>
+<div id="label">label</div>
+ `,
+ async function (browser, docAcc) {
+ const figure1 = findAccessibleChildByID(docAcc, "figure1");
+ let caption1 = findAccessibleChildByID(docAcc, "caption1");
+ await testCachedRelation(figure1, RELATION_LABELLED_BY, caption1);
+ await testCachedRelation(caption1, RELATION_LABEL_FOR, figure1);
+
+ info("Hiding caption1");
+ let mutated = waitForEvent(EVENT_HIDE, caption1);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("caption1").hidden = true;
+ });
+ await mutated;
+ await testCachedRelation(figure1, RELATION_LABELLED_BY, null);
+
+ info("Showing caption1");
+ mutated = waitForEvent(EVENT_SHOW, "caption1");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("caption1").hidden = false;
+ });
+ caption1 = (await mutated).accessible;
+ await testCachedRelation(figure1, RELATION_LABELLED_BY, caption1);
+ await testCachedRelation(caption1, RELATION_LABEL_FOR, figure1);
+
+ const figure2 = findAccessibleChildByID(docAcc, "figure2");
+ const caption2 = findAccessibleChildByID(docAcc, "caption2");
+ const label = findAccessibleChildByID(docAcc, "label");
+ await testCachedRelation(figure2, RELATION_LABELLED_BY, [label, caption2]);
+ await testCachedRelation(caption2, RELATION_LABEL_FOR, figure2);
+ await testCachedRelation(label, RELATION_LABEL_FOR, figure2);
+ },
+ { chrome: true, topLevel: true }
+);
+
+/**
+ * Test details relations on popovers and their invokers.
+ */
+addAccessibleTask(
+ `
+<button id="hide" popovertarget="popover" popovertargetaction="hide">hide</button>
+<button id="toggle1" popovertarget="popover">toggle1</button>
+<button id="toggle2">toggle2</button>
+<button id="toggleSibling">toggleSibling</button>
+<div id="popover" popover>popover</div>
+<div id="details">details</div>
+ `,
+ async function testPopover(browser, docAcc) {
+ // The popover is hidden, so nothing should be referring to it.
+ const hide = findAccessibleChildByID(docAcc, "hide");
+ await testCachedRelation(hide, RELATION_DETAILS, []);
+ const toggle1 = findAccessibleChildByID(docAcc, "toggle1");
+ await testCachedRelation(toggle1, RELATION_DETAILS, []);
+ const toggle2 = findAccessibleChildByID(docAcc, "toggle2");
+ await testCachedRelation(toggle2, RELATION_DETAILS, []);
+ const toggleSibling = findAccessibleChildByID(docAcc, "toggleSibling");
+ await testCachedRelation(toggleSibling, RELATION_DETAILS, []);
+
+ info("Showing popover");
+ let shown = waitForEvent(EVENT_SHOW, "popover");
+ toggle1.doAction(0);
+ const popover = (await shown).accessible;
+ await testCachedRelation(toggle1, RELATION_DETAILS, popover);
+ // toggle2 shouldn't have a details relation because it doesn't have a
+ // popovertarget.
+ await testCachedRelation(toggle2, RELATION_DETAILS, []);
+ // hide shouldn't have a details relation because its action is hide.
+ await testCachedRelation(hide, RELATION_DETAILS, []);
+ // toggleSibling shouldn't have a details relation because it is a sibling
+ // of the popover.
+ await testCachedRelation(toggleSibling, RELATION_DETAILS, []);
+ await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1);
+
+ info("Setting toggle2 popovertargetaction");
+ await invokeSetAttribute(browser, "toggle2", "popovertarget", "popover");
+ await testCachedRelation(toggle2, RELATION_DETAILS, popover);
+ await testCachedRelation(popover, RELATION_DETAILS_FOR, [toggle1, toggle2]);
+
+ info("Removing toggle2 popovertarget");
+ await invokeSetAttribute(browser, "toggle2", "popovertarget", null);
+ await testCachedRelation(toggle2, RELATION_DETAILS, []);
+ await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1);
+
+ info("Setting aria-details on toggle1");
+ await invokeSetAttribute(browser, "toggle1", "aria-details", "details");
+ const details = findAccessibleChildByID(docAcc, "details");
+ // aria-details overrides popover.
+ await testCachedRelation(toggle1, RELATION_DETAILS, details);
+ await testCachedRelation(popover, RELATION_DETAILS_FOR, []);
+
+ info("Removing aria-details from toggle1");
+ await invokeSetAttribute(browser, "toggle1", "aria-details", null);
+ await testCachedRelation(toggle1, RELATION_DETAILS, popover);
+ await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1);
+
+ info("Hiding popover");
+ let hidden = waitForEvent(EVENT_HIDE, popover);
+ toggle1.doAction(0);
+ // The relations between toggle1 and popover are removed when popover shuts
+ // down. However, this doesn't cause a cache update notification. Therefore,
+ // to avoid timing out in testCachedRelation, we must wait for a hide event
+ // first.
+ await hidden;
+ await testCachedRelation(toggle1, RELATION_DETAILS, []);
+ },
+ { chrome: false, topLevel: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_states.js b/accessible/tests/browser/e10s/browser_caching_states.js
new file mode 100644
index 0000000000..37f8c46966
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_states.js
@@ -0,0 +1,552 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * expected {Array} expected states for a given accessible that have the
+ * following format:
+ * [
+ * expected state,
+ * expected extra state,
+ * absent state,
+ * absent extra state
+ * ]
+ * attrs {?Array} an optional list of attributes to update
+ * }
+ */
+
+// State caching tests for attribute changes
+const attributeTests = [
+ {
+ desc:
+ "Checkbox with @checked attribute set to true should have checked " +
+ "state",
+ attrs: [
+ {
+ attr: "checked",
+ value: "true",
+ },
+ ],
+ expected: [STATE_CHECKED, 0],
+ },
+ {
+ desc: "Checkbox with no @checked attribute should not have checked state",
+ attrs: [
+ {
+ attr: "checked",
+ },
+ ],
+ expected: [0, 0, STATE_CHECKED],
+ },
+];
+
+// State caching tests for ARIA changes
+const ariaTests = [
+ {
+ desc: "File input has busy state when @aria-busy attribute is set to true",
+ attrs: [
+ {
+ attr: "aria-busy",
+ value: "true",
+ },
+ ],
+ expected: [STATE_BUSY, 0, STATE_REQUIRED | STATE_INVALID],
+ },
+ {
+ desc:
+ "File input has required state when @aria-required attribute is set " +
+ "to true",
+ attrs: [
+ {
+ attr: "aria-required",
+ value: "true",
+ },
+ ],
+ expected: [STATE_REQUIRED, 0, STATE_INVALID],
+ },
+ {
+ desc:
+ "File input has invalid state when @aria-invalid attribute is set to " +
+ "true",
+ attrs: [
+ {
+ attr: "aria-invalid",
+ value: "true",
+ },
+ ],
+ expected: [STATE_INVALID, 0],
+ },
+];
+
+// Extra state caching tests
+const extraStateTests = [
+ {
+ desc:
+ "Input has no extra enabled state when aria and native disabled " +
+ "attributes are set at once",
+ attrs: [
+ {
+ attr: "aria-disabled",
+ value: "true",
+ },
+ {
+ attr: "disabled",
+ value: "true",
+ },
+ ],
+ expected: [0, 0, 0, EXT_STATE_ENABLED],
+ },
+ {
+ desc:
+ "Input has an extra enabled state when aria and native disabled " +
+ "attributes are unset at once",
+ attrs: [
+ {
+ attr: "aria-disabled",
+ },
+ {
+ attr: "disabled",
+ },
+ ],
+ expected: [0, EXT_STATE_ENABLED],
+ },
+];
+
+async function runStateTests(browser, accDoc, id, tests) {
+ let acc = findAccessibleChildByID(accDoc, id);
+ for (let { desc, attrs, expected } of tests) {
+ const [expState, expExtState, absState, absExtState] = expected;
+ info(desc);
+ let onUpdate = waitForEvent(EVENT_STATE_CHANGE, evt => {
+ if (getAccessibleDOMNodeID(evt.accessible) != id) {
+ return false;
+ }
+ // Events can be fired for states other than the ones we're interested
+ // in. If this happens, the states we're expecting might not be exposed
+ // yet.
+ const scEvt = evt.QueryInterface(nsIAccessibleStateChangeEvent);
+ if (scEvt.isExtraState) {
+ if (scEvt.state & expExtState || scEvt.state & absExtState) {
+ return true;
+ }
+ return false;
+ }
+ return scEvt.state & expState || scEvt.state & absState;
+ });
+ for (let { attr, value } of attrs) {
+ await invokeSetAttribute(browser, id, attr, value);
+ }
+ await onUpdate;
+ testStates(acc, ...expected);
+ }
+}
+
+/**
+ * Test caching of accessible object states
+ */
+addAccessibleTask(
+ `
+ <input id="checkbox" type="checkbox">
+ <input id="file" type="file">
+ <input id="text">`,
+ async function (browser, accDoc) {
+ await runStateTests(browser, accDoc, "checkbox", attributeTests);
+ await runStateTests(browser, accDoc, "file", ariaTests);
+ await runStateTests(browser, accDoc, "text", extraStateTests);
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of the focused state.
+ */
+addAccessibleTask(
+ `
+ <button id="b1">b1</button>
+ <button id="b2">b2</button>
+ `,
+ async function (browser, docAcc) {
+ const b1 = findAccessibleChildByID(docAcc, "b1");
+ const b2 = findAccessibleChildByID(docAcc, "b2");
+
+ let focused = waitForEvent(EVENT_FOCUS, b1);
+ await invokeFocus(browser, "b1");
+ await focused;
+ testStates(docAcc, 0, 0, STATE_FOCUSED);
+ testStates(b1, STATE_FOCUSED);
+ testStates(b2, 0, 0, STATE_FOCUSED);
+
+ focused = waitForEvent(EVENT_FOCUS, b2);
+ await invokeFocus(browser, "b2");
+ await focused;
+ testStates(b2, STATE_FOCUSED);
+ testStates(b1, 0, 0, STATE_FOCUSED);
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test that the document initially gets the focused state.
+ * We can't do this in the test above because that test runs in iframes as well
+ * as a top level document.
+ */
+addAccessibleTask(
+ `
+ <button id="b1">b1</button>
+ <button id="b2">b2</button>
+ `,
+ async function (browser, docAcc) {
+ testStates(docAcc, STATE_FOCUSED);
+ }
+);
+
+/**
+ * Test caching of the focused state in iframes.
+ */
+addAccessibleTask(
+ `
+ <button id="button">button</button>
+ `,
+ async function (browser, iframeDocAcc, topDocAcc) {
+ testStates(topDocAcc, STATE_FOCUSED);
+ const button = findAccessibleChildByID(iframeDocAcc, "button");
+ testStates(button, 0, 0, STATE_FOCUSED);
+ let focused = waitForEvent(EVENT_FOCUS, button);
+ info("Focusing button in iframe");
+ button.takeFocus();
+ await focused;
+ testStates(topDocAcc, 0, 0, STATE_FOCUSED);
+ testStates(button, STATE_FOCUSED);
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of the focusable state in iframes which are initially visibility: hidden.
+ */
+addAccessibleTask(
+ `
+<button id="button"></button>
+<span id="span" tabindex="-1">span</span>`,
+ async function (browser, topDocAcc) {
+ info("Changing visibility on iframe");
+ let reordered = waitForEvent(EVENT_REORDER, topDocAcc);
+ await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], iframeId => {
+ content.document.getElementById(iframeId).style.visibility = "";
+ });
+ await reordered;
+ // The iframe doc a11y tree might not be built yet.
+ const iframeDoc = await TestUtils.waitForCondition(() =>
+ findAccessibleChildByID(topDocAcc, DEFAULT_IFRAME_DOC_BODY_ID)
+ );
+ // Log/verify whether this is an in-process or OOP iframe.
+ await comparePIDs(browser, gIsRemoteIframe);
+ const button = findAccessibleChildByID(iframeDoc, "button");
+ testStates(button, STATE_FOCUSABLE);
+ const span = findAccessibleChildByID(iframeDoc, "span");
+ ok(span, "span Accessible exists");
+ testStates(span, STATE_FOCUSABLE);
+ },
+ {
+ topLevel: false,
+ iframe: true,
+ remoteIframe: true,
+ iframeAttrs: { style: "visibility: hidden;" },
+ skipFissionDocLoad: true,
+ }
+);
+
+function checkOpacity(acc, present) {
+ let [, extraState] = getStates(acc);
+ let currOpacity = extraState & EXT_STATE_OPAQUE;
+ return present ? currOpacity : !currOpacity;
+}
+
+/**
+ * Test caching of the OPAQUE1 state.
+ */
+addAccessibleTask(
+ `
+ <div id="div">hello world</div>
+ `,
+ async function (browser, docAcc) {
+ const div = findAccessibleChildByID(docAcc, "div");
+ await untilCacheOk(() => checkOpacity(div, true), "Found opaque state");
+
+ await invokeContentTask(browser, [], () => {
+ let elm = content.document.getElementById("div");
+ elm.style = "opacity: 0.4;";
+ elm.offsetTop; // Flush layout.
+ });
+
+ await untilCacheOk(
+ () => checkOpacity(div, false),
+ "Did not find opaque state"
+ );
+
+ await invokeContentTask(browser, [], () => {
+ let elm = content.document.getElementById("div");
+ elm.style = "opacity: 1;";
+ elm.offsetTop; // Flush layout.
+ });
+
+ await untilCacheOk(() => checkOpacity(div, true), "Found opaque state");
+ },
+ { iframe: true, remoteIframe: true, chrome: true }
+);
+
+/**
+ * Test caching of the editable state.
+ */
+addAccessibleTask(
+ `<div id="div" contenteditable></div>`,
+ async function (browser, docAcc) {
+ const div = findAccessibleChildByID(docAcc, "div");
+ testStates(div, 0, EXT_STATE_EDITABLE, 0, 0);
+ // Ensure that a contentEditable descendant doesn't cause editable to be
+ // exposed on the document.
+ testStates(docAcc, STATE_READONLY, 0, 0, EXT_STATE_EDITABLE);
+
+ info("Setting contentEditable on the body");
+ let stateChanged = Promise.all([
+ waitForStateChange(docAcc, EXT_STATE_EDITABLE, true, true),
+ waitForStateChange(docAcc, STATE_READONLY, false, false),
+ ]);
+ await invokeContentTask(browser, [], () => {
+ content.document.body.contentEditable = true;
+ });
+ await stateChanged;
+ testStates(docAcc, 0, EXT_STATE_EDITABLE, STATE_READONLY, 0);
+
+ info("Clearing contentEditable on the body");
+ stateChanged = Promise.all([
+ waitForStateChange(docAcc, EXT_STATE_EDITABLE, false, true),
+ waitForStateChange(docAcc, STATE_READONLY, true, false),
+ ]);
+ await invokeContentTask(browser, [], () => {
+ content.document.body.contentEditable = false;
+ });
+ await stateChanged;
+ testStates(docAcc, STATE_READONLY, 0, 0, EXT_STATE_EDITABLE);
+
+ info("Clearing contentEditable on div");
+ stateChanged = waitForStateChange(div, EXT_STATE_EDITABLE, false, true);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").contentEditable = false;
+ });
+ await stateChanged;
+ testStates(div, 0, 0, 0, EXT_STATE_EDITABLE);
+
+ info("Setting contentEditable on div");
+ stateChanged = waitForStateChange(div, EXT_STATE_EDITABLE, true, true);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").contentEditable = true;
+ });
+ await stateChanged;
+ testStates(div, 0, EXT_STATE_EDITABLE, 0, 0);
+
+ info("Setting designMode on document");
+ stateChanged = Promise.all([
+ waitForStateChange(docAcc, EXT_STATE_EDITABLE, true, true),
+ waitForStateChange(docAcc, STATE_READONLY, false, false),
+ ]);
+ await invokeContentTask(browser, [], () => {
+ content.document.designMode = "on";
+ });
+ await stateChanged;
+ testStates(docAcc, 0, EXT_STATE_EDITABLE, STATE_READONLY, 0);
+
+ info("Clearing designMode on document");
+ stateChanged = Promise.all([
+ waitForStateChange(docAcc, EXT_STATE_EDITABLE, false, true),
+ waitForStateChange(docAcc, STATE_READONLY, true, false),
+ ]);
+ await invokeContentTask(browser, [], () => {
+ content.document.designMode = "off";
+ });
+ await stateChanged;
+ testStates(docAcc, STATE_READONLY, 0, 0, EXT_STATE_EDITABLE);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true, chrome: true }
+);
+
+/**
+ * Test caching of the stale and busy states.
+ */
+addAccessibleTask(
+ `<iframe id="iframe"></iframe>`,
+ async function (browser, docAcc) {
+ const iframe = findAccessibleChildByID(docAcc, "iframe");
+ info("Setting iframe src");
+ // This iframe won't finish loading. Thus, it will get the stale state and
+ // won't fire a document load complete event. We use the reorder event on
+ // the iframe to know when the document has been created.
+ let reordered = waitForEvent(EVENT_REORDER, iframe);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("iframe").src =
+ 'data:text/html,<img src="http://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs">';
+ });
+ const iframeDoc = (await reordered).accessible.firstChild;
+ testStates(iframeDoc, STATE_BUSY, EXT_STATE_STALE, 0, 0);
+
+ info("Finishing load of iframe doc");
+ let loadCompleted = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, iframeDoc);
+ await fetch(
+ "https://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs?complete"
+ );
+ await loadCompleted;
+ testStates(iframeDoc, 0, 0, STATE_BUSY, EXT_STATE_STALE);
+ },
+ { topLevel: true, chrome: true }
+);
+
+/**
+ * Test implicit selected state.
+ */
+addAccessibleTask(
+ `
+<div role="tablist">
+ <div id="noSel" role="tab" tabindex="0">noSel</div>
+ <div id="selFalse" role="tab" aria-selected="false" tabindex="0">selFalse</div>
+</div>
+<div role="listbox" aria-multiselectable="true">
+ <div id="multiNoSel" role="option" tabindex="0">multiNoSel</div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const noSel = findAccessibleChildByID(docAcc, "noSel");
+ testStates(noSel, 0, 0, STATE_FOCUSED | STATE_SELECTED, 0);
+ info("Focusing noSel");
+ let focused = waitForEvent(EVENT_FOCUS, noSel);
+ noSel.takeFocus();
+ await focused;
+ testStates(noSel, STATE_FOCUSED | STATE_SELECTED, 0, 0, 0);
+
+ const selFalse = findAccessibleChildByID(docAcc, "selFalse");
+ testStates(selFalse, 0, 0, STATE_FOCUSED | STATE_SELECTED, 0);
+ info("Focusing selFalse");
+ focused = waitForEvent(EVENT_FOCUS, selFalse);
+ selFalse.takeFocus();
+ await focused;
+ testStates(selFalse, STATE_FOCUSED, 0, STATE_SELECTED, 0);
+
+ const multiNoSel = findAccessibleChildByID(docAcc, "multiNoSel");
+ testStates(multiNoSel, 0, 0, STATE_FOCUSED | STATE_SELECTED, 0);
+ info("Focusing multiNoSel");
+ focused = waitForEvent(EVENT_FOCUS, multiNoSel);
+ multiNoSel.takeFocus();
+ await focused;
+ testStates(multiNoSel, STATE_FOCUSED, 0, STATE_SELECTED, 0);
+ },
+ { topLevel: true, iframe: true, remoteIframe: true, chrome: true }
+);
+
+/**
+ * Test invalid state determined via DOM.
+ */
+addAccessibleTask(
+ `<input type="email" id="email">`,
+ async function (browser, docAcc) {
+ const email = findAccessibleChildByID(docAcc, "email");
+ info("Focusing email");
+ let focused = waitForEvent(EVENT_FOCUS, email);
+ email.takeFocus();
+ await focused;
+ info("Typing a");
+ let invalidChanged = waitForStateChange(email, STATE_INVALID, true);
+ EventUtils.sendString("a");
+ await invalidChanged;
+ testStates(email, STATE_INVALID);
+ info("Typing @b");
+ invalidChanged = waitForStateChange(email, STATE_INVALID, false);
+ EventUtils.sendString("@b");
+ await invalidChanged;
+ testStates(email, 0, 0, STATE_INVALID);
+ info("Typing backspace");
+ invalidChanged = waitForStateChange(email, STATE_INVALID, true);
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await invalidChanged;
+ testStates(email, STATE_INVALID);
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of the expanded state for popover target element.
+ */
+addAccessibleTask(
+ `
+ <button id="show-popover-btn" popovertarget="mypopover" popovertargetaction="show">Show popover</button>
+ <button id="hide-popover-btn" popovertarget="mypopover" popovertargetaction="hide">Hide popover</button>
+ <button id="toggle">toggle</button>
+ <div id="mypopover" popover>
+ Popover content
+ <button id="hide-inside" popovertarget="mypopover" popovertargetaction="hide">Hide inside popover</button>
+ </div>
+ `,
+ async function (browser, docAcc) {
+ const show = findAccessibleChildByID(docAcc, "show-popover-btn");
+ const hide = findAccessibleChildByID(docAcc, "hide-popover-btn");
+ testStates(show, STATE_COLLAPSED, 0);
+ testStates(hide, STATE_COLLAPSED, 0);
+ const toggle = findAccessibleChildByID(docAcc, "toggle");
+ testStates(
+ toggle,
+ 0,
+ 0,
+ STATE_EXPANDED | STATE_COLLAPSED,
+ EXT_STATE_EXPANDABLE
+ );
+
+ info("Setting toggle's popovertarget");
+ let stateChanged = waitForStateChange(
+ toggle,
+ EXT_STATE_EXPANDABLE,
+ true,
+ true
+ );
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("toggle")
+ .setAttribute("popovertarget", "mypopover");
+ });
+ await stateChanged;
+
+ // Changes to the popover should fire events on all invokers.
+ const changeEvents = [
+ [EVENT_STATE_CHANGE, show],
+ [EVENT_STATE_CHANGE, hide],
+ [EVENT_STATE_CHANGE, toggle],
+ ];
+ info("Expanding popover");
+ let onShowing = waitForEvents(changeEvents);
+ await show.doAction(0);
+ await onShowing;
+ testStates(show, STATE_EXPANDED, 0);
+ testStates(hide, STATE_EXPANDED, 0);
+ testStates(toggle, STATE_EXPANDED, 0);
+ const hideInside = findAccessibleChildByID(show, "hide-inside");
+ testStates(hideInside, 0, 0, STATE_EXPANDED | STATE_COLLAPSED, 0);
+
+ info("Collapsing popover");
+ let onHiding = waitForEvents(changeEvents);
+ await hide.doAction(0);
+ await onHiding;
+ testStates(hide, STATE_COLLAPSED, 0);
+ testStates(show, STATE_COLLAPSED, 0);
+ testStates(toggle, STATE_COLLAPSED, 0);
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_table.js b/accessible/tests/browser/e10s/browser_caching_table.js
new file mode 100644
index 0000000000..9c8bcb9616
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_table.js
@@ -0,0 +1,665 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test tables for both local and remote Accessibles. There is more extensive
+ * coverage in ../../mochitest/table. These tests are primarily to ensure that
+ * the cache works as expected and that there is consistency between local and
+ * remote.
+ */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/table.js */
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts(
+ { name: "table.js", dir: MOCHITESTS_DIR },
+ { name: "attributes.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test table counts, indexes, extents and implicit headers.
+ */
+addAccessibleTask(
+ `
+<table id="table">
+ <thead>
+ <tr><th id="a">a</th><th id="bc" colspan="2">bc</th><th id="d">d</th></tr>
+ </thead>
+ <tbody>
+ <tr><th id="ei" rowspan="2">ei</th><td id="fj" rowspan="0">fj</td><td id="g">g</td><td id="h">h</td></tr>
+ <tr><td id="k">k</td></tr>
+ </tbody>
+</table>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+ is(table.rowCount, 3, "table rowCount correct");
+ is(table.columnCount, 4, "table columnCount correct");
+ testTableIndexes(table, [
+ [0, 1, 1, 2],
+ [3, 4, 5, 6],
+ [3, 4, 7, -1],
+ ]);
+ const cells = {};
+ for (const id of ["a", "bc", "d", "ei", "fj", "g", "h", "k"]) {
+ cells[id] = findAccessibleChildByID(docAcc, id, [nsIAccessibleTableCell]);
+ }
+ is(cells.a.rowExtent, 1, "a rowExtent correct");
+ is(cells.a.columnExtent, 1, "a columnExtent correct");
+ is(cells.bc.rowExtent, 1, "bc rowExtent correct");
+ is(cells.bc.columnExtent, 2, "bc columnExtent correct");
+ is(cells.ei.rowExtent, 2, "ei rowExtent correct");
+ is(cells.fj.rowExtent, 2, "fj rowExtent correct");
+ testHeaderCells([
+ {
+ cell: cells.ei,
+ rowHeaderCells: [],
+ columnHeaderCells: [cells.a],
+ },
+ {
+ cell: cells.g,
+ rowHeaderCells: [cells.ei],
+ columnHeaderCells: [cells.bc],
+ },
+ {
+ cell: cells.k,
+ rowHeaderCells: [cells.ei],
+ columnHeaderCells: [cells.bc],
+ },
+ ]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test table explicit headers.
+ */
+addAccessibleTask(
+ `
+<table id="table">
+ <tr><th id="a">a</th><th id="b">b</th></tr>
+ <tr><td id="c" headers="b d">c</td><th scope="row" id="d">d</th></tr>
+ <tr><td id="e" headers="c f">e</td><td id="f">f</td></tr>
+</table>
+ `,
+ async function (browser, docAcc) {
+ const cells = {};
+ for (const id of ["a", "b", "c", "d", "e", "f"]) {
+ cells[id] = findAccessibleChildByID(docAcc, id, [nsIAccessibleTableCell]);
+ }
+ testHeaderCells([
+ {
+ cell: cells.c,
+ rowHeaderCells: [cells.d],
+ columnHeaderCells: [cells.b],
+ },
+ {
+ cell: cells.e,
+ rowHeaderCells: [cells.f],
+ columnHeaderCells: [cells.c],
+ },
+ ]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test that an inner table doesn't impact an outer table.
+ */
+addAccessibleTask(
+ `
+<table id="outerTable">
+ <tr><th id="outerCell">outerCell<table id="innerTable">
+ <tr><th id="innerCell">a</th></tr></table>
+ </table></th></tr>
+</table>
+ `,
+ async function (browser, docAcc) {
+ const outerTable = findAccessibleChildByID(docAcc, "outerTable", [
+ nsIAccessibleTable,
+ ]);
+ is(outerTable.rowCount, 1, "outerTable rowCount correct");
+ is(outerTable.columnCount, 1, "outerTable columnCount correct");
+ const outerCell = findAccessibleChildByID(docAcc, "outerCell");
+ is(
+ outerTable.getCellAt(0, 0),
+ outerCell,
+ "outerTable returns correct cell"
+ );
+ const innerTable = findAccessibleChildByID(docAcc, "innerTable", [
+ nsIAccessibleTable,
+ ]);
+ is(innerTable.rowCount, 1, "innerTable rowCount correct");
+ is(innerTable.columnCount, 1, "innerTable columnCount correct");
+ const innerCell = findAccessibleChildByID(docAcc, "innerCell");
+ is(
+ innerTable.getCellAt(0, 0),
+ innerCell,
+ "innerTable returns correct cell"
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test table caption and summary.
+ */
+addAccessibleTask(
+ `
+<table id="t1">
+ <caption id="c1">c1</caption>
+ <tr><th>a</th></tr>
+</table>
+<table id="t2" summary="s2">
+ <tr><th>a</th></tr>
+</table>
+<table id="t3" summary="s3">
+ <caption id="c3">c3</caption>
+ <tr><th>a</th></tr>
+</table>
+ `,
+ async function (browser, docAcc) {
+ const t1 = findAccessibleChildByID(docAcc, "t1", [nsIAccessibleTable]);
+ const c1 = findAccessibleChildByID(docAcc, "c1");
+ is(t1.caption, c1, "t1 caption correct");
+ ok(!t1.summary, "t1 no summary");
+ const t2 = findAccessibleChildByID(docAcc, "t2", [nsIAccessibleTable]);
+ ok(!t2.caption, "t2 caption is null");
+ is(t2.summary, "s2", "t2 summary correct");
+ const t3 = findAccessibleChildByID(docAcc, "t3", [nsIAccessibleTable]);
+ const c3 = findAccessibleChildByID(docAcc, "c3");
+ is(t3.caption, c3, "t3 caption correct");
+ is(t3.summary, "s3", "t3 summary correct");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test table layout guess.
+ */
+addAccessibleTask(
+ `
+<table id="layout"><tr><td>a</td></tr></table>
+<table id="data"><tr><th>a</th></tr></table>
+<table id="mutate"><tr><td>a</td><td>b</td></tr></table>
+<div id="newTableContainer"></div>
+ `,
+ async function (browser, docAcc) {
+ const layout = findAccessibleChildByID(docAcc, "layout");
+ testAttrs(layout, { "layout-guess": "true" }, true);
+ const data = findAccessibleChildByID(docAcc, "data");
+ testAbsentAttrs(data, { "layout-guess": "true" });
+ const mutate = findAccessibleChildByID(docAcc, "mutate");
+ testAttrs(mutate, { "layout-guess": "true" }, true);
+
+ info("mutate: Adding 5 rows");
+ let reordered = waitForEvent(EVENT_REORDER, mutate);
+ await invokeContentTask(browser, [], () => {
+ const frag = content.document.createDocumentFragment();
+ for (let r = 0; r < 6; ++r) {
+ const tr = content.document.createElement("tr");
+ tr.innerHTML = "<td>a</td><td>b</td>";
+ frag.append(tr);
+ }
+ content.document.getElementById("mutate").tBodies[0].append(frag);
+ });
+ await reordered;
+ testAbsentAttrs(mutate, { "layout-guess": "true" });
+
+ info("mutate: Removing 5 rows");
+ reordered = waitForEvent(EVENT_REORDER, mutate);
+ await invokeContentTask(browser, [], () => {
+ // Pause refresh driver so all the children removals below will
+ // be collated into the same tick and only one 'reorder' event will
+ // be dispatched.
+ content.windowUtils.advanceTimeAndRefresh(100);
+
+ let tBody = content.document.getElementById("mutate").tBodies[0];
+ for (let r = 0; r < 6; ++r) {
+ tBody.lastChild.remove();
+ }
+
+ // Resume refresh driver
+ content.windowUtils.restoreNormalRefresh();
+ });
+ await reordered;
+ testAttrs(mutate, { "layout-guess": "true" }, true);
+
+ info("mutate: Adding new table");
+ let shown = waitForEvent(EVENT_SHOW, "newTable");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById(
+ "newTableContainer"
+ ).innerHTML = `<table id="newTable"><tr><th>a</th></tr></table>`;
+ });
+ let newTable = (await shown).accessible;
+ testAbsentAttrs(newTable, { "layout-guess": "true" });
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test table layout guess with border styling changes.
+ */
+addAccessibleTask(
+ `
+ <table id="layout"><tr><td id="cell">a</td><td>b</td></tr>
+ <tr><td>c</td><td>d</td></tr><tr><td>c</td><td>d</td></tr></table>
+ `,
+ async function (browser, docAcc) {
+ const layout = findAccessibleChildByID(docAcc, "layout");
+ testAttrs(layout, { "layout-guess": "true" }, true);
+ info("changing border style on table cell");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("cell").style.border = "1px solid black";
+ content.document.body.offsetTop; // Flush layout.
+ });
+ await untilCacheOk(() => {
+ // manually verify the attribute doesn't exist, since `testAbsentAttrs`
+ // has internal calls to ok() which fail if the cache hasn't yet updated
+ for (let prop of layout.attributes.enumerate()) {
+ if (prop.key == "layout-guess") {
+ return false;
+ }
+ }
+ return true;
+ }, "Table is a data table");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test ARIA grid.
+ */
+addAccessibleTask(
+ `
+<div id="grid" role="grid">
+ <div role="rowgroup">
+ <div role="row"><div id="a" role="columnheader">a</div><div id="b" role="columnheader">b</div></div>
+ </div>
+ <div tabindex="-1">
+ <div role="row"><div id="c" role="rowheader">c</div><div id="d" role="gridcell">d</div></div>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const grid = findAccessibleChildByID(docAcc, "grid", [nsIAccessibleTable]);
+ is(grid.rowCount, 2, "grid rowCount correct");
+ is(grid.columnCount, 2, "grid columnCount correct");
+ testTableIndexes(grid, [
+ [0, 1],
+ [2, 3],
+ ]);
+ const cells = {};
+ for (const id of ["a", "b", "c", "d"]) {
+ cells[id] = findAccessibleChildByID(docAcc, id, [nsIAccessibleTableCell]);
+ }
+ is(cells.a.rowExtent, 1, "a rowExtent correct");
+ is(cells.a.columnExtent, 1, "a columnExtent correct");
+ testHeaderCells([
+ {
+ cell: cells.c,
+ rowHeaderCells: [],
+ columnHeaderCells: [cells.a],
+ },
+ {
+ cell: cells.d,
+ rowHeaderCells: [cells.c],
+ columnHeaderCells: [cells.b],
+ },
+ ]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+function setNodeHidden(browser, id, hidden) {
+ return invokeContentTask(browser, [id, hidden], (cId, cHidden) => {
+ content.document.getElementById(cId).hidden = cHidden;
+ });
+}
+
+/**
+ * Test that the table is updated correctly when it is mutated.
+ */
+addAccessibleTask(
+ `
+<table id="table">
+ <tr id="r1"><td>a</td><td id="b">b</td></tr>
+ <tr id="r2" hidden><td>c</td><td>d</td></tr>
+</table>
+<div id="owner"></div>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+ is(table.rowCount, 1, "table rowCount correct");
+ is(table.columnCount, 2, "table columnCount correct");
+ testTableIndexes(table, [[0, 1]]);
+ info("Showing r2");
+ let reordered = waitForEvent(EVENT_REORDER, table);
+ await setNodeHidden(browser, "r2", false);
+ await reordered;
+ is(table.rowCount, 2, "table rowCount correct");
+ testTableIndexes(table, [
+ [0, 1],
+ [2, 3],
+ ]);
+ info("Hiding r2");
+ reordered = waitForEvent(EVENT_REORDER, table);
+ await setNodeHidden(browser, "r2", true);
+ await reordered;
+ is(table.rowCount, 1, "table rowCount correct");
+ testTableIndexes(table, [[0, 1]]);
+ info("Hiding b");
+ reordered = waitForEvent(EVENT_REORDER, "r1");
+ await setNodeHidden(browser, "b", true);
+ await reordered;
+ is(table.columnCount, 1, "table columnCount correct");
+ testTableIndexes(table, [[0]]);
+ info("Showing b");
+ reordered = waitForEvent(EVENT_REORDER, "r1");
+ await setNodeHidden(browser, "b", false);
+ await reordered;
+ is(table.columnCount, 2, "table columnCount correct");
+ info("Moving b out of table using aria-owns");
+ reordered = waitForEvent(EVENT_REORDER, "r1");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("owner").setAttribute("aria-owns", "b");
+ });
+ await reordered;
+ is(table.columnCount, 1, "table columnCount correct");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test the handling of ARIA tables with display: contents.
+ */
+addAccessibleTask(
+ `
+<div id="table" role="table" style="display: contents;">
+ <div role="row"><div role="cell">a</div></div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+ is(table.rowCount, 1, "table rowCount correct");
+ is(table.columnCount, 1, "table columnCount correct");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test a broken ARIA table with an invalid cell.
+ */
+addAccessibleTask(
+ `
+<div id="table" role="table">
+ <div role="main">
+ <div role="row">
+ <div id="cell" role="cell">a</div>
+ </div>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+ is(table.rowCount, 0, "table rowCount correct");
+ is(table.columnCount, 0, "table columnCount correct");
+ const cell = findAccessibleChildByID(docAcc, "cell");
+ let queryOk = false;
+ try {
+ cell.QueryInterface(nsIAccessibleTableCell);
+ queryOk = true;
+ } catch (e) {}
+ ok(!queryOk, "Got nsIAccessibleTableCell on an invalid cell");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test that building the cache for a malformed table with an iframe inside a
+ * row doesn't crash (bug 1800780).
+ */
+addAccessibleTask(
+ `<table><tr id="tr"></tr></table>`,
+ async function (browser, docAcc) {
+ let reordered = waitForEvent(EVENT_REORDER, "tr");
+ await invokeContentTask(browser, [], () => {
+ const iframe = content.document.createElement("iframe");
+ content.document.getElementById("tr").append(iframe);
+ });
+ await reordered;
+ },
+ { topLevel: true }
+);
+
+/**
+ * Verify that table row and column information is correct when there are
+ * intervening generics between the table and a rowgroup.
+ */
+addAccessibleTask(
+ `
+<div id="table" role="grid">
+ <div role="rowgroup">
+ <div role="row">
+ <div role="columnheader">a</div>
+ </div>
+ </div>
+ <div tabindex="-1" style="height: 1px; overflow: auto;">
+ <div role="rowgroup">
+ <div role="row">
+ <div id="cell" role="gridcell">b</div>
+ </div>
+ </div>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+
+ info("Verifying that the table row and column counts are correct.");
+ is(table.rowCount, 2, "table rowCount correct");
+ is(table.columnCount, 1, "table columnCount correct");
+
+ info("Verifying that the cell row and column extents are correct.");
+ const cell = findAccessibleChildByID(docAcc, "cell", [
+ nsIAccessibleTableCell,
+ ]);
+ is(cell.rowExtent, 1, "cell rowExtent correct");
+ is(cell.columnExtent, 1, "cell colExtent correct");
+ is(cell.rowIndex, 1, "cell rowIndex correct");
+ is(cell.columnIndex, 0, "cell columnIndex correct");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Verify that table row and column information is correct when there are
+ * intervening generics between rows and cells.
+ */
+addAccessibleTask(
+ `
+<div id="table" role="grid">
+ <div role="rowgroup">
+ <div role="row">
+ <div role="columnheader">a</div>
+ </div>
+ </div>
+ <div role="rowgroup">
+ <div role="row">
+ <div tabindex="-1" style="height: 1px; overflow: auto;">
+ <div id="cell" role="gridcell">b</div>
+ </div>
+ </div>
+ </div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+
+ info("Verifying that the table row and column counts are correct.");
+ is(table.rowCount, 2, "table rowCount correct");
+ is(table.columnCount, 1, "table columnCount correct");
+
+ info("Verifying that the cell row and column extents are correct.");
+ const cell = findAccessibleChildByID(docAcc, "cell", [
+ nsIAccessibleTableCell,
+ ]);
+ is(cell.rowExtent, 1, "cell rowExtent correct");
+ is(cell.columnExtent, 1, "cell colExtent correct");
+ is(cell.rowIndex, 1, "cell rowIndex correct");
+ is(cell.columnIndex, 0, "cell columnIndex correct");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Verify that we don't crash for authoring error like <table role="gridcell">.
+ */
+addAccessibleTask(
+ `<table id="table" role="gridcell">`,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table");
+ ok(table, "Retrieved table Accessible");
+ },
+ { chrome: true, topLevel: true }
+);
+
+/**
+ * Test ARIA tables in SVG.
+ */
+addAccessibleTask(
+ `
+<svg id="table" role="table">
+ <text id="caption" role="caption">caption</text>
+ <g role="row">
+ <text id="a" role="columnheader">a</text>
+ <text id="b" role="columnheader">b</text>
+ </g>
+ <g role="row">
+ <text id="c" role="cell">c</text>
+ <text id="d" role="cell">d</text>
+ </g>
+</svg>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+ is(table.rowCount, 2, "table rowCount correct");
+ is(table.columnCount, 2, "table columnCount correct");
+ const caption = findAccessibleChildByID(docAcc, "caption");
+ is(table.caption, caption, "table caption correct");
+ testTableIndexes(table, [
+ [0, 1],
+ [2, 3],
+ ]);
+ const cells = {};
+ for (const id of ["a", "b", "c", "d"]) {
+ cells[id] = findAccessibleChildByID(docAcc, id, [nsIAccessibleTableCell]);
+ }
+ testHeaderCells([
+ {
+ cell: cells.c,
+ rowHeaderCells: [],
+ columnHeaderCells: [cells.a],
+ },
+ {
+ cell: cells.d,
+ rowHeaderCells: [],
+ columnHeaderCells: [cells.b],
+ },
+ ]);
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
+
+/**
+ * Verify that we don't crash for authoring error like <tr role="grid">.
+ */
+addAccessibleTask(
+ `
+<table id="table">
+ <tr><th>a</th></tr>
+ <tr role="grid"><td id="b">b</td></tr>
+</table>
+ `,
+ async function (browser, docAcc) {
+ const table = findAccessibleChildByID(docAcc, "table", [
+ nsIAccessibleTable,
+ ]);
+ is(table.rowCount, 1, "table rowCount correct");
+ is(table.columnCount, 1, "table columnCount correct");
+ const b = findAccessibleChildByID(docAcc, "b");
+ let queryOk = false;
+ try {
+ b.QueryInterface(nsIAccessibleTableCell);
+ queryOk = true;
+ } catch (e) {}
+ ok(!queryOk, "No nsIAccessibleTableCell on invalid cell b");
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_text_bounds.js b/accessible/tests/browser/e10s/browser_caching_text_bounds.js
new file mode 100644
index 0000000000..3e37bf7490
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_text_bounds.js
@@ -0,0 +1,737 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+requestLongerTimeout(3);
+
+/* import-globals-from ../../mochitest/layout.js */
+loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR });
+
+// Note that testTextNode, testChar and testTextRange currently don't handle
+// white space in the code that doesn't get rendered on screen. To work around
+// this, ensure that containers you want to test are all on a single line in the
+// test snippet.
+
+async function testTextNode(accDoc, browser, id) {
+ await testTextRange(accDoc, browser, id, 0, -1);
+}
+
+async function testChar(accDoc, browser, id, idx) {
+ await testTextRange(accDoc, browser, id, idx, idx + 1);
+}
+
+async function testTextRange(accDoc, browser, id, start, end) {
+ const r = await invokeContentTask(
+ browser,
+ [id, start, end],
+ (_id, _start, _end) => {
+ const htNode = content.document.getElementById(_id);
+ let [eX, eY, eW, eH] = [
+ Number.MAX_SAFE_INTEGER,
+ Number.MAX_SAFE_INTEGER,
+ 0,
+ 0,
+ ];
+ let traversed = 0;
+ let localStart = _start;
+ let endTraversal = false;
+ for (let element of htNode.childNodes) {
+ // ignore whitespace, but not embedded elements
+ let isEmbeddedElement = false;
+ if (element.length == undefined) {
+ let potentialTextContainer = element;
+ while (
+ potentialTextContainer &&
+ potentialTextContainer.length == undefined
+ ) {
+ potentialTextContainer = element.firstChild;
+ }
+ if (potentialTextContainer && potentialTextContainer.length) {
+ // If we can reach some text from this container, use that as part
+ // of our range. This is important when testing with intervening inline
+ // elements. ie. <pre><code>ab%0acd
+ element = potentialTextContainer;
+ } else if (element.firstChild) {
+ isEmbeddedElement = true;
+ } else {
+ continue;
+ }
+ }
+ if (element.length + traversed < _start) {
+ // If our start index is not within this
+ // node, keep looking.
+ traversed += element.length;
+ localStart -= element.length;
+ continue;
+ }
+
+ let rect;
+ if (isEmbeddedElement) {
+ rect = element.getBoundingClientRect();
+ } else {
+ const range = content.document.createRange();
+ range.setStart(element, localStart);
+
+ if (_end != -1 && _end - traversed <= element.length) {
+ // If the current node contains
+ // our end index, stop here.
+ endTraversal = true;
+ range.setEnd(element, _end - traversed);
+ } else {
+ range.setEnd(element, element.length);
+ }
+
+ rect = range.getBoundingClientRect();
+ }
+
+ const oldX = eX == Number.MAX_SAFE_INTEGER ? 0 : eX;
+ const oldY = eY == Number.MAX_SAFE_INTEGER ? 0 : eY;
+ eX = Math.min(eX, rect.x);
+ eY = Math.min(eY, rect.y);
+ eW = Math.abs(Math.max(oldX + eW, rect.x + rect.width) - eX);
+ eH = Math.abs(Math.max(oldY + eH, rect.y + rect.height) - eY);
+
+ if (endTraversal) {
+ break;
+ }
+ localStart = 0;
+ traversed += element.length;
+ }
+ return [Math.round(eX), Math.round(eY), Math.round(eW), Math.round(eH)];
+ }
+ );
+ let hyperTextNode = findAccessibleChildByID(accDoc, id);
+
+ // Add in the doc's screen coords because getBoundingClientRect
+ // is relative to the document, not the screen. This assumes the doc's
+ // screen coords are correct. We use getBoundsInCSSPixels to avoid factoring
+ // in the DPR ourselves.
+ let x = {};
+ let y = {};
+ let w = {};
+ let h = {};
+ accDoc.getBoundsInCSSPixels(x, y, w, h);
+ r[0] += x.value;
+ r[1] += y.value;
+ if (end != -1 && end - start == 1) {
+ // If we're only testing a character, use this function because it calls
+ // CharBounds() directly instead of TextBounds().
+ testTextPos(hyperTextNode, start, [r[0], r[1]], COORDTYPE_SCREEN_RELATIVE);
+ } else {
+ testTextBounds(hyperTextNode, start, end, r, COORDTYPE_SCREEN_RELATIVE);
+ }
+}
+
+/**
+ * Since testTextChar can't handle non-rendered white space, this function first
+ * uses testTextChar to verify the first character and then ensures all
+ * characters thereafter have an incrementing x and a non-0 width.
+ */
+async function testLineWithNonRenderedSpace(docAcc, browser, id, length) {
+ await testChar(docAcc, browser, id, 0);
+ const acc = findAccessibleChildByID(docAcc, id, [nsIAccessibleText]);
+ let prevX = -1;
+ for (let offset = 0; offset < length; ++offset) {
+ const x = {};
+ const y = {};
+ const w = {};
+ const h = {};
+ acc.getCharacterExtents(offset, x, y, w, h, COORDTYPE_SCREEN_RELATIVE);
+ ok(x.value > prevX, `${id}: offset ${offset} x is larger (${x.value})`);
+ prevX = x.value;
+ ok(w.value > 0, `${id}: offset ${offset} width > 0`);
+ }
+}
+
+/**
+ * Test the text range boundary for simple LtR text
+ */
+addAccessibleTask(
+ `
+ <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p>
+ <p id='p2' style='font-family: monospace;'>ل</p>
+ <p id='p3' dir='ltr' style='font-family: monospace;'>Привіт Світ</p>
+ <pre id='p4' style='font-family: monospace;'>a%0abcdef</pre>
+ `,
+ async function (browser, accDoc) {
+ info("Testing simple LtR text");
+ await testTextNode(accDoc, browser, "p1");
+ await testTextNode(accDoc, browser, "p2");
+ await testTextNode(accDoc, browser, "p3");
+ await testTextNode(accDoc, browser, "p4");
+ },
+ {
+ iframe: true,
+ }
+);
+
+/**
+ * Test the partial text range boundary for LtR text
+ */
+addAccessibleTask(
+ `
+ <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p>
+ <p id='p2' dir='ltr' style='font-family: monospace;'>Привіт Світ</p>
+ `,
+ async function (browser, accDoc) {
+ info("Testing partial ranges in LtR text");
+ await testTextRange(accDoc, browser, "p1", 0, 4);
+ await testTextRange(accDoc, browser, "p1", 2, 8);
+ await testTextRange(accDoc, browser, "p1", 12, 17);
+ await testTextRange(accDoc, browser, "p2", 0, 4);
+ await testTextRange(accDoc, browser, "p2", 2, 8);
+ await testTextRange(accDoc, browser, "p2", 6, 11);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test the text boundary for multiline LtR text
+ */
+addAccessibleTask(
+ `
+ <p id='p4' dir='ltr' style='font-family: monospace;'>Привіт Світ<br>Привіт Світ</p>
+ <p id='p5' dir='ltr' style='font-family: monospace;'>Привіт Світ<br> Я ще трохи тексту в другому рядку</p>
+ <p id='p6' style='font-family: monospace;'>hello world I'm on line one<br> and I'm a separate line two with slightly more text</p>
+ <p id='p7' style='font-family: monospace;'>hello world<br>hello world</p>
+ `,
+ async function (browser, accDoc) {
+ info("Testing multiline LtR text");
+ await testTextNode(accDoc, browser, "p4");
+ await testTextNode(accDoc, browser, "p5");
+ // await testTextNode(accDoc, browser, "p6"); // w/o cache, fails width (a 259, e 250), w/ cache wrong w, h in iframe (line wrapping)
+ await testTextNode(accDoc, browser, "p7");
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test the text boundary for simple RtL text
+ */
+addAccessibleTask(
+ `
+ <p id='p1' dir='rtl' style='font-family: monospace;'>Tilimilitryamdiya</p>
+ <p id='p2' dir='rtl' style='font-family: monospace;'>ل</p>
+ <p id='p3' dir='rtl' style='font-family: monospace;'>لل لللل لل</p>
+ <pre id='p4' dir='rtl' style='font-family: monospace;'>a%0abcdef</pre>
+ `,
+ async function (browser, accDoc) {
+ info("Testing simple RtL text");
+ await testTextNode(accDoc, browser, "p1");
+ await testTextNode(accDoc, browser, "p2");
+ await testTextNode(accDoc, browser, "p3");
+ await testTextNode(accDoc, browser, "p4");
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test the text boundary for multiline RtL text
+ */
+addAccessibleTask(
+ `
+ <p id='p4' dir='rtl' style='font-family: monospace;'>لل لللل لل<br>لل لللل لل</p>
+ <p id='p5' dir='rtl' style='font-family: monospace;'>لل لللل لل<br> لل لل لل لل ل لل لل لل</p>
+ <p id='p6' dir='rtl' style='font-family: monospace;'>hello world I'm on line one<br> and I'm a separate line two with slightly more text</p>
+ <p id='p7' dir='rtl' style='font-family: monospace;'>hello world<br>hello world</p>
+ `,
+ async function (browser, accDoc) {
+ info("Testing multiline RtL text");
+ await testTextNode(accDoc, browser, "p4");
+ //await testTextNode(accDoc, browser, "p5"); // w/ cache fails x, w - off by one char
+ // await testTextNode(accDoc, browser, "p6"); // w/o cache, fails width (a 259, e 250), w/ cache fails w, h in iframe (line wrapping)
+ await testTextNode(accDoc, browser, "p7");
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test the partial text range boundary for RtL text
+ */
+addAccessibleTask(
+ `
+ <p id='p1' dir='rtl' style='font-family: monospace;'>Tilimilitryamdiya</p>
+ <p id='p2' dir='rtl' style='font-family: monospace;'>لل لللل لل</p>
+ `,
+ async function (browser, accDoc) {
+ info("Testing partial ranges in RtL text");
+ await testTextRange(accDoc, browser, "p1", 0, 4);
+ await testTextRange(accDoc, browser, "p1", 2, 8);
+ await testTextRange(accDoc, browser, "p1", 12, 17);
+ await testTextRange(accDoc, browser, "p2", 0, 4);
+ await testTextRange(accDoc, browser, "p2", 2, 8);
+ await testTextRange(accDoc, browser, "p2", 6, 10);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test simple vertical text in rl and lr layouts
+ */
+addAccessibleTask(
+ `
+ <div style="writing-mode: vertical-rl;">
+ <p id='p1'>你好世界</p>
+ <p id='p2'>hello world</p>
+ <br>
+ <p id='p3'>こんにちは世界</p>
+ </div>
+ <div style="writing-mode: vertical-lr;">
+ <p id='p4'>你好世界</p>
+ <p id='p5'>hello world</p>
+ <br>
+ <p id='p6'>こんにちは世界</p>
+ </div>
+ `,
+ async function (browser, accDoc) {
+ info("Testing vertical-rl");
+ await testTextNode(accDoc, browser, "p1");
+ await testTextNode(accDoc, browser, "p2");
+ await testTextNode(accDoc, browser, "p3");
+ info("Testing vertical-lr");
+ await testTextNode(accDoc, browser, "p4");
+ await testTextNode(accDoc, browser, "p5");
+ await testTextNode(accDoc, browser, "p6");
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test multiline vertical-rl text
+ */
+addAccessibleTask(
+ `
+ <p id='p1' style='writing-mode: vertical-rl;'>你好世界<br>你好世界</p>
+ <p id='p2' style='writing-mode: vertical-rl;'>hello world<br>hello world</p>
+ <br>
+ <p id='p3' style='writing-mode: vertical-rl;'>你好世界<br> 你好世界 你好世界</p>
+ <p id='p4' style='writing-mode: vertical-rl;'>hello world<br> hello world hello world</p>
+ `,
+ async function (browser, accDoc) {
+ info("Testing vertical-rl multiline");
+ await testTextNode(accDoc, browser, "p1");
+ await testTextNode(accDoc, browser, "p2");
+ await testTextNode(accDoc, browser, "p3");
+ // await testTextNode(accDoc, browser, "p4"); // off by 4 with caching, iframe
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test text with embedded chars
+ */
+addAccessibleTask(
+ `<p id='p1' style='font-family: monospace;'>hello <a href="google.com">world</a></p>
+ <p id='p2' style='font-family: monospace;'>hello<br><a href="google.com">world</a></p>
+ <div id='d3'><p></p>hello world</div>
+ <div id='d4'>hello world<p></p></div>
+ <div id='d5'>oh<p></p>hello world</div>`,
+ async function (browser, accDoc) {
+ info("Testing embedded chars");
+ await testTextNode(accDoc, browser, "p1");
+ await testTextNode(accDoc, browser, "p2");
+ await testTextNode(accDoc, browser, "d3");
+ await testTextNode(accDoc, browser, "d4");
+ await testTextNode(accDoc, browser, "d5");
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test bounds after text mutations.
+ */
+addAccessibleTask(
+ `<p id="p">a</p>`,
+ async function (browser, docAcc) {
+ await testTextNode(docAcc, browser, "p");
+ const p = findAccessibleChildByID(docAcc, "p");
+ info("Appending a character to text leaf");
+ let textInserted = waitForEvent(EVENT_TEXT_INSERTED, p);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("p").firstChild.data = "ab";
+ });
+ await textInserted;
+ await testTextNode(docAcc, browser, "p");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test character bounds on the insertion point at the end of a text box.
+ */
+addAccessibleTask(
+ `<input id="input" value="a">`,
+ async function (browser, docAcc) {
+ const input = findAccessibleChildByID(docAcc, "input");
+ testTextPos(input, 1, [0, 0], COORDTYPE_SCREEN_RELATIVE);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test character bounds after non-br line break.
+ */
+addAccessibleTask(
+ `
+ <style>
+ @font-face {
+ font-family: Ahem;
+ src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
+ }
+ pre {
+ font: 20px/20px Ahem;
+ }
+ </style>
+ <pre id="t">XX
+XXX</pre>`,
+ async function (browser, docAcc) {
+ await testChar(docAcc, browser, "t", 3);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test character bounds in a pre with padding.
+ */
+addAccessibleTask(
+ `
+ <style>
+ @font-face {
+ font-family: Ahem;
+ src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
+ }
+ pre {
+ font: 20px/20px Ahem;
+ padding: 20px;
+ }
+ </style>
+ <pre id="t">XX
+XXX</pre>`,
+ async function (browser, docAcc) {
+ await testTextNode(docAcc, browser, "t");
+ await testChar(docAcc, browser, "t", 3);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test text bounds with an invalid end offset.
+ */
+addAccessibleTask(
+ `<p id="p">a</p>`,
+ async function (browser, docAcc) {
+ const p = findAccessibleChildByID(docAcc, "p");
+ testTextBounds(p, 0, 2, [0, 0, 0, 0], COORDTYPE_SCREEN_RELATIVE);
+ },
+ { chrome: true, topLevel: !true }
+);
+
+/**
+ * Test character bounds in an intervening inline element with non-br line breaks
+ */
+addAccessibleTask(
+ `
+ <style>
+ @font-face {
+ font-family: Ahem;
+ src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
+ }
+ pre {
+ font: 20px/20px Ahem;
+ }
+ </style>
+ <pre id="t"><code role="none">XX
+XXX
+XX
+X</pre>`,
+ async function (browser, docAcc) {
+ await testChar(docAcc, browser, "t", 0);
+ await testChar(docAcc, browser, "t", 3);
+ await testChar(docAcc, browser, "t", 7);
+ await testChar(docAcc, browser, "t", 10);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test character bounds in an intervening inline element with margins
+ * and with non-br line breaks
+ */
+addAccessibleTask(
+ `
+ <style>
+ @font-face {
+ font-family: Ahem;
+ src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
+ }
+ </style>
+ <div>hello<pre id="t" style="margin-left:100px;margin-top:30px;background-color:blue;">XX
+XXX
+XX
+X</pre></div>`,
+ async function (browser, docAcc) {
+ await testChar(docAcc, browser, "t", 0);
+ await testChar(docAcc, browser, "t", 3);
+ await testChar(docAcc, browser, "t", 7);
+ await testChar(docAcc, browser, "t", 10);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test text bounds in a textarea after scrolling.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea" rows="1">a
+b
+c</textarea>
+ `,
+ async function (browser, docAcc) {
+ // We can't use testChar because Range.getBoundingClientRect isn't supported
+ // inside textareas.
+ const textarea = findAccessibleChildByID(docAcc, "textarea");
+ textarea.QueryInterface(nsIAccessibleText);
+ const oldY = {};
+ textarea.getCharacterExtents(
+ 4,
+ {},
+ oldY,
+ {},
+ {},
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ info("Moving textarea caret to c");
+ await invokeContentTask(browser, [], () => {
+ const textareaDom = content.document.getElementById("textarea");
+ textareaDom.focus();
+ textareaDom.selectionStart = 4;
+ });
+ await waitForContentPaint(browser);
+ const newY = {};
+ textarea.getCharacterExtents(
+ 4,
+ {},
+ newY,
+ {},
+ {},
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ ok(newY.value < oldY.value, "y coordinate smaller after scrolling down");
+ },
+ { chrome: true, topLevel: true, iframe: !true }
+);
+
+/**
+ * Test magic offsets with GetCharacter/RangeExtents.
+ */
+addAccessibleTask(
+ `<input id="input" value="abc">`,
+ async function (browser, docAcc) {
+ const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]);
+ info("Setting caret and focusing input");
+ let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
+ await invokeContentTask(browser, [], () => {
+ const inputDom = content.document.getElementById("input");
+ inputDom.selectionStart = inputDom.selectionEnd = 1;
+ inputDom.focus();
+ });
+ await caretMoved;
+ is(input.caretOffset, 1, "input caretOffset is 1");
+ let expectedX = {};
+ let expectedY = {};
+ let expectedW = {};
+ let expectedH = {};
+ let magicX = {};
+ let magicY = {};
+ let magicW = {};
+ let magicH = {};
+ input.getCharacterExtents(
+ 1,
+ expectedX,
+ expectedY,
+ expectedW,
+ expectedH,
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ input.getCharacterExtents(
+ nsIAccessibleText.TEXT_OFFSET_CARET,
+ magicX,
+ magicY,
+ magicW,
+ magicH,
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ Assert.deepEqual(
+ [magicX.value, magicY.value, magicW.value, magicH.value],
+ [expectedX.value, expectedY.value, expectedW.value, expectedH.value],
+ "GetCharacterExtents correct with TEXT_OFFSET_CARET"
+ );
+ input.getRangeExtents(
+ 1,
+ 3,
+ expectedX,
+ expectedY,
+ expectedW,
+ expectedH,
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ input.getRangeExtents(
+ nsIAccessibleText.TEXT_OFFSET_CARET,
+ nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT,
+ magicX,
+ magicY,
+ magicW,
+ magicH,
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ Assert.deepEqual(
+ [magicX.value, magicY.value, magicW.value, magicH.value],
+ [expectedX.value, expectedY.value, expectedW.value, expectedH.value],
+ "GetRangeExtents correct with TEXT_OFFSET_CARET/END_OF_TEXT"
+ );
+ },
+ { chrome: true, topLevel: true, remoteIframe: !true }
+);
+
+/**
+ * Test wrapped text and pre-formatted text beginning with an empty line.
+ */
+addAccessibleTask(
+ `
+<style>
+ #wrappedText {
+ width: 3ch;
+ font-family: monospace;
+ }
+</style>
+<p id="wrappedText"><a href="https://example.com/">a</a>b cd</p>
+<p id="emptyFirstLine" style="white-space: pre-line;">
+foo</p>
+ `,
+ async function (browser, docAcc) {
+ await testChar(docAcc, browser, "wrappedText", 0);
+ await testChar(docAcc, browser, "wrappedText", 1);
+ await testChar(docAcc, browser, "wrappedText", 2);
+ await testChar(docAcc, browser, "wrappedText", 3);
+ await testChar(docAcc, browser, "wrappedText", 4);
+
+ // We can't use testChar for emptyFirstLine because it doesn't handle white
+ // space properly. Instead, verify that the first character is at the top
+ // left of the text leaf.
+ const emptyFirstLine = findAccessibleChildByID(docAcc, "emptyFirstLine", [
+ nsIAccessibleText,
+ ]);
+ const emptyFirstLineLeaf = emptyFirstLine.firstChild;
+ const leafX = {};
+ const leafY = {};
+ emptyFirstLineLeaf.getBounds(leafX, leafY, {}, {});
+ testTextPos(
+ emptyFirstLine,
+ 0,
+ [leafX.value, leafY.value],
+ COORDTYPE_SCREEN_RELATIVE
+ );
+ },
+ { chrome: true, topLevel: true, remoteIframe: !true }
+);
+
+/**
+ * Test character bounds in an intervening inline element with non-br line breaks
+ */
+addAccessibleTask(
+ `
+ <style>
+ @font-face {
+ font-family: Ahem;
+ src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
+ }
+ pre {
+ font: 20px/20px Ahem;
+ }
+ </style>
+ <pre><code id="t" role="group">XX
+XXX
+XX
+X</pre>`,
+ async function (browser, docAcc) {
+ await testChar(docAcc, browser, "t", 0);
+ await testChar(docAcc, browser, "t", 3);
+ await testChar(docAcc, browser, "t", 7);
+ await testChar(docAcc, browser, "t", 10);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ }
+);
+
+/**
+ * Test character bounds where content white space isn't rendered.
+ */
+addAccessibleTask(
+ `
+<p id="single">a b</p>
+<p id="multi"><ins>a </ins>
+b</p>
+<pre id="pre">a b</pre>
+ `,
+ async function (browser, docAcc) {
+ await testLineWithNonRenderedSpace(docAcc, browser, "single", 3);
+ await testLineWithNonRenderedSpace(docAcc, browser, "multi", 2);
+ for (let offset = 0; offset < 4; ++offset) {
+ await testChar(docAcc, browser, "pre", offset);
+ }
+ },
+ { chrome: true, topLevel: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_uniqueid.js b/accessible/tests/browser/e10s/browser_caching_uniqueid.js
new file mode 100644
index 0000000000..92eb2fe998
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_uniqueid.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test UniqueID property.
+ */
+addAccessibleTask(
+ '<div id="div"></div>',
+ async function (browser, accDoc) {
+ const div = findAccessibleChildByID(accDoc, "div");
+ const accUniqueID = await invokeContentTask(browser, [], () => {
+ const accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+
+ return accService.getAccessibleFor(content.document.getElementById("div"))
+ .uniqueID;
+ });
+
+ is(
+ accUniqueID,
+ div.uniqueID,
+ "Both proxy and the accessible return correct unique ID."
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_caching_value.js b/accessible/tests/browser/e10s/browser_caching_value.js
new file mode 100644
index 0000000000..2b968b5948
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_caching_value.js
@@ -0,0 +1,457 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * id {String} given accessible DOMNode ID
+ * expected {String} expected value for a given accessible
+ * action {?AsyncFunction} an optional action that awaits a value change
+ * attrs {?Array} an optional list of attributes to update
+ * waitFor {?Number} an optional value change event to wait for
+ * }
+ */
+const valueTests = [
+ {
+ desc: "Initially value is set to 1st element of select",
+ id: "select",
+ expected: "1st",
+ },
+ {
+ desc: "Value should update to 3rd when 3 is pressed",
+ id: "select",
+ async action(browser) {
+ await invokeFocus(browser, "select");
+ await invokeContentTask(browser, [], () => {
+ const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeKey("3", {}, content);
+ });
+ },
+ waitFor: EVENT_TEXT_VALUE_CHANGE,
+ expected: "3rd",
+ },
+ {
+ desc: "Initially value is set to @aria-valuenow for slider",
+ id: "slider",
+ expected: ["5", 5, 0, 7, 0],
+ },
+ {
+ desc: "Value should change when currentValue is called",
+ id: "slider",
+ async action(browser, acc) {
+ acc.QueryInterface(nsIAccessibleValue);
+ acc.currentValue = 4;
+ },
+ waitFor: EVENT_VALUE_CHANGE,
+ expected: ["4", 4, 0, 7, 0],
+ },
+ {
+ desc: "Value should change when @aria-valuenow is updated",
+ id: "slider",
+ attrs: [
+ {
+ attr: "aria-valuenow",
+ value: "6",
+ },
+ ],
+ waitFor: EVENT_VALUE_CHANGE,
+ expected: ["6", 6, 0, 7, 0],
+ },
+ {
+ desc: "Value should change when @aria-valuetext is set",
+ id: "slider",
+ attrs: [
+ {
+ attr: "aria-valuetext",
+ value: "plain",
+ },
+ ],
+ waitFor: EVENT_TEXT_VALUE_CHANGE,
+ expected: ["plain", 6, 0, 7, 0],
+ },
+ {
+ desc: "Value should change when @aria-valuetext is updated",
+ id: "slider",
+ attrs: [
+ {
+ attr: "aria-valuetext",
+ value: "hey!",
+ },
+ ],
+ waitFor: EVENT_TEXT_VALUE_CHANGE,
+ expected: ["hey!", 6, 0, 7, 0],
+ },
+ {
+ desc: "Value should change to @aria-valuetext when @aria-valuenow is removed",
+ id: "slider",
+ attrs: [
+ {
+ attr: "aria-valuenow",
+ },
+ ],
+ expected: ["hey!", 3.5, 0, 7, 0],
+ },
+ {
+ desc: "Initially value is not set for combobox",
+ id: "combobox",
+ expected: "",
+ },
+ {
+ desc: "Value should change when @value attribute is updated",
+ id: "combobox",
+ attrs: [
+ {
+ attr: "value",
+ value: "hello",
+ },
+ ],
+ waitFor: EVENT_TEXT_VALUE_CHANGE,
+ expected: "hello",
+ },
+ {
+ desc: "Initially value corresponds to @value attribute for progress",
+ id: "progress",
+ expected: "22%",
+ },
+ {
+ desc: "Value should change when @value attribute is updated",
+ id: "progress",
+ attrs: [
+ {
+ attr: "value",
+ value: "50",
+ },
+ ],
+ waitFor: EVENT_VALUE_CHANGE,
+ expected: "50%",
+ },
+ {
+ desc: "Setting currentValue on a progress accessible should fail",
+ id: "progress",
+ async action(browser, acc) {
+ acc.QueryInterface(nsIAccessibleValue);
+ try {
+ acc.currentValue = 25;
+ ok(false, "Setting currValue on progress element should fail");
+ } catch (e) {}
+ },
+ expected: "50%",
+ },
+ {
+ desc: "Initially value corresponds to @value attribute for range",
+ id: "range",
+ expected: "6",
+ },
+ {
+ desc: "Value should change when slider is moved",
+ id: "range",
+ async action(browser) {
+ await invokeFocus(browser, "range");
+ await invokeContentTask(browser, [], () => {
+ const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeKey("VK_LEFT", {}, content);
+ });
+ },
+ waitFor: EVENT_VALUE_CHANGE,
+ expected: "5",
+ },
+ {
+ desc: "Value should change when currentValue is called",
+ id: "range",
+ async action(browser, acc) {
+ acc.QueryInterface(nsIAccessibleValue);
+ acc.currentValue = 4;
+ },
+ waitFor: EVENT_VALUE_CHANGE,
+ expected: "4",
+ },
+ {
+ desc: "Initially textbox value is text subtree",
+ id: "textbox",
+ expected: "Some rich text",
+ },
+ {
+ desc: "Textbox value changes when subtree changes",
+ id: "textbox",
+ async action(browser) {
+ await invokeContentTask(browser, [], () => {
+ let boldText = content.document.createElement("strong");
+ boldText.textContent = " bold";
+ content.document.getElementById("textbox").appendChild(boldText);
+ });
+ },
+ waitFor: EVENT_TEXT_VALUE_CHANGE,
+ expected: "Some rich text bold",
+ },
+];
+
+/**
+ * Like testValue in accessible/tests/mochitest/value.js, but waits for cache
+ * updates.
+ */
+async function testValue(acc, value, currValue, minValue, maxValue, minIncr) {
+ const pretty = prettyName(acc);
+ await untilCacheIs(() => acc.value, value, `Wrong value of ${pretty}`);
+
+ await untilCacheIs(
+ () => acc.currentValue,
+ currValue,
+ `Wrong current value of ${pretty}`
+ );
+ await untilCacheIs(
+ () => acc.minimumValue,
+ minValue,
+ `Wrong minimum value of ${pretty}`
+ );
+ await untilCacheIs(
+ () => acc.maximumValue,
+ maxValue,
+ `Wrong maximum value of ${pretty}`
+ );
+ await untilCacheIs(
+ () => acc.minimumIncrement,
+ minIncr,
+ `Wrong minimum increment value of ${pretty}`
+ );
+}
+
+/**
+ * Test caching of accessible object values
+ */
+addAccessibleTask(
+ `
+ <div id="slider" role="slider" aria-valuenow="5"
+ aria-valuemin="0" aria-valuemax="7">slider</div>
+ <select id="select">
+ <option>1st</option>
+ <option>2nd</option>
+ <option>3rd</option>
+ </select>
+ <input id="combobox" role="combobox" aria-autocomplete="inline">
+ <progress id="progress" value="22" max="100"></progress>
+ <input type="range" id="range" min="0" max="10" value="6">
+ <div contenteditable="yes" role="textbox" id="textbox">Some <a href="#">rich</a> text</div>`,
+ async function (browser, accDoc) {
+ for (let { desc, id, action, attrs, expected, waitFor } of valueTests) {
+ info(desc);
+ let acc = findAccessibleChildByID(accDoc, id);
+ let onUpdate;
+
+ if (waitFor) {
+ onUpdate = waitForEvent(waitFor, id);
+ }
+
+ if (action) {
+ await action(browser, acc);
+ } else if (attrs) {
+ for (let { attr, value } of attrs) {
+ await invokeSetAttribute(browser, id, attr, value);
+ }
+ }
+
+ await onUpdate;
+ if (Array.isArray(expected)) {
+ acc.QueryInterface(nsIAccessibleValue);
+ await testValue(acc, ...expected);
+ } else {
+ is(acc.value, expected, `Correct value for ${prettyName(acc)}`);
+ }
+ }
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of link URL values.
+ */
+addAccessibleTask(
+ `<a id="link" href="https://example.com/">Test</a>`,
+ async function (browser, docAcc) {
+ let link = findAccessibleChildByID(docAcc, "link");
+ is(link.value, "https://example.com/", "link initial value correct");
+ const textLeaf = link.firstChild;
+ is(textLeaf.value, "https://example.com/", "link initial value correct");
+
+ info("Changing link href");
+ await invokeSetAttribute(browser, "link", "href", "https://example.net/");
+ await untilCacheIs(
+ () => link.value,
+ "https://example.net/",
+ "link value correct after change"
+ );
+
+ info("Removing link href");
+ let onRecreation = waitForEvents({
+ expected: [
+ [EVENT_HIDE, link],
+ [EVENT_SHOW, "link"],
+ ],
+ });
+ await invokeSetAttribute(browser, "link", "href");
+ await onRecreation;
+ link = findAccessibleChildByID(docAcc, "link");
+ await untilCacheIs(() => link.value, "", "link value empty after removal");
+
+ info("Setting link href");
+ onRecreation = waitForEvents({
+ expected: [
+ [EVENT_HIDE, link],
+ [EVENT_SHOW, "link"],
+ ],
+ });
+ await invokeSetAttribute(browser, "link", "href", "https://example.com/");
+ await onRecreation;
+ link = findAccessibleChildByID(docAcc, "link");
+ await untilCacheIs(
+ () => link.value,
+ "https://example.com/",
+ "link value correct after change"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test caching of active state for select options - see bug 1788143.
+ */
+addAccessibleTask(
+ `
+ <select id="select">
+ <option id="first_option">First</option>
+ <option id="second_option">Second</option>
+ </select>`,
+ async function (browser, docAcc) {
+ const select = findAccessibleChildByID(docAcc, "select");
+ is(select.value, "First", "Select initial value correct");
+
+ // Focus the combo box.
+ await invokeFocus(browser, "select");
+
+ // Select the second option (drop-down collapsed).
+ let p = waitForEvents({
+ expected: [
+ [EVENT_SELECTION, "second_option"],
+ [EVENT_TEXT_VALUE_CHANGE, "select"],
+ ],
+ unexpected: [
+ stateChangeEventArgs("second_option", EXT_STATE_ACTIVE, true, true),
+ stateChangeEventArgs("first_option", EXT_STATE_ACTIVE, false, true),
+ ],
+ });
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("select").selectedIndex = 1;
+ });
+ await p;
+
+ is(select.value, "Second", "Select value correct after changing option");
+
+ // Expand the combobox dropdown.
+ p = waitForEvent(EVENT_STATE_CHANGE, "ContentSelectDropdown");
+ EventUtils.synthesizeKey("VK_SPACE");
+ await p;
+
+ p = waitForEvents({
+ expected: [
+ [EVENT_SELECTION, "first_option"],
+ [EVENT_TEXT_VALUE_CHANGE, "select"],
+ [EVENT_HIDE, "ContentSelectDropdown"],
+ ],
+ unexpected: [
+ stateChangeEventArgs("first_option", EXT_STATE_ACTIVE, true, true),
+ stateChangeEventArgs("second_option", EXT_STATE_ACTIVE, false, true),
+ ],
+ });
+
+ // Press the up arrow to select the first option (drop-down expanded).
+ // Then, press Enter to confirm the selection and close the dropdown.
+ // We do both of these together to unify testing across platforms, since
+ // events are not entirely consistent on Windows vs. Linux + macOS.
+ EventUtils.synthesizeKey("VK_UP");
+ EventUtils.synthesizeKey("VK_RETURN");
+ await p;
+
+ is(
+ select.value,
+ "First",
+ "Select value correct after changing option back"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test combobox values for non-editable comboboxes.
+ */
+addAccessibleTask(
+ `
+ <div id="combo-div-1" role="combobox">value</div>
+ <div id="combo-div-2" role="combobox">
+ <div role="listbox">
+ <div role="option">value</div>
+ </div>
+ </div>
+ <div id="combo-div-3" role="combobox">
+ <div role="group">value</div>
+ </div>
+ <div id="combo-div-4" role="combobox">foo
+ <div role="listbox">
+ <div role="option">bar</div>
+ </div>
+ </div>
+
+ <input id="combo-input-1" role="combobox" value="value" disabled></input>
+ <input id="combo-input-2" role="combobox" value="value" disabled>testing</input>
+
+ <div id="combo-div-selected" role="combobox">
+ <div role="listbox">
+ <div aria-selected="true" role="option">value</div>
+ </div>
+ </div>
+`,
+ async function (browser, docAcc) {
+ const comboDiv1 = findAccessibleChildByID(docAcc, "combo-div-1");
+ const comboDiv2 = findAccessibleChildByID(docAcc, "combo-div-2");
+ const comboDiv3 = findAccessibleChildByID(docAcc, "combo-div-3");
+ const comboDiv4 = findAccessibleChildByID(docAcc, "combo-div-4");
+ const comboInput1 = findAccessibleChildByID(docAcc, "combo-input-1");
+ const comboInput2 = findAccessibleChildByID(docAcc, "combo-input-2");
+ const comboDivSelected = findAccessibleChildByID(
+ docAcc,
+ "combo-div-selected"
+ );
+
+ // Text as a descendant of the combobox: included in the value.
+ is(comboDiv1.value, "value", "Combobox value correct");
+
+ // Text as the descendant of a listbox: excluded from the value.
+ is(comboDiv2.value, "", "Combobox value correct");
+
+ // Text as the descendant of some other role that includes text in name computation.
+ // Here, the group role contains the text node with "value" in it.
+ is(comboDiv3.value, "value", "Combobox value correct");
+
+ // Some descendant text included, but text descendant of a listbox excluded.
+ is(comboDiv4.value, "foo", "Combobox value correct");
+
+ // Combobox inputs with explicit value report that value.
+ is(comboInput1.value, "value", "Combobox value correct");
+ is(comboInput2.value, "value", "Combobox value correct");
+
+ // Combobox role with aria-selected reports correct value.
+ is(comboDivSelected.value, "value", "Combobox value correct");
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_events_announcement.js b/accessible/tests/browser/e10s/browser_events_announcement.js
new file mode 100644
index 0000000000..046a7706e3
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_announcement.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+addAccessibleTask(
+ `<p id="p">abc</p>`,
+ async function (browser, accDoc) {
+ let acc = findAccessibleChildByID(accDoc, "p");
+ let onAnnounce = waitForEvent(EVENT_ANNOUNCEMENT, acc);
+ acc.announce("please", nsIAccessibleAnnouncementEvent.POLITE);
+ let evt = await onAnnounce;
+ evt.QueryInterface(nsIAccessibleAnnouncementEvent);
+ is(evt.announcement, "please", "announcement matches.");
+ is(evt.priority, nsIAccessibleAnnouncementEvent.POLITE, "priority matches");
+
+ onAnnounce = waitForEvent(EVENT_ANNOUNCEMENT, acc);
+ acc.announce("do it", nsIAccessibleAnnouncementEvent.ASSERTIVE);
+ evt = await onAnnounce;
+ evt.QueryInterface(nsIAccessibleAnnouncementEvent);
+ is(evt.announcement, "do it", "announcement matches.");
+ is(
+ evt.priority,
+ nsIAccessibleAnnouncementEvent.ASSERTIVE,
+ "priority matches"
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_events_caretmove.js b/accessible/tests/browser/e10s/browser_events_caretmove.js
new file mode 100644
index 0000000000..dff6586bf3
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_caretmove.js
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test caret move event and its interface:
+ * - caretOffset
+ */
+addAccessibleTask(
+ '<input id="textbox" value="hello"/>',
+ async function (browser) {
+ let onCaretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, "textbox");
+ await invokeFocus(browser, "textbox");
+ let event = await onCaretMoved;
+
+ let caretMovedEvent = event.QueryInterface(nsIAccessibleCaretMoveEvent);
+ is(caretMovedEvent.caretOffset, 5, "Correct caret offset.");
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_events_hide.js b/accessible/tests/browser/e10s/browser_events_hide.js
new file mode 100644
index 0000000000..77bd70c0f6
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_hide.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test hide event and its interface:
+ * - targetParent
+ * - targetNextSibling
+ * - targetPrevSibling
+ */
+addAccessibleTask(
+ `
+ <div id="parent">
+ <div id="previous"></div>
+ <div id="to-hide"></div>
+ <div id="next"></div>
+ </div>`,
+ async function (browser, accDoc) {
+ let acc = findAccessibleChildByID(accDoc, "to-hide");
+ let onHide = waitForEvent(EVENT_HIDE, acc);
+ await invokeSetStyle(browser, "to-hide", "visibility", "hidden");
+ let event = await onHide;
+ let hideEvent = event.QueryInterface(Ci.nsIAccessibleHideEvent);
+
+ is(
+ getAccessibleDOMNodeID(hideEvent.targetParent),
+ "parent",
+ "Correct target parent."
+ );
+ is(
+ getAccessibleDOMNodeID(hideEvent.targetNextSibling),
+ "next",
+ "Correct target next sibling."
+ );
+ is(
+ getAccessibleDOMNodeID(hideEvent.targetPrevSibling),
+ "previous",
+ "Correct target previous sibling."
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_events_show.js b/accessible/tests/browser/e10s/browser_events_show.js
new file mode 100644
index 0000000000..fb03ce2329
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_show.js
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test show event
+ */
+addAccessibleTask(
+ '<div id="div" style="visibility: hidden;"></div>',
+ async function (browser) {
+ let onShow = waitForEvent(EVENT_SHOW, "div");
+ await invokeSetStyle(browser, "div", "visibility", "visible");
+ let showEvent = await onShow;
+ ok(
+ showEvent.accessibleDocument instanceof nsIAccessibleDocument,
+ "Accessible document not present."
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_events_statechange.js b/accessible/tests/browser/e10s/browser_events_statechange.js
new file mode 100644
index 0000000000..a510c5b9b5
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_statechange.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+function checkStateChangeEvent(event, state, isExtraState, isEnabled) {
+ let scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent);
+ is(scEvent.state, state, "Correct state of the statechange event.");
+ is(
+ scEvent.isExtraState,
+ isExtraState,
+ "Correct extra state bit of the statechange event."
+ );
+ is(scEvent.isEnabled, isEnabled, "Correct state of statechange event state");
+}
+
+// Insert mock source into the iframe to be able to verify the right document
+// body id.
+let iframeSrc = `data:text/html,
+ <html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Inner Iframe</title>
+ </head>
+ <body id='iframe'></body>
+ </html>`;
+
+/**
+ * Test state change event and its interface:
+ * - state
+ * - isExtraState
+ * - isEnabled
+ */
+addAccessibleTask(
+ `
+ <iframe id="iframe" src="${iframeSrc}"></iframe>
+ <input id="checkbox" type="checkbox" />`,
+ async function (browser) {
+ // Test state change
+ let onStateChange = waitForEvent(EVENT_STATE_CHANGE, "checkbox");
+ // Set checked for a checkbox.
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("checkbox").checked = true;
+ });
+ let event = await onStateChange;
+
+ checkStateChangeEvent(event, STATE_CHECKED, false, true);
+ testStates(event.accessible, STATE_CHECKED, 0);
+
+ // Test extra state
+ onStateChange = waitForEvent(EVENT_STATE_CHANGE, "iframe");
+ // Set design mode on.
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("iframe").contentDocument.designMode =
+ "on";
+ });
+ event = await onStateChange;
+
+ checkStateChangeEvent(event, EXT_STATE_EDITABLE, true, true);
+ testStates(event.accessible, 0, EXT_STATE_EDITABLE);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_events_textchange.js b/accessible/tests/browser/e10s/browser_events_textchange.js
new file mode 100644
index 0000000000..f39ecea8c4
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_events_textchange.js
@@ -0,0 +1,119 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function checkTextChangeEvent(
+ event,
+ id,
+ text,
+ start,
+ end,
+ isInserted,
+ isFromUserInput
+) {
+ let tcEvent = event.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(tcEvent.start, start, `Correct start offset for ${prettyName(id)}`);
+ is(tcEvent.length, end - start, `Correct length for ${prettyName(id)}`);
+ is(
+ tcEvent.isInserted,
+ isInserted,
+ `Correct isInserted flag for ${prettyName(id)}`
+ );
+ is(tcEvent.modifiedText, text, `Correct text for ${prettyName(id)}`);
+ is(
+ tcEvent.isFromUserInput,
+ isFromUserInput,
+ `Correct value of isFromUserInput for ${prettyName(id)}`
+ );
+ ok(
+ tcEvent.accessibleDocument instanceof nsIAccessibleDocument,
+ "Accessible document not present."
+ );
+}
+
+async function changeText(browser, id, value, events) {
+ let onEvents = waitForOrderedEvents(
+ events.map(({ isInserted }) => {
+ let eventType = isInserted ? EVENT_TEXT_INSERTED : EVENT_TEXT_REMOVED;
+ return [eventType, id];
+ })
+ );
+ // Change text in the subtree.
+ await invokeContentTask(browser, [id, value], (contentId, contentValue) => {
+ content.document.getElementById(contentId).firstChild.textContent =
+ contentValue;
+ });
+ let resolvedEvents = await onEvents;
+
+ events.forEach(({ isInserted, str, offset }, idx) =>
+ checkTextChangeEvent(
+ resolvedEvents[idx],
+ id,
+ str,
+ offset,
+ offset + str.length,
+ isInserted,
+ false
+ )
+ );
+}
+
+async function removeTextFromInput(browser, id, value, start, end) {
+ let onTextRemoved = waitForEvent(EVENT_TEXT_REMOVED, id);
+ // Select text and delete it.
+ await invokeContentTask(
+ browser,
+ [id, start, end],
+ (contentId, contentStart, contentEnd) => {
+ let el = content.document.getElementById(contentId);
+ el.focus();
+ el.setSelectionRange(contentStart, contentEnd);
+ }
+ );
+ await invokeContentTask(browser, [], () => {
+ const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.sendChar("VK_DELETE", content);
+ });
+
+ let event = await onTextRemoved;
+ checkTextChangeEvent(event, id, value, start, end, false, true);
+}
+
+/**
+ * Test text change event and its interface:
+ * - start
+ * - length
+ * - isInserted
+ * - modifiedText
+ * - isFromUserInput
+ */
+addAccessibleTask(
+ `
+ <p id="p">abc</p>
+ <input id="input" value="input" />`,
+ async function (browser) {
+ let events = [
+ { isInserted: false, str: "abc", offset: 0 },
+ { isInserted: true, str: "def", offset: 0 },
+ ];
+ await changeText(browser, "p", "def", events);
+
+ // Adding text should not send events with diffs for non-editable text.
+ // We do this to avoid screen readers reading out confusing diffs for
+ // live regions.
+ events = [
+ { isInserted: false, str: "def", offset: 0 },
+ { isInserted: true, str: "deDEFf", offset: 0 },
+ ];
+ await changeText(browser, "p", "deDEFf", events);
+
+ // Test isFromUserInput property.
+ await removeTextFromInput(browser, "input", "n", 1, 2);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_file_input.js b/accessible/tests/browser/e10s/browser_file_input.js
new file mode 100644
index 0000000000..238e48740e
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_file_input.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/name.js */
+loadScripts({ name: "name.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `
+<input type="file" id="noName">
+<input type="file" id="ariaLabel" aria-label="ariaLabel">
+<label>wrappingLabel <input type="file" id="wrappingLabel"></label>
+<label for="labelFor">labelFor</label> <input type="file" id="labelFor">
+<input type="file" id="title" title="title">
+ `,
+ async function (browser, docAcc) {
+ const browseButton = "Browse…";
+ const noFileSuffix = `${browseButton} No file selected.`;
+ const noName = findAccessibleChildByID(docAcc, "noName");
+ testName(noName, noFileSuffix);
+ const ariaLabel = findAccessibleChildByID(docAcc, "ariaLabel");
+ testName(ariaLabel, `ariaLabel ${noFileSuffix}`);
+ const wrappingLabel = findAccessibleChildByID(docAcc, "wrappingLabel");
+ testName(wrappingLabel, `wrappingLabel ${noFileSuffix}`);
+ const labelFor = findAccessibleChildByID(docAcc, "labelFor");
+ testName(labelFor, `labelFor ${noFileSuffix}`);
+ const title = findAccessibleChildByID(docAcc, "title");
+ testName(title, noFileSuffix);
+ testDescr(title, "title");
+
+ // Test that the name of the button changes correctly when a file is chosen.
+ function chooseFile(id) {
+ return invokeContentTask(browser, [id], contentId => {
+ const MockFilePicker = content.SpecialPowers.MockFilePicker;
+ MockFilePicker.init(content);
+ MockFilePicker.useBlobFile();
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ const input = content.document.getElementById(contentId);
+ const inputReceived = new Promise(resolve =>
+ input.addEventListener(
+ "input",
+ event => {
+ MockFilePicker.cleanup();
+ resolve(event.target.files[0].name);
+ },
+ { once: true }
+ )
+ );
+ input.click();
+ return inputReceived;
+ });
+ }
+
+ info("noName: Choosing file");
+ let nameChanged = waitForEvent(EVENT_NAME_CHANGE, "noName");
+ const fn = await chooseFile("noName");
+ // e.g. "Browse…helloworld.txt"
+ const withFileSuffix = `${browseButton} ${fn}`;
+ await nameChanged;
+ testName(noName, withFileSuffix);
+
+ info("ariaLabel: Choosing file");
+ nameChanged = waitForEvent(EVENT_NAME_CHANGE, "ariaLabel");
+ await chooseFile("ariaLabel");
+ await nameChanged;
+ testName(ariaLabel, `ariaLabel ${withFileSuffix}`);
+
+ info("wrappingLabel: Choosing file");
+ nameChanged = waitForEvent(EVENT_NAME_CHANGE, "wrappingLabel");
+ await chooseFile("wrappingLabel");
+ await nameChanged;
+ testName(wrappingLabel, `wrappingLabel ${withFileSuffix}`);
+ },
+ { topLevel: true, chrome: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_language.js b/accessible/tests/browser/e10s/browser_language.js
new file mode 100644
index 0000000000..684d915693
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_language.js
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+addAccessibleTask(
+ `
+<script>
+ // We can't include the html element in snippets, so set lang on it here.
+ document.documentElement.lang = "en";
+</script>
+<div id="inheritEn"></div>
+<div id="de" lang="de">
+ <div id="inheritDe"></div>
+ <div id="fr" lang="fr"></div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ is(docAcc.language, "en", "Document language correct");
+ const inheritEn = findAccessibleChildByID(docAcc, "inheritEn");
+ is(inheritEn.language, "en", "inheritEn language correct");
+ const de = findAccessibleChildByID(docAcc, "de");
+ is(de.language, "de", "de language correct");
+ const fr = findAccessibleChildByID(docAcc, "fr");
+ is(fr.language, "fr", "fr language correct");
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_obj_group.js b/accessible/tests/browser/e10s/browser_obj_group.js
new file mode 100644
index 0000000000..7e22b8b491
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_obj_group.js
@@ -0,0 +1,430 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
+
+/**
+ * select elements
+ */
+addAccessibleTask(
+ `<select>
+ <option id="opt1-nosize">option1</option>
+ <option id="opt2-nosize">option2</option>
+ <option id="opt3-nosize">option3</option>
+ <option id="opt4-nosize">option4</option>
+ </select>
+
+ <select size="4">
+ <option id="opt1">option1</option>
+ <option id="opt2">option2</option>
+ </select>
+
+ <select size="4">
+ <optgroup id="select2_optgroup" label="group">
+ <option id="select2_opt1">option1</option>
+ <option id="select2_opt2">option2</option>
+ </optgroup>
+ <option id="select2_opt3">option3</option>
+ <option id="select2_opt4">option4</option>
+ </select>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML select with no size attribute.
+ testGroupAttrs(getAcc("opt1-nosize"), 1, 4);
+ testGroupAttrs(getAcc("opt2-nosize"), 2, 4);
+ testGroupAttrs(getAcc("opt3-nosize"), 3, 4);
+ testGroupAttrs(getAcc("opt4-nosize"), 4, 4);
+
+ // Container should have item count and not hierarchical
+ testGroupParentAttrs(getAcc("opt1-nosize").parent, 4, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML select
+ testGroupAttrs(getAcc("opt1"), 1, 2);
+ testGroupAttrs(getAcc("opt2"), 2, 2);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML select with optgroup
+ testGroupAttrs(getAcc("select2_opt3"), 1, 2, 1);
+ testGroupAttrs(getAcc("select2_opt4"), 2, 2, 1);
+ testGroupAttrs(getAcc("select2_opt1"), 1, 2, 2);
+ testGroupAttrs(getAcc("select2_opt2"), 2, 2, 2);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+/**
+ * HTML radios
+ */
+addAccessibleTask(
+ `<form>
+ <input type="radio" id="radio1" name="group1"/>
+ <input type="radio" id="radio2" name="group1"/>
+ </form>
+
+ <input type="radio" id="radio3" name="group2"/>
+ <label><input type="radio" id="radio4" name="group2"/></label>
+
+ <form>
+ <input type="radio" style="display: none;" name="group3">
+ <input type="radio" id="radio5" name="group3">
+ <input type="radio" id="radio6" name="group4">
+ </form>
+
+ <input type="radio" id="radio7">`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML input@type="radio" within form
+ testGroupAttrs(getAcc("radio1"), 1, 2);
+ testGroupAttrs(getAcc("radio2"), 2, 2);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML input@type="radio" within document
+ testGroupAttrs(getAcc("radio3"), 1, 2);
+ // radio4 is wrapped in a label
+ testGroupAttrs(getAcc("radio4"), 2, 2);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Hidden HTML input@type="radio"
+ testGroupAttrs(getAcc("radio5"), 1, 1);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML input@type="radio" with different name but same parent
+ testGroupAttrs(getAcc("radio6"), 1, 1);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML input@type="radio" with no name
+ testGroupAttrs(getAcc("radio7"), 0, 0);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+/**
+ * lists
+ */
+addAccessibleTask(
+ `<ul id="ul">
+ <li id="li1">Oranges</li>
+ <li id="li2">Apples</li>
+ <li id="li3">Bananas</li>
+ </ul>
+
+ <ol id="ol">
+ <li id="li4">Oranges</li>
+ <li id="li5">Apples</li>
+ <li id="li6">Bananas
+ <ul id="ol_nested">
+ <li id="n_li4">Oranges</li>
+ <li id="n_li5">Apples</li>
+ <li id="n_li6">Bananas</li>
+ </ul>
+ </li>
+ </ol>
+
+ <span role="list" id="aria-list_1">
+ <span role="listitem" id="li7">Oranges</span>
+ <span role="listitem" id="li8">Apples</span>
+ <span role="listitem" id="li9">Bananas</span>
+ </span>
+
+ <span role="list" id="aria-list_2">
+ <span role="listitem" id="li10">Oranges</span>
+ <span role="listitem" id="li11">Apples</span>
+ <span role="listitem" id="li12">Bananas
+ <span role="list" id="aria-list_2_1">
+ <span role="listitem" id="n_li10">Oranges</span>
+ <span role="listitem" id="n_li11">Apples</span>
+ <span role="listitem" id="n_li12">Bananas</span>
+ </span>
+ </span>
+ </span>
+
+ <div role="list" id="aria-list_3">
+ <div role="listitem" id="lgt_li1">Item 1
+ <div role="group">
+ <div role="listitem" id="lgt_li1_nli1">Item 1A</div>
+ <div role="listitem" id="lgt_li1_nli2">Item 1B</div>
+ </div>
+ </div>
+ <div role="listitem" id="lgt_li2">Item 2
+ <div role="group">
+ <div role="listitem" id="lgt_li2_nli1">Item 2A</div>
+ <div role="listitem" id="lgt_li2_nli2">Item 2B</div>
+ </div>
+ </div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML ul/ol
+ testGroupAttrs(getAcc("li1"), 1, 3);
+ testGroupAttrs(getAcc("li2"), 2, 3);
+ testGroupAttrs(getAcc("li3"), 3, 3);
+
+ // ul should have item count and not hierarchical
+ testGroupParentAttrs(getAcc("ul"), 3, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML ul/ol (nested lists)
+
+ testGroupAttrs(getAcc("li4"), 1, 3, 1);
+ testGroupAttrs(getAcc("li5"), 2, 3, 1);
+ testGroupAttrs(getAcc("li6"), 3, 3, 1);
+ // ol with nested list should have 1st level item count and be hierarchical
+ testGroupParentAttrs(getAcc("ol"), 3, true);
+
+ testGroupAttrs(getAcc("n_li4"), 1, 3, 2);
+ testGroupAttrs(getAcc("n_li5"), 2, 3, 2);
+ testGroupAttrs(getAcc("n_li6"), 3, 3, 2);
+ // nested ol should have item count and be hierarchical
+ testGroupParentAttrs(getAcc("ol_nested"), 3, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA list
+ testGroupAttrs(getAcc("li7"), 1, 3);
+ testGroupAttrs(getAcc("li8"), 2, 3);
+ testGroupAttrs(getAcc("li9"), 3, 3);
+ // simple flat aria list
+ testGroupParentAttrs(getAcc("aria-list_1"), 3, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA list (nested lists: list -> listitem -> list -> listitem)
+ testGroupAttrs(getAcc("li10"), 1, 3, 1);
+ testGroupAttrs(getAcc("li11"), 2, 3, 1);
+ testGroupAttrs(getAcc("li12"), 3, 3, 1);
+ // aria list with nested list
+ testGroupParentAttrs(getAcc("aria-list_2"), 3, true);
+
+ testGroupAttrs(getAcc("n_li10"), 1, 3, 2);
+ testGroupAttrs(getAcc("n_li11"), 2, 3, 2);
+ testGroupAttrs(getAcc("n_li12"), 3, 3, 2);
+ // nested aria list.
+ testGroupParentAttrs(getAcc("aria-list_2_1"), 3, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA list (nested lists: list -> listitem -> group -> listitem)
+ testGroupAttrs(getAcc("lgt_li1"), 1, 2, 1);
+ testGroupAttrs(getAcc("lgt_li1_nli1"), 1, 2, 2);
+ testGroupAttrs(getAcc("lgt_li1_nli2"), 2, 2, 2);
+ testGroupAttrs(getAcc("lgt_li2"), 2, 2, 1);
+ testGroupAttrs(getAcc("lgt_li2_nli1"), 1, 2, 2);
+ testGroupAttrs(getAcc("lgt_li2_nli2"), 2, 2, 2);
+ // aria list with nested list
+ testGroupParentAttrs(getAcc("aria-list_3"), 2, true);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<ul role="menubar" id="menubar">
+ <li role="menuitem" aria-haspopup="true" id="menu_item1">File
+ <ul role="menu" id="menu">
+ <li role="menuitem" id="menu_item1.1">New</li>
+ <li role="menuitem" id="menu_item1.2">Open…</li>
+ <li role="separator">-----</li>
+ <li role="menuitem" id="menu_item1.3">Item</li>
+ <li role="menuitemradio" id="menu_item1.4">Radio</li>
+ <li role="menuitemcheckbox" id="menu_item1.5">Checkbox</li>
+ </ul>
+ </li>
+ <li role="menuitem" aria-haspopup="false" id="menu_item2">Help</li>
+ </ul>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA menu (menuitem, separator, menuitemradio and menuitemcheckbox)
+ testGroupAttrs(getAcc("menu_item1"), 1, 2);
+ testGroupAttrs(getAcc("menu_item2"), 2, 2);
+ testGroupAttrs(getAcc("menu_item1.1"), 1, 2);
+ testGroupAttrs(getAcc("menu_item1.2"), 2, 2);
+ testGroupAttrs(getAcc("menu_item1.3"), 1, 3);
+ testGroupAttrs(getAcc("menu_item1.4"), 2, 3);
+ testGroupAttrs(getAcc("menu_item1.5"), 3, 3);
+ // menu bar item count
+ testGroupParentAttrs(getAcc("menubar"), 2, false);
+ // Bug 1492529. Menu should have total number of items 5 from both sets,
+ // but only has the first 2 item set.
+ todoAttr(getAcc("menu"), "child-item-count", "5");
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<ul id="tablist_1" role="tablist">
+ <li id="tab_1" role="tab">Crust</li>
+ <li id="tab_2" role="tab">Veges</li>
+ <li id="tab_3" role="tab">Carnivore</li>
+ </ul>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA tab
+ testGroupAttrs(getAcc("tab_1"), 1, 3);
+ testGroupAttrs(getAcc("tab_2"), 2, 3);
+ testGroupAttrs(getAcc("tab_3"), 3, 3);
+ // tab list tab count
+ testGroupParentAttrs(getAcc("tablist_1"), 3, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<ul id="rg1" role="radiogroup">
+ <li id="r1" role="radio" aria-checked="false">Thai</li>
+ <li id="r2" role="radio" aria-checked="false">Subway</li>
+ <li id="r3" role="radio" aria-checked="false">Jimmy Johns</li>
+ </ul>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA radio
+ testGroupAttrs(getAcc("r1"), 1, 3);
+ testGroupAttrs(getAcc("r2"), 2, 3);
+ testGroupAttrs(getAcc("r3"), 3, 3);
+ // explicit aria radio group
+ testGroupParentAttrs(getAcc("rg1"), 3, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<table role="tree" id="tree_1">
+ <tr role="presentation">
+ <td role="treeitem" aria-expanded="true" aria-level="1"
+ id="ti1">vegetables</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti2">cucumber</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti3">carrot</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-expanded="false" aria-level="1"
+ id="ti4">cars</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti5">mercedes</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti6">BMW</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti7">Audi</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="1" id="ti8">people</td>
+ </tr>
+ </table>
+
+ <ul role="tree" id="tree_2">
+ <li role="treeitem" id="tree2_ti1">Item 1
+ <ul role="group">
+ <li role="treeitem" id="tree2_ti1a">Item 1A</li>
+ <li role="treeitem" id="tree2_ti1b">Item 1B</li>
+ </ul>
+ </li>
+ <li role="treeitem" id="tree2_ti2">Item 2
+ <ul role="group">
+ <li role="treeitem" id="tree2_ti2a">Item 2A</li>
+ <li role="treeitem" id="tree2_ti2b">Item 2B</li>
+ </ul>
+ </li>
+ </div>
+
+ <div role="tree" id="tree_3">
+ <div role="treeitem" id="tree3_ti1">Item 1</div>
+ <div role="group">
+ <li role="treeitem" id="tree3_ti1a">Item 1A</li>
+ <li role="treeitem" id="tree3_ti1b">Item 1B</li>
+ </div>
+ <div role="treeitem" id="tree3_ti2">Item 2</div>
+ <div role="group">
+ <div role="treeitem" id="tree3_ti2a">Item 2A</div>
+ <div role="treeitem" id="tree3_ti2b">Item 2B</div>
+ </div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA tree
+ testGroupAttrs(getAcc("ti1"), 1, 3, 1);
+ testGroupAttrs(getAcc("ti2"), 1, 2, 2);
+ testGroupAttrs(getAcc("ti3"), 2, 2, 2);
+ testGroupAttrs(getAcc("ti4"), 2, 3, 1);
+ testGroupAttrs(getAcc("ti5"), 1, 3, 2);
+ testGroupAttrs(getAcc("ti6"), 2, 3, 2);
+ testGroupAttrs(getAcc("ti7"), 3, 3, 2);
+ testGroupAttrs(getAcc("ti8"), 3, 3, 1);
+ testGroupParentAttrs(getAcc("tree_1"), 3, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA tree (tree -> treeitem -> group -> treeitem)
+ testGroupAttrs(getAcc("tree2_ti1"), 1, 2, 1);
+ testGroupAttrs(getAcc("tree2_ti1a"), 1, 2, 2);
+ testGroupAttrs(getAcc("tree2_ti1b"), 2, 2, 2);
+ testGroupAttrs(getAcc("tree2_ti2"), 2, 2, 1);
+ testGroupAttrs(getAcc("tree2_ti2a"), 1, 2, 2);
+ testGroupAttrs(getAcc("tree2_ti2b"), 2, 2, 2);
+ testGroupParentAttrs(getAcc("tree_2"), 2, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA tree (tree -> treeitem, group -> treeitem)
+ testGroupAttrs(getAcc("tree3_ti1"), 1, 2, 1);
+ testGroupAttrs(getAcc("tree3_ti1a"), 1, 2, 2);
+ testGroupAttrs(getAcc("tree3_ti1b"), 2, 2, 2);
+ testGroupAttrs(getAcc("tree3_ti2"), 2, 2, 1);
+ testGroupAttrs(getAcc("tree3_ti2a"), 1, 2, 2);
+ testGroupAttrs(getAcc("tree3_ti2b"), 2, 2, 2);
+ testGroupParentAttrs(getAcc("tree_3"), 2, true);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_obj_group_002.js b/accessible/tests/browser/e10s/browser_obj_group_002.js
new file mode 100644
index 0000000000..54cad4a019
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_obj_group_002.js
@@ -0,0 +1,390 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `<table role="grid" id="grid">
+ <tr role="row" id="grid_row1">
+ <td role="gridcell" id="grid_cell1">cell1</td>
+ <td role="gridcell" id="grid_cell2">cell2</td>
+ </tr>
+ <tr role="row" id="grid_row2">
+ <td role="gridcell" id="grid_cell3">cell3</td>
+ <td role="gridcell" id="grid_cell4">cell4</td>
+ </tr>
+ </table>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA grid
+ testGroupAttrs(getAcc("grid_row1"), 1, 2);
+ testAbsentAttrs(getAcc("grid_cell1"), { posinset: "", setsize: "" });
+ testAbsentAttrs(getAcc("grid_cell2"), { posinset: "", setsize: "" });
+
+ testGroupAttrs(getAcc("grid_row2"), 2, 2);
+ testAbsentAttrs(getAcc("grid_cell3"), { posinset: "", setsize: "" });
+ testAbsentAttrs(getAcc("grid_cell4"), { posinset: "", setsize: "" });
+ testGroupParentAttrs(getAcc("grid"), 2, false, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<div role="treegrid" id="treegrid" aria-colcount="4">
+ <div role="row" aria-level="1" id="treegrid_row1">
+ <div role="gridcell" id="treegrid_cell1">cell1</div>
+ <div role="gridcell" id="treegrid_cell2">cell2</div>
+ </div>
+ <div role="row" aria-level="2" id="treegrid_row2">
+ <div role="gridcell" id="treegrid_cell3">cell1</div>
+ <div role="gridcell" id="treegrid_cell4">cell2</div>
+ </div>
+ <div role="row" id="treegrid_row3">
+ <div role="gridcell" id="treegrid_cell5">cell1</div>
+ <div role="gridcell" id="treegrid_cell6">cell2</div>
+ </div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA treegrid
+ testGroupAttrs(getAcc("treegrid_row1"), 1, 2, 1);
+ testAbsentAttrs(getAcc("treegrid_cell1"), { posinset: "", setsize: "" });
+ testAbsentAttrs(getAcc("treegrid_cell2"), { posinset: "", setsize: "" });
+
+ testGroupAttrs(getAcc("treegrid_row2"), 1, 1, 2);
+ testAbsentAttrs(getAcc("treegrid_cell3"), { posinset: "", setsize: "" });
+ testAbsentAttrs(getAcc("treegrid_cell4"), { posinset: "", setsize: "" });
+
+ testGroupAttrs(getAcc("treegrid_row3"), 2, 2, 1);
+ testAbsentAttrs(getAcc("treegrid_cell5"), { posinset: "", setsize: "" });
+ testAbsentAttrs(getAcc("treegrid_cell6"), { posinset: "", setsize: "" });
+
+ testGroupParentAttrs(getAcc("treegrid"), 2, true);
+ // row child item count provided by parent grid's aria-colcount
+ testGroupParentAttrs(getAcc("treegrid_row1"), 4, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<div id="headings">
+ <h1 id="h1">heading1</h1>
+ <h2 id="h2">heading2</h2>
+ <h3 id="h3">heading3</h3>
+ <h4 id="h4">heading4</h4>
+ <h5 id="h5">heading5</h5>
+ <h6 id="h6">heading6</h6>
+ <div id="ariaHeadingNoLevel" role="heading">ariaHeadingNoLevel</div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML headings
+ testGroupAttrs(getAcc("h1"), 0, 0, 1);
+ testGroupAttrs(getAcc("h2"), 0, 0, 2);
+ testGroupAttrs(getAcc("h3"), 0, 0, 3);
+ testGroupAttrs(getAcc("h4"), 0, 0, 4);
+ testGroupAttrs(getAcc("h5"), 0, 0, 5);
+ testGroupAttrs(getAcc("h6"), 0, 0, 6);
+ testGroupAttrs(getAcc("ariaHeadingNoLevel"), 0, 0, 2);
+ // No child item counts or "tree" flag for parent of headings
+ testAbsentAttrs(getAcc("headings"), { "child-item-count": "", tree: "" });
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<ul id="combo1" role="combobox">Password
+ <li id="combo1_opt1" role="option">Xyzzy</li>
+ <li id="combo1_opt2" role="option">Plughs</li>
+ <li id="combo1_opt3" role="option">Shazaam</li>
+ <li id="combo1_opt4" role="option">JoeSentMe</li>
+ </ul>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA combobox
+ testGroupAttrs(getAcc("combo1_opt1"), 1, 4);
+ testGroupAttrs(getAcc("combo1_opt2"), 2, 4);
+ testGroupAttrs(getAcc("combo1_opt3"), 3, 4);
+ testGroupAttrs(getAcc("combo1_opt4"), 4, 4);
+ testGroupParentAttrs(getAcc("combo1"), 4, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<div role="table" aria-colcount="4" aria-rowcount="2" id="table">
+ <div role="row" id="table_row" aria-rowindex="2">
+ <div role="cell" id="table_cell" aria-colindex="3">cell</div>
+ </div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA table
+ testGroupAttrs(getAcc("table_cell"), 3, 4);
+ testGroupAttrs(getAcc("table_row"), 2, 2);
+
+ // grid child item count provided by aria-rowcount
+ testGroupParentAttrs(getAcc("table"), 2, false);
+ // row child item count provided by parent grid's aria-colcount
+ testGroupParentAttrs(getAcc("table_row"), 4, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<div role="grid" aria-readonly="true">
+ <div tabindex="-1">
+ <div role="row" id="wrapped_row_1">
+ <div role="gridcell">cell content</div>
+ </div>
+ </div>
+ <div tabindex="-1">
+ <div role="row" id="wrapped_row_2">
+ <div role="gridcell">cell content</div>
+ </div>
+ </div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // Attributes calculated even when row is wrapped in a div.
+ testGroupAttrs(getAcc("wrapped_row_1"), 1, 2, null);
+ testGroupAttrs(getAcc("wrapped_row_2"), 2, 2, null);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<div role="list" aria-owns="t1_li1 t1_li2 t1_li3" id="aria-list_4">
+ <div role="listitem" id="t1_li2">Apples</div>
+ <div role="listitem" id="t1_li1">Oranges</div>
+ </div>
+ <div role="listitem" id="t1_li3">Bananas</div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA list constructed by ARIA owns
+ testGroupAttrs(getAcc("t1_li1"), 1, 3);
+ testGroupAttrs(getAcc("t1_li2"), 2, 3);
+ testGroupAttrs(getAcc("t1_li3"), 3, 3);
+ testGroupParentAttrs(getAcc("aria-list_4"), 3, false);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<!-- ARIA comments, 1 level, group pos and size calculation -->
+ <article>
+ <p id="comm_single_1" role="comment">Comment 1</p>
+ <p id="comm_single_2" role="comment">Comment 2</p>
+ </article>
+
+ <!-- Nested comments -->
+ <article>
+ <div id="comm_nested_1" role="comment"><p>Comment 1 level 1</p>
+ <div id="comm_nested_1_1" role="comment"><p>Comment 1 level 2</p></div>
+ <div id="comm_nested_1_2" role="comment"><p>Comment 2 level 2</p></div>
+ </div>
+ <div id="comm_nested_2" role="comment"><p>Comment 2 level 1</p>
+ <div id="comm_nested_2_1" role="comment"><p>Comment 3 level 2</p>
+ <div id="comm_nested_2_1_1" role="comment"><p>Comment 1 level 3</p></div>
+ </div>
+ </div>
+ <div id="comm_nested_3" role="comment"><p>Comment 3 level 1</p></div>
+ </article>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // Test group attributes of ARIA comments
+ testGroupAttrs(getAcc("comm_single_1"), 1, 2, 1);
+ testGroupAttrs(getAcc("comm_single_2"), 2, 2, 1);
+ testGroupAttrs(getAcc("comm_nested_1"), 1, 3, 1);
+ testGroupAttrs(getAcc("comm_nested_1_1"), 1, 2, 2);
+ testGroupAttrs(getAcc("comm_nested_1_2"), 2, 2, 2);
+ testGroupAttrs(getAcc("comm_nested_2"), 2, 3, 1);
+ testGroupAttrs(getAcc("comm_nested_2_1"), 1, 1, 2);
+ testGroupAttrs(getAcc("comm_nested_2_1_1"), 1, 1, 3);
+ testGroupAttrs(getAcc("comm_nested_3"), 3, 3, 1);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+addAccessibleTask(
+ `<div role="tree" id="tree4"><div role="treeitem"
+ id="tree4_ti1">Item 1</div><div role="treeitem"
+ id="tree4_ti2">Item 2</div></div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // Test that group position information updates after deleting node.
+ testGroupAttrs(getAcc("tree4_ti1"), 1, 2, 1);
+ testGroupAttrs(getAcc("tree4_ti2"), 2, 2, 1);
+ testGroupParentAttrs(getAcc("tree4"), 2, true);
+
+ let p = waitForEvent(EVENT_REORDER, "tree4");
+ invokeContentTask(browser, [], () => {
+ content.document.getElementById("tree4_ti1").remove();
+ });
+
+ await p;
+ testGroupAttrs(getAcc("tree4_ti2"), 1, 1, 1);
+ testGroupParentAttrs(getAcc("tree4"), 1, true);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+// Verify that intervening SECTION accs in ARIA compound widgets do not split
+// up the group info for descendant owned elements. Test various types of
+// widgets that should all be treated the same.
+addAccessibleTask(
+ `<div role="tree" id="tree">
+ <div tabindex="0">
+ <div role="treeitem" id="ti1">treeitem 1</div>
+ </div>
+ <div tabindex="0">
+ <div role="treeitem" id="ti2">treeitem 2</div>
+ </div>
+ </div>
+ <div role="listbox" id="listbox">
+ <div tabindex="0">
+ <div role="option" id="opt1">option 1</div>
+ </div>
+ <div tabindex="0">
+ <div role="option" id="opt2">option 2</div>
+ </div>
+ </div>
+ <div role="list" id="list">
+ <div tabindex="0">
+ <div role="listitem" id="li1">listitem 1</div>
+ </div>
+ <div tabindex="0">
+ <div role="listitem" id="li2">listitem 2</div>
+ </div>
+ </div>
+ <div role="menu" id="menu">
+ <div tabindex="0">
+ <div role="menuitem" id="mi1">menuitem 1</div>
+ </div>
+ <div tabindex="0">
+ <div role="menuitem" id="mi2">menuitem 2</div>
+ </div>
+ </div>
+ <div role="radiogroup" id="radiogroup">
+ <div tabindex="0">
+ <div role="radio" id="r1">radio 1</div>
+ </div>
+ <div tabindex="0">
+ <div role="radio" id="r2">radio 2</div>
+ </div>
+ </div>
+`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ testGroupAttrs(getAcc("ti1"), 1, 2, 1);
+ testGroupAttrs(getAcc("ti2"), 2, 2, 1);
+
+ testGroupAttrs(getAcc("opt1"), 1, 2, 0);
+ testGroupAttrs(getAcc("opt2"), 2, 2, 0);
+
+ testGroupAttrs(getAcc("li1"), 1, 2, 0);
+ testGroupAttrs(getAcc("li2"), 2, 2, 0);
+
+ testGroupAttrs(getAcc("mi1"), 1, 2, 0);
+ testGroupAttrs(getAcc("mi2"), 2, 2, 0);
+
+ testGroupAttrs(getAcc("r1"), 1, 2, 0);
+ testGroupAttrs(getAcc("r2"), 2, 2, 0);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
+
+// Verify that non-generic accessibles (like buttons) correctly split the group
+// info of descendant owned elements.
+addAccessibleTask(
+ `<div role="tree" id="tree">
+ <div role="button">
+ <div role="treeitem" id="ti1">first</div>
+ </div>
+ <div tabindex="0">
+ <div role="treeitem" id="ti2">second</div>
+ </div>
+ </div>`,
+ async function (browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ testGroupAttrs(getAcc("ti1"), 1, 1, 1);
+ testGroupAttrs(getAcc("ti2"), 1, 1, 1);
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js b/accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js
new file mode 100644
index 0000000000..6d5995531e
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+// Test ARIA Dialog
+addAccessibleTask(
+ "e10s/doc_treeupdate_ariadialog.html",
+ async function (browser, accDoc) {
+ testAccessibleTree(accDoc, {
+ role: ROLE_DOCUMENT,
+ children: [],
+ });
+
+ // Make dialog visible and update its inner content.
+ let onShow = waitForEvent(EVENT_SHOW, "dialog");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("dialog").style.display = "block";
+ });
+ await onShow;
+
+ testAccessibleTree(accDoc, {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_DIALOG,
+ children: [
+ {
+ role: ROLE_PUSHBUTTON,
+ children: [{ role: ROLE_TEXT_LEAF }],
+ },
+ {
+ role: ROLE_ENTRY,
+ },
+ ],
+ },
+ ],
+ });
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js
new file mode 100644
index 0000000000..78f52d3162
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js
@@ -0,0 +1,457 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+async function testContainer1(browser, accDoc) {
+ const id = "t1_container";
+ const docID = getAccessibleDOMNodeID(accDoc);
+ const acc = findAccessibleChildByID(accDoc, id);
+
+ /* ================= Initial tree test ==================================== */
+ // children are swapped by ARIA owns
+ let tree = {
+ SECTION: [{ CHECKBUTTON: [{ SECTION: [] }] }, { PUSHBUTTON: [] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Change ARIA owns ====================================== */
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetAttribute(browser, id, "aria-owns", "t1_button t1_subdiv");
+ await onReorder;
+
+ // children are swapped again, button and subdiv are appended to
+ // the children.
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // checkbox, native order
+ { PUSHBUTTON: [] }, // button, rearranged by ARIA own
+ { SECTION: [] }, // subdiv from the subtree, ARIA owned
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Remove ARIA owns ====================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetAttribute(browser, id, "aria-owns");
+ await onReorder;
+
+ // children follow the DOM order
+ tree = {
+ SECTION: [{ PUSHBUTTON: [] }, { CHECKBUTTON: [{ SECTION: [] }] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Set ARIA owns ========================================= */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetAttribute(browser, id, "aria-owns", "t1_button t1_subdiv");
+ await onReorder;
+
+ // children are swapped again, button and subdiv are appended to
+ // the children.
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // checkbox
+ { PUSHBUTTON: [] }, // button, rearranged by ARIA own
+ { SECTION: [] }, // subdiv from the subtree, ARIA owned
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Add ID to ARIA owns =================================== */
+ onReorder = waitForEvent(EVENT_REORDER, docID);
+ await invokeSetAttribute(
+ browser,
+ id,
+ "aria-owns",
+ "t1_button t1_subdiv t1_group"
+ );
+ await onReorder;
+
+ // children are swapped again, button and subdiv are appended to
+ // the children.
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // t1_checkbox
+ { PUSHBUTTON: [] }, // button, t1_button
+ { SECTION: [] }, // subdiv from the subtree, t1_subdiv
+ { GROUPING: [] }, // group from outside, t1_group
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Append element ======================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ let div = content.document.createElement("div");
+ div.setAttribute("id", "t1_child3");
+ div.setAttribute("role", "radio");
+ content.document.getElementById(contentId).appendChild(div);
+ });
+ await onReorder;
+
+ // children are invalidated, they includes aria-owns swapped kids and
+ // newly inserted child.
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // existing explicit, t1_checkbox
+ { RADIOBUTTON: [] }, // new explicit, t1_child3
+ { PUSHBUTTON: [] }, // ARIA owned, t1_button
+ { SECTION: [] }, // ARIA owned, t1_subdiv
+ { GROUPING: [] }, // ARIA owned, t1_group
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Remove element ======================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("t1_span").remove();
+ });
+ await onReorder;
+
+ // subdiv should go away
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // explicit, t1_checkbox
+ { RADIOBUTTON: [] }, // explicit, t1_child3
+ { PUSHBUTTON: [] }, // ARIA owned, t1_button
+ { GROUPING: [] }, // ARIA owned, t1_group
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Remove ID ============================================= */
+ onReorder = waitForEvent(EVENT_REORDER, docID);
+ await invokeSetAttribute(browser, "t1_group", "id");
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] },
+ { RADIOBUTTON: [] },
+ { PUSHBUTTON: [] }, // ARIA owned, t1_button
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================ Set ID ================================================ */
+ onReorder = waitForEvent(EVENT_REORDER, docID);
+ await invokeSetAttribute(browser, "t1_grouptmp", "id", "t1_group");
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ { CHECKBUTTON: [] },
+ { RADIOBUTTON: [] },
+ { PUSHBUTTON: [] }, // ARIA owned, t1_button
+ { GROUPING: [] }, // ARIA owned, t1_group, previously t1_grouptmp
+ ],
+ };
+ testAccessibleTree(acc, tree);
+}
+
+async function removeContainer(browser, accDoc) {
+ const id = "t2_container1";
+ const acc = findAccessibleChildByID(accDoc, id);
+
+ let tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // ARIA owned, 't2_owned'
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("t2_container2")
+ .removeChild(content.document.getElementById("t2_container3"));
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [],
+ };
+ testAccessibleTree(acc, tree);
+}
+
+async function stealAndRecacheChildren(browser, accDoc) {
+ const id1 = "t3_container1";
+ const id2 = "t3_container2";
+ const acc1 = findAccessibleChildByID(accDoc, id1);
+ const acc2 = findAccessibleChildByID(accDoc, id2);
+
+ /* ================ Attempt to steal from other ARIA owns ================= */
+ let onReorder = waitForEvent(EVENT_REORDER, id2);
+ await invokeSetAttribute(browser, id2, "aria-owns", "t3_child");
+ await invokeContentTask(browser, [id2], id => {
+ let div = content.document.createElement("div");
+ div.setAttribute("role", "radio");
+ content.document.getElementById(id).appendChild(div);
+ });
+ await onReorder;
+
+ let tree = {
+ SECTION: [
+ { CHECKBUTTON: [] }, // ARIA owned
+ ],
+ };
+ testAccessibleTree(acc1, tree);
+
+ tree = {
+ SECTION: [{ RADIOBUTTON: [] }],
+ };
+ testAccessibleTree(acc2, tree);
+}
+
+async function showHiddenElement(browser, accDoc) {
+ const id = "t4_container1";
+ const acc = findAccessibleChildByID(accDoc, id);
+
+ let tree = {
+ SECTION: [{ RADIOBUTTON: [] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetStyle(browser, "t4_child1", "display", "block");
+ await onReorder;
+
+ tree = {
+ SECTION: [{ CHECKBUTTON: [] }, { RADIOBUTTON: [] }],
+ };
+ testAccessibleTree(acc, tree);
+}
+
+async function rearrangeARIAOwns(browser, accDoc) {
+ const id = "t5_container";
+ const acc = findAccessibleChildByID(accDoc, id);
+ const tests = [
+ {
+ val: "t5_checkbox t5_radio t5_button",
+ roleList: ["CHECKBUTTON", "RADIOBUTTON", "PUSHBUTTON"],
+ },
+ {
+ val: "t5_radio t5_button t5_checkbox",
+ roleList: ["RADIOBUTTON", "PUSHBUTTON", "CHECKBUTTON"],
+ },
+ ];
+
+ for (let { val, roleList } of tests) {
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetAttribute(browser, id, "aria-owns", val);
+ await onReorder;
+
+ let tree = { SECTION: [] };
+ for (let role of roleList) {
+ let ch = {};
+ ch[role] = [];
+ tree.SECTION.push(ch);
+ }
+ testAccessibleTree(acc, tree);
+ }
+}
+
+async function removeNotARIAOwnedEl(browser, accDoc) {
+ const id = "t6_container";
+ const acc = findAccessibleChildByID(accDoc, id);
+
+ let tree = {
+ SECTION: [{ TEXT_LEAF: [] }, { GROUPING: [] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ content.document
+ .getElementById(contentId)
+ .removeChild(content.document.getElementById("t6_span"));
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [{ GROUPING: [] }],
+ };
+ testAccessibleTree(acc, tree);
+}
+
+addAccessibleTask(
+ "e10s/doc_treeupdate_ariaowns.html",
+ async function (browser, accDoc) {
+ await testContainer1(browser, accDoc);
+ await removeContainer(browser, accDoc);
+ await stealAndRecacheChildren(browser, accDoc);
+ await showHiddenElement(browser, accDoc);
+ await rearrangeARIAOwns(browser, accDoc);
+ await removeNotARIAOwnedEl(browser, accDoc);
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+// Test owning an ancestor which isn't created yet with an iframe in the
+// subtree.
+addAccessibleTask(
+ `
+ <span id="a">
+ <div id="b" aria-owns="c"></div>
+ </span>
+ <div id="c">
+ <iframe></iframe>
+ </div>
+ <script>
+ document.getElementById("c").setAttribute("aria-owns", "a");
+ </script>
+ `,
+ async function (browser, accDoc) {
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [
+ {
+ // b
+ SECTION: [
+ {
+ // c
+ SECTION: [{ INTERNAL_FRAME: [{ DOCUMENT: [] }] }],
+ },
+ ],
+ },
+ ],
+ });
+ }
+);
+
+// Verify that removing the parent of a DOM-sibling aria-owned child keeps the
+// formerly-owned child in the tree.
+addAccessibleTask(
+ `<input id='x'></input><div aria-owns='x'></div>`,
+ async function (browser, accDoc) {
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [{ SECTION: [{ ENTRY: [] }] }],
+ });
+
+ info("Removing the div that aria-owns a DOM sibling");
+ let onReorder = waitForEvent(EVENT_REORDER, accDoc);
+ await invokeContentTask(browser, [], () => {
+ content.document.querySelector("div").remove();
+ });
+ await onReorder;
+
+ info("Verifying that the formerly-owned child is still present");
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [{ ENTRY: [] }],
+ });
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that removing the parent of multiple DOM-sibling aria-owned children
+// keeps all formerly-owned children in the tree.
+addAccessibleTask(
+ `<input id='x'></input><input id='y'><div aria-owns='x y'></div>`,
+ async function (browser, accDoc) {
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [
+ {
+ SECTION: [{ ENTRY: [] }, { ENTRY: [] }],
+ },
+ ],
+ });
+
+ info("Removing the div that aria-owns DOM siblings");
+ let onReorder = waitForEvent(EVENT_REORDER, accDoc);
+ await invokeContentTask(browser, [], () => {
+ content.document.querySelector("div").remove();
+ });
+ await onReorder;
+
+ info("Verifying that the formerly-owned children are still present");
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [{ ENTRY: [] }, { ENTRY: [] }],
+ });
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that reordering owned elements by changing the aria-owns attribute
+// properly reorders owned elements.
+addAccessibleTask(
+ `
+<div id="container" aria-owns="b d c a">
+ <div id="a" role="button"></div>
+ <div id="b" role="checkbox"></div>
+</div>
+<div id="c" role="radio"></div>
+<div id="d"></div>`,
+ async function (browser, accDoc) {
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [
+ {
+ SECTION: [
+ { CHECKBUTTON: [] }, // b
+ { SECTION: [] }, // d
+ { RADIOBUTTON: [] }, // c
+ { PUSHBUTTON: [] }, // a
+ ],
+ },
+ ],
+ });
+
+ info("Removing the div that aria-owns other elements");
+ let onReorder = waitForEvent(EVENT_REORDER, accDoc);
+ await invokeContentTask(browser, [], () => {
+ content.document.querySelector("#container").remove();
+ });
+ await onReorder;
+
+ info(
+ "Verify DOM children are removed, order of remaining elements is correct"
+ );
+ testAccessibleTree(accDoc, {
+ DOCUMENT: [
+ { RADIOBUTTON: [] }, // c
+ { SECTION: [] }, // d
+ ],
+ });
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that we avoid sending unwanted hide events when doing multiple
+// aria-owns relocations in a single tick. Note that we're avoiding testing
+// chrome here since parent process locals don't track moves in the same way,
+// meaning our mechanism for avoiding duplicate hide events doesn't work.
+addAccessibleTask(
+ `
+<div id='b' aria-owns='a'></div>
+<div id='d'></div>
+<dd id='f'>
+ <div id='a' aria-owns='d'></div>
+</dd>
+ `,
+ async function (browser, accDoc) {
+ const b = findAccessibleChildByID(accDoc, "b");
+ const waitFor = {
+ expected: [
+ [EVENT_HIDE, b],
+ [EVENT_SHOW, "d"],
+ [EVENT_REORDER, accDoc],
+ ],
+ unexpected: [
+ [EVENT_HIDE, "d"],
+ [EVENT_REORDER, "a"],
+ ],
+ };
+ info(
+ "Verifying that events are fired properly after doing two aria-owns relocations"
+ );
+ await contentSpawnMutation(browser, waitFor, function () {
+ content.document.querySelector("#b").remove();
+ content.document.querySelector("#f").remove();
+ });
+ },
+ { chrome: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_canvas.js b/accessible/tests/browser/e10s/browser_treeupdate_canvas.js
new file mode 100644
index 0000000000..ad7338f725
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_canvas.js
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `
+ <canvas id="canvas">
+ <div id="dialog" role="dialog" style="display: none;"></div>
+ </canvas>`,
+ async function (browser, accDoc) {
+ let canvas = findAccessibleChildByID(accDoc, "canvas");
+ let dialog = findAccessibleChildByID(accDoc, "dialog");
+
+ testAccessibleTree(canvas, { CANVAS: [] });
+
+ let onShow = waitForEvent(EVENT_SHOW, "dialog");
+ await invokeSetStyle(browser, "dialog", "display", "block");
+ await onShow;
+
+ testAccessibleTree(dialog, { DIALOG: [] });
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_csscontentvisibility.js b/accessible/tests/browser/e10s/browser_treeupdate_csscontentvisibility.js
new file mode 100644
index 0000000000..8a1f93d0be
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_csscontentvisibility.js
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+const snippet = `
+ <style>
+ #target {
+ width: 150px;
+ height: 150px;
+ background-color: lightblue;
+ }
+ #child {
+ width: 100px;
+ height: 100px;
+ background-color: lightgreen;
+ }
+ #content-child {
+ width: 100px;
+ height: 100px;
+ background-color: green;
+ display: contents;
+ }
+ .hidden {
+ content-visibility: hidden;
+ }
+ .auto {
+ content-visibility: auto;
+ }
+ #hidden-subtree-2 {
+ visibility: hidden;
+ }
+ </style>
+ <div class="hidden" id="target">
+ <div id="child">A</div>
+ <div id="content-child">B</div>
+ <div id="hidden-subtree-1" class="hidden">C</div>
+ <div id="hidden-subtree-2">D</div>
+ <div id="shadow-host"></div>
+ </div>
+ <script>
+ const host = document.querySelector("#shadow-host");
+ const shadowRoot = host.attachShadow({ mode: "open" });
+ shadowRoot.innerHTML = "<div id='shadowDiv'>E</div>";
+ </script>
+ `;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["layout.css.content-visibility.enabled", true]],
+ });
+});
+
+async function setContentVisibility(browser, value) {
+ let mutationPromise = (() => {
+ switch (value) {
+ case "hidden":
+ return waitForEvent(EVENT_REORDER, "target");
+ case "auto":
+ return waitForEvents({
+ expected: [
+ [EVENT_REORDER, "child"],
+ [EVENT_REORDER, "content-child"],
+ [EVENT_REORDER, "shadowDiv"],
+ [EVENT_REORDER, "target"],
+ ],
+ });
+ default:
+ throw new Error(`unexpected content-visibility: ${value}`);
+ }
+ })();
+
+ // Change the value of `content-visibility` property for the target
+ info(`Setting content-visibility: ${value}`);
+ await invokeSetAttribute(browser, "target", "class", value);
+ await mutationPromise;
+}
+
+addAccessibleTask(
+ snippet,
+ async function (browser, accDoc) {
+ const target = findAccessibleChildByID(accDoc, "target");
+
+ info("Initial Accessibility Structure Test");
+ testAccessibleTree(target, { SECTION: [] });
+
+ await setContentVisibility(browser, "auto");
+ testAccessibleTree(target, {
+ SECTION: [
+ { SECTION: [{ TEXT_LEAF: [] }] } /* child */,
+ { SECTION: [{ TEXT_LEAF: [] }] } /* content-child */,
+ { SECTION: [] } /* hidden-subtree-1 */,
+ { SECTION: [{ SECTION: [{ TEXT_LEAF: [] }] }] } /* shadow-host */,
+ ],
+ });
+
+ await setContentVisibility(browser, "hidden");
+ testAccessibleTree(target, { SECTION: [] });
+ },
+ { iframe: true, remoteIframe: true, chrome: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js b/accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js
new file mode 100644
index 0000000000..4d18f1c08d
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `
+ <div id="container"><div id="scrollarea" style="overflow:auto;"><input>`,
+ async function (browser, accDoc) {
+ const id1 = "container";
+ const container = findAccessibleChildByID(accDoc, id1);
+
+ /* ================= Change scroll range ================================== */
+ let tree = {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // scroll area
+ ENTRY: [], // child content
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(container, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, id1);
+ await invokeContentTask(browser, [id1], id => {
+ let doc = content.document;
+ doc.getElementById("scrollarea").style.width = "20px";
+ doc.getElementById(id).appendChild(doc.createElement("input"));
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // scroll area
+ ENTRY: [], // child content
+ },
+ ],
+ },
+ {
+ ENTRY: [], // inserted input
+ },
+ ],
+ };
+ testAccessibleTree(container, tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_doc.js b/accessible/tests/browser/e10s/browser_treeupdate_doc.js
new file mode 100644
index 0000000000..982b039762
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_doc.js
@@ -0,0 +1,320 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+const iframeSrc = `data:text/html,
+ <html>
+ <head>
+ <meta charset='utf-8'/>
+ <title>Inner Iframe</title>
+ </head>
+ <body id='inner-iframe'></body>
+ </html>`;
+
+addAccessibleTask(
+ `
+ <iframe id="iframe" src="${iframeSrc}"></iframe>`,
+ async function (browser, accDoc) {
+ // ID of the iframe that is being tested
+ const id = "inner-iframe";
+
+ let iframe = findAccessibleChildByID(accDoc, id);
+
+ /* ================= Initial tree check =================================== */
+ let tree = {
+ role: ROLE_DOCUMENT,
+ children: [],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Write iframe document ================================ */
+ let reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ let newHTMLNode = docNode.createElement("html");
+ let newBodyNode = docNode.createElement("body");
+ let newTextNode = docNode.createTextNode("New Wave");
+ newBodyNode.id = contentId;
+ newBodyNode.appendChild(newTextNode);
+ newHTMLNode.appendChild(newBodyNode);
+ docNode.replaceChild(newHTMLNode, docNode.documentElement);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "New Wave",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Replace iframe HTML element ========================== */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ // We can't use open/write/close outside of iframe document because of
+ // security error.
+ let script = docNode.createElement("script");
+ script.textContent = `
+ document.open();
+ document.write('<body id="${contentId}">hello</body>');
+ document.close();`;
+ docNode.body.appendChild(script);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "hello",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Replace iframe body ================================== */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ let newBodyNode = docNode.createElement("body");
+ let newTextNode = docNode.createTextNode("New Hello");
+ newBodyNode.id = contentId;
+ newBodyNode.appendChild(newTextNode);
+ newBodyNode.setAttribute("role", "application");
+ docNode.documentElement.replaceChild(newBodyNode, docNode.body);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_APPLICATION,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "New Hello",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Open iframe document ================================= */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ // Open document.
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ let script = docNode.createElement("script");
+ script.textContent = `
+ function closeMe() {
+ document.write('Works?');
+ document.close();
+ }
+ window.closeMe = closeMe;
+ document.open();
+ document.write('<body id="${contentId}"></body>');`;
+ docNode.body.appendChild(script);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Close iframe document ================================ */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ // Write and close document.
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ docNode.write("Works?");
+ docNode.close();
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Works?",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Remove HTML from iframe document ===================== */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, iframe);
+ await invokeContentTask(browser, [], () => {
+ // Remove HTML element.
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ docNode.firstChild.remove();
+ });
+ let event = await reorderEventPromise;
+
+ ok(
+ event.accessible instanceof nsIAccessibleDocument,
+ "Reorder should happen on the document"
+ );
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Insert HTML to iframe document ======================= */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ // Insert HTML element.
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ let html = docNode.createElement("html");
+ let body = docNode.createElement("body");
+ let text = docNode.createTextNode("Haha");
+ body.appendChild(text);
+ body.id = contentId;
+ html.appendChild(body);
+ docNode.appendChild(html);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Haha",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Remove body from iframe document ===================== */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, iframe);
+ await invokeContentTask(browser, [], () => {
+ // Remove body element.
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ docNode.documentElement.removeChild(docNode.body);
+ });
+ event = await reorderEventPromise;
+
+ ok(
+ event.accessible instanceof nsIAccessibleDocument,
+ "Reorder should happen on the document"
+ );
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================ Insert element under document element while body missed */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, iframe);
+ await invokeContentTask(browser, [], () => {
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ let inputNode = (content.window.inputNode =
+ docNode.createElement("input"));
+ docNode.documentElement.appendChild(inputNode);
+ });
+ event = await reorderEventPromise;
+
+ ok(
+ event.accessible instanceof nsIAccessibleDocument,
+ "Reorder should happen on the document"
+ );
+ tree = {
+ DOCUMENT: [{ ENTRY: [] }],
+ };
+ testAccessibleTree(iframe, tree);
+
+ reorderEventPromise = waitForEvent(EVENT_REORDER, iframe);
+ await invokeContentTask(browser, [], () => {
+ let docEl =
+ content.document.getElementById("iframe").contentDocument
+ .documentElement;
+ // Remove aftermath of this test before next test starts.
+ docEl.firstChild.remove();
+ });
+ // Make sure reorder event was fired and that the input was removed.
+ await reorderEventPromise;
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Insert body to iframe document ======================= */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ // Write and close document.
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ // Insert body element.
+ let body = docNode.createElement("body");
+ let text = docNode.createTextNode("Yo ho ho i butylka roma!");
+ body.appendChild(text);
+ body.id = contentId;
+ docNode.documentElement.appendChild(body);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Yo ho ho i butylka roma!",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+
+ /* ================= Change source ======================================== */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, "iframe");
+ await invokeSetAttribute(
+ browser,
+ "iframe",
+ "src",
+ `data:text/html,<html><body id="${id}"><input></body></html>`
+ );
+ event = await reorderEventPromise;
+
+ tree = {
+ INTERNAL_FRAME: [{ DOCUMENT: [{ ENTRY: [] }] }],
+ };
+ testAccessibleTree(event.accessible, tree);
+ iframe = findAccessibleChildByID(event.accessible, id);
+
+ /* ================= Replace iframe body on ARIA role body ================ */
+ reorderEventPromise = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ let docNode = content.document.getElementById("iframe").contentDocument;
+ let newBodyNode = docNode.createElement("body");
+ let newTextNode = docNode.createTextNode("New Hello");
+ newBodyNode.appendChild(newTextNode);
+ newBodyNode.setAttribute("role", "application");
+ newBodyNode.id = contentId;
+ docNode.documentElement.replaceChild(newBodyNode, docNode.body);
+ });
+ await reorderEventPromise;
+
+ tree = {
+ role: ROLE_APPLICATION,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "New Hello",
+ },
+ ],
+ };
+ testAccessibleTree(iframe, tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_gencontent.js b/accessible/tests/browser/e10s/browser_treeupdate_gencontent.js
new file mode 100644
index 0000000000..95406d96cf
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_gencontent.js
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `
+ <style>
+ .gentext:before {
+ content: "START"
+ }
+ .gentext:after {
+ content: "END"
+ }
+ </style>
+ <div id="container1"></div>
+ <div id="container2"><div id="container2_child">text</div></div>`,
+ async function (browser, accDoc) {
+ const id1 = "container1";
+ const id2 = "container2";
+ let container1 = findAccessibleChildByID(accDoc, id1);
+ let container2 = findAccessibleChildByID(accDoc, id2);
+
+ let tree = {
+ SECTION: [], // container
+ };
+ testAccessibleTree(container1, tree);
+
+ tree = {
+ SECTION: [
+ {
+ // container2
+ SECTION: [
+ {
+ // container2 child
+ TEXT_LEAF: [], // primary text
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(container2, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, id1);
+ // Create and add an element with CSS generated content to container1
+ await invokeContentTask(browser, [id1], id => {
+ let node = content.document.createElement("div");
+ node.textContent = "text";
+ node.setAttribute("class", "gentext");
+ content.document.getElementById(id).appendChild(node);
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ // container
+ {
+ SECTION: [
+ // inserted node
+ { STATICTEXT: [] }, // :before
+ { TEXT_LEAF: [] }, // primary text
+ { STATICTEXT: [] }, // :after
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(container1, tree);
+
+ onReorder = waitForEvent(EVENT_REORDER, "container2_child");
+ // Add CSS generated content to an element in container2's subtree
+ await invokeSetAttribute(browser, "container2_child", "class", "gentext");
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ // container2
+ {
+ SECTION: [
+ // container2 child
+ { STATICTEXT: [] }, // :before
+ { TEXT_LEAF: [] }, // primary text
+ { STATICTEXT: [] }, // :after
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(container2, tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_hidden.js b/accessible/tests/browser/e10s/browser_treeupdate_hidden.js
new file mode 100644
index 0000000000..d3817a003b
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_hidden.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+async function setHidden(browser, value) {
+ let onReorder = waitForEvent(EVENT_REORDER, "container");
+ await invokeSetAttribute(browser, "child", "hidden", value);
+ await onReorder;
+}
+
+addAccessibleTask(
+ '<div id="container"><input id="child"></div>',
+ async function (browser, accDoc) {
+ let container = findAccessibleChildByID(accDoc, "container");
+
+ testAccessibleTree(container, { SECTION: [{ ENTRY: [] }] });
+
+ // Set @hidden attribute
+ await setHidden(browser, "true");
+ testAccessibleTree(container, { SECTION: [] });
+
+ // Remove @hidden attribute
+ await setHidden(browser);
+ testAccessibleTree(container, { SECTION: [{ ENTRY: [] }] });
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_image.js b/accessible/tests/browser/e10s/browser_treeupdate_image.js
new file mode 100644
index 0000000000..cf45de65e0
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_image.js
@@ -0,0 +1,192 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+const IMG_ID = "img";
+const ALT_TEXT = "some-text";
+const ARIA_LABEL = "some-label";
+
+// Verify that granting alt text adds the graphic accessible.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" alt=""/>`,
+ async function (browser, accDoc) {
+ // Test initial state; the img has empty alt text so it should not be in the tree.
+ const acc = findAccessibleChildByID(accDoc, IMG_ID);
+ ok(!acc, "Image has no Accessible");
+
+ // Add the alt text. The graphic should have been inserted into the tree.
+ info(`Adding alt text "${ALT_TEXT}" to img id '${IMG_ID}'`);
+ const shown = waitForEvent(EVENT_SHOW, IMG_ID);
+ await invokeSetAttribute(browser, IMG_ID, "alt", ALT_TEXT);
+ await shown;
+ let tree = {
+ role: ROLE_GRAPHIC,
+ name: ALT_TEXT,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that the graphic accessible exists even with a missing alt attribute.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png"/>`,
+ async function (browser, accDoc) {
+ // Test initial state; the img has no alt attribute so the name is empty.
+ const acc = findAccessibleChildByID(accDoc, IMG_ID);
+ let tree = {
+ role: ROLE_GRAPHIC,
+ name: null,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+
+ // Add the alt text. The graphic should still be present in the tree.
+ info(`Adding alt attribute with text "${ALT_TEXT}" to id ${IMG_ID}`);
+ const shown = waitForEvent(EVENT_NAME_CHANGE, IMG_ID);
+ await invokeSetAttribute(browser, IMG_ID, "alt", ALT_TEXT);
+ await shown;
+ tree = {
+ role: ROLE_GRAPHIC,
+ name: ALT_TEXT,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that removing alt text removes the graphic accessible.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" alt="${ALT_TEXT}"/>`,
+ async function (browser, accDoc) {
+ // Test initial state; the img has alt text so it should be in the tree.
+ let acc = findAccessibleChildByID(accDoc, IMG_ID);
+ let tree = {
+ role: ROLE_GRAPHIC,
+ name: ALT_TEXT,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+
+ // Set the alt text empty. The graphic should have been removed from the tree.
+ info(`Setting empty alt text for img id ${IMG_ID}`);
+ const hidden = waitForEvent(EVENT_HIDE, acc);
+ await invokeContentTask(browser, [IMG_ID, "alt", ""], (id, attr, value) => {
+ let elm = content.document.getElementById(id);
+ elm.setAttribute(attr, value);
+ });
+ await hidden;
+ acc = findAccessibleChildByID(accDoc, IMG_ID);
+ ok(!acc, "Image has no Accessible");
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that the presence of an aria-label creates an accessible, even if
+// there is no alt text.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" aria-label="${ARIA_LABEL}" alt=""/>`,
+ async function (browser, accDoc) {
+ // Test initial state; the img has empty alt text, but it does have an
+ // aria-label, so it should be in the tree.
+ const acc = findAccessibleChildByID(accDoc, IMG_ID);
+ let tree = {
+ role: ROLE_GRAPHIC,
+ name: ARIA_LABEL,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+
+ // Add the alt text. The graphic should still be in the tree.
+ info(`Adding alt text "${ALT_TEXT}" to img id '${IMG_ID}'`);
+ await invokeSetAttribute(browser, IMG_ID, "alt", ALT_TEXT);
+ tree = {
+ role: ROLE_GRAPHIC,
+ name: ARIA_LABEL,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that the presence of a click listener results in the graphic
+// accessible's presence in the tree.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" alt=""/>`,
+ async function (browser, accDoc) {
+ // Add a click listener to the img element.
+ info(`Adding click listener to img id '${IMG_ID}'`);
+ const shown = waitForEvent(EVENT_SHOW, IMG_ID);
+ await invokeContentTask(browser, [IMG_ID], id => {
+ content.document.getElementById(id).addEventListener("click", () => {});
+ });
+ await shown;
+
+ // Test initial state; the img has empty alt text, but it does have a click
+ // listener, so it should be in the tree.
+ let acc = findAccessibleChildByID(accDoc, IMG_ID);
+ let tree = {
+ role: ROLE_GRAPHIC,
+ name: null,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that the presentation role prevents creation of the graphic accessible.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" role="presentation"/>`,
+ async function (browser, accDoc) {
+ // Test initial state; the img is presentational and should not be in the tree.
+ const acc = findAccessibleChildByID(accDoc, IMG_ID);
+ ok(!acc, "Image has no Accessible");
+
+ // Add some alt text. There should still be no accessible for the img in the tree.
+ info(`Adding alt attribute with text "${ALT_TEXT}" to id ${IMG_ID}`);
+ await invokeSetAttribute(browser, IMG_ID, "alt", ALT_TEXT);
+ ok(!acc, "Image has no Accessible");
+
+ // Remove the presentation role. The accessible should be created.
+ info(`Removing presentation role from img id ${IMG_ID}`);
+ const shown = waitForEvent(EVENT_SHOW, IMG_ID);
+ await invokeSetAttribute(browser, IMG_ID, "role", "");
+ await shown;
+ let tree = {
+ role: ROLE_GRAPHIC,
+ name: ALT_TEXT,
+ children: [],
+ };
+ testAccessibleTree(acc, tree);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// Verify that setting empty alt text on a hidden image does not crash.
+// See Bug 1799208 for more info.
+addAccessibleTask(
+ `<img id="${IMG_ID}" src="${MOCHITESTS_DIR}/moz.png" hidden/>`,
+ async function (browser, accDoc) {
+ // Test initial state; should be no accessible since img is hidden.
+ const acc = findAccessibleChildByID(accDoc, IMG_ID);
+ ok(!acc, "Image has no Accessible");
+
+ // Add empty alt text. We shouldn't crash.
+ info(`Adding empty alt text "" to img id '${IMG_ID}'`);
+ await invokeContentTask(browser, [IMG_ID, "alt", ""], (id, attr, value) => {
+ let elm = content.document.getElementById(id);
+ elm.setAttribute(attr, value);
+ });
+ ok(true, "Setting empty alt text on a hidden image did not crash");
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js b/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js
new file mode 100644
index 0000000000..82fbd3427e
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js
@@ -0,0 +1,190 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+async function testImageMap(browser, accDoc) {
+ const id = "imgmap";
+ const acc = findAccessibleChildByID(accDoc, id);
+
+ /* ================= Initial tree test ==================================== */
+ let tree = {
+ IMAGE_MAP: [{ role: ROLE_LINK, name: "b", children: [] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Insert area ========================================== */
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ let areaElm = content.document.createElement("area");
+ let mapNode = content.document.getElementById("map");
+ areaElm.setAttribute(
+ "href",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.bbc.co.uk/radio4/atoz/index.shtml#a"
+ );
+ areaElm.setAttribute("coords", "0,0,13,14");
+ areaElm.setAttribute("alt", "a");
+ areaElm.setAttribute("shape", "rect");
+ mapNode.insertBefore(areaElm, mapNode.firstChild);
+ });
+ await onReorder;
+
+ tree = {
+ IMAGE_MAP: [
+ { role: ROLE_LINK, name: "a", children: [] },
+ { role: ROLE_LINK, name: "b", children: [] },
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Append area ========================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ let areaElm = content.document.createElement("area");
+ let mapNode = content.document.getElementById("map");
+ areaElm.setAttribute(
+ "href",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.bbc.co.uk/radio4/atoz/index.shtml#c"
+ );
+ areaElm.setAttribute("coords", "34,0,47,14");
+ areaElm.setAttribute("alt", "c");
+ areaElm.setAttribute("shape", "rect");
+ mapNode.appendChild(areaElm);
+ });
+ await onReorder;
+
+ tree = {
+ IMAGE_MAP: [
+ { role: ROLE_LINK, name: "a", children: [] },
+ { role: ROLE_LINK, name: "b", children: [] },
+ { role: ROLE_LINK, name: "c", children: [] },
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Remove area ========================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ let mapNode = content.document.getElementById("map");
+ mapNode.removeChild(mapNode.firstElementChild);
+ });
+ await onReorder;
+
+ tree = {
+ IMAGE_MAP: [
+ { role: ROLE_LINK, name: "b", children: [] },
+ { role: ROLE_LINK, name: "c", children: [] },
+ ],
+ };
+ testAccessibleTree(acc, tree);
+}
+
+async function testContainer(browser) {
+ const id = "container";
+ /* ================= Remove name on map =================================== */
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetAttribute(browser, "map", "name");
+ let event = await onReorder;
+ const acc = event.accessible;
+
+ let tree = {
+ SECTION: [{ GRAPHIC: [] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Restore name on map ================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetAttribute(browser, "map", "name", "atoz_map");
+ // XXX: force repainting of the image (see bug 745788 for details).
+ await invokeContentTask(browser, [], () => {
+ const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeMouse(
+ content.document.getElementById("imgmap"),
+ 10,
+ 10,
+ { type: "mousemove" },
+ content
+ );
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ {
+ IMAGE_MAP: [{ LINK: [] }, { LINK: [] }],
+ },
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Remove map =========================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [], () => {
+ let mapNode = content.document.getElementById("map");
+ mapNode.remove();
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [{ GRAPHIC: [] }],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Insert map =========================================== */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ let map = content.document.createElement("map");
+ let area = content.document.createElement("area");
+
+ map.setAttribute("name", "atoz_map");
+ map.setAttribute("id", "map");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ area.setAttribute("href", "http://www.bbc.co.uk/radio4/atoz/index.shtml#b");
+ area.setAttribute("coords", "17,0,30,14");
+ area.setAttribute("alt", "b");
+ area.setAttribute("shape", "rect");
+
+ map.appendChild(area);
+ content.document.getElementById(contentId).appendChild(map);
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ {
+ IMAGE_MAP: [{ LINK: [] }],
+ },
+ ],
+ };
+ testAccessibleTree(acc, tree);
+
+ /* ================= Hide image map ======================================= */
+ onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeSetStyle(browser, "imgmap", "display", "none");
+ await onReorder;
+
+ tree = {
+ SECTION: [],
+ };
+ testAccessibleTree(acc, tree);
+}
+
+addAccessibleTask(
+ "e10s/doc_treeupdate_imagemap.html",
+ async function (browser, accDoc) {
+ await waitForImageMap(browser, accDoc);
+ await testImageMap(browser, accDoc);
+ await testContainer(browser);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_list.js b/accessible/tests/browser/e10s/browser_treeupdate_list.js
new file mode 100644
index 0000000000..2ca14d5572
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_list.js
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+async function setDisplayAndWaitForReorder(browser, value) {
+ let onReorder = waitForEvent(EVENT_REORDER, "ul");
+ await invokeSetStyle(browser, "li", "display", value);
+ return onReorder;
+}
+
+addAccessibleTask(
+ `
+ <ul id="ul">
+ <li id="li">item1</li>
+ </ul>`,
+ async function (browser, accDoc) {
+ let li = findAccessibleChildByID(accDoc, "li");
+ let bullet = li.firstChild;
+ let accTree = {
+ role: ROLE_LISTITEM,
+ children: [
+ {
+ role: ROLE_LISTITEM_MARKER,
+ children: [],
+ },
+ {
+ role: ROLE_TEXT_LEAF,
+ children: [],
+ },
+ ],
+ };
+ testAccessibleTree(li, accTree);
+
+ await setDisplayAndWaitForReorder(browser, "none");
+
+ ok(isDefunct(li), "Check that li is defunct.");
+ ok(isDefunct(bullet), "Check that bullet is defunct.");
+
+ let event = await setDisplayAndWaitForReorder(browser, "list-item");
+
+ testAccessibleTree(
+ findAccessibleChildByID(event.accessible, "li"),
+ accTree
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js b/accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js
new file mode 100644
index 0000000000..dd678d93fa
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ '<ol id="list"></ol>',
+ async function (browser, accDoc) {
+ let list = findAccessibleChildByID(accDoc, "list");
+
+ testAccessibleTree(list, {
+ role: ROLE_LIST,
+ children: [],
+ });
+
+ await invokeSetAttribute(
+ browser,
+ currentContentDoc(),
+ "contentEditable",
+ "true"
+ );
+ let onReorder = waitForEvent(EVENT_REORDER, "list");
+ await invokeContentTask(browser, [], () => {
+ let li = content.document.createElement("li");
+ li.textContent = "item";
+ content.document.getElementById("list").appendChild(li);
+ });
+ await onReorder;
+
+ testAccessibleTree(list, {
+ role: ROLE_LIST,
+ children: [
+ {
+ role: ROLE_LISTITEM,
+ children: [
+ { role: ROLE_LISTITEM_MARKER, name: "1. ", children: [] },
+ { role: ROLE_TEXT_LEAF, children: [] },
+ ],
+ },
+ ],
+ });
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_listener.js b/accessible/tests/browser/e10s/browser_treeupdate_listener.js
new file mode 100644
index 0000000000..735f7871af
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_listener.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ '<span id="parent"><span id="child"></span></span>',
+ async function (browser, accDoc) {
+ is(
+ findAccessibleChildByID(accDoc, "parent"),
+ null,
+ "Check that parent is not accessible."
+ );
+ is(
+ findAccessibleChildByID(accDoc, "child"),
+ null,
+ "Check that child is not accessible."
+ );
+
+ let onReorder = waitForEvent(EVENT_REORDER, matchContentDoc);
+ // Add an event listener to parent.
+ await invokeContentTask(browser, [], () => {
+ content.window.dummyListener = () => {};
+ content.document
+ .getElementById("parent")
+ .addEventListener("click", content.window.dummyListener);
+ });
+ await onReorder;
+
+ let tree = { TEXT: [] };
+ testAccessibleTree(findAccessibleChildByID(accDoc, "parent"), tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_move.js b/accessible/tests/browser/e10s/browser_treeupdate_move.js
new file mode 100644
index 0000000000..8ed6188ef3
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_move.js
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test moving Accessibles:
+ * 1. A moved Accessible keeps the same Accessible.
+ * 2. If the moved Accessible is focused, it remains focused.
+ * 3. A child of the moved Accessible also keeps the same Accessible.
+ * 4. A child removed at the same time as the move gets shut down.
+ */
+addAccessibleTask(
+ `
+<div id="scrollable" role="presentation" style="height: 1px;">
+ <div contenteditable id="textbox" role="textbox">
+ <h1 id="heading">Heading</h1>
+ <p id="para">Para</p>
+ </div>
+ <iframe id="iframe" src="https://example.com/"></iframe>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const textbox = findAccessibleChildByID(docAcc, "textbox");
+ const heading = findAccessibleChildByID(docAcc, "heading");
+ const para = findAccessibleChildByID(docAcc, "para");
+ const iframe = findAccessibleChildByID(docAcc, "iframe");
+ const iframeDoc = iframe.firstChild;
+ ok(iframeDoc, "iframe contains a document");
+
+ let focused = waitForEvent(EVENT_FOCUS, textbox);
+ textbox.takeFocus();
+ await focused;
+ testStates(textbox, STATE_FOCUSED, 0, 0, EXT_STATE_DEFUNCT);
+
+ let reordered = waitForEvent(EVENT_REORDER, docAcc);
+ await invokeContentTask(browser, [], () => {
+ // scrollable wasn't in the a11y tree, but this will force it to be created.
+ // textbox will be moved inside it.
+ content.document.getElementById("scrollable").style.overflow = "scroll";
+ content.document.getElementById("heading").remove();
+ });
+ await reordered;
+ // Despite the move, ensure textbox is still alive and is focused.
+ testStates(textbox, STATE_FOCUSED, 0, 0, EXT_STATE_DEFUNCT);
+ // Ensure para (a child of textbox) is also still alive.
+ ok(!isDefunct(para), "para is alive");
+ // heading was a child of textbox, but was removed when textbox
+ // was moved. Ensure it is dead.
+ ok(isDefunct(heading), "heading is dead");
+ // Ensure the iframe and its embedded document are alive.
+ ok(!isDefunct(iframe), "iframe is alive");
+ ok(!isDefunct(iframeDoc), "iframeDoc is alive");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test that moving a subtree containing an iframe doesn't cause assertions or
+ * crashes. Note that aria-owns moves Accessibles even if it is set before load.
+ */
+addAccessibleTask(
+ `
+<div id="container">
+ <iframe id="iframe"></iframe>
+ <div aria-owns="iframe"></div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "container");
+ testAccessibleTree(container, {
+ SECTION: [{ SECTION: [{ INTERNAL_FRAME: [{ DOCUMENT: [] }] }] }],
+ });
+ },
+ { topLevel: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_optgroup.js b/accessible/tests/browser/e10s/browser_treeupdate_optgroup.js
new file mode 100644
index 0000000000..ec7eed0919
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_optgroup.js
@@ -0,0 +1,100 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ '<select id="select"></select>',
+ async function (browser, accDoc) {
+ let select = findAccessibleChildByID(accDoc, "select");
+
+ let onEvent = waitForEvent(EVENT_REORDER, "select");
+ // Create a combobox with grouping and 2 standalone options
+ await invokeContentTask(browser, [], () => {
+ let doc = content.document;
+ let contentSelect = doc.getElementById("select");
+ let optGroup = doc.createElement("optgroup");
+
+ for (let i = 0; i < 2; i++) {
+ let opt = doc.createElement("option");
+ opt.value = i;
+ opt.text = "Option: Value " + i;
+ optGroup.appendChild(opt);
+ }
+ contentSelect.add(optGroup, null);
+
+ for (let i = 0; i < 2; i++) {
+ let opt = doc.createElement("option");
+ contentSelect.add(opt, null);
+ }
+ contentSelect.firstChild.firstChild.id = "option1Node";
+ });
+ let event = await onEvent;
+ let option1Node = findAccessibleChildByID(event.accessible, "option1Node");
+
+ let tree = {
+ COMBOBOX: [
+ {
+ COMBOBOX_LIST: [
+ {
+ GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }],
+ },
+ {
+ COMBOBOX_OPTION: [],
+ },
+ {
+ COMBOBOX_OPTION: [],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(select, tree);
+ ok(!isDefunct(option1Node), "option shouldn't be defunct");
+
+ onEvent = waitForEvent(EVENT_REORDER, "select");
+ // Remove grouping from combobox
+ await invokeContentTask(browser, [], () => {
+ let contentSelect = content.document.getElementById("select");
+ contentSelect.firstChild.remove();
+ });
+ await onEvent;
+
+ tree = {
+ COMBOBOX: [
+ {
+ COMBOBOX_LIST: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }],
+ },
+ ],
+ };
+ testAccessibleTree(select, tree);
+ ok(
+ isDefunct(option1Node),
+ "removed option shouldn't be accessible anymore!"
+ );
+
+ onEvent = waitForEvent(EVENT_REORDER, "select");
+ // Remove all options from combobox
+ await invokeContentTask(browser, [], () => {
+ let contentSelect = content.document.getElementById("select");
+ while (contentSelect.length) {
+ contentSelect.remove(0);
+ }
+ });
+ await onEvent;
+
+ tree = {
+ COMBOBOX: [
+ {
+ COMBOBOX_LIST: [],
+ },
+ ],
+ };
+ testAccessibleTree(select, tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_removal.js b/accessible/tests/browser/e10s/browser_treeupdate_removal.js
new file mode 100644
index 0000000000..6b5246f0bf
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_removal.js
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ "e10s/doc_treeupdate_removal.xhtml",
+ async function (browser, accDoc) {
+ ok(
+ isAccessible(findAccessibleChildByID(accDoc, "the_table")),
+ "table should be accessible"
+ );
+
+ // Move the_table element into hidden subtree.
+ let onReorder = waitForEvent(EVENT_REORDER, matchContentDoc);
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("the_displaynone")
+ .appendChild(content.document.getElementById("the_table"));
+ });
+ await onReorder;
+
+ ok(
+ !isAccessible(findAccessibleChildByID(accDoc, "the_table")),
+ "table in display none tree shouldn't be accessible"
+ );
+ ok(
+ !isAccessible(findAccessibleChildByID(accDoc, "the_row")),
+ "row shouldn't be accessible"
+ );
+
+ // Remove the_row element (since it did not have accessible, no event needed).
+ await invokeContentTask(browser, [], () => {
+ content.document.body.removeChild(
+ content.document.getElementById("the_row")
+ );
+ });
+
+ // make sure no accessibles have stuck around.
+ ok(
+ !isAccessible(findAccessibleChildByID(accDoc, "the_row")),
+ "row shouldn't be accessible"
+ );
+ ok(
+ !isAccessible(findAccessibleChildByID(accDoc, "the_table")),
+ "table shouldn't be accessible"
+ );
+ ok(
+ !isAccessible(findAccessibleChildByID(accDoc, "the_displayNone")),
+ "display none things shouldn't be accessible"
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js b/accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js
new file mode 100644
index 0000000000..a82fc4c04d
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+const snippet = `
+<select id="select">
+ <option>o1</option>
+ <optgroup label="g1">
+ <option>g1o1</option>
+ <option>g1o2</option>
+ </optgroup>
+ <optgroup label="g2">
+ <option>g2o1</option>
+ <option>g2o2</option>
+ </optgroup>
+ <option>o2</option>
+</select>
+`;
+
+addAccessibleTask(
+ snippet,
+ async function (browser, accDoc) {
+ await invokeFocus(browser, "select");
+ // Expand the select. A dropdown item should get focus.
+ // Note that the dropdown is rendered in the parent process.
+ let focused = waitForEvent(
+ EVENT_FOCUS,
+ event => event.accessible.role == ROLE_COMBOBOX_OPTION,
+ "Dropdown item focused after select expanded"
+ );
+ await invokeContentTask(browser, [], () => {
+ const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeKey("VK_DOWN", { altKey: true }, content);
+ });
+ info("Waiting for parent focus");
+ let event = await focused;
+ let dropdown = event.accessible.parent;
+
+ let selectedOptionChildren = [];
+ if (MAC) {
+ // Checkmark is part of the Mac menu styling.
+ selectedOptionChildren = [{ STATICTEXT: [] }];
+ }
+ let tree = {
+ COMBOBOX_LIST: [
+ { COMBOBOX_OPTION: selectedOptionChildren },
+ { GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }] },
+ { GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }] },
+ { COMBOBOX_OPTION: [] },
+ ],
+ };
+ testAccessibleTree(dropdown, tree);
+
+ // Collapse the select. Focus should return to the select.
+ focused = waitForEvent(
+ EVENT_FOCUS,
+ "select",
+ "select focused after collapsed"
+ );
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, window);
+ info("Waiting for child focus");
+ await focused;
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_table.js b/accessible/tests/browser/e10s/browser_treeupdate_table.js
new file mode 100644
index 0000000000..c188a06044
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_table.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `
+ <table id="table">
+ <tr>
+ <td>cell1</td>
+ <td>cell2</td>
+ </tr>
+ </table>`,
+ async function (browser, accDoc) {
+ let table = findAccessibleChildByID(accDoc, "table");
+
+ let tree = {
+ TABLE: [
+ { ROW: [{ CELL: [{ TEXT_LEAF: [] }] }, { CELL: [{ TEXT_LEAF: [] }] }] },
+ ],
+ };
+ testAccessibleTree(table, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, "table");
+ await invokeContentTask(browser, [], () => {
+ // append a caption, it should appear as a first element in the
+ // accessible tree.
+ let doc = content.document;
+ let caption = doc.createElement("caption");
+ caption.textContent = "table caption";
+ doc.getElementById("table").appendChild(caption);
+ });
+ await onReorder;
+
+ tree = {
+ TABLE: [
+ { CAPTION: [{ TEXT_LEAF: [] }] },
+ { ROW: [{ CELL: [{ TEXT_LEAF: [] }] }, { CELL: [{ TEXT_LEAF: [] }] }] },
+ ],
+ };
+ testAccessibleTree(table, tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_textleaf.js b/accessible/tests/browser/e10s/browser_treeupdate_textleaf.js
new file mode 100644
index 0000000000..0c617e7026
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_textleaf.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+async function removeTextData(browser, accessible, id, role) {
+ let tree = {
+ role,
+ children: [{ role: ROLE_TEXT_LEAF, name: "text" }],
+ };
+ testAccessibleTree(accessible, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, id);
+ await invokeContentTask(browser, [id], contentId => {
+ content.document.getElementById(contentId).firstChild.textContent = "";
+ });
+ await onReorder;
+
+ tree = { role, children: [] };
+ testAccessibleTree(accessible, tree);
+}
+
+addAccessibleTask(
+ `
+ <p id="p">text</p>
+ <pre id="pre">text</pre>`,
+ async function (browser, accDoc) {
+ let p = findAccessibleChildByID(accDoc, "p");
+ let pre = findAccessibleChildByID(accDoc, "pre");
+ await removeTextData(browser, p, "p", ROLE_PARAGRAPH);
+ await removeTextData(browser, pre, "pre", ROLE_TEXT_CONTAINER);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_visibility.js b/accessible/tests/browser/e10s/browser_treeupdate_visibility.js
new file mode 100644
index 0000000000..636a00e210
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_visibility.js
@@ -0,0 +1,342 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+async function testTreeOnHide(browser, accDoc, containerID, id, before, after) {
+ let acc = findAccessibleChildByID(accDoc, containerID);
+ testAccessibleTree(acc, before);
+
+ let onReorder = waitForEvent(EVENT_REORDER, containerID);
+ await invokeSetStyle(browser, id, "visibility", "hidden");
+ await onReorder;
+
+ testAccessibleTree(acc, after);
+}
+
+async function test3(browser, accessible) {
+ let tree = {
+ SECTION: [
+ // container
+ {
+ SECTION: [
+ // parent
+ {
+ SECTION: [
+ // child
+ { TEXT_LEAF: [] },
+ ],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ // parent2
+ {
+ SECTION: [
+ // child2
+ { TEXT_LEAF: [] },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(accessible, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, "t3_container");
+ await invokeContentTask(browser, [], () => {
+ let doc = content.document;
+ doc.getElementById("t3_container").style.color = "red";
+ doc.getElementById("t3_parent").style.visibility = "hidden";
+ doc.getElementById("t3_parent2").style.visibility = "hidden";
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ // container
+ {
+ SECTION: [
+ // child
+ { TEXT_LEAF: [] },
+ ],
+ },
+ {
+ SECTION: [
+ // child2
+ { TEXT_LEAF: [] },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(accessible, tree);
+}
+
+async function test4(browser, accessible) {
+ let tree = {
+ SECTION: [{ TABLE: [{ ROW: [{ CELL: [] }] }] }],
+ };
+ testAccessibleTree(accessible, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, "t4_parent");
+ await invokeContentTask(browser, [], () => {
+ let doc = content.document;
+ doc.getElementById("t4_container").style.color = "red";
+ doc.getElementById("t4_child").style.visibility = "visible";
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ {
+ TABLE: [
+ {
+ ROW: [
+ {
+ CELL: [
+ {
+ SECTION: [
+ {
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(accessible, tree);
+}
+
+addAccessibleTask(
+ "e10s/doc_treeupdate_visibility.html",
+ async function (browser, accDoc) {
+ let t3Container = findAccessibleChildByID(accDoc, "t3_container");
+ let t4Container = findAccessibleChildByID(accDoc, "t4_container");
+
+ await testTreeOnHide(
+ browser,
+ accDoc,
+ "t1_container",
+ "t1_parent",
+ {
+ SECTION: [
+ {
+ SECTION: [
+ {
+ SECTION: [{ TEXT_LEAF: [] }],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ SECTION: [{ TEXT_LEAF: [] }],
+ },
+ ],
+ }
+ );
+
+ await testTreeOnHide(
+ browser,
+ accDoc,
+ "t2_container",
+ "t2_grandparent",
+ {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // grand parent
+ SECTION: [
+ {
+ SECTION: [
+ {
+ // child
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // child2
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // child
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // child2
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ }
+ );
+
+ await test3(browser, t3Container);
+ await test4(browser, t4Container);
+
+ await testTreeOnHide(
+ browser,
+ accDoc,
+ "t5_container",
+ "t5_subcontainer",
+ {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // subcontainer
+ TABLE: [
+ {
+ ROW: [
+ {
+ CELL: [
+ {
+ SECTION: [
+ {
+ // child
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // child
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ }
+ );
+
+ await testTreeOnHide(
+ browser,
+ accDoc,
+ "t6_container",
+ "t6_subcontainer",
+ {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // subcontainer
+ TABLE: [
+ {
+ ROW: [
+ {
+ CELL: [
+ {
+ TABLE: [
+ {
+ // nested table
+ ROW: [
+ {
+ CELL: [
+ {
+ SECTION: [
+ {
+ // child
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // child2
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // container
+ SECTION: [
+ {
+ // child
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ {
+ SECTION: [
+ {
+ // child2
+ TEXT_LEAF: [],
+ },
+ ],
+ },
+ ],
+ }
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js b/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js
new file mode 100644
index 0000000000..78ab47cd51
--- /dev/null
+++ b/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ "e10s/doc_treeupdate_whitespace.html",
+ async function (browser, accDoc) {
+ let container1 = findAccessibleChildByID(accDoc, "container1");
+ let container2Parent = findAccessibleChildByID(accDoc, "container2-parent");
+
+ let tree = {
+ SECTION: [
+ { GRAPHIC: [] },
+ { TEXT_LEAF: [] },
+ { GRAPHIC: [] },
+ { TEXT_LEAF: [] },
+ { GRAPHIC: [] },
+ ],
+ };
+ testAccessibleTree(container1, tree);
+
+ let onReorder = waitForEvent(EVENT_REORDER, "container1");
+ // Remove img1 from container1
+ await invokeContentTask(browser, [], () => {
+ let doc = content.document;
+ doc.getElementById("container1").removeChild(doc.getElementById("img1"));
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [{ GRAPHIC: [] }, { TEXT_LEAF: [] }, { GRAPHIC: [] }],
+ };
+ testAccessibleTree(container1, tree);
+
+ tree = {
+ SECTION: [{ LINK: [] }, { LINK: [{ GRAPHIC: [] }] }],
+ };
+ testAccessibleTree(container2Parent, tree);
+
+ onReorder = waitForEvent(EVENT_REORDER, "container2-parent");
+ // Append an img with valid src to container2
+ await invokeContentTask(browser, [], () => {
+ let doc = content.document;
+ let img = doc.createElement("img");
+ img.setAttribute(
+ "src",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.com/a11y/accessible/tests/mochitest/moz.png"
+ );
+ doc.getElementById("container2").appendChild(img);
+ });
+ await onReorder;
+
+ tree = {
+ SECTION: [
+ { LINK: [{ GRAPHIC: [] }] },
+ { TEXT_LEAF: [] },
+ { LINK: [{ GRAPHIC: [] }] },
+ ],
+ };
+ testAccessibleTree(container2Parent, tree);
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html b/accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html
new file mode 100644
index 0000000000..089391523d
--- /dev/null
+++ b/accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html
@@ -0,0 +1,23 @@
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Tree Update ARIA Dialog Test</title>
+ </head>
+ <body id="body">
+ <div id="dialog" role="dialog" style="display: none;">
+ <table id="table" role="presentation"
+ style="display: block; position: absolute; top: 88px; left: 312.5px; z-index: 10010;">
+ <tbody>
+ <tr>
+ <td role="presentation">
+ <div role="presentation">
+ <a id="a" role="button">text</a>
+ </div>
+ <input id="input">
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </body>
+</html>
diff --git a/accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html b/accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html
new file mode 100644
index 0000000000..38b5c333a1
--- /dev/null
+++ b/accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html
@@ -0,0 +1,44 @@
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Tree Update ARIA Owns Test</title>
+ </head>
+ <body id="body">
+ <div id="t1_container" aria-owns="t1_checkbox t1_button">
+ <div role="button" id="t1_button"></div>
+ <div role="checkbox" id="t1_checkbox">
+ <span id="t1_span">
+ <div id="t1_subdiv"></div>
+ </span>
+ </div>
+ </div>
+ <div id="t1_group" role="group"></div>
+ <div id="t1_grouptmp" role="group"></div>
+
+ <div id="t2_container1" aria-owns="t2_owned"></div>
+ <div id="t2_container2">
+ <div id="t2_container3"><div id="t2_owned" role="checkbox"></div></div>
+ </div>
+
+ <div id="t3_container1" aria-owns="t3_child"></div>
+ <div id="t3_child" role="checkbox"></div>
+ <div id="t3_container2"></div>
+
+ <div id="t4_container1" aria-owns="t4_child1 t4_child2"></div>
+ <div id="t4_container2">
+ <div id="t4_child1" style="display:none" role="checkbox"></div>
+ <div id="t4_child2" role="radio"></div>
+ </div>
+
+ <div id="t5_container">
+ <div role="button" id="t5_button"></div>
+ <div role="checkbox" id="t5_checkbox"></div>
+ <div role="radio" id="t5_radio"></div>
+ </div>
+
+ <div id="t6_container" aria-owns="t6_fake">
+ <span id="t6_span">hey</span>
+ </div>
+ <div id="t6_fake" role="group"></div>
+ </body>
+</html>
diff --git a/accessible/tests/browser/e10s/doc_treeupdate_imagemap.html b/accessible/tests/browser/e10s/doc_treeupdate_imagemap.html
new file mode 100644
index 0000000000..4dd230fc28
--- /dev/null
+++ b/accessible/tests/browser/e10s/doc_treeupdate_imagemap.html
@@ -0,0 +1,21 @@
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Tree Update Imagemap Test</title>
+ </head>
+ <body id="body">
+ <map name="atoz_map" id="map">
+ <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b"
+ coords="17,0,30,14" alt="b" shape="rect">
+ </map>
+
+ <div id="container">
+ <img id="imgmap" width="447" height="15"
+ usemap="#atoz_map"
+ src="http://example.com/a11y/accessible/tests/mochitest/letters.gif"><!--
+ Important: no whitespace between the <img> and the </div>, so we
+ don't end up with textframes there, because those would be reflected
+ in our accessible tree in some cases.
+ --></div>
+ </body>
+</html>
diff --git a/accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml b/accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml
new file mode 100644
index 0000000000..9c59fb9d11
--- /dev/null
+++ b/accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml
@@ -0,0 +1,11 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf-8"/>
+ <title>Tree Update Removal Test</title>
+ </head>
+ <body id="body">
+ <div id="the_displaynone" style="display: none;"></div>
+ <table id="the_table"></table>
+ <tr id="the_row"></tr>
+ </body>
+</html>
diff --git a/accessible/tests/browser/e10s/doc_treeupdate_visibility.html b/accessible/tests/browser/e10s/doc_treeupdate_visibility.html
new file mode 100644
index 0000000000..00213b2b70
--- /dev/null
+++ b/accessible/tests/browser/e10s/doc_treeupdate_visibility.html
@@ -0,0 +1,78 @@
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Tree Update Visibility Test</title>
+ </head>
+ <body id="body">
+ <!-- hide parent while child stays visible -->
+ <div id="t1_container">
+ <div id="t1_parent">
+ <div id="t1_child" style="visibility: visible">text</div>
+ </div>
+ </div>
+
+ <!-- hide grandparent while its children stay visible -->
+ <div id="t2_container">
+ <div id="t2_grandparent">
+ <div id="t2_parent">
+ <div id="t2_child" style="visibility: visible">text</div>
+ <div id="t2_child2" style="visibility: visible">text</div>
+ </div>
+ </div>
+ </div>
+
+ <!-- change container style, hide parents while their children stay visible -->
+ <div id="t3_container">
+ <div id="t3_parent">
+ <div id="t3_child" style="visibility: visible">text</div>
+ </div>
+ <div id="t3_parent2">
+ <div id="t3_child2" style="visibility: visible">text</div>
+ </div>
+ </div>
+
+ <!-- change container style, show child inside the table -->
+ <div id="t4_container">
+ <table>
+ <tr>
+ <td id="t4_parent">
+ <div id="t4_child" style="visibility: hidden;">text</div>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ <!-- hide subcontainer while child inside the table stays visible -->
+ <div id="t5_container">
+ <div id="t5_subcontainer">
+ <table>
+ <tr>
+ <td>
+ <div id="t5_child" style="visibility: visible;">text</div>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+
+ <!-- hide subcontainer while its child and child inside the nested table stays visible -->
+ <div id="t6_container">
+ <div id="t6_subcontainer">
+ <table>
+ <tr>
+ <td>
+ <table>
+ <tr>
+ <td>
+ <div id="t6_child" style="visibility: visible;">text</div>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ <div id="t6_child2" style="visibility: visible">text</div>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html b/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html
new file mode 100644
index 0000000000..18a65d8192
--- /dev/null
+++ b/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html
@@ -0,0 +1,10 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf-8"/>
+ <title>Whitespace text accessible creation/destruction</title>
+ </head>
+ <body id="body">
+ <div id="container1"> <img src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> <img id="img1" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> <img src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> </div>
+ <div id="container2-parent"> <a id="container2" href=""></a> <a href=""><img src="http://example.com/a11y/accessible/tests/mochitest/moz.png"></a> </div>
+ </body>
+</html>
diff --git a/accessible/tests/browser/e10s/fonts/Ahem.sjs b/accessible/tests/browser/e10s/fonts/Ahem.sjs
new file mode 100644
index 0000000000..e801a801ab
--- /dev/null
+++ b/accessible/tests/browser/e10s/fonts/Ahem.sjs
@@ -0,0 +1,241 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/*
+ * A CORS-enabled font resource.
+ */
+
+const FONT_BYTES = atob(
+ "AAEAAAALAIAAAwAwT1MvMnhQSo0AAAE4AAAAYGNtYXAP1hZGAAAFbAAABnJnYXNwABcACQAAMLAA" +
+ "AAAQZ2x5ZkmzdNoAAAvgAAAaZGhlYWTWok4cAAAAvAAAADZoaGVhBwoEFgAAAPQAAAAkaG10eLkg" +
+ "AH0AAAGYAAAD1GxvY2EgdSciAAAmRAAAAextYXhwAPgACQAAARgAAAAgbmFtZX4UjLgAACgwAAAG" +
+ "aHBvc3SN0B2KAAAumAAAAhgAAQAAAAEAQhIXUWdfDzz1AAkD6AAAAACzb19ZAAAAAMAtq0kAAP84" +
+ "A+gDIAAAAAMAAgAAAAAAAAABAAADIP84AAAD6AAAAAAD6AABAAAAAAAAAAAAAAAAAAAA9QABAAAA" +
+ "9QAIAAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAID6AGQAAUAAAK8AooAAACPArwCigAAAcUAMgED" +
+ "AAACAAQJAAAAAAAAgAAArxAAIEgAAAAAAAAAAFczQwAAQAAg8AIDIP84AAADIADIIAABEUAAAAAD" +
+ "IAMgAAAAIAAAA+gAfQAAAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" +
+ "A+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD" +
+ "6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPo" +
+ "AAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gA" +
+ "AAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" +
+ "A+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD" +
+ "6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPo" +
+ "AAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gA" +
+ "AAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" +
+ "A+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD" +
+ "6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPo" +
+ "AAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gA" +
+ "AAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" +
+ "A+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD" +
+ "6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPo" +
+ "AAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gA" +
+ "AAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAAA+gAAAPoAAAD6AAA" +
+ "A+gAAAPoAAAD6AAAA+gAAAPoAAAAAAADAAAAAwAABEwAAQAAAAAAHAADAAEAAAImAAYCCgAAAAAB" +
+ "AAABAAAAAAAAAAAAAAAAAAAAAQACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAQAAAAAAAwAEAAUABgAHAAgACQAAAAoACwAMAA0ADgAPABAAEQASABMAFAAVABYAFwAYABkA" +
+ "GgAbABwAHQAeAB8AIAAhACIAIwAkACUAJgAnACgAKQAqACsALAAtAC4ALwAwADEAMgAzADQANQA2" +
+ "ADcAOAA5ADoAOwA8AD0APgA/AEAAQQBCAEMARABFAEYARwBIAEkASgBLAEwATQBOAE8AUABRAFIA" +
+ "UwBUAFUAVgBXAFgAWQBaAFsAXABdAF4AXwBgAAAAYQBiAGMAZABlAGYAZwBoAGkAagBrAGwAbQBu" +
+ "AG8AcABxAHIAcwB0AHUAdgB3AHgAeQB6AHsAfAB9AH4AfwCAANsAgQCCAIMAhADdAIUAhgCHAIgA" +
+ "4wCJAIoA6gCLAIwA6ACNAOsA7ACOAI8A5ADmAOUA1ADpAJAAkQDTAJIAkwCUAJUAlgDnANEA7QDS" +
+ "AJcAmADeAAMAmgCbAJwAzgDPANUA1gDYANkAnQCeAJ8A7gCgANAA4gChAOAA4QAAAAAA3ACiANcA" +
+ "2gDfAKMApAClAKYApwCoAKkAqgCrAKwArQAAAK4ArwCwALEAsgCzALQAtQC2ALcAuAC5ALoAuwC8" +
+ "AAQCJgAAAE4AQAAFAA4AJgB+AP8BMQFTAXgBkgLHAskC3QOUA6kDvAPAIBAgFCAaIB4gIiAmIDAg" +
+ "OiBEISIhJiICIgYiDyISIhoiHiIrIkgiYCJlIvIlyvAC//8AAAAgACgAoAExAVIBeAGSAsYCyQLY" +
+ "A5QDqQO8A8AgECATIBggHCAgICYgMCA5IEQhIiEmIgIiBiIPIhEiGSIeIisiSCJgImQi8iXK8AD/" +
+ "///j/+IAAP+B/3z/WP8/AAD97AAA/T79KvzT/RTf/+DCAADgvOC74Ljgr+Cn4J7fwd+t3uLezN7W" +
+ "AAAAAN7K3r7epd6K3ofd+9skEO8AAQAAAAAASgAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAP4A" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAOwA7gAAAAAAAAAAAAAAAAAAAAAAAACZAJUAggCDAKEAjgC9" +
+ "AIQAigCIAJAAlwCWAMQAhwC1AIEAjQDHAMgAiQCPAIUAogC5AMYAkQCYAMoAyQDLAJQAmgClAKMA" +
+ "mwBhAGIAiwBjAKcAZACkAKYAqwCoAKkAqgC+AGUArgCsAK0AnABmAMUAjACxAK8AsABnAMAAwgCG" +
+ "AGkAaABqAGwAawBtAJIAbgBwAG8AcQByAHQAcwB1AHYAvwB3AHkAeAB6AHwAewCfAJMAfgB9AH8A" +
+ "gADBAMMAoACzALwAtgC3ALgAuwC0ALoAnQCeANcA5gDEAKIA5wAEAiYAAABOAEAABQAOACYAfgD/" +
+ "ATEBUwF4AZICxwLJAt0DlAOpA7wDwCAQIBQgGiAeICIgJiAwIDogRCEiISYiAiIGIg8iEiIaIh4i" +
+ "KyJIImAiZSLyJcrwAv//AAAAIAAoAKABMQFSAXgBkgLGAskC2AOUA6kDvAPAIBAgEyAYIBwgICAm" +
+ "IDAgOSBEISIhJiICIgYiDyIRIhkiHiIrIkgiYCJkIvIlyvAA////4//iAAD/gf98/1j/PwAA/ewA" +
+ "AP0+/Sr80/0U3//gwgAA4Lzgu+C44K/gp+Ce38Hfrd7i3sze1gAAAADeyt6+3qXeit6H3fvbJBDv" +
+ "AAEAAAAAAEoAAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAD+AAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AADsAO4AAAAAAAAAAAAAAAAAAAAAAAAAmQCVAIIAgwChAI4AvQCEAIoAiACQAJcAlgDEAIcAtQCB" +
+ "AI0AxwDIAIkAjwCFAKIAuQDGAJEAmADKAMkAywCUAJoApQCjAJsAYQBiAIsAYwCnAGQApACmAKsA" +
+ "qACpAKoAvgBlAK4ArACtAJwAZgDFAIwAsQCvALAAZwDAAMIAhgBpAGgAagBsAGsAbQCSAG4AcABv" +
+ "AHEAcgB0AHMAdQB2AL8AdwB5AHgAegB8AHsAnwCTAH4AfQB/AIAAwQDDAKAAswC8ALYAtwC4ALsA" +
+ "tAC6AJ0AngDXAOYAxACiAOcAAAACAH0AAANrAyAAAwAHAAAzESERJSERIX0C7v2PAfT+DAMg/OB9" +
+ "AiYAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" +
+ "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" +
+ "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" +
+ "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" +
+ "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" +
+ "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" +
+ "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" +
+ "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" +
+ "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" +
+ "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPo" +
+ "AyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gD" +
+ "IAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMg" +
+ "AAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAA" +
+ "AwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAAD" +
+ "AAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMA" +
+ "ABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAA" +
+ "ESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAAR" +
+ "IREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEh" +
+ "ESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESER" +
+ "IQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREh" +
+ "A+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED" +
+ "6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo" +
+ "/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8" +
+ "GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwY" +
+ "AyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgD" +
+ "IPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg" +
+ "/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8" +
+ "GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwY" +
+ "AAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" +
+ "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" +
+ "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" +
+ "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" +
+ "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" +
+ "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" +
+ "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" +
+ "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" +
+ "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" +
+ "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPo" +
+ "AyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AAAAAMAADEhFSED6PwYyAAAAQAA/zgD6AMgAAMA" +
+ "ABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAA" +
+ "ESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAAR" +
+ "IREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEh" +
+ "ESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESER" +
+ "IQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREh" +
+ "A+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED" +
+ "6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo" +
+ "/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8" +
+ "GAMg/BgAAAABAAAAAAPoAyAAAwAAESERIQPo/BgDIPzgAAAAAQAA/zgD6AMgAAMAABEhESED6PwY" +
+ "AyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgD" +
+ "IPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg" +
+ "/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8" +
+ "GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwY" +
+ "AAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" +
+ "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" +
+ "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" +
+ "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" +
+ "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" +
+ "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" +
+ "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" +
+ "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" +
+ "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" +
+ "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPo" +
+ "AyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gD" +
+ "IAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMg" +
+ "AAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAA" +
+ "AwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAAD" +
+ "AAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMA" +
+ "ABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAA" +
+ "ESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAAR" +
+ "IREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEh" +
+ "ESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESER" +
+ "IQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREh" +
+ "A+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED" +
+ "6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo" +
+ "/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8" +
+ "GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwY" +
+ "AyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgD" +
+ "IPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg" +
+ "/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8" +
+ "GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwY" +
+ "AAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" +
+ "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" +
+ "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" +
+ "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" +
+ "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" +
+ "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" +
+ "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" +
+ "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" +
+ "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" +
+ "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPo" +
+ "AyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gD" +
+ "IAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMg" +
+ "AAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAA" +
+ "AwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAAD" +
+ "AAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMA" +
+ "ABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAA" +
+ "ESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAAR" +
+ "IREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEh" +
+ "ESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESER" +
+ "IQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREh" +
+ "A+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED" +
+ "6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo" +
+ "/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8" +
+ "GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwY" +
+ "AyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgD" +
+ "IPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg" +
+ "/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8" +
+ "GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwY" +
+ "AAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgA" +
+ "AAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAA" +
+ "AAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAA" +
+ "AQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAB" +
+ "AAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEA" +
+ "AP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA" +
+ "/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/" +
+ "OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84" +
+ "A+gDIAADAAARIREhA+j8GAMg/BgAAAABAAD/OAPoAyAAAwAAESERIQPo/BgDIPwYAAAAAQAA/zgD" +
+ "6AMgAAMAABEhESED6PwYAyD8GAAAAAEAAP84A+gDIAADAAARIREhA+j8GAMg/BgAAAAAABQAFAAU" +
+ "ABQAIgAwAD4ATABaAGgAdgCEAJIAoACuALwAygDYAOYA9AECARABHgEsAToBSAFWAWQBcgGAAY4B" +
+ "nAGqAbgBxgHUAeIB8AH+AgwCGgIoAjYCRAJSAmACbgJ8AooCmAKmArQCwgLQAt4C7AL6AwgDFgMk" +
+ "AzIDQANOA1wDagN4A4YDlAOiA7ADvgPMA9oD6AP2BAQEEgQgBC4EPARKBFgEZARyBIAEjgScBKoE" +
+ "uATGBNQE4gTwBP4FDAUaBSgFNgVEBVIFYAVuBXwFigWYBaYFtAXCBdAF3gXsBfoGCAYWBiQGMgZA" +
+ "Bk4GXAZqBngGhgaUBqIGsAa+BswG2gboBvYHBAcSByAHLgc8B0oHWAdmB3QHggeQB54HrAe6B8gH" +
+ "1gfkB/IIAAgOCBwIKgg4CDgIRghUCGIIcAh+CIwImgioCLYIxAjSCOAI7gj8CQoJGAkmCTQJQglQ" +
+ "CV4JbAl6CYgJlgmkCbIJwAnOCdwJ6gn4CgYKFAoiCjAKPgpMCloKaAp2CoQKkgqgCq4KvArKCtgK" +
+ "5gr0CwILEAseCywLOgtIC1YLZAtyC4ALjgucC6oLuAvGC9QL4gvwC/4MDAwaDCgMNgxEDFIMYAxu" +
+ "DHwMigyYDKYMtAzCDNAM3gzsDPoNCA0WDSQNMgAAABsBSgAAAAAAAAAAAZ4AAAAAAAAAAAABAAgB" +
+ "ngAAAAAAAAACAA4BpgAAAAAAAAADACABtAAAAAAAAAAEAAgB1AAAAAAAAAAFABYB3AAAAAAAAAAG" +
+ "AAgB8gABAAAAAAAAAM8B+gABAAAAAAABAAQCyQABAAAAAAACAAcCzQABAAAAAAADABAC1AABAAAA" +
+ "AAAEAAQC5AABAAAAAAAFAAsC6AABAAAAAAAGAAQC8wABAAAAAAAQAAQC9wABAAAAAAARAAcC+wAB" +
+ "AAAAAAASAAQDAgADAAEECQAAAZ4DBgADAAEECQABAAgEpAADAAEECQACAA4ErAADAAEECQADACAE" +
+ "ugADAAEECQAEAAgE2gADAAEECQAFABYE4gADAAEECQAGAAgE+AADAAEECQAQAAgFAAADAAEECQAR" +
+ "AA4FCAADAAEECQASAAgFFgBNAG8AcwB0ACAAYwBoAGEAcgBhAGMAdABlAHIAcwAgAGEAcgBlACAA" +
+ "dABoAGUAIABlAG0AIABzAHEAdQBhAHIAZQAsACAAZQB4AGMAZQBwAHQAIAAmAEUAQQBjAHUAdABl" +
+ "ACAAYQBuAGQAIAAiAHAAIgAsACAAdwBoAGkAYwBoACAAcwBoAG8AdwAgAGEAcwBjAGUAbgB0AC8A" +
+ "ZABlAHMAYwBlAG4AdAAgAGYAcgBvAG0AIAB0AGgAZQAgAGIAYQBzAGUAbABpAG4AZQAuACAAVQBz" +
+ "AGUAZgB1AGwAIABmAG8AcgAgAHQAZQBzAHQAaQBuAGcAIABjAG8AbQBwAG8AcwBpAHQAaQBvAG4A" +
+ "IABzAHkAcwB0AGUAbQBzAC4AIABQAHIAbwBkAHUAYwBlAGQAIABiAHkAIABUAG8AZABkACAARgBh" +
+ "AGgAcgBuAGUAcgAgAGYAbwByACAAdABoAGUAIABDAFMAUwAgAFMAYQBtAHUAcgBhAGkAJwBzACAA" +
+ "YgByAG8AdwBzAGUAcgAgAHQAZQBzAHQAaQBuAGcALgBBAGgAZQBtAFIAZQBnAHUAbABhAHIAVgBl" +
+ "AHIAcwBpAG8AbgAgADEALgAxACAAQQBoAGUAbQBBAGgAZQBtAFYAZQByAHMAaQBvAG4AIAAxAC4A" +
+ "MQBBAGgAZQBtTW9zdCBjaGFyYWN0ZXJzIGFyZSB0aGUgZW0gc3F1YXJlLCBleGNlcHQgJkVBY3V0" +
+ "ZSBhbmQgInAiLCB3aGljaCBzaG93IGFzY2VudC9kZXNjZW50IGZyb20gdGhlIGJhc2VsaW5lLiBV" +
+ "c2VmdWwgZm9yIHRlc3RpbmcgY29tcG9zaXRpb24gc3lzdGVtcy4gUHJvZHVjZWQgYnkgVG9kZCBG" +
+ "YWhybmVyIGZvciB0aGUgQ1NTIFNhbXVyYWkncyBicm93c2VyIHRlc3RpbmcuQWhlbVJlZ3VsYXJW" +
+ "ZXJzaW9uIDEuMSBBaGVtQWhlbVZlcnNpb24gMS4xQWhlbUFoZW1SZWd1bGFyQWhlbQBNAG8AcwB0" +
+ "ACAAYwBoAGEAcgBhAGMAdABlAHIAcwAgAGEAcgBlACAAdABoAGUAIABlAG0AIABzAHEAdQBhAHIA" +
+ "ZQAsACAAZQB4AGMAZQBwAHQAIAAmAEUAQQBjAHUAdABlACAAYQBuAGQAIAAiAHAAIgAsACAAdwBo" +
+ "AGkAYwBoACAAcwBoAG8AdwAgAGEAcwBjAGUAbgB0AC8AZABlAHMAYwBlAG4AdAAgAGYAcgBvAG0A" +
+ "IAB0AGgAZQAgAGIAYQBzAGUAbABpAG4AZQAuACAAVQBzAGUAZgB1AGwAIABmAG8AcgAgAHQAZQBz" +
+ "AHQAaQBuAGcAIABjAG8AbQBwAG8AcwBpAHQAaQBvAG4AIABzAHkAcwB0AGUAbQBzAC4AIABQAHIA" +
+ "bwBkAHUAYwBlAGQAIABiAHkAIABUAG8AZABkACAARgBhAGgAcgBuAGUAcgAgAGYAbwByACAAdABo" +
+ "AGUAIABDAFMAUwAgAFMAYQBtAHUAcgBhAGkAJwBzACAAYgByAG8AdwBzAGUAcgAgAHQAZQBzAHQA" +
+ "aQBuAGcALgBBAGgAZQBtAFIAZQBnAHUAbABhAHIAVgBlAHIAcwBpAG8AbgAgADEALgAxACAAQQBo" +
+ "AGUAbQBBAGgAZQBtAFYAZQByAHMAaQBvAG4AIAAxAC4AMQBBAGgAZQBtAEEAaABlAG0AUgBlAGcA" +
+ "dQBsAGEAcgBBAGgAZQBtAAIAAAAAAAD/ewAUAAAAAQAAAAAAAAAAAAAAAAAAAAAA9QAAAQIAAgAD" +
+ "AAQABQAGAAcACAAJAAsADAANAA4ADwAQABEAEgATABQAFQAWABcAGAAZABoAGwAcAB0AHgAfACAA" +
+ "IQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAAxADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9" +
+ "AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0ATgBPAFAAUQBSAFMAVABVAFYAVwBYAFkA" +
+ "WgBbAFwAXQBeAF8AYABhAGIAYwBkAGUAZgBnAGgAaQBqAGsAbABtAG4AbwBwAHEAcgBzAHQAdQB2" +
+ "AHcAeAB5AHoAewB8AH0AfgB/AIAAgQCDAIQAhQCGAIgAiQCKAIsAjQCOAJAAkQCTAJYAlwCdAJ4A" +
+ "oAChAKIAowCkAKkAqgCsAK0ArgCvALYAtwC4ALoAvQDDAMcAyADJAMoAywDMAM0AzgDPANAA0QDT" +
+ "ANQA1QDWANcA2ADZANoA2wDcAN0A3gDfAOAA4QDoAOkA6gDrAOwA7QDuAO8A8ADxAPIA8wD0APUA" +
+ "9gAAAAAAsACxALsApgCoAJ8AmwCyALMAxAC0ALUAxQCCAMIAhwCrAMYAvgC/ALwAjACYAJoAmQCl" +
+ "AJIAnACPAJQAlQCnALkA0gDAAMEBAwACAQQETlVMTAJIVANERUwAAAADAAgAAgAQAAH//wAD"
+);
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Content-Type", "application/octet-stream", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.write(FONT_BYTES);
+}
diff --git a/accessible/tests/browser/e10s/head.js b/accessible/tests/browser/e10s/head.js
new file mode 100644
index 0000000000..bdbcb7445f
--- /dev/null
+++ b/accessible/tests/browser/e10s/head.js
@@ -0,0 +1,192 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* exported testCachedRelation, testRelated */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js and relations.js.
+/* import-globals-from ../../mochitest/relations.js */
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR },
+ { name: "relations.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test the accessible relation.
+ *
+ * @param identifier [in] identifier to get an accessible, may be ID
+ * attribute or DOM element or accessible object
+ * @param relType [in] relation type (see constants above)
+ * @param relatedIdentifiers [in] identifier or array of identifiers of
+ * expected related accessibles
+ */
+async function testCachedRelation(identifier, relType, relatedIdentifiers) {
+ const relDescr = getRelationErrorMsg(identifier, relType);
+ const relDescrStart = getRelationErrorMsg(identifier, relType, true);
+ info(`Testing ${relDescr}`);
+
+ if (!relatedIdentifiers) {
+ await untilCacheOk(function () {
+ let r = getRelationByType(identifier, relType);
+ if (r) {
+ info(`Fetched ${r.targetsCount} relations from cache`);
+ } else {
+ info("Could not fetch relations");
+ }
+ return r && !r.targetsCount;
+ }, relDescrStart + " has no targets, as expected");
+ return;
+ }
+
+ const relatedIds =
+ relatedIdentifiers instanceof Array
+ ? relatedIdentifiers
+ : [relatedIdentifiers];
+ await untilCacheOk(function () {
+ let r = getRelationByType(identifier, relType);
+ if (r) {
+ info(
+ `Fetched ${r.targetsCount} relations from cache, looking for ${relatedIds.length}`
+ );
+ } else {
+ info("Could not fetch relations");
+ }
+
+ return r && r.targetsCount == relatedIds.length;
+ }, "Found correct number of expected relations");
+
+ let targets = [];
+ for (let idx = 0; idx < relatedIds.length; idx++) {
+ targets.push(getAccessible(relatedIds[idx]));
+ }
+
+ if (targets.length != relatedIds.length) {
+ return;
+ }
+
+ await untilCacheOk(function () {
+ const relation = getRelationByType(identifier, relType);
+ const actualTargets = relation ? relation.getTargets() : null;
+ if (!actualTargets) {
+ info("Could not fetch relations");
+ return false;
+ }
+
+ // Check if all given related accessibles are targets of obtained relation.
+ for (let idx = 0; idx < targets.length; idx++) {
+ let isFound = false;
+ for (let relatedAcc of actualTargets.enumerate(Ci.nsIAccessible)) {
+ if (targets[idx] == relatedAcc) {
+ isFound = true;
+ break;
+ }
+ }
+
+ if (!isFound) {
+ info(
+ prettyName(relatedIds[idx]) +
+ " could not be found in relation: " +
+ relDescr
+ );
+ return false;
+ }
+ }
+
+ return true;
+ }, "All given related accessibles are targets of fetched relation.");
+
+ await untilCacheOk(function () {
+ const relation = getRelationByType(identifier, relType);
+ const actualTargets = relation ? relation.getTargets() : null;
+ if (!actualTargets) {
+ info("Could not fetch relations");
+ return false;
+ }
+
+ // Check if all obtained targets are given related accessibles.
+ for (let relatedAcc of actualTargets.enumerate(Ci.nsIAccessible)) {
+ let wasFound = false;
+ for (let idx = 0; idx < targets.length; idx++) {
+ if (relatedAcc == targets[idx]) {
+ wasFound = true;
+ }
+ }
+ if (!wasFound) {
+ info(
+ prettyName(relatedAcc) +
+ " was found, but shouldn't be in relation: " +
+ relDescr
+ );
+ return false;
+ }
+ }
+ return true;
+ }, "No unexpected targets found.");
+}
+
+async function testRelated(
+ browser,
+ accDoc,
+ attr,
+ hostRelation,
+ dependantRelation
+) {
+ let host = findAccessibleChildByID(accDoc, "host");
+ let dependant1 = findAccessibleChildByID(accDoc, "dependant1");
+ let dependant2 = findAccessibleChildByID(accDoc, "dependant2");
+
+ /**
+ * Test data has the format of:
+ * {
+ * desc {String} description for better logging
+ * attrs {?Array} an optional list of attributes to update
+ * expected {Array} expected relation values for dependant1, dependant2
+ * and host respectively.
+ * }
+ */
+ const tests = [
+ {
+ desc: "No attribute",
+ expected: [null, null, null],
+ },
+ {
+ desc: "Set attribute",
+ attrs: [{ key: attr, value: "dependant1" }],
+ expected: [host, null, dependant1],
+ },
+ {
+ desc: "Change attribute",
+ attrs: [{ key: attr, value: "dependant2" }],
+ expected: [null, host, dependant2],
+ },
+ {
+ desc: "Remove attribute",
+ attrs: [{ key: attr }],
+ expected: [null, null, null],
+ },
+ ];
+
+ for (let { desc, attrs, expected } of tests) {
+ info(desc);
+
+ if (attrs) {
+ for (let { key, value } of attrs) {
+ await invokeSetAttribute(browser, "host", key, value);
+ }
+ }
+
+ await testCachedRelation(dependant1, dependantRelation, expected[0]);
+ await testCachedRelation(dependant2, dependantRelation, expected[1]);
+ await testCachedRelation(host, hostRelation, expected[2]);
+ }
+}
diff --git a/accessible/tests/browser/events/browser.toml b/accessible/tests/browser/events/browser.toml
new file mode 100644
index 0000000000..7ec3c3621a
--- /dev/null
+++ b/accessible/tests/browser/events/browser.toml
@@ -0,0 +1,33 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/browser/*.jsm",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_alert.js"]
+
+["browser_test_A11yUtils_announce.js"]
+
+["browser_test_caret_move_granularity.js"]
+
+["browser_test_docload.js"]
+skip-if = ["true"]
+
+["browser_test_focus_browserui.js"]
+
+["browser_test_focus_dialog.js"]
+
+["browser_test_focus_urlbar.js"]
+skip-if = ["os == 'win'"] # Bug 1818994
+
+["browser_test_panel.js"]
+
+["browser_test_scrolling.js"]
+
+["browser_test_selection_urlbar.js"]
+
+["browser_test_textcaret.js"]
diff --git a/accessible/tests/browser/events/browser_alert.js b/accessible/tests/browser/events/browser_alert.js
new file mode 100644
index 0000000000..f35a602fa3
--- /dev/null
+++ b/accessible/tests/browser/events/browser_alert.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that alert events aren't fired when reflow happens but no actual
+ * insertion occurs.
+ */
+addAccessibleTask(
+ `
+<div id="alert" role="alert">
+ <div id="content" hidden>content</div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const alert = findAccessibleChildByID(docAcc, "alert");
+ info("Showing content");
+ await contentSpawnMutation(
+ browser,
+ { expected: [[EVENT_ALERT, alert]] },
+ () => {
+ content.document.getElementById("content").hidden = false;
+ }
+ );
+ info("Changing content display style and removing text");
+ const content = findAccessibleChildByID(docAcc, "content");
+ await contentSpawnMutation(
+ browser,
+ {
+ expected: [[EVENT_REORDER, content]],
+ unexpected: [[EVENT_ALERT, alert]],
+ },
+ () => {
+ const node = content.document.getElementById("content");
+ node.textContent = "";
+ // This causes the node's layout frame to be reconstructed. This in
+ // turn causes a11y to queue it as an insertion in case there were
+ // changes. Because it already has an Accessible, This node is skipped
+ // when processing insertions, so we should not fire an alert event.
+ node.style.display = "flex";
+ }
+ );
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/events/browser_test_A11yUtils_announce.js b/accessible/tests/browser/events/browser_test_A11yUtils_announce.js
new file mode 100644
index 0000000000..b2848f35c2
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_A11yUtils_announce.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+// Check that the browser A11yUtils.announce() function works correctly.
+// Note that this does not use mozilla::a11y::Accessible::Announce and a11y
+// announcement events, as these aren't yet supported on desktop.
+async function runTests() {
+ const alert = document.getElementById("a11y-announcement");
+ let alerted = waitForEvent(EVENT_ALERT, alert);
+ A11yUtils.announce({ raw: "first" });
+ let event = await alerted;
+ const alertAcc = event.accessible;
+ is(alertAcc.role, ROLE_ALERT);
+ ok(!alertAcc.name);
+ is(alertAcc.childCount, 1);
+ is(alertAcc.firstChild.name, "first");
+
+ alerted = waitForEvent(EVENT_ALERT, alertAcc);
+ A11yUtils.announce({ raw: "second" });
+ event = await alerted;
+ ok(!alertAcc.name);
+ is(alertAcc.childCount, 1);
+ is(alertAcc.firstChild.name, "second");
+
+ info("Testing Fluent message");
+ // We need a simple Fluent message here without arguments or attributes.
+ const fluentId = "search-one-offs-with-title";
+ const fluentMessage = await document.l10n.formatValue(fluentId);
+ alerted = waitForEvent(EVENT_ALERT, alertAcc);
+ A11yUtils.announce({ id: fluentId });
+ event = await alerted;
+ ok(!alertAcc.name);
+ is(alertAcc.childCount, 1);
+ is(alertAcc.firstChild.name, fluentMessage);
+
+ info("Ensuring Fluent message is cancelled if announce is re-entered");
+ alerted = waitForEvent(EVENT_ALERT, alertAcc);
+ // This call runs async.
+ let asyncAnnounce = A11yUtils.announce({ id: fluentId });
+ // Before the async call finishes, call announce again.
+ A11yUtils.announce({ raw: "third" });
+ // Wait for the async call to complete.
+ await asyncAnnounce;
+ event = await alerted;
+ ok(!alertAcc.name);
+ is(alertAcc.childCount, 1);
+ // The async call should have been cancelled. If it wasn't, we would get
+ // fluentMessage here instead of "third".
+ is(alertAcc.firstChild.name, "third");
+}
+
+addAccessibleTask(``, runTests);
diff --git a/accessible/tests/browser/events/browser_test_caret_move_granularity.js b/accessible/tests/browser/events/browser_test_caret_move_granularity.js
new file mode 100644
index 0000000000..c72ae42d85
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_caret_move_granularity.js
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const CLUSTER_AMOUNT = Ci.nsISelectionListener.CLUSTER_AMOUNT;
+const WORD_AMOUNT = Ci.nsISelectionListener.WORD_AMOUNT;
+const LINE_AMOUNT = Ci.nsISelectionListener.LINE_AMOUNT;
+const BEGINLINE_AMOUNT = Ci.nsISelectionListener.BEGINLINE_AMOUNT;
+const ENDLINE_AMOUNT = Ci.nsISelectionListener.ENDLINE_AMOUNT;
+
+const isMac = AppConstants.platform == "macosx";
+
+function matchCaretMoveEvent(id, caretOffset) {
+ return evt => {
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ return (
+ getAccessibleDOMNodeID(evt.accessible) == id &&
+ evt.caretOffset == caretOffset
+ );
+ };
+}
+
+addAccessibleTask(
+ `<textarea id="textarea" style="scrollbar-width: none;" cols="15">` +
+ `one two three four five six seven eight` +
+ `</textarea>`,
+ async function (browser, accDoc) {
+ const textarea = findAccessibleChildByID(accDoc, "textarea");
+ let caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ matchCaretMoveEvent("textarea", 0)
+ );
+ textarea.takeFocus();
+ let evt = await caretMoved;
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+
+ caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ matchCaretMoveEvent("textarea", 1)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ is(evt.granularity, CLUSTER_AMOUNT, "Caret moved by cluster");
+
+ caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ matchCaretMoveEvent("textarea", 15)
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ evt = await caretMoved;
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ todo(!evt.isAtEndOfLine, "Caret is not at end of line");
+ is(evt.granularity, LINE_AMOUNT, "Caret moved by line");
+
+ caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ matchCaretMoveEvent("textarea", 14)
+ );
+ if (isMac) {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true });
+ } else {
+ EventUtils.synthesizeKey("KEY_Home");
+ }
+ evt = await caretMoved;
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ is(evt.granularity, BEGINLINE_AMOUNT, "Caret moved to line start");
+
+ caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ matchCaretMoveEvent("textarea", 28)
+ );
+ if (isMac) {
+ EventUtils.synthesizeKey("KEY_ArrowRight", { metaKey: true });
+ } else {
+ EventUtils.synthesizeKey("KEY_End");
+ }
+ evt = await caretMoved;
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(evt.isAtEndOfLine, "Caret is at end of line");
+ is(evt.granularity, ENDLINE_AMOUNT, "Caret moved to line end");
+
+ caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ matchCaretMoveEvent("textarea", 24)
+ );
+ if (isMac) {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { altKey: true });
+ } else {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { ctrlKey: true });
+ }
+ evt = await caretMoved;
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ is(evt.granularity, WORD_AMOUNT, "Caret moved by word");
+ }
+);
diff --git a/accessible/tests/browser/events/browser_test_docload.js b/accessible/tests/browser/events/browser_test_docload.js
new file mode 100644
index 0000000000..78ac77fd8c
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_docload.js
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function busyChecker(isBusy) {
+ return function (event) {
+ let scEvent;
+ try {
+ scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent);
+ } catch (e) {
+ return false;
+ }
+
+ return scEvent.state == STATE_BUSY && scEvent.isEnabled == isBusy;
+ };
+}
+
+function inIframeChecker(iframeId) {
+ return function (event) {
+ return getAccessibleDOMNodeID(event.accessibleDocument.parent) == iframeId;
+ };
+}
+
+function urlChecker(url) {
+ return function (event) {
+ info(`${event.accessibleDocument.URL} == ${url}`);
+ return event.accessibleDocument.URL == url;
+ };
+}
+
+async function runTests(browser, accDoc) {
+ let onLoadEvents = waitForEvents({
+ expected: [
+ [EVENT_REORDER, getAccessible(browser)],
+ [EVENT_DOCUMENT_LOAD_COMPLETE, "body2"],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ ],
+ unexpected: [
+ [EVENT_DOCUMENT_LOAD_COMPLETE, inIframeChecker("iframe1")],
+ [EVENT_STATE_CHANGE, inIframeChecker("iframe1")],
+ ],
+ });
+
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ `data:text/html;charset=utf-8,
+ <html><body id="body2">
+ <iframe id="iframe1" src="http://example.com"></iframe>
+ </body></html>`
+ );
+
+ await onLoadEvents;
+
+ onLoadEvents = waitForEvents([
+ [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("about:about")],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ [EVENT_REORDER, getAccessible(browser)],
+ ]);
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:about");
+
+ await onLoadEvents;
+
+ onLoadEvents = waitForEvents([
+ [EVENT_DOCUMENT_RELOAD, evt => evt.isFromUserInput],
+ [EVENT_REORDER, getAccessible(browser)],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ ]);
+
+ EventUtils.synthesizeKey("VK_F5", {}, browser.ownerGlobal);
+
+ await onLoadEvents;
+
+ onLoadEvents = waitForEvents([
+ [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("about:mozilla")],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ [EVENT_REORDER, getAccessible(browser)],
+ ]);
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:mozilla");
+
+ await onLoadEvents;
+
+ onLoadEvents = waitForEvents([
+ [EVENT_DOCUMENT_RELOAD, evt => !evt.isFromUserInput],
+ [EVENT_REORDER, getAccessible(browser)],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ ]);
+
+ browser.reload();
+
+ await onLoadEvents;
+
+ onLoadEvents = waitForEvents([
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("http://www.wronguri.wronguri/")],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ [EVENT_REORDER, getAccessible(browser)],
+ ]);
+
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.wronguri.wronguri/"
+ );
+
+ await onLoadEvents;
+
+ onLoadEvents = waitForEvents([
+ [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("https://nocert.example.com/")],
+ [EVENT_STATE_CHANGE, busyChecker(false)],
+ [EVENT_REORDER, getAccessible(browser)],
+ ]);
+
+ BrowserTestUtils.startLoadingURIString(
+ browser,
+ "https://nocert.example.com:443/"
+ );
+
+ await onLoadEvents;
+}
+
+/**
+ * Test caching of accessible object states
+ */
+addAccessibleTask("", runTests);
diff --git a/accessible/tests/browser/events/browser_test_focus_browserui.js b/accessible/tests/browser/events/browser_test_focus_browserui.js
new file mode 100644
index 0000000000..969d336c74
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_focus_browserui.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts(
+ { name: "states.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR }
+);
+
+async function runTests(browser, accDoc) {
+ await SpecialPowers.pushPrefEnv({
+ // If Fission is disabled, the pref is no-op.
+ set: [["fission.bfcacheInParent", true]],
+ });
+
+ let onFocus = waitForEvent(EVENT_FOCUS, "input");
+ EventUtils.synthesizeKey("VK_TAB", {}, browser.ownerGlobal);
+ let evt = await onFocus;
+ testStates(evt.accessible, STATE_FOCUSED);
+
+ onFocus = waitForEvent(EVENT_FOCUS, "buttonInputDoc");
+ let url = snippetToURL(`<input id="input" type="button" value="button">`, {
+ contentDocBodyAttrs: { id: "buttonInputDoc" },
+ });
+ browser.loadURI(Services.io.newURI(url), {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ evt = await onFocus;
+ testStates(evt.accessible, STATE_FOCUSED);
+
+ onFocus = waitForEvent(EVENT_FOCUS, "input");
+ browser.goBack();
+ evt = await onFocus;
+ testStates(evt.accessible, STATE_FOCUSED);
+
+ onFocus = waitForEvent(
+ EVENT_FOCUS,
+ event => event.accessible.DOMNode == gURLBar.inputField
+ );
+ EventUtils.synthesizeKey("t", { accelKey: true }, browser.ownerGlobal);
+ evt = await onFocus;
+ testStates(evt.accessible, STATE_FOCUSED);
+
+ onFocus = waitForEvent(EVENT_FOCUS, "input");
+ EventUtils.synthesizeKey("w", { accelKey: true }, browser.ownerGlobal);
+ evt = await onFocus;
+ testStates(evt.accessible, STATE_FOCUSED);
+}
+
+/**
+ * Accessibility loading document events test.
+ */
+addAccessibleTask(`<input id="input">`, runTests);
diff --git a/accessible/tests/browser/events/browser_test_focus_dialog.js b/accessible/tests/browser/events/browser_test_focus_dialog.js
new file mode 100644
index 0000000000..71485a678d
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_focus_dialog.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts(
+ { name: "states.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR }
+);
+
+async function runTests(browser, accDoc) {
+ let onFocus = waitForEvent(EVENT_FOCUS, "button");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("button").focus();
+ });
+ let button = (await onFocus).accessible;
+ testStates(button, STATE_FOCUSED);
+
+ // Bug 1377942 - The target of the focus event changes under different
+ // circumstances.
+ // In e10s the focus event is the new window, in non-e10s it's the doc.
+ onFocus = waitForEvent(EVENT_FOCUS, () => true);
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ // button should be blurred
+ await onFocus;
+ testStates(button, 0, 0, STATE_FOCUSED);
+
+ onFocus = waitForEvent(EVENT_FOCUS, "button");
+ await BrowserTestUtils.closeWindow(newWin);
+ testStates((await onFocus).accessible, STATE_FOCUSED);
+
+ onFocus = waitForEvent(EVENT_FOCUS, "body2");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("editabledoc")
+ .contentWindow.document.body.focus();
+ });
+ testStates((await onFocus).accessible, STATE_FOCUSED);
+
+ onFocus = waitForEvent(EVENT_FOCUS, "body2");
+ newWin = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.closeWindow(newWin);
+ testStates((await onFocus).accessible, STATE_FOCUSED);
+
+ let onShow = waitForEvent(EVENT_SHOW, "alertdialog");
+ onFocus = waitForEvent(EVENT_FOCUS, "alertdialog");
+ await SpecialPowers.spawn(browser, [], () => {
+ let alertDialog = content.document.getElementById("alertdialog");
+ alertDialog.style.display = "block";
+ alertDialog.focus();
+ });
+ await onShow;
+ testStates((await onFocus).accessible, STATE_FOCUSED);
+}
+
+/**
+ * Accessible dialog focus testing
+ */
+addAccessibleTask(
+ `
+ <button id="button">button</button>
+ <iframe id="editabledoc"
+ src="${snippetToURL("", {
+ contentDocBodyAttrs: { id: "body2", contentEditable: "true" },
+ })}">
+ </iframe>
+ <div id="alertdialog" style="display: none" tabindex="-1" role="alertdialog" aria-labelledby="title2" aria-describedby="desc2">
+ <div id="title2">Blah blah</div>
+ <div id="desc2">Woof woof woof.</div>
+ <button>Close</button>
+ </div>`,
+ runTests
+);
diff --git a/accessible/tests/browser/events/browser_test_focus_urlbar.js b/accessible/tests/browser/events/browser_test_focus_urlbar.js
new file mode 100644
index 0000000000..68b2b07f3c
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_focus_urlbar.js
@@ -0,0 +1,438 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts(
+ { name: "states.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR }
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ UrlbarProvider: "resource:///modules/UrlbarUtils.sys.mjs",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+});
+
+function isEventForAutocompleteItem(event) {
+ return event.accessible.role == ROLE_COMBOBOX_OPTION;
+}
+
+function isEventForButton(event) {
+ return event.accessible.role == ROLE_PUSHBUTTON;
+}
+
+function isEventForOneOffEngine(event) {
+ let parent = event.accessible.parent;
+ return (
+ event.accessible.role == ROLE_PUSHBUTTON &&
+ parent &&
+ parent.role == ROLE_GROUPING &&
+ parent.name
+ );
+}
+
+function isEventForMenuPopup(event) {
+ return event.accessible.role == ROLE_MENUPOPUP;
+}
+
+function isEventForMenuItem(event) {
+ return event.accessible.role == ROLE_MENUITEM;
+}
+
+function isEventForResultButton(event) {
+ let parent = event.accessible.parent;
+ return (
+ event.accessible.role == ROLE_PUSHBUTTON &&
+ parent?.role == ROLE_COMBOBOX_LIST
+ );
+}
+
+/**
+ * A test provider.
+ */
+class TipTestProvider extends UrlbarProvider {
+ constructor(matches) {
+ super();
+ this._matches = matches;
+ }
+ get name() {
+ return "TipTestProvider";
+ }
+ get type() {
+ return UrlbarUtils.PROVIDER_TYPE.PROFILE;
+ }
+ isActive(context) {
+ return true;
+ }
+ isRestricting(context) {
+ return true;
+ }
+ async startQuery(context, addCallback) {
+ this._context = context;
+ for (const match of this._matches) {
+ addCallback(this, match);
+ }
+ }
+}
+
+// Check that the URL bar manages accessibility focus appropriately.
+async function runTests() {
+ registerCleanupFunction(async function () {
+ await UrlbarTestUtils.promisePopupClose(window);
+ await PlacesUtils.history.clear();
+ });
+
+ await PlacesTestUtils.addVisits([
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example1.com/blah",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example2.com/blah",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example1.com/",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example2.com/",
+ ]);
+
+ // Ensure initial state.
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ let focused = waitForEvent(
+ EVENT_FOCUS,
+ event => event.accessible.role == ROLE_ENTRY
+ );
+ gURLBar.focus();
+ let event = await focused;
+ let textBox = event.accessible;
+ // Ensure the URL bar is ready for a new URL to be typed.
+ // Sometimes, when this test runs, the existing text isn't selected when the
+ // URL bar is focused. Pressing escape twice ensures that the popup is
+ // closed and that the existing text is selected.
+ EventUtils.synthesizeKey("KEY_Escape");
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ info("Ensuring no focus change when first text is typed");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "example",
+ fireInputEvent: true,
+ });
+ // Wait a tick for a11y events to fire.
+ await TestUtils.waitForTick();
+ testStates(textBox, STATE_FOCUSED);
+
+ info("Ensuring no focus change on backspace");
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ // Wait a tick for a11y events to fire.
+ await TestUtils.waitForTick();
+ testStates(textBox, STATE_FOCUSED);
+
+ info("Ensuring no focus change on text selection and delete");
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ EventUtils.synthesizeKey("KEY_Delete");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ // Wait a tick for a11y events to fire.
+ await TestUtils.waitForTick();
+ testStates(textBox, STATE_FOCUSED);
+
+ info("Ensuring autocomplete focus on down arrow (1)");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring focus of another autocomplete item on down arrow");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring previous arrow selection state doesn't get stale on input");
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ EventUtils.sendString("z");
+ await focused;
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ testStates(textBox, STATE_FOCUSED);
+
+ info("Ensuring focus of another autocomplete item on down arrow");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ if (AppConstants.platform == "macosx") {
+ info("Ensuring focus of another autocomplete item on ctrl-n");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring focus of another autocomplete item on ctrl-p");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("p", { ctrlKey: true });
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+ }
+
+ info("Ensuring focus of another autocomplete item on up arrow");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring text box focus on left arrow");
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ await focused;
+ testStates(textBox, STATE_FOCUSED);
+
+ gURLBar.view.close();
+ // On Mac, down arrow when not at the end of the field moves to the end.
+ // Move back to the end so the next press of down arrow opens the popup.
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+
+ info("Ensuring autocomplete focus on down arrow (2)");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring autocomplete focus on arrow up for search settings button");
+ focused = waitForEvent(EVENT_FOCUS, isEventForButton);
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring text box focus when text is typed");
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ EventUtils.sendString("z");
+ await focused;
+ testStates(textBox, STATE_FOCUSED);
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ info("Ensuring autocomplete focus on down arrow (3)");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring text box focus on backspace");
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await focused;
+ testStates(textBox, STATE_FOCUSED);
+ await UrlbarTestUtils.promiseSearchComplete(window);
+
+ info("Ensuring autocomplete focus on arrow down (4)");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ // Arrow down to the last result.
+ const resultCount = UrlbarTestUtils.getResultCount(window);
+ while (UrlbarTestUtils.getSelectedRowIndex(window) != resultCount - 1) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ info("Ensuring one-off search button focus on arrow down");
+ focused = waitForEvent(EVENT_FOCUS, isEventForOneOffEngine);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring autocomplete focus on arrow up");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring text box focus on text selection");
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ await focused;
+ testStates(textBox, STATE_FOCUSED);
+
+ if (AppConstants.platform == "macosx") {
+ // On Mac, ctrl-n after arrow left/right does not re-open the popup.
+ // Type some text so the next press of ctrl-n opens the popup.
+ EventUtils.sendString("ple");
+
+ info("Ensuring autocomplete focus on ctrl-n");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("n", { ctrlKey: true });
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+ }
+
+ if (
+ AppConstants.platform == "macosx" &&
+ Services.prefs.getBoolPref("widget.macos.native-context-menus", false)
+ ) {
+ // With native context menus, we do not observe accessibility events and we
+ // cannot send synthetic key events to the menu.
+ info("Opening and closing context native context menu");
+ let contextMenu = gURLBar.querySelector("menupopup");
+ let popupshown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gURLBar.querySelector("moz-input-box"), {
+ type: "contextmenu",
+ });
+ await popupshown;
+ let popuphidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.hidePopup();
+ await popuphidden;
+ } else {
+ info(
+ "Ensuring context menu gets menu event on launch, and item focus on down"
+ );
+ let menuEvent = waitForEvent(
+ nsIAccessibleEvent.EVENT_MENUPOPUP_START,
+ isEventForMenuPopup
+ );
+ EventUtils.synthesizeMouseAtCenter(gURLBar.querySelector("moz-input-box"), {
+ type: "contextmenu",
+ });
+ await menuEvent;
+
+ focused = waitForEvent(EVENT_FOCUS, isEventForMenuItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ let closed = waitForEvent(
+ nsIAccessibleEvent.EVENT_MENUPOPUP_END,
+ isEventForMenuPopup
+ );
+ EventUtils.synthesizeKey("KEY_Escape");
+ await closed;
+ await focused;
+ }
+ info("Ensuring address bar is focused after context menu is dismissed.");
+ testStates(textBox, STATE_FOCUSED);
+}
+
+// We test TIP results in their own test so the spoofed results don't interfere
+// with the main test.
+async function runTipTests() {
+ let matches = [
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ { url: "http://mozilla.org/a" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TIP,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ helpUrl: "http://example.com/",
+ type: "test",
+ titleL10n: { id: "urlbar-search-tips-confirm" },
+ buttons: [
+ {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: "http://example.com/",
+ l10n: { id: "urlbar-search-tips-confirm" },
+ },
+ ],
+ }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ { url: "http://mozilla.org/b" }
+ ),
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.HISTORY,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ { url: "http://mozilla.org/c" }
+ ),
+ ];
+
+ // Ensure the tip appears in the expected position.
+ matches[1].suggestedIndex = 2;
+
+ let provider = new TipTestProvider(matches);
+ UrlbarProvidersManager.registerProvider(provider);
+
+ registerCleanupFunction(async function () {
+ UrlbarProvidersManager.unregisterProvider(provider);
+ });
+
+ let focused = waitForEvent(
+ EVENT_FOCUS,
+ event => event.accessible.role == ROLE_ENTRY
+ );
+ gURLBar.focus();
+ let event = await focused;
+ let textBox = event.accessible;
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ info("Ensuring no focus change when first text is typed");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "example",
+ fireInputEvent: true,
+ });
+ // Wait a tick for a11y events to fire.
+ await TestUtils.waitForTick();
+ testStates(textBox, STATE_FOCUSED);
+
+ info("Ensuring autocomplete focus on down arrow (1)");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring the tip button is focused on down arrow");
+ info("Also ensuring that the tip button is a part of a labelled group");
+ focused = waitForEvent(EVENT_FOCUS, isEventForResultButton);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring the help button is focused on tab");
+ info("Also ensuring that the help button is a part of a labelled group");
+ focused = waitForEvent(EVENT_FOCUS, isEventForResultButton);
+ EventUtils.synthesizeKey("KEY_Tab");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring autocomplete focus on down arrow (2)");
+ focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring the help button is focused on shift+tab");
+ focused = waitForEvent(EVENT_FOCUS, isEventForResultButton);
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ event = await focused;
+ testStates(event.accessible, STATE_FOCUSED);
+
+ info("Ensuring text box focus on left arrow, and not back to the tip button");
+ focused = waitForEvent(EVENT_FOCUS, textBox);
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ await focused;
+ testStates(textBox, STATE_FOCUSED);
+}
+
+addAccessibleTask(``, runTests);
+addAccessibleTask(``, runTipTests);
diff --git a/accessible/tests/browser/events/browser_test_panel.js b/accessible/tests/browser/events/browser_test_panel.js
new file mode 100644
index 0000000000..8a4ab89705
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_panel.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+// Verify we receive hide and show notifications when the chrome
+// XUL alert is closed or opened. Mac expects both notifications to
+// properly communicate live region changes.
+async function runTests(browser) {
+ ok(PopupNotifications, "PopupNotifications object exists");
+ ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+ // When available, the popup panel makes itself a child of the chrome window.
+ // To verify it isn't accessible without reproducing the entirety of the chrome
+ // window tree, we check instead that the panel is not accessible.
+ ok(!isAccessible(PopupNotifications.panel), "Popup panel is not accessible");
+
+ const panelShown = waitForEvent(EVENT_SHOW, PopupNotifications.panel);
+ const notification = PopupNotifications.show(
+ browser,
+ "test-notification",
+ "hello world",
+ PopupNotifications.panel.id
+ );
+
+ await panelShown;
+
+ ok(isAccessible(PopupNotifications.panel), "Popup panel is accessible");
+ testAccessibleTree(PopupNotifications.panel, {
+ ALERT: [
+ { LABEL: [{ TEXT_LEAF: [] }] },
+ { PUSHBUTTON: [] },
+ { PUSHBUTTON: [] },
+ ],
+ });
+ // Verify the popup panel is associated with the chrome window.
+ is(
+ PopupNotifications.panel.ownerGlobal,
+ getMainChromeWindow(window),
+ "Popup panel is associated with the chrome window"
+ );
+
+ const panelHidden = waitForEvent(EVENT_HIDE, PopupNotifications.panel);
+ PopupNotifications.remove(notification);
+
+ await panelHidden;
+
+ ok(!isAccessible(PopupNotifications.panel), "Popup panel is not accessible");
+}
+
+addAccessibleTask(``, runTests);
diff --git a/accessible/tests/browser/events/browser_test_scrolling.js b/accessible/tests/browser/events/browser_test_scrolling.js
new file mode 100644
index 0000000000..9678ee767b
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_scrolling.js
@@ -0,0 +1,153 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+addAccessibleTask(
+ `
+ <div style="height: 100vh" id="one">one</div>
+ <div style="height: 100vh" id="two">two</div>
+ <div style="height: 100vh; width: 200vw; overflow: auto;" id="three">
+ <div style="height: 300%;">three</div>
+ </div>
+ <textarea id="textarea" rows="1">a
+b
+c</textarea>
+ `,
+ async function (browser, accDoc) {
+ let onScrolling = waitForEvents([
+ [EVENT_SCROLLING, accDoc],
+ [EVENT_SCROLLING_END, accDoc],
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.location.hash = "#two";
+ });
+ let [scrollEvent1, scrollEndEvent1] = await onScrolling;
+ scrollEvent1.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEvent1.maxScrollY >= scrollEvent1.scrollY,
+ "scrollY is within max"
+ );
+ scrollEndEvent1.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEndEvent1.maxScrollY >= scrollEndEvent1.scrollY,
+ "scrollY is within max"
+ );
+
+ onScrolling = waitForEvents([
+ [EVENT_SCROLLING, accDoc],
+ [EVENT_SCROLLING_END, accDoc],
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.location.hash = "#three";
+ });
+ let [scrollEvent2, scrollEndEvent2] = await onScrolling;
+ scrollEvent2.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEvent2.scrollY > scrollEvent1.scrollY,
+ `${scrollEvent2.scrollY} > ${scrollEvent1.scrollY}`
+ );
+ scrollEndEvent2.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEndEvent2.maxScrollY >= scrollEndEvent2.scrollY,
+ "scrollY is within max"
+ );
+
+ onScrolling = waitForEvents([
+ [EVENT_SCROLLING, accDoc],
+ [EVENT_SCROLLING_END, accDoc],
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.scrollTo(10, 0);
+ });
+ let [scrollEvent3, scrollEndEvent3] = await onScrolling;
+ scrollEvent3.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEvent3.maxScrollX >= scrollEvent3.scrollX,
+ "scrollX is within max"
+ );
+ scrollEndEvent3.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEndEvent3.maxScrollX >= scrollEndEvent3.scrollX,
+ "scrollY is within max"
+ );
+ ok(
+ scrollEvent3.scrollX > scrollEvent2.scrollX,
+ `${scrollEvent3.scrollX} > ${scrollEvent2.scrollX}`
+ );
+
+ // non-doc scrolling
+ onScrolling = waitForEvents([
+ [EVENT_SCROLLING, "three"],
+ [EVENT_SCROLLING_END, "three"],
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("#three").scrollTo(0, 10);
+ });
+ let [scrollEvent4, scrollEndEvent4] = await onScrolling;
+ scrollEvent4.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEvent4.maxScrollY >= scrollEvent4.scrollY,
+ "scrollY is within max"
+ );
+ scrollEndEvent4.QueryInterface(nsIAccessibleScrollingEvent);
+ ok(
+ scrollEndEvent4.maxScrollY >= scrollEndEvent4.scrollY,
+ "scrollY is within max"
+ );
+
+ // textarea scrolling
+ info("Moving textarea caret to c");
+ onScrolling = waitForEvents([
+ [EVENT_SCROLLING, "textarea"],
+ [EVENT_SCROLLING_END, "textarea"],
+ ]);
+ await invokeContentTask(browser, [], () => {
+ const textareaDom = content.document.getElementById("textarea");
+ textareaDom.focus();
+ textareaDom.selectionStart = 4;
+ });
+ await onScrolling;
+ }
+);
+
+// Verify that the scrolling start event is fired for an anchor change.
+addAccessibleTask(
+ `
+ <p>a</p>
+ <p>b</p>
+ <p id="c">c</p>
+ `,
+ async function (browser, accDoc) {
+ let onScrollingStart = waitForEvent(EVENT_SCROLLING_START, "c");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.location.hash = "#c";
+ });
+ await onScrollingStart;
+ },
+ { chrome: true, topLevel: true }
+);
+
+// Ensure that a scrollable, focused non-interactive element receives a
+// scrolling start event when an anchor jump to that element is triggered.
+addAccessibleTask(
+ `
+<div style="height: 100vh; width: 100vw; overflow: auto;" id="scrollable">
+ <h1 style="height: 300%;" id="inside-scrollable">test</h1>
+</div>
+ `,
+ async function (browser, accDoc) {
+ let onScrollingStart = waitForEvent(
+ EVENT_SCROLLING_START,
+ "inside-scrollable"
+ );
+ await invokeContentTask(browser, [], () => {
+ const scrollable = content.document.getElementById("scrollable");
+ scrollable.focus();
+ content.location.hash = "#inside-scrollable";
+ });
+ await onScrollingStart;
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/events/browser_test_selection_urlbar.js b/accessible/tests/browser/events/browser_test_selection_urlbar.js
new file mode 100644
index 0000000000..8f8fdb92f7
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_selection_urlbar.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+// Check that the URL bar manages accessibility
+// selection notifications appropriately on startup (new window).
+async function runTests() {
+ let focused = waitForEvent(
+ EVENT_FOCUS,
+ event => event.accessible.role == ROLE_ENTRY
+ );
+ info("Creating new window");
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "addons",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: Services.io.newURI("http://www.addons.mozilla.org/"),
+ });
+
+ registerCleanupFunction(async function () {
+ await BrowserTestUtils.closeWindow(newWin);
+ await PlacesUtils.bookmarks.remove(bookmark);
+ });
+ info("Focusing window");
+ newWin.focus();
+ await focused;
+
+ // Ensure the URL bar is ready for a new URL to be typed.
+ // Sometimes, when this test runs, the existing text isn't selected when the
+ // URL bar is focused. Pressing escape twice ensures that the popup is
+ // closed and that the existing text is selected.
+ EventUtils.synthesizeKey("KEY_Escape", {}, newWin);
+ EventUtils.synthesizeKey("KEY_Escape", {}, newWin);
+ let caretMoved = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ event => event.accessible.role == ROLE_ENTRY
+ );
+
+ info("Autofilling after typing `a` in new window URL bar.");
+ EventUtils.synthesizeKey("a", {}, newWin);
+ await UrlbarTestUtils.promiseSearchComplete(newWin);
+ Assert.equal(
+ newWin.gURLBar.inputField.value,
+ "addons.mozilla.org/",
+ "autofilled value as expected"
+ );
+
+ info("Ensuring caret moved on text selection");
+ await caretMoved;
+}
+
+addAccessibleTask(``, runTests);
diff --git a/accessible/tests/browser/events/browser_test_textcaret.js b/accessible/tests/browser/events/browser_test_textcaret.js
new file mode 100644
index 0000000000..e0f6f1a176
--- /dev/null
+++ b/accessible/tests/browser/events/browser_test_textcaret.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Caret move events checker.
+ */
+function caretMoveChecker(target, caretOffset) {
+ return function (event) {
+ let cmEvent = event.QueryInterface(nsIAccessibleCaretMoveEvent);
+ return (
+ cmEvent.accessible == getAccessible(target) &&
+ cmEvent.caretOffset == caretOffset
+ );
+ };
+}
+
+async function checkURLBarCaretEvents() {
+ const kURL = "about:mozilla";
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.startLoadingURIString(newWin.gBrowser.selectedBrowser, kURL);
+ newWin.gBrowser.selectedBrowser.focus();
+
+ await waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, event => {
+ try {
+ return event.accessible.QueryInterface(nsIAccessibleDocument).URL == kURL;
+ } catch (e) {
+ return false;
+ }
+ });
+ info("Loaded " + kURL);
+
+ let urlbarInputEl = newWin.gURLBar.inputField;
+ let urlbarInput = getAccessible(urlbarInputEl, [nsIAccessibleText]);
+
+ let onCaretMove = waitForEvents([
+ [EVENT_TEXT_CARET_MOVED, caretMoveChecker(urlbarInput, kURL.length)],
+ [EVENT_FOCUS, urlbarInput],
+ ]);
+
+ urlbarInput.caretOffset = -1;
+ await onCaretMove;
+ ok(true, "Caret move in URL bar #1");
+
+ onCaretMove = waitForEvent(
+ EVENT_TEXT_CARET_MOVED,
+ caretMoveChecker(urlbarInput, 0)
+ );
+
+ urlbarInput.caretOffset = 0;
+ await onCaretMove;
+ ok(true, "Caret move in URL bar #2");
+
+ await BrowserTestUtils.closeWindow(newWin);
+}
+
+add_task(checkURLBarCaretEvents);
diff --git a/accessible/tests/browser/events/head.js b/accessible/tests/browser/events/head.js
new file mode 100644
index 0000000000..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/events/head.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
diff --git a/accessible/tests/browser/fission/browser.toml b/accessible/tests/browser/fission/browser.toml
new file mode 100644
index 0000000000..0332573db9
--- /dev/null
+++ b/accessible/tests/browser/fission/browser.toml
@@ -0,0 +1,24 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_content_tree.js"]
+
+["browser_hidden_iframe.js"]
+https_first_disabled = true
+
+["browser_nested_iframe.js"]
+
+["browser_reframe_root.js"]
+
+["browser_reframe_visibility.js"]
+
+["browser_src_change.js"]
+
+["browser_take_focus.js"]
diff --git a/accessible/tests/browser/fission/browser_content_tree.js b/accessible/tests/browser/fission/browser_content_tree.js
new file mode 100644
index 0000000000..1592ae6a1a
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_content_tree.js
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `<table id="table">
+ <tr>
+ <td>cell1</td>
+ <td>cell2</td>
+ </tr>
+ </table>
+ <ul id="ul">
+ <li id="li">item1</li>
+ </ul>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ ok(iframeDocAcc, "IFRAME document accessible is present");
+ (gIsRemoteIframe ? isnot : is)(
+ browser.browsingContext.currentWindowGlobal.osPid,
+ browser.browsingContext.children[0].currentWindowGlobal.osPid,
+ `Content and IFRAME documents are in ${
+ gIsRemoteIframe ? "separate processes" : "same process"
+ }.`
+ );
+
+ const tree = {
+ DOCUMENT: [
+ {
+ INTERNAL_FRAME: [
+ {
+ DOCUMENT: [
+ {
+ TABLE: [
+ {
+ ROW: [
+ { CELL: [{ TEXT_LEAF: [] }] },
+ { CELL: [{ TEXT_LEAF: [] }] },
+ ],
+ },
+ ],
+ },
+ {
+ LIST: [
+ {
+ LISTITEM: [{ LISTITEM_MARKER: [] }, { TEXT_LEAF: [] }],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(contentDocAcc, tree);
+
+ const iframeAcc = contentDocAcc.getChildAt(0);
+ is(
+ iframeAcc.getChildAt(0),
+ iframeDocAcc,
+ "Document for the IFRAME matches IFRAME's first child."
+ );
+
+ is(
+ iframeDocAcc.parent,
+ iframeAcc,
+ "IFRAME document's parent matches the IFRAME."
+ );
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/fission/browser_hidden_iframe.js b/accessible/tests/browser/fission/browser_hidden_iframe.js
new file mode 100644
index 0000000000..61414b611d
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_hidden_iframe.js
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `<input id="textbox" value="hello"/>`,
+ async function (browser, contentDocAcc) {
+ info(
+ "Check that the IFRAME and the IFRAME document are not accessible initially."
+ );
+ let iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ let iframeDocAcc = findAccessibleChildByID(
+ contentDocAcc,
+ DEFAULT_IFRAME_DOC_BODY_ID
+ );
+ ok(!iframeAcc, "IFRAME is hidden and should not be accessible");
+ ok(!iframeDocAcc, "IFRAME document is hidden and should not be accessible");
+
+ info(
+ "Show the IFRAME and check that it's now available in the accessibility tree."
+ );
+
+ const events = [[EVENT_REORDER, contentDocAcc]];
+
+ const onEvents = waitForEvents(events);
+ await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], contentId => {
+ content.document.getElementById(contentId).style.display = "";
+ });
+ await onEvents;
+
+ iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ ok(!isDefunct(iframeAcc), "IFRAME should be accessible");
+
+ // Wait for the child iframe to layout itself. This can happen during or
+ // after the reorder event, depending on timing.
+ iframeDocAcc = await TestUtils.waitForCondition(() => {
+ return findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_DOC_BODY_ID);
+ });
+
+ is(iframeAcc.childCount, 1, "IFRAME accessible should have a single child");
+ ok(iframeDocAcc, "IFRAME document exists");
+ ok(!isDefunct(iframeDocAcc), "IFRAME document should be accessible");
+ is(
+ iframeAcc.firstChild,
+ iframeDocAcc,
+ "An accessible for a IFRAME document is the child of the IFRAME accessible"
+ );
+ is(
+ iframeDocAcc.parent,
+ iframeAcc,
+ "IFRAME document's parent matches the IFRAME."
+ );
+ },
+ {
+ topLevel: false,
+ iframe: true,
+ remoteIframe: true,
+ iframeAttrs: {
+ style: "display: none;",
+ },
+ skipFissionDocLoad: true,
+ }
+);
diff --git a/accessible/tests/browser/fission/browser_nested_iframe.js b/accessible/tests/browser/fission/browser_nested_iframe.js
new file mode 100644
index 0000000000..d6600a2d5e
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_nested_iframe.js
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+const NESTED_IFRAME_DOC_BODY_ID = "nested-iframe-body";
+const NESTED_IFRAME_ID = "nested-iframe";
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const nestedURL = new URL(`http://example.com/document-builder.sjs`);
+nestedURL.searchParams.append(
+ "html",
+ `<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Nested Iframe Frame Test</title>
+ </head>
+ <body id="${NESTED_IFRAME_DOC_BODY_ID}">
+ <table id="table">
+ <tr>
+ <td>cell1</td>
+ <td>cell2</td>
+ </tr>
+ </table>
+ <ul id="ul">
+ <li id="li">item1</li>
+ </ul>
+ </body>
+ </html>`
+);
+
+function getOsPid(browsingContext) {
+ return browsingContext.currentWindowGlobal.osPid;
+}
+
+addAccessibleTask(
+ `<iframe id="${NESTED_IFRAME_ID}" src="${nestedURL.href}"/>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ ok(iframeDocAcc, "IFRAME document accessible is present");
+ let nestedDocAcc = findAccessibleChildByID(
+ iframeDocAcc,
+ NESTED_IFRAME_DOC_BODY_ID
+ );
+ let waitForNestedDocLoad = false;
+ if (nestedDocAcc) {
+ const state = {};
+ nestedDocAcc.getState(state, {});
+ if (state.value & STATE_BUSY) {
+ info("Nested IFRAME document accessible is present but busy");
+ waitForNestedDocLoad = true;
+ } else {
+ ok(true, "Nested IFRAME document accessible is present and ready");
+ }
+ } else {
+ info("Nested IFRAME document accessible is not present yet");
+ waitForNestedDocLoad = true;
+ }
+ if (waitForNestedDocLoad) {
+ info("Waiting for doc load complete on nested iframe document");
+ nestedDocAcc = (
+ await waitForEvent(
+ EVENT_DOCUMENT_LOAD_COMPLETE,
+ NESTED_IFRAME_DOC_BODY_ID
+ )
+ ).accessible;
+ }
+
+ if (gIsRemoteIframe) {
+ isnot(
+ getOsPid(browser.browsingContext),
+ getOsPid(browser.browsingContext.children[0]),
+ `Content and IFRAME documents are in separate processes.`
+ );
+ isnot(
+ getOsPid(browser.browsingContext),
+ getOsPid(browser.browsingContext.children[0].children[0]),
+ `Content and nested IFRAME documents are in separate processes.`
+ );
+ isnot(
+ getOsPid(browser.browsingContext.children[0]),
+ getOsPid(browser.browsingContext.children[0].children[0]),
+ `IFRAME and nested IFRAME documents are in separate processes.`
+ );
+ } else {
+ is(
+ getOsPid(browser.browsingContext),
+ getOsPid(browser.browsingContext.children[0]),
+ `Content and IFRAME documents are in same processes.`
+ );
+ if (gFissionBrowser) {
+ isnot(
+ getOsPid(browser.browsingContext.children[0]),
+ getOsPid(browser.browsingContext.children[0].children[0]),
+ `IFRAME and nested IFRAME documents are in separate processes.`
+ );
+ } else {
+ is(
+ getOsPid(browser.browsingContext),
+ getOsPid(browser.browsingContext.children[0].children[0]),
+ `Content and nested IFRAME documents are in same processes.`
+ );
+ }
+ }
+
+ const tree = {
+ DOCUMENT: [
+ {
+ INTERNAL_FRAME: [
+ {
+ DOCUMENT: [
+ {
+ INTERNAL_FRAME: [
+ {
+ DOCUMENT: [
+ {
+ TABLE: [
+ {
+ ROW: [
+ { CELL: [{ TEXT_LEAF: [] }] },
+ { CELL: [{ TEXT_LEAF: [] }] },
+ ],
+ },
+ ],
+ },
+ {
+ LIST: [
+ {
+ LISTITEM: [
+ { LISTITEM_MARKER: [] },
+ { TEXT_LEAF: [] },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(contentDocAcc, tree);
+
+ const nestedIframeAcc = iframeDocAcc.getChildAt(0);
+ is(
+ nestedIframeAcc.getChildAt(0),
+ nestedDocAcc,
+ "Document for nested IFRAME matches."
+ );
+
+ is(
+ nestedDocAcc.parent,
+ nestedIframeAcc,
+ "Nested IFRAME document's parent matches the nested IFRAME."
+ );
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/fission/browser_reframe_root.js b/accessible/tests/browser/fission/browser_reframe_root.js
new file mode 100644
index 0000000000..d7123109f4
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_reframe_root.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+addAccessibleTask(
+ `<input id="textbox" value="hello"/>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ info(
+ "Check that the IFRAME and the IFRAME document are accessible initially."
+ );
+ let iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ ok(!isDefunct(iframeAcc), "IFRAME should be accessible");
+ ok(!isDefunct(iframeDocAcc), "IFRAME document should be accessible");
+
+ info("Move the IFRAME under a new hidden root.");
+ let onEvents = waitForEvent(EVENT_REORDER, contentDocAcc);
+ await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], id => {
+ const doc = content.document;
+ const root = doc.createElement("div");
+ root.style.display = "none";
+ doc.body.appendChild(root);
+ root.appendChild(doc.getElementById(id));
+ });
+ await onEvents;
+
+ ok(
+ isDefunct(iframeAcc),
+ "IFRAME accessible should be defunct when hidden."
+ );
+ ok(
+ isDefunct(iframeDocAcc),
+ "IFRAME document's accessible should be defunct when the IFRAME is hidden."
+ );
+ ok(
+ !findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID),
+ "No accessible for an IFRAME present."
+ );
+ ok(
+ !findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_DOC_BODY_ID),
+ "No accessible for the IFRAME document present."
+ );
+
+ info("Move the IFRAME back under the content document's body.");
+ onEvents = waitForEvents([
+ [EVENT_REORDER, contentDocAcc],
+ [
+ EVENT_STATE_CHANGE,
+ event => {
+ const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent);
+ const id = getAccessibleDOMNodeID(event.accessible);
+ return (
+ id === DEFAULT_IFRAME_DOC_BODY_ID &&
+ scEvent.state === STATE_BUSY &&
+ scEvent.isEnabled === false
+ );
+ },
+ ],
+ ]);
+ await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], id => {
+ content.document.body.appendChild(content.document.getElementById(id));
+ });
+ await onEvents;
+
+ iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ const newiframeDocAcc = iframeAcc.firstChild;
+
+ ok(!isDefunct(iframeAcc), "IFRAME should be accessible");
+ is(iframeAcc.childCount, 1, "IFRAME accessible should have a single child");
+ ok(!isDefunct(newiframeDocAcc), "IFRAME document should be accessible");
+ ok(
+ isDefunct(iframeDocAcc),
+ "Original IFRAME document accessible should be defunct."
+ );
+ isnot(
+ iframeAcc.firstChild,
+ iframeDocAcc,
+ "A new accessible is created for a IFRAME document."
+ );
+ is(
+ iframeAcc.firstChild,
+ newiframeDocAcc,
+ "A new accessible for a IFRAME document is the child of the IFRAME accessible"
+ );
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/fission/browser_reframe_visibility.js b/accessible/tests/browser/fission/browser_reframe_visibility.js
new file mode 100644
index 0000000000..8aee02037c
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_reframe_visibility.js
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `<input id="textbox" value="hello"/>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ info(
+ "Check that the IFRAME and the IFRAME document are accessible initially."
+ );
+ let iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ ok(!isDefunct(iframeAcc), "IFRAME should be accessible");
+ ok(!isDefunct(iframeDocAcc), "IFRAME document should be accessible");
+
+ info(
+ "Hide the IFRAME and check that it's gone along with the IFRAME document."
+ );
+ let onEvents = waitForEvent(EVENT_REORDER, contentDocAcc);
+ await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], contentId => {
+ content.document.getElementById(contentId).style.display = "none";
+ });
+ await onEvents;
+
+ ok(
+ isDefunct(iframeAcc),
+ "IFRAME accessible should be defunct when hidden."
+ );
+ if (gIsRemoteIframe) {
+ ok(
+ !isDefunct(iframeDocAcc),
+ "IFRAME document's accessible is not defunct when the IFRAME is hidden and fission is enabled."
+ );
+ } else {
+ ok(
+ isDefunct(iframeDocAcc),
+ "IFRAME document's accessible is defunct when the IFRAME is hidden and fission is not enabled."
+ );
+ }
+ ok(
+ !findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID),
+ "No accessible for an IFRAME present."
+ );
+ ok(
+ !findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_DOC_BODY_ID),
+ "No accessible for the IFRAME document present."
+ );
+
+ info(
+ "Show the IFRAME and check that a new accessible is created for it as " +
+ "well as the IFRAME document."
+ );
+
+ const events = [[EVENT_REORDER, contentDocAcc]];
+ if (!gIsRemoteIframe) {
+ events.push([
+ EVENT_STATE_CHANGE,
+ event => {
+ const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent);
+ const id = getAccessibleDOMNodeID(event.accessible);
+ return (
+ id === DEFAULT_IFRAME_DOC_BODY_ID &&
+ scEvent.state === STATE_BUSY &&
+ scEvent.isEnabled === false
+ );
+ },
+ ]);
+ }
+ onEvents = waitForEvents(events);
+ await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], contentId => {
+ content.document.getElementById(contentId).style.display = "block";
+ });
+ await onEvents;
+
+ iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ const newiframeDocAcc = iframeAcc.firstChild;
+
+ ok(!isDefunct(iframeAcc), "IFRAME should be accessible");
+ is(iframeAcc.childCount, 1, "IFRAME accessible should have a single child");
+ ok(newiframeDocAcc, "IFRAME document exists");
+ ok(!isDefunct(newiframeDocAcc), "IFRAME document should be accessible");
+ if (gIsRemoteIframe) {
+ ok(
+ !isDefunct(iframeDocAcc),
+ "Original IFRAME document accessible should not be defunct when fission is enabled."
+ );
+ is(
+ iframeAcc.firstChild,
+ iframeDocAcc,
+ "Existing accessible is used for a IFRAME document."
+ );
+ } else {
+ ok(
+ isDefunct(iframeDocAcc),
+ "Original IFRAME document accessible should be defunct when fission is not enabled."
+ );
+ isnot(
+ iframeAcc.firstChild,
+ iframeDocAcc,
+ "A new accessible is created for a IFRAME document."
+ );
+ }
+ is(
+ iframeAcc.firstChild,
+ newiframeDocAcc,
+ "A new accessible for a IFRAME document is the child of the IFRAME accessible"
+ );
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/fission/browser_src_change.js b/accessible/tests/browser/fission/browser_src_change.js
new file mode 100644
index 0000000000..e97cda3cc3
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_src_change.js
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ `<input id="textbox" value="hello"/>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ info(
+ "Check that the IFRAME and the IFRAME document are accessible initially."
+ );
+ let iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID);
+ ok(isAccessible(iframeAcc), "IFRAME should be accessible");
+ ok(isAccessible(iframeDocAcc), "IFRAME document should be accessible");
+
+ info("Replace src URL for the IFRAME with one with different origin.");
+ const onDocLoad = waitForEvent(
+ EVENT_DOCUMENT_LOAD_COMPLETE,
+ DEFAULT_IFRAME_DOC_BODY_ID
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ [DEFAULT_IFRAME_ID, CURRENT_CONTENT_DIR],
+ (id, olddir) => {
+ const { src } = content.document.getElementById(id);
+ content.document.getElementById(id).src = src.replace(
+ olddir,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://example.net/browser/accessible/tests/browser/"
+ );
+ }
+ );
+ const newiframeDocAcc = (await onDocLoad).accessible;
+
+ ok(isAccessible(iframeAcc), "IFRAME should be accessible");
+ ok(
+ isAccessible(newiframeDocAcc),
+ "new IFRAME document should be accessible"
+ );
+ isnot(
+ iframeDocAcc,
+ newiframeDocAcc,
+ "A new accessible is created for a IFRAME document."
+ );
+ is(
+ iframeAcc.firstChild,
+ newiframeDocAcc,
+ "An IFRAME has a new accessible for a IFRAME document as a child."
+ );
+ is(
+ newiframeDocAcc.parent,
+ iframeAcc,
+ "A new accessible for a IFRAME document has an IFRAME as a parent."
+ );
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/fission/browser_take_focus.js b/accessible/tests/browser/fission/browser_take_focus.js
new file mode 100644
index 0000000000..7ec037566d
--- /dev/null
+++ b/accessible/tests/browser/fission/browser_take_focus.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+addAccessibleTask(
+ `<div role="group"><input id="textbox" value="hello"/></div>`,
+ async function (browser, iframeDocAcc, contentDocAcc) {
+ const textbox = findAccessibleChildByID(iframeDocAcc, "textbox");
+ const iframe = findAccessibleChildByID(contentDocAcc, "default-iframe-id");
+ const iframeDoc = findAccessibleChildByID(
+ contentDocAcc,
+ "default-iframe-body-id"
+ );
+ const root = getRootAccessible(document);
+
+ testStates(textbox, STATE_FOCUSABLE, 0, STATE_FOCUSED);
+
+ let onFocus = waitForEvent(EVENT_FOCUS, textbox);
+ textbox.takeFocus();
+ await onFocus;
+
+ testStates(textbox, STATE_FOCUSABLE | STATE_FOCUSED, 0);
+
+ is(
+ getAccessibleDOMNodeID(contentDocAcc.focusedChild),
+ "textbox",
+ "correct focusedChild from top doc"
+ );
+
+ is(
+ getAccessibleDOMNodeID(iframeDocAcc.focusedChild),
+ "textbox",
+ "correct focusedChild from iframe"
+ );
+
+ is(
+ getAccessibleDOMNodeID(root.focusedChild),
+ "textbox",
+ "correct focusedChild from root"
+ );
+
+ ok(!iframe.focusedChild, "correct focusedChild from iframe (null)");
+
+ onFocus = waitForEvent(EVENT_FOCUS, iframeDoc);
+ iframeDoc.takeFocus();
+ await onFocus;
+
+ is(
+ getAccessibleDOMNodeID(contentDocAcc.focusedChild),
+ "default-iframe-body-id",
+ "correct focusedChild of child doc from top doc"
+ );
+ is(
+ getAccessibleDOMNodeID(iframe.focusedChild),
+ "default-iframe-body-id",
+ "correct focusedChild of child doc from iframe"
+ );
+ is(
+ getAccessibleDOMNodeID(root.focusedChild),
+ "default-iframe-body-id",
+ "correct focusedChild of child doc from root"
+ );
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/fission/head.js b/accessible/tests/browser/fission/head.js
new file mode 100644
index 0000000000..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/fission/head.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
diff --git a/accessible/tests/browser/general/browser.toml b/accessible/tests/browser/general/browser.toml
new file mode 100644
index 0000000000..95c66e0904
--- /dev/null
+++ b/accessible/tests/browser/general/browser.toml
@@ -0,0 +1,13 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "!/accessible/tests/browser/shared-head.js",
+ "head.js",
+ "!/accessible/tests/mochitest/*.js",
+]
+skip-if = ["a11y_checks"]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_test_doc_creation.js"]
+
+["browser_test_urlbar.js"]
diff --git a/accessible/tests/browser/general/browser_test_doc_creation.js b/accessible/tests/browser/general/browser_test_doc_creation.js
new file mode 100644
index 0000000000..7ee07f63fd
--- /dev/null
+++ b/accessible/tests/browser/general/browser_test_doc_creation.js
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const tab1URL = `data:text/html,
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf-8"/>
+ <title>First tab to be loaded</title>
+ </head>
+ <body>
+ <butotn>JUST A BUTTON</butotn>
+ </body>
+ </html>`;
+
+const tab2URL = `data:text/html,
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="utf-8"/>
+ <title>Second tab to be loaded</title>
+ </head>
+ <body>
+ <butotn>JUST A BUTTON</butotn>
+ </body>
+ </html>`;
+
+// Checking that, if there are open windows before accessibility was started,
+// root accessibles for open windows are created so that all root accessibles
+// are stored in application accessible children array.
+add_task(async function testDocumentCreation() {
+ let tab1 = await openNewTab(tab1URL);
+ let tab2 = await openNewTab(tab2URL);
+ let accService = await initAccessibilityService();
+
+ info("Verifying that each tab content document is in accessible cache.");
+ for (const browser of [...gBrowser.browsers]) {
+ await SpecialPowers.spawn(browser, [], async () => {
+ let accServiceContent = Cc[
+ "@mozilla.org/accessibilityService;1"
+ ].getService(Ci.nsIAccessibilityService);
+ Assert.ok(
+ !!accServiceContent.getAccessibleFromCache(content.document),
+ "Document accessible is in cache."
+ );
+ });
+ }
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+
+ accService = null; // eslint-disable-line no-unused-vars
+ await shutdownAccessibilityService();
+});
diff --git a/accessible/tests/browser/general/browser_test_urlbar.js b/accessible/tests/browser/general/browser_test_urlbar.js
new file mode 100644
index 0000000000..5cfd66ee3a
--- /dev/null
+++ b/accessible/tests/browser/general/browser_test_urlbar.js
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { UrlbarTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+);
+
+// Checking that the awesomebar popup gets COMBOBOX_LIST role instead of
+// LISTBOX, since its parent is a <panel> (see Bug 1422465)
+add_task(async function testAutocompleteRichResult() {
+ let tab = await openNewTab("data:text/html;charset=utf-8,");
+ let accService = await initAccessibilityService();
+
+ info("Opening the URL bar and entering a key to show the urlbar panel");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "a",
+ });
+
+ info("Waiting for accessibility to be created for the results list");
+ let resultsView;
+ resultsView = gURLBar.view.panel.querySelector(".urlbarView-results");
+ await TestUtils.waitForCondition(() =>
+ accService.getAccessibleFor(resultsView)
+ );
+
+ info("Confirming that the special case is handled in XULListboxAccessible");
+ let accessible = accService.getAccessibleFor(resultsView);
+ is(accessible.role, ROLE_COMBOBOX_LIST, "Right role");
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+registerCleanupFunction(async function () {
+ await shutdownAccessibilityService();
+});
diff --git a/accessible/tests/browser/general/head.js b/accessible/tests/browser/general/head.js
new file mode 100644
index 0000000000..841436043c
--- /dev/null
+++ b/accessible/tests/browser/general/head.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* exported initAccessibilityService, openNewTab, shutdownAccessibilityService */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+const nsIAccessibleRole = Ci.nsIAccessibleRole; // eslint-disable-line no-unused-vars
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+async function openNewTab(url) {
+ const forceNewProcess = true;
+
+ return BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url,
+ forceNewProcess,
+ });
+}
+
+async function initAccessibilityService() {
+ info("Create accessibility service.");
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ Ci.nsIAccessibilityService
+ );
+
+ await new Promise(resolve => {
+ if (Services.appinfo.accessibilityEnabled) {
+ resolve();
+ return;
+ }
+
+ let observe = (subject, topic, data) => {
+ if (data === "1") {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ resolve();
+ }
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ });
+
+ return accService;
+}
+
+function shutdownAccessibilityService() {
+ forceGC();
+
+ return new Promise(resolve => {
+ if (!Services.appinfo.accessibilityEnabled) {
+ resolve();
+ return;
+ }
+
+ let observe = (subject, topic, data) => {
+ if (data === "0") {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ resolve();
+ }
+ };
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ });
+}
diff --git a/accessible/tests/browser/head.js b/accessible/tests/browser/head.js
new file mode 100644
index 0000000000..2e1ab7f897
--- /dev/null
+++ b/accessible/tests/browser/head.js
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* exported initAccService, shutdownAccService, waitForEvent, accConsumersChanged */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+const { CommonUtils } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs"
+);
+
+/**
+ * Capture when 'a11y-consumers-changed' event is fired.
+ *
+ * @param {?Object} target
+ * [optional] browser object that indicates that accessibility service
+ * is in content process.
+ * @return {Array}
+ * List of promises where first one is the promise for when the event
+ * observer is added and the second one for when the event is observed.
+ */
+function accConsumersChanged(target) {
+ return target
+ ? [
+ SpecialPowers.spawn(target, [], () =>
+ content.CommonUtils.addAccConsumersChangedObserver()
+ ),
+ SpecialPowers.spawn(target, [], () =>
+ content.CommonUtils.observeAccConsumersChanged()
+ ),
+ ]
+ : [
+ CommonUtils.addAccConsumersChangedObserver(),
+ CommonUtils.observeAccConsumersChanged(),
+ ];
+}
+
+/**
+ * Capture when accessibility service is initialized.
+ *
+ * @param {?Object} target
+ * [optional] browser object that indicates that accessibility service
+ * is expected to be initialized in content process.
+ * @return {Array}
+ * List of promises where first one is the promise for when the event
+ * observer is added and the second one for when the event is observed.
+ */
+function initAccService(target) {
+ return target
+ ? [
+ SpecialPowers.spawn(target, [], () =>
+ content.CommonUtils.addAccServiceInitializedObserver()
+ ),
+ SpecialPowers.spawn(target, [], () =>
+ content.CommonUtils.observeAccServiceInitialized()
+ ),
+ ]
+ : [
+ CommonUtils.addAccServiceInitializedObserver(),
+ CommonUtils.observeAccServiceInitialized(),
+ ];
+}
+
+/**
+ * Capture when accessibility service is shutdown.
+ *
+ * @param {?Object} target
+ * [optional] browser object that indicates that accessibility service
+ * is expected to be shutdown in content process.
+ * @return {Array}
+ * List of promises where first one is the promise for when the event
+ * observer is added and the second one for when the event is observed.
+ */
+function shutdownAccService(target) {
+ return target
+ ? [
+ SpecialPowers.spawn(target, [], () =>
+ content.CommonUtils.addAccServiceShutdownObserver()
+ ),
+ SpecialPowers.spawn(target, [], () =>
+ content.CommonUtils.observeAccServiceShutdown()
+ ),
+ ]
+ : [
+ CommonUtils.addAccServiceShutdownObserver(),
+ CommonUtils.observeAccServiceShutdown(),
+ ];
+}
+
+/**
+ * Simpler verions of waitForEvent defined in
+ * accessible/tests/browser/events.js
+ */
+function waitForEvent(eventType, expectedId) {
+ return new Promise(resolve => {
+ let eventObserver = {
+ observe(subject) {
+ let event = subject.QueryInterface(Ci.nsIAccessibleEvent);
+ let id;
+ try {
+ id = event.accessible.id;
+ } catch (e) {
+ // This can throw NS_ERROR_FAILURE.
+ }
+ if (event.eventType === eventType && id === expectedId) {
+ Services.obs.removeObserver(this, "accessible-event");
+ resolve(event);
+ }
+ },
+ };
+ Services.obs.addObserver(eventObserver, "accessible-event");
+ });
+}
diff --git a/accessible/tests/browser/hittest/browser.toml b/accessible/tests/browser/hittest/browser.toml
new file mode 100644
index 0000000000..7f82a6ff49
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser.toml
@@ -0,0 +1,24 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/letters.gif",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_test_browser.js"]
+
+["browser_test_general.js"]
+
+["browser_test_scroll_hittest.js"]
+
+["browser_test_shadowroot.js"]
+
+["browser_test_text.js"]
+
+["browser_test_zoom.js"]
+
+["browser_test_zoom_text.js"]
diff --git a/accessible/tests/browser/hittest/browser_test_browser.js b/accessible/tests/browser/hittest/browser_test_browser.js
new file mode 100644
index 0000000000..477af42fe9
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_browser.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+async function runTests(browser, accDoc) {
+ // Hit testing. See bug #726097
+ await invokeContentTask(browser, [], () =>
+ content.document.getElementById("hittest").scrollIntoView(true)
+ );
+
+ const dpr = await getContentDPR(browser);
+ const hititem = findAccessibleChildByID(accDoc, "hititem");
+ const hittest = findAccessibleChildByID(accDoc, "hittest");
+ const outerDocAcc = accDoc.parent;
+ const rootAcc = CommonUtils.getRootAccessible(document);
+
+ const [hitX, hitY, hitWidth, hitHeight] = Layout.getBounds(hititem, dpr);
+ // "hititem" node has the full screen width, so when we divide it by 2, we are
+ // still way outside the inline content.
+ const tgtX = hitX + hitWidth / 2;
+ const tgtY = hitY + hitHeight / 2;
+
+ let hitAcc = rootAcc.getDeepestChildAtPoint(tgtX, tgtY);
+ is(
+ hitAcc,
+ hititem,
+ `Hit match at ${tgtX},${tgtY} (root doc deepest child). Found: ${prettyName(
+ hitAcc
+ )}`
+ );
+
+ const hitAcc2 = accDoc.getDeepestChildAtPoint(tgtX, tgtY);
+ is(
+ hitAcc,
+ hitAcc2,
+ `Hit match at ${tgtX},${tgtY} (doc deepest child). Found: ${prettyName(
+ hitAcc2
+ )}`
+ );
+
+ hitAcc = outerDocAcc.getChildAtPoint(tgtX, tgtY);
+ is(
+ hitAcc,
+ accDoc,
+ `Hit match at ${tgtX},${tgtY} (outer doc child). Found: ${prettyName(
+ hitAcc
+ )}`
+ );
+
+ hitAcc = accDoc.getChildAtPoint(tgtX, tgtY);
+ is(
+ hitAcc,
+ hittest,
+ `Hit match at ${tgtX},${tgtY} (doc child). Found: ${prettyName(hitAcc)}`
+ );
+}
+
+addAccessibleTask(
+ `
+ <div id="hittest">
+ <div id="hititem"><span role="img">img</span>item</div>
+ </div>
+ `,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/hittest/browser_test_general.js b/accessible/tests/browser/hittest/browser_test_general.js
new file mode 100644
index 0000000000..ca3a879c18
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_general.js
@@ -0,0 +1,425 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+async function runTests(browser, accDoc) {
+ const dpr = await getContentDPR(browser);
+
+ await testChildAtPoint(
+ dpr,
+ 3,
+ 3,
+ findAccessibleChildByID(accDoc, "list"),
+ findAccessibleChildByID(accDoc, "listitem"),
+ findAccessibleChildByID(accDoc, "inner").firstChild
+ );
+ todo(
+ false,
+ "Bug 746974 - children must match on all platforms. On Windows, " +
+ "ChildAtPoint with eDeepestChild is incorrectly ignoring MustPrune " +
+ "for the graphic."
+ );
+
+ const txt = findAccessibleChildByID(accDoc, "txt");
+ await testChildAtPoint(dpr, 1, 1, txt, txt, txt);
+
+ info(
+ "::MustPrune case, point is outside of textbox accessible but is in document."
+ );
+ await testChildAtPoint(dpr, -1, -1, txt, null, null);
+
+ info("::MustPrune case, point is outside of root accessible.");
+ await testChildAtPoint(dpr, -10000, -10000, txt, null, null);
+
+ info("Not specific case, point is inside of btn accessible.");
+ const btn = findAccessibleChildByID(accDoc, "btn");
+ await testChildAtPoint(dpr, 1, 1, btn, btn, btn);
+
+ info("Not specific case, point is outside of btn accessible.");
+ await testChildAtPoint(dpr, -1, -1, btn, null, null);
+
+ info(
+ "Out of flow accessible testing, do not return out of flow accessible " +
+ "because it's not a child of the accessible even though visually it is."
+ );
+ await invokeContentTask(browser, [], () => {
+ const { CommonUtils } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs"
+ );
+ const doc = content.document;
+ const rectArea = CommonUtils.getNode("area", doc).getBoundingClientRect();
+ const outOfFlow = CommonUtils.getNode("outofflow", doc);
+ outOfFlow.style.left = rectArea.left + "px";
+ outOfFlow.style.top = rectArea.top + "px";
+ });
+
+ const area = findAccessibleChildByID(accDoc, "area");
+ await testChildAtPoint(dpr, 1, 1, area, area, area);
+
+ info("Test image maps. Their children are not in the layout tree.");
+ await waitForImageMap(browser, accDoc);
+ const imgmap = findAccessibleChildByID(accDoc, "imgmap");
+ ok(imgmap, "Image map exists");
+ const theLetterA = imgmap.firstChild;
+ await hitTest(browser, imgmap, theLetterA, theLetterA);
+ await hitTest(
+ browser,
+ findAccessibleChildByID(accDoc, "container"),
+ imgmap,
+ theLetterA
+ );
+
+ info("hit testing for element contained by zero-width element");
+ const container2Input = findAccessibleChildByID(accDoc, "container2_input");
+ await hitTest(
+ browser,
+ findAccessibleChildByID(accDoc, "container2"),
+ container2Input,
+ container2Input
+ );
+
+ info("hittesting table, row, cells -- rows are not in the layout tree");
+ const table = findAccessibleChildByID(accDoc, "table");
+ const row = findAccessibleChildByID(accDoc, "row");
+ const cell1 = findAccessibleChildByID(accDoc, "cell1");
+
+ await hitTest(browser, table, row, cell1);
+
+ info("Testing that an inaccessible child doesn't break hit testing");
+ const containerWithInaccessibleChild = findAccessibleChildByID(
+ accDoc,
+ "containerWithInaccessibleChild"
+ );
+ const containerWithInaccessibleChildP2 = findAccessibleChildByID(
+ accDoc,
+ "containerWithInaccessibleChild_p2"
+ );
+ await hitTest(
+ browser,
+ containerWithInaccessibleChild,
+ containerWithInaccessibleChildP2,
+ containerWithInaccessibleChildP2.firstChild
+ );
+
+ info("Testing wrapped text");
+ const wrappedTextLinkFirstP = findAccessibleChildByID(
+ accDoc,
+ "wrappedTextLinkFirstP"
+ );
+ const wrappedTextLinkFirstA = findAccessibleChildByID(
+ accDoc,
+ "wrappedTextLinkFirstA"
+ );
+ await hitTest(
+ browser,
+ wrappedTextLinkFirstP,
+ wrappedTextLinkFirstA,
+ wrappedTextLinkFirstA.firstChild
+ );
+ const wrappedTextLeafFirstP = findAccessibleChildByID(
+ accDoc,
+ "wrappedTextLeafFirstP"
+ );
+ const wrappedTextLeafFirstMark = findAccessibleChildByID(
+ accDoc,
+ "wrappedTextLeafFirstMark"
+ );
+ await hitTest(
+ browser,
+ wrappedTextLeafFirstP,
+ wrappedTextLeafFirstMark,
+ wrappedTextLeafFirstMark.firstChild
+ );
+
+ info("Testing image");
+ const imageP = findAccessibleChildByID(accDoc, "imageP");
+ const image = findAccessibleChildByID(accDoc, "image");
+ await hitTest(browser, imageP, image, image);
+
+ info("Testing image map with 0-sized area");
+ const mapWith0AreaP = findAccessibleChildByID(accDoc, "mapWith0AreaP");
+ const mapWith0Area = findAccessibleChildByID(accDoc, "mapWith0Area");
+ await hitTest(browser, mapWith0AreaP, mapWith0Area, mapWith0Area);
+}
+
+addAccessibleTask(
+ `
+ <div role="list" id="list">
+ <div role="listitem" id="listitem"><span title="foo" id="inner">inner</span>item</div>
+ </div>
+
+ <span role="button">button1</span><span role="button" id="btn">button2</span>
+
+ <span role="textbox">textbox1</span><span role="textbox" id="txt">textbox2</span>
+
+ <div id="outofflow" style="width: 10px; height: 10px; position: absolute; left: 0px; top: 0px; background-color: yellow;">
+ </div>
+ <div id="area" style="width: 100px; height: 100px; background-color: blue;"></div>
+
+ <map name="atoz_map">
+ <area id="thelettera" href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a"
+ coords="0,0,15,15" alt="thelettera" shape="rect"/>
+ </map>
+
+ <div id="container">
+ <img id="imgmap" width="447" height="15" usemap="#atoz_map" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif"/>
+ </div>
+
+ <div id="container2" style="width: 0px">
+ <input id="container2_input">
+ </div>
+
+ <table id="table" border>
+ <tr id="row">
+ <td id="cell1">hello</td>
+ <td id="cell2">world</td>
+ </tr>
+ </table>
+
+ <div id="containerWithInaccessibleChild">
+ <p>hi</p>
+ <p aria-hidden="true">hi</p>
+ <p id="containerWithInaccessibleChild_p2">bye</p>
+ </div>
+
+ <p id="wrappedTextLinkFirstP" style="width: 3ch; font-family: monospace;">
+ <a id="wrappedTextLinkFirstA" href="https://example.com/">a</a>b cd
+ </p>
+
+ <p id="wrappedTextLeafFirstP" style="width: 3ch; font-family: monospace;">
+ <mark id="wrappedTextLeafFirstMark">a</mark><a href="https://example.com/">b cd</a>
+ </p>
+
+ <p id="imageP">
+ <img id="image" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif">
+ </p>
+
+ <map id="0Area">
+ <area shape="rect">
+ </map>
+ <p id="mapWith0AreaP">
+ <img id="mapWith0Area" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif" usemap="#0Area">
+ </p>
+ `,
+ runTests,
+ {
+ iframe: true,
+ remoteIframe: true,
+ // Ensure that all hittest elements are in view.
+ iframeAttrs: { style: "width: 600px; height: 600px; padding: 10px;" },
+ }
+);
+
+addAccessibleTask(
+ `
+ <div id="container">
+ <h1 id="a">A</h1><h1 id="b">B</h1>
+ </div>
+ `,
+ async function (browser, accDoc) {
+ const a = findAccessibleChildByID(accDoc, "a");
+ const b = findAccessibleChildByID(accDoc, "b");
+ const dpr = await getContentDPR(browser);
+ // eslint-disable-next-line no-unused-vars
+ const [x, y, w, h] = Layout.getBounds(a, dpr);
+ // The point passed below will be made relative to `b`, but
+ // we'd like to test a point within `a`. Pass `a`s negative
+ // width for an x offset. Pass zero as a y offset,
+ // assuming the headings are on the same line.
+ await testChildAtPoint(dpr, -w, 0, b, null, null);
+ },
+ {
+ iframe: true,
+ remoteIframe: true,
+ // Ensure that all hittest elements are in view.
+ iframeAttrs: { style: "width: 600px; height: 600px; padding: 10px;" },
+ }
+);
+
+addAccessibleTask(
+ `
+ <style>
+ div {
+ width: 50px;
+ height: 50px;
+ position: relative;
+ }
+
+ div > div {
+ width: 30px;
+ height: 30px;
+ position: absolute;
+ opacity: 0.9;
+ }
+ </style>
+ <div id="a" style="background-color: orange;">
+ <div id="aa" style="background-color: purple;"></div>
+ </div>
+ <div id="b" style="background-color: yellowgreen;">
+ <div id="bb" style="top: -30px; background-color: turquoise"></div>
+ </div>`,
+ async function (browser, accDoc) {
+ const a = findAccessibleChildByID(accDoc, "a");
+ const aa = findAccessibleChildByID(accDoc, "aa");
+ const dpr = await getContentDPR(browser);
+ const [, , w, h] = Layout.getBounds(a, dpr);
+ // test upper left of `a`
+ await testChildAtPoint(dpr, 1, 1, a, aa, aa);
+ // test upper right of `a`
+ await testChildAtPoint(dpr, w - 1, 1, a, a, a);
+ // test just outside upper left of `a`
+ await testChildAtPoint(dpr, 1, -1, a, null, null);
+ // test halfway down/left of `a`
+ await testChildAtPoint(dpr, 1, Math.round(h / 2), a, a, a);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: false,
+ remoteIframe: false,
+ // Ensure that all hittest elements are in view.
+ iframeAttrs: { style: "width: 600px; height: 600px; padding: 10px;" },
+ }
+);
+
+/**
+ * Verify that hit testing returns the proper accessible when one acc content
+ * is partially hidden due to overflow:hidden;
+ */
+addAccessibleTask(
+ `
+ <style>
+ div div {
+ overflow: hidden;
+ font-family: monospace;
+ width: 2ch;
+ }
+ </style>
+ <div id="container" style="display: flex; flex-direction: row-reverse;">
+ <div id="aNode">abcde</div><div id="fNode">fghij</div>
+ </div>`,
+ async function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "container");
+ const aNode = findAccessibleChildByID(docAcc, "aNode");
+ const fNode = findAccessibleChildByID(docAcc, "fNode");
+ const dpr = await getContentDPR(browser);
+ const [, , containerWidth] = Layout.getBounds(container, dpr);
+ const [, , aNodeWidth] = Layout.getBounds(aNode, dpr);
+
+ await testChildAtPoint(
+ dpr,
+ containerWidth - 1,
+ 1,
+ container,
+ aNode,
+ aNode.firstChild
+ );
+ await testChildAtPoint(
+ dpr,
+ containerWidth - aNodeWidth - 1,
+ 1,
+ container,
+ fNode,
+ fNode.firstChild
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Verify that hit testing is appropriately fuzzy when working with generics.
+ * If we match on a generic which contains additional generics and a single text
+ * leaf, we should return the text leaf as the deepest match instead of the
+ * generic itself.
+ */
+addAccessibleTask(
+ `
+ <a href="example.com" id="link">
+ <span style="overflow:hidden;" id="generic"><span aria-hidden="true" id="visible">I am some visible text</span><span id="invisible" style="overflow:hidden; height: 1px; width: 1px; position:absolute; clip: rect(0 0 0 0); display:block;">I am some invisible text</span></span>
+ </a>`,
+ async function (browser, docAcc) {
+ const link = findAccessibleChildByID(docAcc, "link");
+ const generic = findAccessibleChildByID(docAcc, "generic");
+ const invisible = findAccessibleChildByID(docAcc, "invisible");
+ const dpr = await getContentDPR(browser);
+
+ await testChildAtPoint(
+ dpr,
+ 1,
+ 1,
+ link,
+ generic, // Direct Child
+ invisible.firstChild // Deepest Child
+ );
+
+ await testOffsetAtPoint(
+ findAccessibleChildByID(docAcc, "invisible", [Ci.nsIAccessibleText]),
+ 1,
+ 1,
+ COORDTYPE_PARENT_RELATIVE,
+ 0
+ );
+ },
+ { chrome: false, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Verify that hit testing is appropriately fuzzy when working with generics with siblings.
+ * We should return the deepest text leaf as the deepest match instead of the generic itself.
+ */
+addAccessibleTask(
+ `
+<div id="generic"><span aria-hidden="true" id="visible">Mozilla</span><span id="invisible" style="display: block !important;border: 0 !important;clip: rect(0 0 0 0) !important;height: 1px !important;margin: -1px !important;overflow: hidden !important;padding: 0 !important;position: absolute !important;white-space: nowrap !important;width: 1px !important;">hello world<br><div id="extraContainer">Mozilla</div></span><br>I am some other text</div>`,
+ async function (browser, docAcc) {
+ const generic = findAccessibleChildByID(docAcc, "generic");
+ const invisible = findAccessibleChildByID(docAcc, "invisible");
+ const dpr = await getContentDPR(browser);
+
+ await testChildAtPoint(
+ dpr,
+ 1,
+ 1,
+ generic,
+ invisible, // Direct Child
+ invisible.firstChild // Deepest Child
+ );
+ },
+ { chrome: false, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Verify that hit testing correctly ignores
+ * elements with pointer-events: none;
+ */
+addAccessibleTask(
+ `<div id="container" style="position:relative;"><button id="obscured">click me</button><div id="overlay" style="pointer-events:none; top:0; bottom:0; left:0; right:0; position: absolute;"></div></div><button id="clickable">I am clickable</button>`,
+ async function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "container");
+ const obscured = findAccessibleChildByID(docAcc, "obscured");
+ const clickable = findAccessibleChildByID(docAcc, "clickable");
+ const dpr = await getContentDPR(browser);
+ let [targetX, targetY, targetW, targetH] = Layout.getBounds(obscured, dpr);
+ const [x, y] = Layout.getBounds(docAcc, dpr);
+ await testChildAtPoint(
+ dpr,
+ targetX - x + targetW / 2,
+ targetY - y + targetH / 2,
+ docAcc,
+ container, // Direct Child
+ obscured // Deepest Child
+ );
+
+ [targetX, targetY, targetW, targetH] = Layout.getBounds(clickable, dpr);
+ await testChildAtPoint(
+ dpr,
+ targetX - x + targetW / 2,
+ targetY - y + targetH / 2,
+ docAcc,
+ clickable, // Direct Child
+ clickable // Deepest Child
+ );
+ },
+ { chrome: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/hittest/browser_test_scroll_hittest.js b/accessible/tests/browser/hittest/browser_test_scroll_hittest.js
new file mode 100644
index 0000000000..246dcd3d09
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_scroll_hittest.js
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Verify that hit testing returns the proper accessible when one accessible
+ * covers another accessible due to scroll clipping. See Bug 1819741.
+ */
+addAccessibleTask(
+ `
+<div id="container" style="height: 100%; position: absolute; flex-direction: column; display: flex;">
+ <div id="title-bar" style="height: 500px; background-color: red;"></div>
+ <div id="message-container" style="overflow-y: hidden; display: flex;">
+ <div style="overflow-y: auto;" id="message-scrollable">
+ <p style="white-space: pre-line;">
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dictum luctus molestie. Nam in libero mi. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
+
+ Praesent aliquet semper libero, eu ullamcorper tortor vestibulum ac. Sed non pharetra sem. Quisque sodales ipsum a ipsum condimentum porttitor. Integer luctus pellentesque ipsum, eu dignissim nunc fermentum in.
+
+ Etiam blandit nisl vitae dolor molestie faucibus. In euismod, massa vitae commodo bibendum, urna augue pharetra nibh, et sagittis libero est in ligula. Mauris tincidunt risus ornare, rutrum augue in, blandit ligula. Aenean ultrices vel risus sit amet varius.
+
+ Vivamus pretium ultricies nisi a cursus. Integer cursus quam a metus ultricies, vel pulvinar nunc varius. Quisque facilisis lorem eget ipsum vehicula, laoreet congue lorem viverra.
+
+ Praesent dignissim, diam sed semper ultricies, diam ex laoreet justo, ac euismod massa metus pharetra nunc. Vestibulum sapien erat, consequat at eleifend id, suscipit sit amet mi.
+
+ Curabitur sed mauris vitae justo rutrum convallis ac sed justo. Ut nec est sed nisi feugiat egestas. Mauris accumsan mi eget nibh fermentum, in dignissim odio feugiat.
+
+ Maecenas augue dolor, gravida ut ultrices ultricies, condimentum et dui. In sed augue fermentum, posuere velit et, pulvinar tellus. Morbi id fermentum quam, at varius arcu.
+
+ Duis elementum vitae sapien id tincidunt. Aliquam velit ligula, sollicitudin eget placerat non, aliquam at erat. Pellentesque non porta metus. Mauris vel finibus sem, nec ullamcorper leo.
+
+ Nulla sit amet lorem vitae diam consectetur porttitor a cursus massa. Sed id ornare lorem. Sed placerat facilisis ipsum et ultricies. Sed eu semper enim, ut aliquet odio.
+
+ Sed nulla ex, pharetra vel porttitor congue, dictum et purus. Suspendisse vel risus sit amet nulla volutpat ullamcorper. Morbi et ullamcorper est. Pellentesque eget porta risus. Nullam non felis elementum, auctor massa et, consectetur neque.
+
+ Fusce sit amet arcu finibus, ornare sem sed, tempus nibh. Donec rutrum odio eget bibendum pulvinar. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
+
+ Phasellus sed risus diam. Vivamus mollis, risus ac feugiat pellentesque, ligula tortor finibus libero, et venenatis turpis lectus et justo. Suspendisse euismod mi at lectus sagittis dignissim. Mauris a ornare enim.
+ </p>
+ </div>
+ </div>
+ <div id="footer-bar" style="height: 500px; background-color: blue;"></div>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "container");
+ const scrollable = findAccessibleChildByID(docAcc, "message-scrollable");
+ const titleBar = findAccessibleChildByID(docAcc, "title-bar");
+ const footerBar = findAccessibleChildByID(docAcc, "footer-bar");
+ const dpr = await getContentDPR(browser);
+ const [, , , titleBarHeight] = Layout.getBounds(titleBar, dpr);
+ const [, , , scrollableHeight] = Layout.getBounds(scrollable, dpr);
+
+ // Verify that the child at this point is not the underlying paragraph.
+ info(
+ "Testing that the deepest child at this point is the overlaid section, not the paragraph beneath it."
+ );
+ await testChildAtPoint(
+ dpr,
+ 1,
+ titleBarHeight - 1,
+ container,
+ titleBar,
+ titleBar
+ );
+ await testChildAtPoint(
+ dpr,
+ 1,
+ titleBarHeight + scrollableHeight + 1,
+ container,
+ footerBar,
+ footerBar
+ );
+
+ await invokeContentTask(browser, [], () => {
+ // Scroll the text down.
+ let elem = content.document.getElementById("message-scrollable");
+ elem.scrollTo(0, elem.scrollHeight);
+ });
+ await waitForContentPaint(browser);
+
+ info(
+ "Testing that the deepest child at this point is still the overlaid section, after scrolling the paragraph."
+ );
+ await testChildAtPoint(
+ dpr,
+ 1,
+ titleBarHeight - 1,
+ container,
+ titleBar,
+ titleBar
+ );
+ await testChildAtPoint(
+ dpr,
+ 1,
+ titleBarHeight + scrollableHeight + 1,
+ container,
+ footerBar,
+ footerBar
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/hittest/browser_test_shadowroot.js b/accessible/tests/browser/hittest/browser_test_shadowroot.js
new file mode 100644
index 0000000000..ed67b02349
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_shadowroot.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+async function runTests(browser, accDoc) {
+ const dpr = await getContentDPR(browser);
+ let componentAcc = findAccessibleChildByID(accDoc, "component1");
+ await testChildAtPoint(
+ dpr,
+ 1,
+ 1,
+ componentAcc,
+ componentAcc.firstChild,
+ componentAcc.firstChild
+ );
+
+ componentAcc = findAccessibleChildByID(accDoc, "component2");
+ await testChildAtPoint(
+ dpr,
+ 1,
+ 1,
+ componentAcc,
+ componentAcc.firstChild,
+ componentAcc.firstChild
+ );
+}
+
+addAccessibleTask(
+ `
+ <div role="group" class="components" id="component1" style="display: inline-block;">
+ <!--
+ <div role="button" id="component-child"
+ style="width: 100px; height: 100px; background-color: pink;">
+ </div>
+ -->
+ </div>
+ <div role="group" class="components" id="component2" style="display: inline-block;">
+ <!--
+ <button>Hello world</button>
+ -->
+ </div>
+ <script>
+ // This routine adds the comment children of each 'component' to its
+ // shadow root.
+ var components = document.querySelectorAll(".components");
+ for (var i = 0; i < components.length; i++) {
+ var component = components[i];
+ var shadow = component.attachShadow({mode: "open"});
+ for (var child = component.firstChild; child; child = child.nextSibling) {
+ if (child.nodeType === 8)
+ shadow.innerHTML = child.data;
+ }
+ }
+ </script>
+ `,
+ runTests,
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/hittest/browser_test_text.js b/accessible/tests/browser/hittest/browser_test_text.js
new file mode 100644
index 0000000000..130f077f29
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_text.js
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+addAccessibleTask(
+ `
+a
+<div id="noChars" style="width: 5px; height: 5px;"><p></p></div>
+<p id="twoText"><span>a</span><span>b</span></p>
+<div id="iframeAtEnd" style="width: 20px; height: 20px;">
+ a
+ <iframe width="1" height="1"></iframe>
+</div>
+<button id="pointBeforeText">
+ <div style="display: flex;">
+ <div style="width: 100px; background-color: red;" role="none"></div>
+ test
+ <div style="width: 100px; background-color: blue;" role="none"></div>
+ </div>
+</button>
+ `,
+ async function (browser, docAcc) {
+ const dpr = await getContentDPR(browser);
+ // Test getOffsetAtPoint on a container containing no characters. The inner
+ // container does not include the requested point, but the outer one does.
+ const noChars = findAccessibleChildByID(docAcc, "noChars", [
+ Ci.nsIAccessibleText,
+ ]);
+ let [x, y] = Layout.getBounds(noChars, dpr);
+ await testOffsetAtPoint(noChars, x, y, COORDTYPE_SCREEN_RELATIVE, -1);
+
+ // Test that the correct offset is returned for a point in a second text
+ // leaf.
+ const twoText = findAccessibleChildByID(docAcc, "twoText", [
+ Ci.nsIAccessibleText,
+ ]);
+ const text2 = twoText.getChildAt(1);
+ [x, y] = Layout.getBounds(text2, dpr);
+ await testOffsetAtPoint(twoText, x, y, COORDTYPE_SCREEN_RELATIVE, 1);
+
+ // Test offsetAtPoint when there is an iframe at the end of the container.
+ const iframeAtEnd = findAccessibleChildByID(docAcc, "iframeAtEnd", [
+ Ci.nsIAccessibleText,
+ ]);
+ let width;
+ let height;
+ [x, y, width, height] = Layout.getBounds(iframeAtEnd, dpr);
+ x += width - 1;
+ y += height - 1;
+ await testOffsetAtPoint(iframeAtEnd, x, y, COORDTYPE_SCREEN_RELATIVE, -1);
+
+ // Test that 0 is returned if the point is within the container but before
+ // the rectangle at offset 0. This is buggy behavior that some users have
+ // unfortunately come to rely on (bug 1816601).
+ const pointBeforeText = findAccessibleChildByID(docAcc, "pointBeforeText", [
+ Ci.nsIAccessibleText,
+ ]);
+ [x, y, width, height] = Layout.getBounds(pointBeforeText, dpr);
+ await testOffsetAtPoint(
+ pointBeforeText,
+ x + 1,
+ y + 1,
+ COORDTYPE_SCREEN_RELATIVE,
+ 0
+ );
+ // But this buggy behavior only applies for a point before offset 0, not
+ // a point after the last offset. So it's asymmetrically buggy. :(
+ await testOffsetAtPoint(
+ pointBeforeText,
+ x + width - 1,
+ y + height - 1,
+ COORDTYPE_SCREEN_RELATIVE,
+ -1
+ );
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
diff --git a/accessible/tests/browser/hittest/browser_test_zoom.js b/accessible/tests/browser/hittest/browser_test_zoom.js
new file mode 100644
index 0000000000..84383df483
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_zoom.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+async function runTests(browser, accDoc) {
+ if (Services.appinfo.OS !== "Darwin") {
+ const p1 = findAccessibleChildByID(accDoc, "p1");
+ const p2 = findAccessibleChildByID(accDoc, "p2");
+ await hitTest(browser, accDoc, p1, p1.firstChild);
+ await hitTest(browser, accDoc, p2, p2.firstChild);
+
+ await invokeContentTask(browser, [], () => {
+ const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+
+ Layout.zoomDocument(content.document, 2.0);
+ content.document.body.offsetTop; // getBounds doesn't flush layout on its own.
+ });
+
+ await hitTest(browser, accDoc, p1, p1.firstChild);
+ await hitTest(browser, accDoc, p2, p2.firstChild);
+ } else {
+ todo(
+ false,
+ "Bug 746974 - deepest child must be correct on all platforms, disabling on Mac!"
+ );
+ }
+}
+
+addAccessibleTask(`<p id="p1">para 1</p><p id="p2">para 2</p>`, runTests, {
+ iframe: true,
+ remoteIframe: true,
+ // Ensure that all hittest elements are in view.
+ iframeAttrs: { style: "left: 100px; top: 100px;" },
+});
diff --git a/accessible/tests/browser/hittest/browser_test_zoom_text.js b/accessible/tests/browser/hittest/browser_test_zoom_text.js
new file mode 100644
index 0000000000..9e429c16b3
--- /dev/null
+++ b/accessible/tests/browser/hittest/browser_test_zoom_text.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+async function runTests(browser, accDoc) {
+ const expectedLength = await invokeContentTask(browser, [], () => {
+ const { CommonUtils } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs"
+ );
+ const hyperText = CommonUtils.getNode("paragraph", content.document);
+ return Math.floor(hyperText.textContent.length / 2);
+ });
+ const hyperText = findAccessibleChildByID(accDoc, "paragraph", [
+ Ci.nsIAccessibleText,
+ ]);
+ const textNode = hyperText.firstChild;
+
+ let [x, y, width, height] = Layout.getBounds(
+ textNode,
+ await getContentDPR(browser)
+ );
+
+ await testOffsetAtPoint(
+ hyperText,
+ x + width / 2,
+ y + height / 2,
+ COORDTYPE_SCREEN_RELATIVE,
+ expectedLength
+ );
+
+ await invokeContentTask(browser, [], () => {
+ const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+
+ Layout.zoomDocument(content.document, 2.0);
+ content.document.body.offsetTop; // getBounds doesn't flush layout on its own.
+ });
+
+ [x, y, width, height] = Layout.getBounds(
+ textNode,
+ await getContentDPR(browser)
+ );
+
+ await testOffsetAtPoint(
+ hyperText,
+ x + width / 2,
+ y + height / 2,
+ COORDTYPE_SCREEN_RELATIVE,
+ expectedLength
+ );
+}
+
+addAccessibleTask(
+ `<p id="paragraph" style="font-family: monospace;">hello world hello world</p>`,
+ runTests,
+ {
+ iframe: true,
+ remoteIframe: true,
+ iframeAttrs: { style: "width: 600px; height: 600px;" },
+ }
+);
diff --git a/accessible/tests/browser/hittest/head.js b/accessible/tests/browser/hittest/head.js
new file mode 100644
index 0000000000..c2904b1578
--- /dev/null
+++ b/accessible/tests/browser/hittest/head.js
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* exported CommonUtils, testChildAtPoint, Layout, hitTest, testOffsetAtPoint */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
+
+const { CommonUtils } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs"
+);
+
+const { Layout } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+);
+
+function getChildAtPoint(container, x, y, findDeepestChild) {
+ try {
+ return findDeepestChild
+ ? container.getDeepestChildAtPoint(x, y)
+ : container.getChildAtPoint(x, y);
+ } catch (e) {
+ // Failed to get child at point.
+ }
+ info("could not get child at point");
+ return null;
+}
+
+async function testChildAtPoint(dpr, x, y, container, child, grandChild) {
+ const [containerX, containerY] = Layout.getBounds(container, dpr);
+ x += containerX;
+ y += containerY;
+ let actual = null;
+ await untilCacheIs(
+ () => {
+ actual = getChildAtPoint(container, x, y, false);
+ info(`Got direct child match of ${CommonUtils.prettyName(actual)}`);
+ return actual;
+ },
+ child,
+ `Wrong direct child accessible at the point (${x}, ${y}) of ${CommonUtils.prettyName(
+ container
+ )}, sought ${CommonUtils.prettyName(
+ child
+ )} and got ${CommonUtils.prettyName(actual)}`
+ );
+ actual = null;
+ await untilCacheIs(
+ () => {
+ actual = getChildAtPoint(container, x, y, true);
+ info(`Got deepest child match of ${CommonUtils.prettyName(actual)}`);
+ return actual;
+ },
+ grandChild,
+ `Wrong deepest child accessible at the point (${x}, ${y}) of ${CommonUtils.prettyName(
+ container
+ )}, sought ${CommonUtils.prettyName(
+ grandChild
+ )} and got ${CommonUtils.prettyName(actual)}`
+ );
+}
+
+/**
+ * Test if getChildAtPoint returns the given child and grand child accessibles
+ * at coordinates of child accessible (direct and deep hit test).
+ */
+async function hitTest(browser, container, child, grandChild) {
+ const [childX, childY] = await getContentBoundsForDOMElm(
+ browser,
+ getAccessibleDOMNodeID(child)
+ );
+ const x = childX + 1;
+ const y = childY + 1;
+
+ await untilCacheIs(
+ () => getChildAtPoint(container, x, y, false),
+ child,
+ `Wrong direct child accessible at the point (${x}, ${y}) of ${CommonUtils.prettyName(
+ container
+ )}, sought ${CommonUtils.prettyName(child)}`
+ );
+ await untilCacheIs(
+ () => getChildAtPoint(container, x, y, true),
+ grandChild,
+ `Wrong deepest child accessible at the point (${x}, ${y}) of ${CommonUtils.prettyName(
+ container
+ )}, sought ${CommonUtils.prettyName(grandChild)}`
+ );
+}
+
+/**
+ * Test if getOffsetAtPoint returns the given text offset at given coordinates.
+ */
+async function testOffsetAtPoint(hyperText, x, y, coordType, expectedOffset) {
+ await untilCacheIs(
+ () => hyperText.getOffsetAtPoint(x, y, coordType),
+ expectedOffset,
+ `Wrong offset at given point (${x}, ${y}) for ${prettyName(hyperText)}`
+ );
+}
diff --git a/accessible/tests/browser/mac/browser.toml b/accessible/tests/browser/mac/browser.toml
new file mode 100644
index 0000000000..b180a42ab7
--- /dev/null
+++ b/accessible/tests/browser/mac/browser.toml
@@ -0,0 +1,95 @@
+[DEFAULT]
+subsuite = "a11y"
+skip-if = ["os != 'mac'"]
+support-files = [
+ "head.js",
+ "doc_aria_tabs.html",
+ "doc_textmarker_test.html",
+ "doc_rich_listbox.xhtml",
+ "doc_menulist.xhtml",
+ "doc_tree.xhtml",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/letters.gif",
+ "!/accessible/tests/mochitest/moz.png",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_app.js"]
+https_first_disabled = true
+
+["browser_aria_busy.js"]
+
+["browser_aria_controls_flowto.js"]
+
+["browser_aria_current.js"]
+
+["browser_aria_expanded.js"]
+
+["browser_aria_haspopup.js"]
+
+["browser_aria_setsize.js"]
+
+["browser_attributed_text.js"]
+
+["browser_bounds.js"]
+
+["browser_details_summary.js"]
+
+["browser_focus.js"]
+
+["browser_heading.js"]
+
+["browser_hierarchy.js"]
+
+["browser_input.js"]
+
+["browser_label_title.js"]
+
+["browser_link.js"]
+
+["browser_live_regions.js"]
+
+["browser_mathml.js"]
+
+["browser_menulist.js"]
+
+["browser_navigate.js"]
+
+["browser_outline.js"]
+
+["browser_outline_xul.js"]
+
+["browser_popupbutton.js"]
+
+["browser_radio_position.js"]
+
+["browser_range.js"]
+
+["browser_required.js"]
+
+["browser_rich_listbox.js"]
+
+["browser_roles_elements.js"]
+
+["browser_rootgroup.js"]
+
+["browser_rotor.js"]
+
+["browser_selectables.js"]
+
+["browser_table.js"]
+
+["browser_text_basics.js"]
+
+["browser_text_input.js"]
+skip-if = ["os == 'mac'"] # Bug 1778821
+
+["browser_text_leaf.js"]
+
+["browser_text_selection.js"]
+
+["browser_toggle_radio_check.js"]
+
+["browser_webarea.js"]
diff --git a/accessible/tests/browser/mac/browser_app.js b/accessible/tests/browser/mac/browser_app.js
new file mode 100644
index 0000000000..bedefae440
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_app.js
@@ -0,0 +1,355 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { UrlbarTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+);
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+function getMacAccessible(accOrElmOrID) {
+ return new Promise(resolve => {
+ let intervalId = setInterval(() => {
+ let acc = getAccessible(accOrElmOrID);
+ if (acc) {
+ clearInterval(intervalId);
+ resolve(
+ acc.nativeInterface.QueryInterface(Ci.nsIAccessibleMacInterface)
+ );
+ }
+ }, 10);
+ });
+}
+
+/**
+ * Test a11yUtils announcements are exposed to VO
+ */
+add_task(async () => {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,"
+ );
+ const alert = document.getElementById("a11y-announcement");
+ ok(alert, "Found alert to send announcements");
+
+ const alerted = waitForMacEvent("AXAnnouncementRequested", (iface, data) => {
+ return data.AXAnnouncementKey == "hello world";
+ });
+
+ A11yUtils.announce({
+ raw: "hello world",
+ });
+ await alerted;
+ await BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Test browser tabs
+ */
+add_task(async () => {
+ let newTabs = await Promise.all([
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<title>Two</title>"
+ ),
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<title>Three</title>"
+ ),
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html,<title>Four</title>"
+ ),
+ ]);
+
+ // Mochitests spawn with a tab, and we've opened 3 more for a total of 4 tabs
+ is(gBrowser.tabs.length, 4, "We now have 4 open tabs");
+
+ let tablist = await getMacAccessible("tabbrowser-tabs");
+ is(
+ tablist.getAttributeValue("AXRole"),
+ "AXTabGroup",
+ "Correct role for tablist"
+ );
+
+ let tabMacAccs = tablist.getAttributeValue("AXTabs");
+ is(tabMacAccs.length, 4, "4 items in AXTabs");
+
+ let selectedTabs = tablist.getAttributeValue("AXSelectedChildren");
+ is(selectedTabs.length, 1, "one selected tab");
+
+ let tab = selectedTabs[0];
+ is(tab.getAttributeValue("AXRole"), "AXRadioButton", "Correct role for tab");
+ is(
+ tab.getAttributeValue("AXSubrole"),
+ "AXTabButton",
+ "Correct subrole for tab"
+ );
+ is(tab.getAttributeValue("AXTitle"), "Four", "Correct title for tab");
+
+ let tabToSelect = tabMacAccs[2];
+ is(
+ tabToSelect.getAttributeValue("AXTitle"),
+ "Three",
+ "Correct title for tab"
+ );
+
+ let actions = tabToSelect.actionNames;
+ ok(true, actions);
+ ok(actions.includes("AXPress"), "Has switch action");
+
+ // When tab is clicked selection of tab group changes,
+ // and focus goes to the web area. Wait for both.
+ let evt = Promise.all([
+ waitForMacEvent("AXSelectedChildrenChanged"),
+ waitForMacEvent(
+ "AXFocusedUIElementChanged",
+ iface => iface.getAttributeValue("AXRole") == "AXWebArea"
+ ),
+ ]);
+ tabToSelect.performAction("AXPress");
+ await evt;
+
+ selectedTabs = tablist.getAttributeValue("AXSelectedChildren");
+ is(selectedTabs.length, 1, "one selected tab");
+ is(
+ selectedTabs[0].getAttributeValue("AXTitle"),
+ "Three",
+ "Correct title for tab"
+ );
+
+ // Close all open tabs
+ await Promise.all(newTabs.map(t => BrowserTestUtils.removeTab(t)));
+});
+
+/**
+ * Test ignored invisible items in root
+ */
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:license",
+ },
+ async browser => {
+ let root = await getMacAccessible(document);
+ let rootChildCount = () => root.getAttributeValue("AXChildren").length;
+
+ // With no popups, the root accessible has 5 visible children:
+ // 1. Tab bar (#TabsToolbar)
+ // 2. Navigation bar (#nav-bar)
+ // 3. Content area (#tabbrowser-tabpanels)
+ // 4. Some fullscreen pointer grabber (#fullscreen-and-pointerlock-wrapper)
+ // 5. Accessibility announcements dialog (#a11y-announcement)
+ let baseRootChildCount = 5;
+ is(
+ rootChildCount(),
+ baseRootChildCount,
+ "Root with no popups has 5 children"
+ );
+
+ // Open a context menu
+ const menu = document.getElementById("contentAreaContextMenu");
+ if (
+ Services.prefs.getBoolPref("widget.macos.native-context-menus", false)
+ ) {
+ // Native context menu - do not expect accessibility notifications.
+ let popupshown = BrowserTestUtils.waitForPopupEvent(menu, "shown");
+ EventUtils.synthesizeMouseAtCenter(document.body, {
+ type: "contextmenu",
+ });
+ await popupshown;
+
+ is(
+ rootChildCount(),
+ baseRootChildCount,
+ "Native context menus do not show up in the root children"
+ );
+
+ // Close context menu
+ let popuphidden = BrowserTestUtils.waitForPopupEvent(menu, "hidden");
+ menu.hidePopup();
+ await popuphidden;
+ } else {
+ // Non-native menu
+ EventUtils.synthesizeMouseAtCenter(document.body, {
+ type: "contextmenu",
+ });
+ await waitForMacEvent("AXMenuOpened");
+
+ // Now root has 1 more child
+ is(rootChildCount(), baseRootChildCount + 1, "Root has 1 more child");
+
+ // Close context menu
+ let closed = waitForMacEvent("AXMenuClosed", "contentAreaContextMenu");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await BrowserTestUtils.waitForPopupEvent(menu, "hidden");
+ await closed;
+ }
+
+ // We're back to base child count
+ is(rootChildCount(), baseRootChildCount, "Root has original child count");
+
+ // Open site identity popup
+ document.getElementById("identity-icon-box").click();
+ const identityPopup = document.getElementById("identity-popup");
+ await BrowserTestUtils.waitForPopupEvent(identityPopup, "shown");
+
+ // Now root has another child
+ is(rootChildCount(), baseRootChildCount + 1, "Root has another child");
+
+ // Close popup
+ EventUtils.synthesizeKey("KEY_Escape");
+ await BrowserTestUtils.waitForPopupEvent(identityPopup, "hidden");
+
+ // We're back to the base child count
+ is(rootChildCount(), baseRootChildCount, "Root has the base child count");
+ }
+ );
+});
+
+/**
+ * Tests for location bar
+ */
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ url: "http://example.com",
+ },
+ async browser => {
+ let input = await getMacAccessible("urlbar-input");
+ is(
+ input.getAttributeValue("AXValue"),
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ UrlbarTestUtils.trimURL("http://example.com"),
+ "Location bar has correct value"
+ );
+ }
+ );
+});
+
+/**
+ * Test context menu
+ */
+add_task(async () => {
+ if (Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) {
+ ok(true, "We cannot inspect native context menu contents; skip this test.");
+ return;
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: 'data:text/html,<a id="exampleLink" href="https://example.com">link</a>',
+ },
+ async browser => {
+ if (!Services.search.isInitialized) {
+ let aStatus = await Services.search.init();
+ Assert.ok(Components.isSuccessCode(aStatus));
+ Assert.ok(Services.search.isInitialized);
+ }
+
+ const hasContainers =
+ Services.prefs.getBoolPref("privacy.userContext.enabled") &&
+ !!ContextualIdentityService.getPublicIdentities().length;
+ info(`${hasContainers ? "Do" : "Don't"} expect containers item.`);
+ const hasInspectA11y =
+ Services.prefs.getBoolPref("devtools.everOpened", false) ||
+ Services.prefs.getIntPref("devtools.selfxss.count", 0) > 0;
+ info(`${hasInspectA11y ? "Do" : "Don't"} expect inspect a11y item.`);
+
+ // synthesize a right click on the link to open the link context menu
+ let menu = document.getElementById("contentAreaContextMenu");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#exampleLink",
+ { type: "contextmenu" },
+ browser
+ );
+ await waitForMacEvent("AXMenuOpened");
+
+ menu = await getMacAccessible(menu);
+ let menuChildren = menu.getAttributeValue("AXChildren");
+ const expectedChildCount = 12 + +hasContainers + +hasInspectA11y;
+ is(
+ menuChildren.length,
+ expectedChildCount,
+ `Context menu on link contains ${expectedChildCount} items.`
+ );
+ // items at indicies 3, 9, and 11 are the splitters when containers exist
+ // everything else should be a menu item, otherwise indicies of splitters are
+ // 3, 8, and 10
+ const splitterIndicies = hasContainers ? [4, 9, 11] : [3, 8, 10];
+ for (let i = 0; i < menuChildren.length; i++) {
+ if (splitterIndicies.includes(i)) {
+ is(
+ menuChildren[i].getAttributeValue("AXRole"),
+ "AXSplitter",
+ "found splitter in menu"
+ );
+ } else {
+ is(
+ menuChildren[i].getAttributeValue("AXRole"),
+ "AXMenuItem",
+ "found menu item in menu"
+ );
+ }
+ }
+
+ // check the containers sub menu in depth if it exists
+ if (hasContainers) {
+ is(
+ menuChildren[1].getAttributeValue("AXVisibleChildren"),
+ null,
+ "Submenu 1 has no visible chldren when hidden"
+ );
+
+ // focus the first submenu
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ await waitForMacEvent("AXMenuOpened");
+
+ // after the submenu is opened, refetch it
+ menu = document.getElementById("contentAreaContextMenu");
+ menu = await getMacAccessible(menu);
+ menuChildren = menu.getAttributeValue("AXChildren");
+
+ // verify submenu-menuitem's attributes
+ is(
+ menuChildren[1].getAttributeValue("AXChildren").length,
+ 1,
+ "Submenu 1 has one child when open"
+ );
+ const subMenu = menuChildren[1].getAttributeValue("AXChildren")[0];
+ is(
+ subMenu.getAttributeValue("AXRole"),
+ "AXMenu",
+ "submenu has role of menu"
+ );
+ const subMenuChildren = subMenu.getAttributeValue("AXChildren");
+ is(subMenuChildren.length, 4, "sub menu has 4 children");
+ is(
+ subMenu.getAttributeValue("AXVisibleChildren").length,
+ 4,
+ "submenu has 4 visible children"
+ );
+
+ // close context menu
+ EventUtils.synthesizeKey("KEY_Escape");
+ await waitForMacEvent("AXMenuClosed");
+ }
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ await waitForMacEvent("AXMenuClosed");
+ }
+ );
+});
diff --git a/accessible/tests/browser/mac/browser_aria_busy.js b/accessible/tests/browser/mac/browser_aria_busy.js
new file mode 100644
index 0000000000..e75d334e29
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_busy.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test aria-busy
+ */
+addAccessibleTask(
+ `<div id="section" role="group">Hello</div>`,
+ async (browser, accDoc) => {
+ let section = getNativeInterface(accDoc, "section");
+
+ ok(!section.getAttributeValue("AXElementBusy"), "section is not busy");
+
+ let busyChanged = waitForMacEvent("AXElementBusyChanged", "section");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("section")
+ .setAttribute("aria-busy", "true");
+ });
+ await busyChanged;
+
+ ok(section.getAttributeValue("AXElementBusy"), "section is busy");
+
+ busyChanged = waitForMacEvent("AXElementBusyChanged", "section");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("section")
+ .setAttribute("aria-busy", "false");
+ });
+ await busyChanged;
+
+ ok(!section.getAttributeValue("AXElementBusy"), "section is not busy");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_aria_controls_flowto.js b/accessible/tests/browser/mac/browser_aria_controls_flowto.js
new file mode 100644
index 0000000000..5950a60399
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_controls_flowto.js
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test aria-controls
+ */
+addAccessibleTask(
+ `<button aria-controls="info" id="info-button">Show info</button>
+ <div id="info">Information.</div>
+ <div id="more-info">More information.</div>`,
+ async (browser, accDoc) => {
+ const getAriaControls = id =>
+ JSON.stringify(
+ getNativeInterface(accDoc, id)
+ .getAttributeValue("AXARIAControls")
+ .map(e => e.getAttributeValue("AXDOMIdentifier"))
+ );
+
+ await untilCacheIs(
+ () => getAriaControls("info-button"),
+ JSON.stringify(["info"]),
+ "Info-button has correct initial controls"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("info-button")
+ .setAttribute("aria-controls", "info more-info");
+ });
+
+ await untilCacheIs(
+ () => getAriaControls("info-button"),
+ JSON.stringify(["info", "more-info"]),
+ "Info-button has correct controls after mutation"
+ );
+ }
+);
+
+function getLinkedUIElements(accDoc, id) {
+ return JSON.stringify(
+ getNativeInterface(accDoc, id)
+ .getAttributeValue("AXLinkedUIElements")
+ .map(e => e.getAttributeValue("AXDOMIdentifier"))
+ );
+}
+
+/**
+ * Test aria-flowto
+ */
+addAccessibleTask(
+ `<button aria-flowto="info" id="info-button">Show info</button>
+ <div id="info">Information.</div>
+ <div id="more-info">More information.</div>`,
+ async (browser, accDoc) => {
+ await untilCacheIs(
+ () => getLinkedUIElements(accDoc, "info-button"),
+ JSON.stringify(["info"]),
+ "Info-button has correct initial linked elements"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("info-button")
+ .setAttribute("aria-flowto", "info more-info");
+ });
+
+ await untilCacheIs(
+ () => getLinkedUIElements(accDoc, "info-button"),
+ JSON.stringify(["info", "more-info"]),
+ "Info-button has correct linked elements after mutation"
+ );
+ }
+);
+
+/**
+ * Test aria-controls
+ */
+addAccessibleTask(
+ `<input type="radio" id="cat-radio" name="animal"><label for="cat">Cat</label>
+ <input type="radio" id="dog-radio" name="animal" aria-flowto="info"><label for="dog">Dog</label>
+ <div id="info">Information.</div>`,
+ async (browser, accDoc) => {
+ await untilCacheIs(
+ () => getLinkedUIElements(accDoc, "dog-radio"),
+ JSON.stringify(["cat-radio", "dog-radio", "info"]),
+ "dog-radio has correct linked elements"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_aria_current.js b/accessible/tests/browser/mac/browser_aria_current.js
new file mode 100644
index 0000000000..02c7a71b67
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_current.js
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test aria-current
+ */
+addAccessibleTask(
+ `<a id="one" href="%23" aria-current="page">One</a><a id="two" href="%23">Two</a>`,
+ async (browser, accDoc) => {
+ let one = getNativeInterface(accDoc, "one");
+ let two = getNativeInterface(accDoc, "two");
+
+ is(
+ one.getAttributeValue("AXARIACurrent"),
+ "page",
+ "Correct aria-current for #one"
+ );
+ is(
+ two.getAttributeValue("AXARIACurrent"),
+ null,
+ "Correct aria-current for #two"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("one")
+ .setAttribute("aria-current", "step");
+ });
+
+ is(
+ one.getAttributeValue("AXARIACurrent"),
+ "step",
+ "Correct aria-current for #one"
+ );
+
+ let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "one");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("one").removeAttribute("aria-current");
+ });
+ await stateChanged;
+
+ is(
+ one.getAttributeValue("AXARIACurrent"),
+ null,
+ "Correct aria-current for #one"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_aria_expanded.js b/accessible/tests/browser/mac/browser_aria_expanded.js
new file mode 100644
index 0000000000..48fb615266
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_expanded.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
+
+// Test aria-expanded on a button
+addAccessibleTask(
+ `hello world<br>
+ <button aria-expanded="false" id="b">I am a button</button><br>
+ goodbye`,
+ async (browser, accDoc) => {
+ let button = getNativeInterface(accDoc, "b");
+ is(button.getAttributeValue("AXExpanded"), 0, "button is not expanded");
+
+ let stateChanged = Promise.all([
+ waitForStateChange("b", STATE_EXPANDED, true),
+ waitForStateChange("b", STATE_COLLAPSED, false),
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("b")
+ .setAttribute("aria-expanded", "true");
+ });
+ await stateChanged;
+ is(button.getAttributeValue("AXExpanded"), 1, "button is expanded");
+
+ stateChanged = Promise.all([
+ waitForStateChange("b", STATE_EXPANDED, false),
+ waitForStateChange("b", EXT_STATE_EXPANDABLE, false, true),
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("b").removeAttribute("aria-expanded");
+ });
+ await stateChanged;
+
+ ok(
+ !button.attributeNames.includes("AXExpanded"),
+ "button has no expanded attr"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_aria_haspopup.js b/accessible/tests/browser/mac/browser_aria_haspopup.js
new file mode 100644
index 0000000000..57f1e50f65
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_haspopup.js
@@ -0,0 +1,320 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test aria-haspopup
+ */
+addAccessibleTask(
+ `
+ <button aria-haspopup="false" id="false">action</button>
+
+ <button aria-haspopup="menu" id="menu">action</button>
+
+ <button aria-haspopup="listbox" id="listbox">action</button>
+
+ <button aria-haspopup="tree" id="tree">action</button>
+
+ <button aria-haspopup="grid" id="grid">action</button>
+
+ <button aria-haspopup="dialog" id="dialog">action</button>
+
+ `,
+ async (browser, accDoc) => {
+ // FALSE
+ let falseID = getNativeInterface(accDoc, "false");
+ is(
+ falseID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup val for button with false"
+ );
+ is(
+ falseID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue val for button with false"
+ );
+ let attrChanged = waitForEvent(EVENT_STATE_CHANGE, "false");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("false")
+ .setAttribute("aria-haspopup", "true");
+ });
+ await attrChanged;
+
+ is(
+ falseID.getAttributeValue("AXPopupValue"),
+ "true",
+ "Correct AXPopupValue after change for false"
+ );
+ is(
+ falseID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup val for button with true"
+ );
+
+ let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "false");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("false").removeAttribute("aria-haspopup");
+ });
+ await stateChanged;
+
+ is(
+ falseID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue after remove for false"
+ );
+ is(
+ falseID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup val for button after remove"
+ );
+
+ // MENU
+ let menuID = getNativeInterface(accDoc, "menu");
+ is(
+ menuID.getAttributeValue("AXPopupValue"),
+ "menu",
+ "Correct AXPopupValue val for button with menu"
+ );
+ is(
+ menuID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup val for button with menu"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("menu")
+ .setAttribute("aria-haspopup", "true");
+ });
+
+ await untilCacheIs(
+ () => menuID.getAttributeValue("AXPopupValue"),
+ "true",
+ "Correct AXPopupValue after change for menu"
+ );
+ is(
+ menuID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup val for button with menu"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "menu");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("menu").removeAttribute("aria-haspopup");
+ });
+ await stateChanged;
+
+ await untilCacheIs(
+ () => menuID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue after remove for menu"
+ );
+ is(
+ menuID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup val for button after remove"
+ );
+
+ // LISTBOX
+ let listboxID = getNativeInterface(accDoc, "listbox");
+ is(
+ listboxID.getAttributeValue("AXPopupValue"),
+ "listbox",
+ "Correct AXPopupValue for button with listbox"
+ );
+ is(
+ listboxID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with listbox"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("listbox")
+ .setAttribute("aria-haspopup", "true");
+ });
+
+ await untilCacheIs(
+ () => listboxID.getAttributeValue("AXPopupValue"),
+ "true",
+ "Correct AXPopupValue after change for listbox"
+ );
+ is(
+ listboxID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with listbox"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "listbox");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("listbox")
+ .removeAttribute("aria-haspopup");
+ });
+ await stateChanged;
+
+ is(
+ listboxID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue after remove for listbox"
+ );
+ is(
+ listboxID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup for button with listbox"
+ );
+
+ // TREE
+ let treeID = getNativeInterface(accDoc, "tree");
+ is(
+ treeID.getAttributeValue("AXPopupValue"),
+ "tree",
+ "Correct AXPopupValue for button with tree"
+ );
+ is(
+ treeID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with tree"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("tree")
+ .setAttribute("aria-haspopup", "true");
+ });
+
+ await untilCacheIs(
+ () => treeID.getAttributeValue("AXPopupValue"),
+ "true",
+ "Correct AXPopupValue after change for tree"
+ );
+ is(
+ treeID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with tree"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "tree");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("tree").removeAttribute("aria-haspopup");
+ });
+ await stateChanged;
+
+ is(
+ treeID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue after remove for tree"
+ );
+ is(
+ treeID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup for button with tree after remove"
+ );
+
+ // GRID
+ let gridID = getNativeInterface(accDoc, "grid");
+ is(
+ gridID.getAttributeValue("AXPopupValue"),
+ "grid",
+ "Correct AXPopupValue for button with grid"
+ );
+ is(
+ gridID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with grid"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("grid")
+ .setAttribute("aria-haspopup", "true");
+ });
+
+ await untilCacheIs(
+ () => gridID.getAttributeValue("AXPopupValue"),
+ "true",
+ "Correct AXPopupValue after change for grid"
+ );
+ is(
+ gridID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with grid"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "grid");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("grid").removeAttribute("aria-haspopup");
+ });
+ await stateChanged;
+
+ is(
+ gridID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue after remove for grid"
+ );
+ is(
+ gridID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup for button with grid after remove"
+ );
+
+ // DIALOG
+ let dialogID = getNativeInterface(accDoc, "dialog");
+ is(
+ dialogID.getAttributeValue("AXPopupValue"),
+ "dialog",
+ "Correct AXPopupValue for button with dialog"
+ );
+ is(
+ dialogID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with dialog"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("dialog")
+ .setAttribute("aria-haspopup", "true");
+ });
+
+ await untilCacheIs(
+ () => dialogID.getAttributeValue("AXPopupValue"),
+ "true",
+ "Correct AXPopupValue after change for dialog"
+ );
+ is(
+ dialogID.getAttributeValue("AXHasPopup"),
+ 1,
+ "Correct AXHasPopup for button with dialog"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "dialog");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("dialog")
+ .removeAttribute("aria-haspopup");
+ });
+ await stateChanged;
+
+ is(
+ dialogID.getAttributeValue("AXPopupValue"),
+ null,
+ "Correct AXPopupValue after remove for dialog"
+ );
+ is(
+ dialogID.getAttributeValue("AXHasPopup"),
+ 0,
+ "Correct AXHasPopup for button with dialog after remove"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_aria_setsize.js b/accessible/tests/browser/mac/browser_aria_setsize.js
new file mode 100644
index 0000000000..e37afb4e63
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_aria_setsize.js
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test aria-setsize and aria-posinset
+ */
+addAccessibleTask(
+ `<div role="listbox" id="select" aria-label="Choose a number">
+ <div role="option" id="two" aria-setsize="5" aria-posinset="2">Two</div>
+ <div role="option" id="three" aria-setsize="5" aria-posinset="3">Three</div>
+ <div role="option" id="four" aria-setsize="5" aria-posinset="4">Four</div>
+ </div>
+ <p id="p">Hello</p>`,
+ async (browser, accDoc) => {
+ function testPosInSet(id, setsize, posinset) {
+ let elem = getNativeInterface(accDoc, id);
+ is(
+ elem.getAttributeValue("AXARIASetSize"),
+ setsize,
+ `${id}: Correct set size`
+ );
+
+ is(
+ elem.getAttributeValue("AXARIAPosInSet"),
+ posinset,
+ `${id}: Correct position in set`
+ );
+ }
+
+ testPosInSet("two", 5, 2);
+ testPosInSet("three", 5, 3);
+ testPosInSet("four", 5, 4);
+
+ let p = getNativeInterface(accDoc, "p");
+ ok(
+ !p.attributeNames.includes("AXARIASetSize"),
+ "Paragraph should not have AXARIASetSize"
+ );
+ ok(
+ !p.attributeNames.includes("AXARIAPosInSet"),
+ "Paragraph should not have AXARIAPosInSet"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_attributed_text.js b/accessible/tests/browser/mac/browser_attributed_text.js
new file mode 100644
index 0000000000..afd4d2c4dc
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_attributed_text.js
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test read-only attributed strings
+addAccessibleTask(
+ `<h1>hello <a href="#" id="a1">world</a></h1>
+ <p>this <b style="color: red; background-color: yellow;" aria-invalid="spelling">is</b> <span style="text-decoration: underline dotted green;">a</span> <a href="#" id="a2">test</a></p>`,
+ async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUnorderedTextMarkers",
+ [
+ macDoc.getAttributeValue("AXStartTextMarker"),
+ macDoc.getAttributeValue("AXEndTextMarker"),
+ ]
+ );
+
+ let attributedText = macDoc.getParameterizedAttributeValue(
+ "AXAttributedStringForTextMarkerRange",
+ range
+ );
+
+ let attributesList = attributedText.map(
+ ({
+ string,
+ AXForegroundColor,
+ AXBackgroundColor,
+ AXUnderline,
+ AXUnderlineColor,
+ AXHeadingLevel,
+ AXFont,
+ AXLink,
+ AXMarkedMisspelled,
+ }) => [
+ string,
+ AXForegroundColor,
+ AXBackgroundColor,
+ AXUnderline,
+ AXUnderlineColor,
+ AXHeadingLevel,
+ AXFont.AXFontSize,
+ AXLink ? AXLink.getAttributeValue("AXDOMIdentifier") : null,
+ AXMarkedMisspelled,
+ ]
+ );
+
+ Assert.deepEqual(attributesList, [
+ // string, fg color, bg color, underline, underline color, heading level, font size, link id, misspelled
+ ["hello ", "#000000", "#ffffff", null, null, 1, 32, null, null],
+ ["world", "#0000ee", "#ffffff", 1, "#0000ee", 1, 32, "a1", null],
+ ["this ", "#000000", "#ffffff", null, null, null, 16, null, null],
+ ["is", "#ff0000", "#ffff00", null, null, null, 16, null, 1],
+ [" ", "#000000", "#ffffff", null, null, null, 16, null, null],
+ ["a", "#000000", "#ffffff", 1, "#008000", null, 16, null, null],
+ [" ", "#000000", "#ffffff", null, null, null, 16, null, null],
+ ["test", "#0000ee", "#ffffff", 1, "#0000ee", null, 16, "a2", null],
+ ]);
+
+ // Test different NSRange parameters for AXAttributedStringForRange
+ let worldLeaf = findAccessibleChildByID(accDoc, "a1").firstChild;
+ let wordStaticText = worldLeaf.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ attributedText = wordStaticText.getParameterizedAttributeValue(
+ "AXAttributedStringForRange",
+ NSRange(4, 1)
+ );
+ is(attributedText.length, 1, "Last character is in single attribute run");
+ is(attributedText[0].string, "d", "Last character matches");
+
+ attributedText = wordStaticText.getParameterizedAttributeValue(
+ "AXAttributedStringForRange",
+ NSRange(5, 1)
+ );
+ is(attributedText.length, 0, "Range is past accessible bounds");
+ }
+);
+
+// Test misspelling in text area
+addAccessibleTask(
+ `<textarea id="t">hello worlf, i love you</textarea>`,
+ async (browser, accDoc) => {
+ let textArea = getNativeInterface(accDoc, "t");
+ let spellDone = waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED, "t");
+ textArea.setAttributeValue("AXFocused", true);
+
+ let attributedText = [];
+
+ // For some internal reason we get several text attribute change events
+ // before the attributed text returned provides the misspelling attributes.
+ while (true) {
+ await spellDone;
+
+ let range = textArea.getAttributeValue("AXVisibleCharacterRange");
+ attributedText = textArea.getParameterizedAttributeValue(
+ "AXAttributedStringForRange",
+ NSRange(...range)
+ );
+
+ if (attributedText.length != 3) {
+ spellDone = waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED, "t");
+ } else {
+ break;
+ }
+ }
+
+ ok(attributedText[1].AXMarkedMisspelled);
+ }
+);
+
+// Test getting a span of attributed text that includes an empty input element.
+addAccessibleTask(
+ `hello <input id="input"> world<button></button>`,
+ async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUnorderedTextMarkers",
+ [
+ macDoc.getAttributeValue("AXStartTextMarker"),
+ macDoc.getAttributeValue("AXEndTextMarker"),
+ ]
+ );
+
+ let attributedText = macDoc.getParameterizedAttributeValue(
+ "AXAttributedStringForTextMarkerRange",
+ range
+ );
+
+ let text = macDoc.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ range
+ );
+
+ is(
+ attributedText.length,
+ 4,
+ "Should be 4 attribute runs for 2 texts, input and button"
+ );
+ is(attributedText[0].string, `hello `, "Attributed string is correct");
+ ok(
+ !attributedText[0].AXAttachment,
+ "Regular string attributes run doesn't have attachment"
+ );
+ is(
+ attributedText[1].AXAttachment.getAttributeValue("AXRole"),
+ "AXTextField",
+ "Entry text attribute run has correct attachment"
+ );
+ is(
+ attributedText[3].AXAttachment.getAttributeValue("AXRole"),
+ "AXButton",
+ "Button text attribute run has correct attachment"
+ );
+ is(text, `hello world${kEmbedChar}`, "Unattributed string is correct");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_bounds.js b/accessible/tests/browser/mac/browser_bounds.js
new file mode 100644
index 0000000000..09343d7c9d
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_bounds.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test position, size for onscreen content
+ */
+addAccessibleTask(
+ `I am some extra content<br>
+ <div id="hello" style="display:inline;">hello</div><br>
+ <div id="world" style="display:inline;">hello world<br>I am some text</div>`,
+ async (browser, accDoc) => {
+ const hello = getNativeInterface(accDoc, "hello");
+ const world = getNativeInterface(accDoc, "world");
+ ok(hello.getAttributeValue("AXFrame"), "Hello's frame attr is not null");
+ ok(world.getAttributeValue("AXFrame"), "World's frame attr is not null");
+
+ // AXSize and AXPosition are composed of AXFrame components, so we
+ // test them here instead of calling AXFrame directly.
+ const [helloWidth, helloHeight] = hello.getAttributeValue("AXSize");
+ const [worldWidth, worldHeight] = world.getAttributeValue("AXSize");
+ ok(helloWidth > 0, "Hello has a positive width");
+ ok(helloHeight > 0, "Hello has a positive height");
+ ok(worldWidth > 0, "World has a positive width");
+ ok(worldHeight > 0, "World has a positive height");
+ ok(helloHeight < worldHeight, "Hello has a smaller height than world");
+ ok(helloWidth < worldWidth, "Hello has a smaller width than world");
+
+ // Note: these are mac screen coords, so our origin is bottom left
+ const [helloX, helloY] = hello.getAttributeValue("AXPosition");
+ const [worldX, worldY] = world.getAttributeValue("AXPosition");
+ ok(helloX > 0, "Hello has a positive X");
+ ok(helloY > 0, "Hello has a positive Y");
+ ok(worldX > 0, "World has a positive X");
+ ok(worldY > 0, "World has a positive Y");
+ ok(helloY > worldY, "Hello has a larger Y than world");
+ ok(helloX == worldX, "Hello and world have the same X");
+ }
+);
+
+/**
+ * Test position, size for offscreen content
+ */
+addAccessibleTask(
+ `I am some extra content<br>
+ <div id="hello" style="display:inline; position:absolute; left:-2000px;">hello</div><br>
+ <div id="world" style="display:inline; position:absolute; left:-2000px;">hello world<br>I am some text</div>`,
+ async (browser, accDoc) => {
+ const hello = getNativeInterface(accDoc, "hello");
+ const world = getNativeInterface(accDoc, "world");
+ ok(hello.getAttributeValue("AXFrame"), "Hello's frame attr is not null");
+ ok(world.getAttributeValue("AXFrame"), "World's frame attr is not null");
+
+ // AXSize and AXPosition are composed of AXFrame components, so we
+ // test them here instead of calling AXFrame directly.
+ const [helloWidth, helloHeight] = hello.getAttributeValue("AXSize");
+ const [worldWidth, worldHeight] = world.getAttributeValue("AXSize");
+ ok(helloWidth > 0, "Hello has a positive width");
+ ok(helloHeight > 0, "Hello has a positive height");
+ ok(worldWidth > 0, "World has a positive width");
+ ok(worldHeight > 0, "World has a positive height");
+ ok(helloHeight < worldHeight, "Hello has a smaller height than world");
+ ok(helloWidth < worldWidth, "Hello has a smaller width than world");
+
+ // Note: these are mac screen coords, so our origin is bottom left
+ const [helloX, helloY] = hello.getAttributeValue("AXPosition");
+ const [worldX, worldY] = world.getAttributeValue("AXPosition");
+ ok(helloX < 0, "Hello has a negative X");
+ ok(helloY > 0, "Hello has a positive Y");
+ ok(worldX < 0, "World has a negative X");
+ ok(worldY > 0, "World has a positive Y");
+ ok(helloY > worldY, "Hello has a larger Y than world");
+ ok(helloX == worldX, "Hello and world have the same X");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_details_summary.js b/accessible/tests/browser/mac/browser_details_summary.js
new file mode 100644
index 0000000000..6157707f79
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_details_summary.js
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test details/summary
+ */
+addAccessibleTask(
+ `<details id="details"><summary id="summary">Foo</summary><p>Bar</p></details>`,
+ async (browser, accDoc) => {
+ let details = getNativeInterface(accDoc, "details");
+ is(
+ details.getAttributeValue("AXRole"),
+ "AXGroup",
+ "Correct role for details"
+ );
+ is(
+ details.getAttributeValue("AXSubrole"),
+ "AXDetails",
+ "Correct subrole for details"
+ );
+
+ let detailsChildren = details.getAttributeValue("AXChildren");
+ is(detailsChildren.length, 1, "collapsed details has only one child");
+
+ let summary = detailsChildren[0];
+ is(
+ summary.getAttributeValue("AXRole"),
+ "AXButton",
+ "Correct role for summary"
+ );
+ is(
+ summary.getAttributeValue("AXSubrole"),
+ "AXSummary",
+ "Correct subrole for summary"
+ );
+ is(summary.getAttributeValue("AXExpanded"), 0, "Summary is collapsed");
+
+ let actions = summary.actionNames;
+ ok(actions.includes("AXPress"), "Summary Has press action");
+
+ let stateChanged = waitForStateChange("summary", STATE_EXPANDED, true);
+ summary.performAction("AXPress");
+ // The reorder gecko event notifies us of a tree change.
+ await stateChanged;
+ is(summary.getAttributeValue("AXExpanded"), 1, "Summary is expanded");
+
+ detailsChildren = details.getAttributeValue("AXChildren");
+ is(detailsChildren.length, 2, "collapsed details has only one child");
+
+ stateChanged = waitForStateChange("summary", STATE_EXPANDED, false);
+ summary.performAction("AXPress");
+ // The reorder gecko event notifies us of a tree change.
+ await stateChanged;
+ is(summary.getAttributeValue("AXExpanded"), 0, "Summary is collapsed 2");
+
+ detailsChildren = details.getAttributeValue("AXChildren");
+ is(detailsChildren.length, 1, "collapsed details has only one child");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_focus.js b/accessible/tests/browser/mac/browser_focus.js
new file mode 100644
index 0000000000..6bceb06c6c
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_focus.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test focusability
+ */
+addAccessibleTask(
+ `
+ <div role="button" id="ariabutton">hello</div> <button id="button">world</button>
+ `,
+ async (browser, accDoc) => {
+ let ariabutton = getNativeInterface(accDoc, "ariabutton");
+ let button = getNativeInterface(accDoc, "button");
+
+ is(
+ ariabutton.getAttributeValue("AXFocused"),
+ 0,
+ "aria button is not focused"
+ );
+
+ is(button.getAttributeValue("AXFocused"), 0, "button is not focused");
+
+ ok(
+ !ariabutton.isAttributeSettable("AXFocused"),
+ "aria button should not be focusable"
+ );
+
+ ok(button.isAttributeSettable("AXFocused"), "button is focusable");
+
+ let evt = waitForMacEvent(
+ "AXFocusedUIElementChanged",
+ iface => iface.getAttributeValue("AXDOMIdentifier") == "button"
+ );
+
+ button.setAttributeValue("AXFocused", true);
+
+ await evt;
+
+ is(button.getAttributeValue("AXFocused"), 1, "button is focused");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_heading.js b/accessible/tests/browser/mac/browser_heading.js
new file mode 100644
index 0000000000..fd8c12883d
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_heading.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test whether line break code in text content will be removed
+ * and extra whitespaces will be trimmed.
+ */
+addAccessibleTask(
+ `
+ <h1 id="single-line-content">We’re building a richer search experience</h1>
+ <h1 id="multi-lines-content">
+We’re building a
+richest
+search experience
+ </h1>
+ `,
+ async (browser, accDoc) => {
+ const singleLineContentHeading = getNativeInterface(
+ accDoc,
+ "single-line-content"
+ );
+ is(
+ singleLineContentHeading.getAttributeValue("AXTitle"),
+ "We’re building a richer search experience"
+ );
+
+ const multiLinesContentHeading = getNativeInterface(
+ accDoc,
+ "multi-lines-content"
+ );
+ is(
+ multiLinesContentHeading.getAttributeValue("AXTitle"),
+ "We’re building a richest search experience"
+ );
+ }
+);
+
+/**
+ * Test AXTitle/AXDescription attributes of heading elements
+ */
+addAccessibleTask(
+ `
+ <h1 id="a">Hello <a href="#">world</a></h1>
+ <h1 id="b">Hello</h1>
+ <h1 id="c" aria-label="Goodbye">Hello</h1>
+ `,
+ async (browser, accDoc) => {
+ const a = getNativeInterface(accDoc, "a");
+ is(
+ a.getAttributeValue("AXTitle"),
+ "Hello world",
+ "Correct AXTitle for 'a'"
+ );
+ ok(
+ !a.getAttributeValue("AXDescription"),
+ "'a' Should not have AXDescription"
+ );
+
+ const b = getNativeInterface(accDoc, "b");
+ is(b.getAttributeValue("AXTitle"), "Hello", "Correct AXTitle for 'b'");
+ ok(
+ !b.getAttributeValue("AXDescription"),
+ "'b' Should not have AXDescription"
+ );
+
+ const c = getNativeInterface(accDoc, "c");
+ is(
+ c.getAttributeValue("AXDescription"),
+ "Goodbye",
+ "Correct AXDescription for 'c'"
+ );
+ ok(!c.getAttributeValue("AXTitle"), "'c' Should not have AXTitle");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_hierarchy.js b/accessible/tests/browser/mac/browser_hierarchy.js
new file mode 100644
index 0000000000..8a97e55c07
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_hierarchy.js
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test AXIndexForChildUIElement
+ */
+addAccessibleTask(
+ `<p id="p">Hello <a href="#" id="link">strange</a> world`,
+ (browser, accDoc) => {
+ let p = getNativeInterface(accDoc, "p");
+
+ let children = p.getAttributeValue("AXChildren");
+ is(children.length, 3, "p has 3 children");
+ is(
+ children[1].getAttributeValue("AXDOMIdentifier"),
+ "link",
+ "second child is link"
+ );
+
+ let index = p.getParameterizedAttributeValue(
+ "AXIndexForChildUIElement",
+ children[1]
+ );
+ is(index, 1, "link is second child");
+ }
+);
+
+/**
+ * Test textbox with more than one child
+ */
+addAccessibleTask(
+ `<div id="textbox" role="textbox">Hello <a href="#">strange</a> world</div>`,
+ (browser, accDoc) => {
+ let textbox = getNativeInterface(accDoc, "textbox");
+
+ is(
+ textbox.getAttributeValue("AXChildren").length,
+ 3,
+ "textbox has 3 children"
+ );
+ }
+);
+
+/**
+ * Test textbox with one child
+ */
+addAccessibleTask(
+ `<div id="textbox" role="textbox">Hello </div>`,
+ async (browser, accDoc) => {
+ let textbox = getNativeInterface(accDoc, "textbox");
+
+ is(
+ textbox.getAttributeValue("AXChildren").length,
+ 0,
+ "textbox with one child is pruned"
+ );
+
+ let reorder = waitForEvent(EVENT_REORDER, "textbox");
+ await SpecialPowers.spawn(browser, [], () => {
+ let link = content.document.createElement("a");
+ link.textContent = "World";
+ content.document.getElementById("textbox").appendChild(link);
+ });
+ await reorder;
+
+ is(
+ textbox.getAttributeValue("AXChildren").length,
+ 2,
+ "textbox with two child is not pruned"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_input.js b/accessible/tests/browser/mac/browser_input.js
new file mode 100644
index 0000000000..7fa20a9d4b
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_input.js
@@ -0,0 +1,225 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function selectedTextEventPromises(stateChangeType) {
+ return [
+ waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => {
+ return (
+ info.AXTextStateChangeType == stateChangeType &&
+ elem.getAttributeValue("AXDOMIdentifier") == "body"
+ );
+ }),
+ waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => {
+ return (
+ info.AXTextStateChangeType == stateChangeType &&
+ elem.getAttributeValue("AXDOMIdentifier") == "input"
+ );
+ }),
+ ];
+}
+
+async function testInput(browser, accDoc) {
+ let input = getNativeInterface(accDoc, "input");
+
+ is(input.getAttributeValue("AXDescription"), "Name", "Correct input label");
+ is(input.getAttributeValue("AXTitle"), "", "Correct input title");
+ is(input.getAttributeValue("AXValue"), "Elmer Fudd", "Correct input value");
+ is(
+ input.getAttributeValue("AXNumberOfCharacters"),
+ 10,
+ "Correct length of value"
+ );
+
+ ok(input.attributeNames.includes("AXSelectedText"), "Has AXSelectedText");
+ ok(
+ input.attributeNames.includes("AXSelectedTextRange"),
+ "Has AXSelectedTextRange"
+ );
+
+ let evt = Promise.all([
+ waitForMacEvent("AXFocusedUIElementChanged", "input"),
+ ...selectedTextEventPromises(AXTextStateChangeTypeSelectionMove),
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("input").focus();
+ });
+ await evt;
+
+ evt = Promise.all(
+ selectedTextEventPromises(AXTextStateChangeTypeSelectionExtend)
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ let elm = content.document.getElementById("input");
+ if (elm.setSelectionRange) {
+ elm.setSelectionRange(6, 9);
+ } else {
+ let r = new content.Range();
+ let textNode = elm.firstElementChild.firstChild;
+ r.setStart(textNode, 6);
+ r.setEnd(textNode, 9);
+
+ let s = content.getSelection();
+ s.removeAllRanges();
+ s.addRange(r);
+ }
+ });
+ await evt;
+
+ is(
+ input.getAttributeValue("AXSelectedText"),
+ "Fud",
+ "Correct text is selected"
+ );
+
+ Assert.deepEqual(
+ input.getAttributeValue("AXSelectedTextRange"),
+ [6, 3],
+ "correct range selected"
+ );
+
+ ok(
+ input.isAttributeSettable("AXSelectedTextRange"),
+ "AXSelectedTextRange is settable"
+ );
+
+ evt = Promise.all(
+ selectedTextEventPromises(AXTextStateChangeTypeSelectionExtend)
+ );
+ input.setAttributeValue("AXSelectedTextRange", NSRange(1, 7));
+ await evt;
+
+ Assert.deepEqual(
+ input.getAttributeValue("AXSelectedTextRange"),
+ [1, 7],
+ "correct range selected"
+ );
+
+ is(
+ input.getAttributeValue("AXSelectedText"),
+ "lmer Fu",
+ "Correct text is selected"
+ );
+
+ let domSelection = await SpecialPowers.spawn(browser, [], () => {
+ let elm = content.document.querySelector("input#input");
+ if (elm) {
+ return elm.value.substring(elm.selectionStart, elm.selectionEnd);
+ }
+
+ return content.getSelection().toString();
+ });
+
+ is(domSelection, "lmer Fu", "correct DOM selection");
+
+ is(
+ input.getParameterizedAttributeValue("AXStringForRange", NSRange(3, 5)),
+ "er Fu",
+ "AXStringForRange works"
+ );
+}
+
+/**
+ * Input selection test
+ */
+addAccessibleTask(
+ `<input aria-label="Name" id="input" value="Elmer Fudd">`,
+ testInput
+);
+
+/**
+ * contenteditable selection test
+ */
+addAccessibleTask(
+ `<div aria-label="Name" tabindex="0" role="textbox" aria-multiline="true" id="input" contenteditable>
+ <p>Elmer Fudd</p>
+ </div>`,
+ testInput
+);
+
+/**
+ * test contenteditable with selection that extends past editable part
+ */
+addAccessibleTask(
+ `<span aria-label="Name"
+ tabindex="0"
+ role="textbox"
+ id="input"
+ contenteditable>Elmer Fudd</span> <span id="notinput">is the name</span>`,
+ async (browser, accDoc) => {
+ let evt = Promise.all([
+ waitForMacEvent("AXFocusedUIElementChanged", "input"),
+ waitForMacEvent("AXSelectedTextChanged", "body"),
+ waitForMacEvent("AXSelectedTextChanged", "input"),
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("input").focus();
+ });
+ await evt;
+
+ evt = waitForEvent(EVENT_TEXT_CARET_MOVED);
+ await SpecialPowers.spawn(browser, [], () => {
+ let input = content.document.getElementById("input");
+ let notinput = content.document.getElementById("notinput");
+
+ let r = new content.Range();
+ r.setStart(input.firstChild, 4);
+ r.setEnd(notinput.firstChild, 6);
+
+ let s = content.getSelection();
+ s.removeAllRanges();
+ s.addRange(r);
+ });
+ await evt;
+
+ let input = getNativeInterface(accDoc, "input");
+
+ is(
+ input.getAttributeValue("AXSelectedText"),
+ "r Fudd",
+ "Correct text is selected in #input"
+ );
+
+ is(
+ stringForRange(
+ input,
+ input.getAttributeValue("AXSelectedTextMarkerRange")
+ ),
+ "r Fudd is the",
+ "Correct text is selected in document"
+ );
+ }
+);
+
+/**
+ * test nested content editables and their ancestor getters.
+ */
+addAccessibleTask(
+ `<div id="outer" role="textbox" contenteditable="true">
+ <p id="p">Bob <a href="#" id="link">Loblaw's</a></p>
+ <div id="inner" role="textbox" contenteditable="true">
+ Law <a href="#" id="inner_link">Blog</a>
+ </div>
+ </div>`,
+ (browser, accDoc) => {
+ let link = getNativeInterface(accDoc, "link");
+ let innerLink = getNativeInterface(accDoc, "inner_link");
+
+ let idmatches = (elem, id) => {
+ is(elem.getAttributeValue("AXDOMIdentifier"), id, "Matches ID");
+ };
+
+ idmatches(link.getAttributeValue("AXEditableAncestor"), "outer");
+ idmatches(link.getAttributeValue("AXFocusableAncestor"), "outer");
+ idmatches(link.getAttributeValue("AXHighestEditableAncestor"), "outer");
+
+ idmatches(innerLink.getAttributeValue("AXEditableAncestor"), "inner");
+ idmatches(innerLink.getAttributeValue("AXFocusableAncestor"), "inner");
+ idmatches(
+ innerLink.getAttributeValue("AXHighestEditableAncestor"),
+ "outer"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_label_title.js b/accessible/tests/browser/mac/browser_label_title.js
new file mode 100644
index 0000000000..2532247e0f
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_label_title.js
@@ -0,0 +1,111 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test different labeling/titling schemes for text fields
+ */
+addAccessibleTask(
+ `<label for="n1">Label</label> <input id="n1">
+ <label for="n2">Two</label> <label for="n2">Labels</label> <input id="n2">
+ <input aria-label="ARIA Label" id="n3">`,
+ (browser, accDoc) => {
+ let n1 = getNativeInterface(accDoc, "n1");
+ let n1Label = n1.getAttributeValue("AXTitleUIElement");
+ // XXX: In Safari the label is an AXText with an AXValue,
+ // here it is an AXGroup witth an AXTitle
+ is(n1Label.getAttributeValue("AXTitle"), "Label");
+
+ let n2 = getNativeInterface(accDoc, "n2");
+ is(n2.getAttributeValue("AXDescription"), "TwoLabels");
+
+ let n3 = getNativeInterface(accDoc, "n3");
+ is(n3.getAttributeValue("AXDescription"), "ARIA Label");
+ }
+);
+
+/**
+ * Test to see that named groups get labels
+ */
+addAccessibleTask(
+ `<fieldset id="fieldset"><legend>Fields</legend><input aria-label="hello"></fieldset>`,
+ (browser, accDoc) => {
+ let fieldset = getNativeInterface(accDoc, "fieldset");
+ is(fieldset.getAttributeValue("AXDescription"), "Fields");
+ }
+);
+
+/**
+ * Test to see that list items don't get titled groups
+ */
+addAccessibleTask(
+ `<ul style="list-style: none;"><li id="unstyled-item">Hello</li></ul>
+ <ul><li id="styled-item">World</li></ul>`,
+ (browser, accDoc) => {
+ let unstyledItem = getNativeInterface(accDoc, "unstyled-item");
+ is(unstyledItem.getAttributeValue("AXTitle"), "");
+
+ let styledItem = getNativeInterface(accDoc, "unstyled-item");
+ is(styledItem.getAttributeValue("AXTitle"), "");
+ }
+);
+
+/**
+ * Test that we fire a title changed notification
+ */
+addAccessibleTask(
+ `<div id="elem" aria-label="Hello world"></div>`,
+ async (browser, accDoc) => {
+ let elem = getNativeInterface(accDoc, "elem");
+ is(elem.getAttributeValue("AXTitle"), "Hello world");
+ let evt = waitForMacEvent("AXTitleChanged", "elem");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("elem")
+ .setAttribute("aria-label", "Hello universe");
+ });
+ await evt;
+ is(elem.getAttributeValue("AXTitle"), "Hello universe");
+ }
+);
+
+/**
+ * Test articles supply only labels not titles
+ */
+addAccessibleTask(
+ `<article id="article" aria-label="Hello world"></article>`,
+ async (browser, accDoc) => {
+ let article = getNativeInterface(accDoc, "article");
+ is(article.getAttributeValue("AXDescription"), "Hello world");
+ ok(!article.getAttributeValue("AXTitle"));
+ }
+);
+
+/**
+ * Test text and number inputs supply only labels not titles
+ */
+addAccessibleTask(
+ `<label for="input">Your favorite number?</label><input type="text" name="input" value="11" id="input" aria-label="The best number you know of">`,
+ async (browser, accDoc) => {
+ let input = getNativeInterface(accDoc, "input");
+ is(input.getAttributeValue("AXDescription"), "The best number you know of");
+ ok(!input.getAttributeValue("AXTitle"));
+ let evt = waitForEvent(EVENT_SHOW, "input");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("input").setAttribute("type", "number");
+ });
+ await evt;
+ input = getNativeInterface(accDoc, "input");
+ is(input.getAttributeValue("AXDescription"), "The best number you know of");
+ ok(!input.getAttributeValue("AXTitle"));
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_link.js b/accessible/tests/browser/mac/browser_link.js
new file mode 100644
index 0000000000..b4597e10f4
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_link.js
@@ -0,0 +1,222 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+/**
+ * Test visited link properties.
+ */
+addAccessibleTask(
+ `
+ <a id="link" href="http://www.example.com/">I am a non-visited link</a><br>
+ `,
+ async (browser, accDoc) => {
+ let link = getNativeInterface(accDoc, "link");
+ let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "link");
+
+ is(link.getAttributeValue("AXVisited"), 0, "Link has not been visited");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await PlacesTestUtils.addVisits(["http://www.example.com/"]);
+
+ await stateChanged;
+ is(link.getAttributeValue("AXVisited"), 1, "Link has been visited");
+
+ // Ensure history is cleared before running
+ await PlacesUtils.history.clear();
+ }
+);
+
+/**
+ * Test linked vs unlinked anchor tags
+ */
+addAccessibleTask(
+ `
+ <a id="link1" href="#">I am a link link</a>
+ <a id="link2" onclick="console.log('hi')">I am a link-ish link</a>
+ <a id="link3">I am a non-link link</a>
+ `,
+ async (browser, accDoc) => {
+ let link1 = getNativeInterface(accDoc, "link1");
+ is(
+ link1.getAttributeValue("AXRole"),
+ "AXLink",
+ "a[href] gets correct link role"
+ );
+ ok(
+ link1.attributeNames.includes("AXVisited"),
+ "Link has visited attribute"
+ );
+ ok(link1.attributeNames.includes("AXURL"), "Link has URL attribute");
+
+ let link2 = getNativeInterface(accDoc, "link2");
+ is(
+ link2.getAttributeValue("AXRole"),
+ "AXLink",
+ "a[onclick] gets correct link role"
+ );
+ ok(
+ link2.attributeNames.includes("AXVisited"),
+ "Link has visited attribute"
+ );
+ ok(link2.attributeNames.includes("AXURL"), "Link has URL attribute");
+
+ let link3 = getNativeInterface(accDoc, "link3");
+ is(
+ link3.getAttributeValue("AXRole"),
+ "AXGroup",
+ "bare <a> gets correct group role"
+ );
+
+ let onRecreation = waitForEvent(EVENT_SHOW, "link1");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("link1").removeAttribute("href");
+ });
+ await onRecreation;
+ link1 = getNativeInterface(accDoc, "link1");
+ is(
+ link1.getAttributeValue("AXRole"),
+ "AXGroup",
+ "<a> stripped from href gets group role"
+ );
+
+ onRecreation = waitForEvent(EVENT_SHOW, "link2");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("link2").removeAttribute("onclick");
+ });
+ await onRecreation;
+ link2 = getNativeInterface(accDoc, "link2");
+ is(
+ link2.getAttributeValue("AXRole"),
+ "AXGroup",
+ "<a> stripped from onclick gets group role"
+ );
+
+ onRecreation = waitForEvent(EVENT_SHOW, "link3");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("link3")
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ .setAttribute("href", "http://example.com");
+ });
+ await onRecreation;
+ link3 = getNativeInterface(accDoc, "link3");
+ is(
+ link3.getAttributeValue("AXRole"),
+ "AXLink",
+ "href added to bare a gets link role"
+ );
+
+ ok(
+ link3.attributeNames.includes("AXVisited"),
+ "Link has visited attribute"
+ );
+ ok(link3.attributeNames.includes("AXURL"), "Link has URL attribute");
+ }
+);
+
+/**
+ * Test anchors and linked ui elements attr
+ */
+addAccessibleTask(
+ `
+ <a id="link0" href="http://example.com">I am a link</a>
+ <a id="link1" href="#">I am a link with an empty anchor</a>
+ <a id="link2" href="#hello">I am a link with no corresponding element</a>
+ <a id="link3" href="#world">I am a link with a corresponding element</a>
+ <a id="link4" href="#empty">I jump to an empty element</a>
+ <a id="link5" href="#namedElem">I jump to a named element</a>
+ <a id="link6" href="#emptyNamed">I jump to an empty named element</a>
+ <h1 id="world">I am that element</h1>
+ <h2 id="empty"></h2>
+ <a name="namedElem">I have a name</a>
+ <a name="emptyNamed"></a>
+ <h3>I have no name and no ID</h3>
+ <h4></h4>
+ `,
+ async (browser, accDoc) => {
+ let link0 = getNativeInterface(accDoc, "link0");
+ let link1 = getNativeInterface(accDoc, "link1");
+ let link2 = getNativeInterface(accDoc, "link2");
+ let link3 = getNativeInterface(accDoc, "link3");
+ let link4 = getNativeInterface(accDoc, "link4");
+ let link5 = getNativeInterface(accDoc, "link5");
+ let link6 = getNativeInterface(accDoc, "link6");
+
+ is(
+ link0.getAttributeValue("AXLinkedUIElements").length,
+ 0,
+ "Link 0 has no linked UI elements"
+ );
+ is(
+ link1.getAttributeValue("AXLinkedUIElements").length,
+ 0,
+ "Link 1 has no linked UI elements"
+ );
+ is(
+ link2.getAttributeValue("AXLinkedUIElements").length,
+ 0,
+ "Link 2 has no linked UI elements"
+ );
+ is(
+ link3.getAttributeValue("AXLinkedUIElements").length,
+ 1,
+ "Link 3 has one linked UI element"
+ );
+ is(
+ link3
+ .getAttributeValue("AXLinkedUIElements")[0]
+ .getAttributeValue("AXTitle"),
+ "I am that element",
+ "Link 3 is linked to the heading"
+ );
+ is(
+ link4.getAttributeValue("AXLinkedUIElements").length,
+ 1,
+ "Link 4 has one linked UI element"
+ );
+ is(
+ link4
+ .getAttributeValue("AXLinkedUIElements")[0]
+ .getAttributeValue("AXTitle"),
+ null,
+ "Link 4 is linked to the heading"
+ );
+ is(
+ link5.getAttributeValue("AXLinkedUIElements").length,
+ 1,
+ "Link 5 has one linked UI element"
+ );
+ is(
+ link5
+ .getAttributeValue("AXLinkedUIElements")[0]
+ .getAttributeValue("AXTitle"),
+ "",
+ "Link 5 is linked to a named element"
+ );
+ is(
+ link6.getAttributeValue("AXLinkedUIElements").length,
+ 1,
+ "Link 6 has one linked UI element"
+ );
+ is(
+ link6
+ .getAttributeValue("AXLinkedUIElements")[0]
+ .getAttributeValue("AXTitle"),
+ "",
+ "Link 6 is linked to an empty named element"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_live_regions.js b/accessible/tests/browser/mac/browser_live_regions.js
new file mode 100644
index 0000000000..10a03120f8
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_live_regions.js
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test live region creation and removal.
+ */
+addAccessibleTask(
+ `
+ <div id="polite" aria-relevant="removals">Polite region</div>
+ <div id="assertive" aria-live="assertive">Assertive region</div>
+ `,
+ async (browser, accDoc) => {
+ let politeRegion = getNativeInterface(accDoc, "polite");
+ ok(
+ !politeRegion.attributeNames.includes("AXARIALive"),
+ "region is not live"
+ );
+
+ let liveRegionAdded = waitForMacEvent("AXLiveRegionCreated", "polite");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("polite")
+ .setAttribute("aria-atomic", "true");
+ content.document
+ .getElementById("polite")
+ .setAttribute("aria-live", "polite");
+ });
+ await liveRegionAdded;
+ is(
+ politeRegion.getAttributeValue("AXARIALive"),
+ "polite",
+ "region is now live"
+ );
+ ok(politeRegion.getAttributeValue("AXARIAAtomic"), "region is atomic");
+ is(
+ politeRegion.getAttributeValue("AXARIARelevant"),
+ "removals",
+ "region has defined aria-relevant"
+ );
+
+ let assertiveRegion = getNativeInterface(accDoc, "assertive");
+ is(
+ assertiveRegion.getAttributeValue("AXARIALive"),
+ "assertive",
+ "region is assertive"
+ );
+ ok(
+ !assertiveRegion.getAttributeValue("AXARIAAtomic"),
+ "region is not atomic"
+ );
+ is(
+ assertiveRegion.getAttributeValue("AXARIARelevant"),
+ "additions text",
+ "region has default aria-relevant"
+ );
+
+ let liveRegionRemoved = waitForEvent(
+ EVENT_LIVE_REGION_REMOVED,
+ "assertive"
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("assertive").removeAttribute("aria-live");
+ });
+ await liveRegionRemoved;
+ ok(!assertiveRegion.getAttributeValue("AXARIALive"), "region is not live");
+
+ liveRegionAdded = waitForMacEvent("AXLiveRegionCreated", "new-region");
+ await SpecialPowers.spawn(browser, [], () => {
+ let newRegionElm = content.document.createElement("div");
+ newRegionElm.id = "new-region";
+ newRegionElm.setAttribute("aria-live", "assertive");
+ content.document.body.appendChild(newRegionElm);
+ });
+ await liveRegionAdded;
+
+ let newRegion = getNativeInterface(accDoc, "new-region");
+ is(
+ newRegion.getAttributeValue("AXARIALive"),
+ "assertive",
+ "region is assertive"
+ );
+
+ let loadComplete = Promise.all([
+ waitForMacEvent("AXLoadComplete"),
+ waitForMacEvent("AXLiveRegionCreated", "region-1"),
+ waitForMacEvent("AXLiveRegionCreated", "region-2"),
+ waitForMacEvent("AXLiveRegionCreated", "status"),
+ waitForMacEvent("AXLiveRegionCreated", "output"),
+ ]);
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.location = `data:text/html;charset=utf-8,
+ <div id="region-1" aria-live="polite"></div>
+ <div id="region-2" aria-live="assertive"></div>
+ <div id="region-3" aria-live="off"></div>
+ <div id="alert" role="alert"></div>
+ <div id="status" role="status"></div>
+ <output id="output"></output>`;
+ });
+ let webArea = (await loadComplete)[0];
+
+ is(webArea.getAttributeValue("AXRole"), "AXWebArea", "web area yeah");
+ const searchPred = {
+ AXSearchKey: "AXLiveRegionSearchKey",
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+ const liveRegions = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ Assert.deepEqual(
+ liveRegions.map(r => r.getAttributeValue("AXDOMIdentifier")),
+ ["region-1", "region-2", "alert", "status", "output"],
+ "SearchPredicate returned all live regions"
+ );
+ }
+);
+
+/**
+ * Test live region changes
+ */
+addAccessibleTask(
+ `
+ <div id="live" aria-live="polite">
+ The time is <span id="time">4:55pm</span>
+ <p id="p" style="display: none">Georgia on my mind</p>
+ <button id="button" aria-label="Start"></button>
+ </div>
+ `,
+ async (browser, accDoc) => {
+ let liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("time").textContent = "4:56pm";
+ });
+ await liveRegionChanged;
+ ok(true, "changed textContent");
+
+ liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("p").style.display = "block";
+ });
+ await liveRegionChanged;
+ ok(true, "changed display style to block");
+
+ liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("p").style.display = "none";
+ });
+ await liveRegionChanged;
+ ok(true, "changed display style to none");
+
+ liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("button")
+ .setAttribute("aria-label", "Stop");
+ });
+ await liveRegionChanged;
+ ok(true, "changed aria-label");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_mathml.js b/accessible/tests/browser/mac/browser_mathml.js
new file mode 100644
index 0000000000..d4cf499810
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_mathml.js
@@ -0,0 +1,146 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function testMathAttr(iface, attr, subrole, textLeafValue) {
+ ok(iface.attributeNames.includes(attr), `Object has ${attr} attribute`);
+ let value = iface.getAttributeValue(attr);
+ is(
+ value.getAttributeValue("AXSubrole"),
+ subrole,
+ `${attr} value has correct subrole`
+ );
+
+ if (textLeafValue) {
+ let children = value.getAttributeValue("AXChildren");
+ is(children.length, 1, `${attr} value has one child`);
+
+ is(
+ children[0].getAttributeValue("AXRole"),
+ "AXStaticText",
+ `${attr} value's child is static text`
+ );
+ is(
+ children[0].getAttributeValue("AXValue"),
+ textLeafValue,
+ `${attr} value has correct text`
+ );
+ }
+}
+
+addAccessibleTask(
+ `<math id="math">
+ <msqrt id="sqrt">
+ <mn>2</mn>
+ </msqrt>
+ </math>`,
+ async (browser, accDoc) => {
+ let math = getNativeInterface(accDoc, "math");
+ is(
+ math.getAttributeValue("AXSubrole"),
+ "AXDocumentMath",
+ "Math element has correct subrole"
+ );
+
+ let sqrt = getNativeInterface(accDoc, "sqrt");
+ is(
+ sqrt.getAttributeValue("AXSubrole"),
+ "AXMathSquareRoot",
+ "msqrt has correct subrole"
+ );
+
+ testMathAttr(sqrt, "AXMathRootRadicand", "AXMathNumber", "2");
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <mroot id="root">
+ <mi>sin</mi>
+ <mn>3</mn>
+ </mroot>
+ </math>`,
+ async (browser, accDoc) => {
+ let root = getNativeInterface(accDoc, "root");
+ is(
+ root.getAttributeValue("AXSubrole"),
+ "AXMathRoot",
+ "mroot has correct subrole"
+ );
+
+ testMathAttr(root, "AXMathRootRadicand", "AXMathIdentifier", "sin");
+ testMathAttr(root, "AXMathRootIndex", "AXMathNumber", "3");
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <mfrac id="fraction">
+ <mn>2</mn>
+ <mn>3</mn>
+ </mfrac>
+ </math>`,
+ async (browser, accDoc) => {
+ let fraction = getNativeInterface(accDoc, "fraction");
+ is(
+ fraction.getAttributeValue("AXSubrole"),
+ "AXMathFraction",
+ "mfrac has correct subrole"
+ );
+ ok(fraction.attributeNames.includes("AXMathFractionNumerator"));
+ ok(fraction.attributeNames.includes("AXMathFractionDenominator"));
+ ok(fraction.attributeNames.includes("AXMathLineThickness"));
+
+ // Bug 1639745
+ todo_is(fraction.getAttributeValue("AXMathLineThickness"), 1);
+
+ testMathAttr(fraction, "AXMathFractionNumerator", "AXMathNumber", "2");
+ testMathAttr(fraction, "AXMathFractionDenominator", "AXMathNumber", "3");
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <msubsup id="subsup">
+ <mo>∫</mo>
+ <mn>0</mn>
+ <mn>1</mn>
+ </msubsup>
+ </math>`,
+ async (browser, accDoc) => {
+ let subsup = getNativeInterface(accDoc, "subsup");
+ is(
+ subsup.getAttributeValue("AXSubrole"),
+ "AXMathSubscriptSuperscript",
+ "msubsup has correct subrole"
+ );
+
+ testMathAttr(subsup, "AXMathSubscript", "AXMathNumber", "0");
+ testMathAttr(subsup, "AXMathSuperscript", "AXMathNumber", "1");
+ testMathAttr(subsup, "AXMathBase", "AXMathOperator", "∫");
+ }
+);
+
+addAccessibleTask(
+ `<math>
+ <munderover id="underover">
+ <mo>∫</mo>
+ <mn>0</mn>
+ <mi>∞</mi>
+ </munderover>
+ </math>`,
+ async (browser, accDoc) => {
+ let underover = getNativeInterface(accDoc, "underover");
+ is(
+ underover.getAttributeValue("AXSubrole"),
+ "AXMathUnderOver",
+ "munderover has correct subrole"
+ );
+
+ testMathAttr(underover, "AXMathUnder", "AXMathNumber", "0");
+ testMathAttr(underover, "AXMathOver", "AXMathIdentifier", "∞");
+ testMathAttr(underover, "AXMathBase", "AXMathOperator", "∫");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_menulist.js b/accessible/tests/browser/mac/browser_menulist.js
new file mode 100644
index 0000000000..b26a0be782
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_menulist.js
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/attributes.js */
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR },
+ { name: "attributes.js", dir: MOCHITESTS_DIR }
+);
+
+addAccessibleTask(
+ "mac/doc_menulist.xhtml",
+ async (browser, accDoc) => {
+ const menulist = getNativeInterface(accDoc, "defaultZoom");
+
+ let actions = menulist.actionNames;
+ ok(actions.includes("AXPress"), "menu has press action");
+
+ let event = waitForMacEvent("AXMenuOpened");
+ menulist.performAction("AXPress");
+ const menupopup = await event;
+
+ const menuItems = menupopup.getAttributeValue("AXChildren");
+ is(menuItems.length, 4, "Found four children in menulist");
+ is(
+ menuItems[0].getAttributeValue("AXTitle"),
+ "50%",
+ "First item has correct title"
+ );
+ is(
+ menuItems[1].getAttributeValue("AXTitle"),
+ "100%",
+ "Second item has correct title"
+ );
+ is(
+ menuItems[2].getAttributeValue("AXTitle"),
+ "150%",
+ "Third item has correct title"
+ );
+ is(
+ menuItems[3].getAttributeValue("AXTitle"),
+ "200%",
+ "Fourth item has correct title"
+ );
+ },
+ { topLevel: false, chrome: true }
+);
+
+addAccessibleTask(
+ "mac/doc_menulist.xhtml",
+ async (browser, accDoc) => {
+ const menulist = getNativeInterface(accDoc, "defaultZoom");
+
+ const actions = menulist.actionNames;
+ ok(actions.includes("AXPress"), "menu has press action");
+ let event = waitForMacEvent("AXMenuOpened");
+ menulist.performAction("AXPress");
+ await event;
+
+ const menu = menulist.getAttributeValue("AXChildren")[0];
+ ok(menu, "Menulist contains menu");
+ const children = menu.getAttributeValue("AXChildren");
+ is(children.length, 4, "Menu has 4 items");
+
+ // Menu is open, initial focus should land on the first item
+ is(
+ children[0].getAttributeValue("AXSelected"),
+ 1,
+ "First menu item is selected"
+ );
+ // focus the second item, and verify it is selected
+ event = waitForMacEvent("AXFocusedUIElementChanged", (iface, data) => {
+ try {
+ return iface.getAttributeValue("AXTitle") == "100%";
+ } catch (e) {
+ return false;
+ }
+ });
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await event;
+
+ is(
+ children[0].getAttributeValue("AXSelected"),
+ 0,
+ "First menu item is no longer selected"
+ );
+ is(
+ children[1].getAttributeValue("AXSelected"),
+ 1,
+ "Second menu item is selected"
+ );
+ // press the second item, check for selected event
+ event = waitForMacEvent("AXMenuItemSelected");
+ children[1].performAction("AXPress");
+ await event;
+ },
+ { topLevel: false, chrome: true }
+);
diff --git a/accessible/tests/browser/mac/browser_navigate.js b/accessible/tests/browser/mac/browser_navigate.js
new file mode 100644
index 0000000000..69486676e4
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_navigate.js
@@ -0,0 +1,394 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test navigation of same/different type content
+ */
+addAccessibleTask(
+ `<h1 id="hello">hello</h1>
+ world<br>
+ <a href="example.com" id="link">I am a link</a>
+ <h1 id="goodbye">goodbye</h1>`,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXSameTypeSearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: 1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const hello = getNativeInterface(accDoc, "hello");
+ const goodbye = getNativeInterface(accDoc, "goodbye");
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ searchPred.AXStartElement = hello;
+
+ let sameItem = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(sameItem.length, 1, "Found one item");
+ is(
+ "goodbye",
+ sameItem[0].getAttributeValue("AXTitle"),
+ "Found correct item of same type"
+ );
+
+ searchPred.AXDirection = "AXDirectionPrevious";
+ searchPred.AXStartElement = goodbye;
+ sameItem = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(sameItem.length, 1, "Found one item");
+ is(
+ "hello",
+ sameItem[0].getAttributeValue("AXTitle"),
+ "Found correct item of same type"
+ );
+
+ searchPred.AXSearchKey = "AXDifferentTypeSearchKey";
+ let diffItem = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(diffItem.length, 1, "Found one item");
+ is(
+ "I am a link",
+ diffItem[0].getAttributeValue("AXValue"),
+ "Found correct item of different type"
+ );
+ }
+);
+
+/**
+ * Test navigation of heading levels
+ */
+addAccessibleTask(
+ `
+ <h1 id="a">a</h1>
+ <h2 id="b">b</h2>
+ <h3 id="c">c</h3>
+ <h4 id="d">d</h4>
+ <h5 id="e">e</h5>
+ <h6 id="f">f</h5>
+ <h1 id="g">g</h1>
+ <h2 id="h">h</h2>
+ <h3 id="i">i</h3>
+ <h4 id="j">j</h4>
+ <h5 id="k">k</h5>
+ <h6 id="l">l</h5>
+ this is some regular text that should be ignored
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXHeadingLevel1SearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let h1Count = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, h1Count, "Found two h1 items");
+
+ let h1s = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const a = getNativeInterface(accDoc, "a");
+ const g = getNativeInterface(accDoc, "g");
+
+ is(
+ a.getAttributeValue("AXValue"),
+ h1s[0].getAttributeValue("AXValue"),
+ "Found correct h1 heading"
+ );
+
+ is(
+ g.getAttributeValue("AXValue"),
+ h1s[1].getAttributeValue("AXValue"),
+ "Found correct h1 heading"
+ );
+
+ searchPred.AXSearchKey = "AXHeadingLevel2SearchKey";
+
+ let h2Count = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, h2Count, "Found two h2 items");
+
+ let h2s = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const b = getNativeInterface(accDoc, "b");
+ const h = getNativeInterface(accDoc, "h");
+
+ is(
+ b.getAttributeValue("AXValue"),
+ h2s[0].getAttributeValue("AXValue"),
+ "Found correct h2 heading"
+ );
+
+ is(
+ h.getAttributeValue("AXValue"),
+ h2s[1].getAttributeValue("AXValue"),
+ "Found correct h2 heading"
+ );
+
+ searchPred.AXSearchKey = "AXHeadingLevel3SearchKey";
+
+ let h3Count = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, h3Count, "Found two h3 items");
+
+ let h3s = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const c = getNativeInterface(accDoc, "c");
+ const i = getNativeInterface(accDoc, "i");
+
+ is(
+ c.getAttributeValue("AXValue"),
+ h3s[0].getAttributeValue("AXValue"),
+ "Found correct h3 heading"
+ );
+
+ is(
+ i.getAttributeValue("AXValue"),
+ h3s[1].getAttributeValue("AXValue"),
+ "Found correct h3 heading"
+ );
+
+ searchPred.AXSearchKey = "AXHeadingLevel4SearchKey";
+
+ let h4Count = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, h4Count, "Found two h4 items");
+
+ let h4s = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const d = getNativeInterface(accDoc, "d");
+ const j = getNativeInterface(accDoc, "j");
+
+ is(
+ d.getAttributeValue("AXValue"),
+ h4s[0].getAttributeValue("AXValue"),
+ "Found correct h4 heading"
+ );
+
+ is(
+ j.getAttributeValue("AXValue"),
+ h4s[1].getAttributeValue("AXValue"),
+ "Found correct h4 heading"
+ );
+
+ searchPred.AXSearchKey = "AXHeadingLevel5SearchKey";
+
+ let h5Count = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, h5Count, "Found two h5 items");
+
+ let h5s = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const e = getNativeInterface(accDoc, "e");
+ const k = getNativeInterface(accDoc, "k");
+
+ is(
+ e.getAttributeValue("AXValue"),
+ h5s[0].getAttributeValue("AXValue"),
+ "Found correct h5 heading"
+ );
+
+ is(
+ k.getAttributeValue("AXValue"),
+ h5s[1].getAttributeValue("AXValue"),
+ "Found correct h5 heading"
+ );
+
+ searchPred.AXSearchKey = "AXHeadingLevel6SearchKey";
+
+ let h6Count = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, h6Count, "Found two h6 items");
+
+ let h6s = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const f = getNativeInterface(accDoc, "f");
+ const l = getNativeInterface(accDoc, "l");
+
+ is(
+ f.getAttributeValue("AXValue"),
+ h6s[0].getAttributeValue("AXValue"),
+ "Found correct h6 heading"
+ );
+
+ is(
+ l.getAttributeValue("AXValue"),
+ h6s[1].getAttributeValue("AXValue"),
+ "Found correct h6 heading"
+ );
+ }
+);
+
+/*
+ * Test rotor with blockquotes
+ */
+addAccessibleTask(
+ `
+ <blockquote id="first">hello I am a blockquote</blockquote>
+ <blockquote id="second">
+ I am also a blockquote of the same level
+ <br>
+ <blockquote id="third">but I have a different level</blockquote>
+ </blockquote>
+ `,
+ (browser, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXBlockquoteSearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ let bquotes = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(bquotes.length, 3, "Found three blockquotes");
+
+ const first = getNativeInterface(accDoc, "first");
+ const second = getNativeInterface(accDoc, "second");
+ const third = getNativeInterface(accDoc, "third");
+ console.log("values :");
+ console.log(first.getAttributeValue("AXValue"));
+ is(
+ first.getAttributeValue("AXValue"),
+ bquotes[0].getAttributeValue("AXValue"),
+ "Found correct first blockquote"
+ );
+
+ is(
+ second.getAttributeValue("AXValue"),
+ bquotes[1].getAttributeValue("AXValue"),
+ "Found correct second blockquote"
+ );
+
+ is(
+ third.getAttributeValue("AXValue"),
+ bquotes[2].getAttributeValue("AXValue"),
+ "Found correct third blockquote"
+ );
+ }
+);
+
+/*
+ * Test rotor with graphics
+ */
+addAccessibleTask(
+ `
+ <img id="img1" alt="image one" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"><br>
+ <a href="http://example.com">
+ <img id="img2" alt="image two" src="http://example.com/a11y/accessible/tests/mochitest/moz.png">
+ </a>
+ <img src="" id="img3">
+ `,
+ (browser, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXGraphicSearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ let images = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(images.length, 3, "Found three images");
+
+ const img1 = getNativeInterface(accDoc, "img1");
+ const img2 = getNativeInterface(accDoc, "img2");
+ const img3 = getNativeInterface(accDoc, "img3");
+
+ is(
+ img1.getAttributeValue("AXDescription"),
+ images[0].getAttributeValue("AXDescription"),
+ "Found correct image"
+ );
+
+ is(
+ img2.getAttributeValue("AXDescription"),
+ images[1].getAttributeValue("AXDescription"),
+ "Found correct image"
+ );
+
+ is(
+ img3.getAttributeValue("AXDescription"),
+ images[2].getAttributeValue("AXDescription"),
+ "Found correct image"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_outline.js b/accessible/tests/browser/mac/browser_outline.js
new file mode 100644
index 0000000000..ba211fdf4b
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_outline.js
@@ -0,0 +1,566 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test outline, outline rows with computed properties
+ */
+addAccessibleTask(
+ `
+ <h3 id="tree1">
+ Foods
+ </h3>
+ <ul role="tree" aria-labelledby="tree1" id="outline">
+ <li role="treeitem" aria-expanded="false">
+ <span>
+ Fruits
+ </span>
+ <ul>
+ <li role="none">Oranges</li>
+ <li role="treeitem" aria-expanded="true">
+ <span>
+ Apples
+ </span>
+ <ul role="group">
+ <li role="none">Honeycrisp</li>
+ <li role="none">Granny Smith</li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ <li id="vegetables" role="treeitem" aria-expanded="false">
+ <span>
+ Vegetables
+ </span>
+ <ul role="group">
+ <li role="treeitem" aria-expanded="true">
+ <span>
+ Podded Vegetables
+ </span>
+ <ul role="group">
+ <li role="none">Lentil</li>
+ <li role="none">Pea</li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ `,
+ async (browser, accDoc) => {
+ const outline = getNativeInterface(accDoc, "outline");
+ is(
+ outline.getAttributeValue("AXRole"),
+ "AXOutline",
+ "Correct role for outline"
+ );
+
+ const outChildren = outline.getAttributeValue("AXChildren");
+ is(outChildren.length, 2, "Outline has two direct children");
+ is(outChildren[0].getAttributeValue("AXSubrole"), "AXOutlineRow");
+ is(outChildren[1].getAttributeValue("AXSubrole"), "AXOutlineRow");
+
+ const outRows = outline.getAttributeValue("AXRows");
+ is(outRows.length, 4, "Outline has four rows");
+ is(
+ outRows[0].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing"
+ );
+ is(
+ outRows[0].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Row is direct child of outline"
+ );
+ is(
+ outRows[0].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Row has no row children, only group"
+ );
+ is(
+ outRows[0].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Row is level zero"
+ );
+
+ is(outRows[1].getAttributeValue("AXDisclosing"), 1, "Row is disclosing");
+ is(
+ outRows[1].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Row is direct child of group"
+ );
+ is(
+ outRows[1].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Row has no row children"
+ );
+ is(
+ outRows[1].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Row is level zero"
+ );
+
+ is(
+ outRows[2].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing"
+ );
+ is(
+ outRows[2].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Row is direct child of outline"
+ );
+ is(
+ outRows[2].getAttributeValue("AXDisclosedRows").length,
+ 1,
+ "Row has one row child"
+ );
+ is(
+ outRows[2].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Row is level zero"
+ );
+
+ is(outRows[3].getAttributeValue("AXDisclosing"), 1, "Row is disclosing");
+ is(
+ outRows[3]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ outRows[2].getAttributeValue("AXDescription"),
+ "Row is direct child of row[2]"
+ );
+ is(
+ outRows[3].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Row has no row children"
+ );
+ is(
+ outRows[3].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Row is level one"
+ );
+
+ let evt = waitForMacEvent("AXRowExpanded", "vegetables");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("vegetables")
+ .setAttribute("aria-expanded", "true");
+ });
+ await evt;
+ is(
+ outRows[2].getAttributeValue("AXDisclosing"),
+ 1,
+ "Row is disclosing after being expanded"
+ );
+
+ evt = waitForMacEvent("AXRowCollapsed", "vegetables");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("vegetables")
+ .setAttribute("aria-expanded", "false");
+ });
+ await evt;
+ is(
+ outRows[2].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing after being collapsed again"
+ );
+ }
+);
+
+/**
+ * Test outline, outline rows with declared properties
+ */
+addAccessibleTask(
+ `
+ <h3 id="tree1">
+ Foods
+ </h3>
+ <ul role="tree" aria-labelledby="tree1" id="outline">
+ <li role="treeitem"
+ aria-level="1"
+ aria-setsize="2"
+ aria-posinset="1"
+ aria-expanded="false">
+ <span>
+ Fruits
+ </span>
+ <ul>
+ <li role="treeitem"
+ aria-level="3"
+ aria-setsize="2"
+ aria-posinset="1">
+ Oranges
+ </li>
+ <li role="treeitem"
+ aria-level="2"
+ aria-setsize="2"
+ aria-posinset="2"
+ aria-expanded="true">
+ <span>
+ Apples
+ </span>
+ <ul role="group">
+ <li role="treeitem"
+ aria-level="3"
+ aria-setsize="2"
+ aria-posinset="1">
+ Honeycrisp
+ </li>
+ <li role="treeitem"
+ aria-level="3"
+ aria-setsize="2"
+ aria-posinset="2">
+ Granny Smith
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ <li role="treeitem"
+ aria-level="1"
+ aria-setsize="2"
+ aria-posinset="2"
+ aria-expanded="false">
+ <span>
+ Vegetables
+ </span>
+ <ul role="group">
+ <li role="treeitem"
+ aria-level="2"
+ aria-setsize="1"
+ aria-posinset="1"
+ aria-expanded="true">
+ <span>
+ Podded Vegetables
+ </span>
+ <ul role="group">
+ <li role="treeitem"
+ aria-level="3"
+ aria-setsize="2"
+ aria-posinset="1">
+ Lentil
+ </li>
+ <li role="treeitem"
+ aria-level="3"
+ aria-setsize="2"
+ aria-posinset="2">
+ Pea
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ `,
+ async (browser, accDoc) => {
+ const outline = getNativeInterface(accDoc, "outline");
+ is(
+ outline.getAttributeValue("AXRole"),
+ "AXOutline",
+ "Correct role for outline"
+ );
+
+ const outChildren = outline.getAttributeValue("AXChildren");
+ is(outChildren.length, 2, "Outline has two direct children");
+ is(outChildren[0].getAttributeValue("AXSubrole"), "AXOutlineRow");
+ is(outChildren[1].getAttributeValue("AXSubrole"), "AXOutlineRow");
+
+ const outRows = outline.getAttributeValue("AXRows");
+ is(outRows.length, 9, "Outline has nine rows");
+ is(
+ outRows[0].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing"
+ );
+ is(
+ outRows[0].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Row is direct child of outline"
+ );
+ is(
+ outRows[0].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Row has no direct row children, has list"
+ );
+ is(
+ outRows[0].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Row is level zero"
+ );
+
+ is(outRows[2].getAttributeValue("AXDisclosing"), 1, "Row is disclosing");
+ is(
+ outRows[2].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Row is direct child of group"
+ );
+ is(
+ outRows[2].getAttributeValue("AXDisclosedRows").length,
+ 2,
+ "Row has two row children"
+ );
+ is(
+ outRows[2].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Row is level one"
+ );
+
+ is(
+ outRows[3].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing"
+ );
+ is(
+ outRows[3]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ outRows[2].getAttributeValue("AXDescription"),
+ "Row is direct child of row 2"
+ );
+
+ is(
+ outRows[3].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Row has no row children"
+ );
+ is(
+ outRows[3].getAttributeValue("AXDisclosureLevel"),
+ 2,
+ "Row is level two"
+ );
+
+ is(
+ outRows[5].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing"
+ );
+ is(
+ outRows[5].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Row is direct child of outline"
+ );
+ is(
+ outRows[5].getAttributeValue("AXDisclosedRows").length,
+ 1,
+ "Row has no one row child"
+ );
+ is(
+ outRows[5].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Row is level zero"
+ );
+
+ is(outRows[6].getAttributeValue("AXDisclosing"), 1, "Row is disclosing");
+ is(
+ outRows[6]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ outRows[5].getAttributeValue("AXDescription"),
+ "Row is direct child of row 5"
+ );
+ is(
+ outRows[6].getAttributeValue("AXDisclosedRows").length,
+ 2,
+ "Row has two row children"
+ );
+ is(
+ outRows[6].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Row is level one"
+ );
+
+ is(
+ outRows[7].getAttributeValue("AXDisclosing"),
+ 0,
+ "Row is not disclosing"
+ );
+ is(
+ outRows[7]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ outRows[6].getAttributeValue("AXDescription"),
+ "Row is direct child of row 6"
+ );
+ is(
+ outRows[7].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Row has no row children"
+ );
+ is(
+ outRows[7].getAttributeValue("AXDisclosureLevel"),
+ 2,
+ "Row is level two"
+ );
+ }
+);
+
+// Test outline that isn't built with li/uls gets correct desc
+addAccessibleTask(
+ `
+ <div role="tree" id="tree" tabindex="0" aria-label="My drive" aria-activedescendant="myfiles">
+ <div id="myfiles" role="treeitem" aria-label="My files" aria-selected="true" aria-expanded="false">My files</div>
+ <div role="treeitem" aria-label="Shared items" aria-selected="false" aria-expanded="false">Shared items</div>
+ </div>
+ `,
+ async (browser, accDoc) => {
+ const tree = getNativeInterface(accDoc, "tree");
+ is(tree.getAttributeValue("AXRole"), "AXOutline", "Correct role for tree");
+
+ const treeItems = tree.getAttributeValue("AXChildren");
+ is(treeItems.length, 2, "Outline has two direct children");
+ is(treeItems[0].getAttributeValue("AXSubrole"), "AXOutlineRow");
+ is(treeItems[1].getAttributeValue("AXSubrole"), "AXOutlineRow");
+
+ const outRows = tree.getAttributeValue("AXRows");
+ is(outRows.length, 2, "Outline has two rows");
+
+ is(
+ outRows[0].getAttributeValue("AXDescription"),
+ "My files",
+ "files labelled correctly"
+ );
+ is(
+ outRows[1].getAttributeValue("AXDescription"),
+ "Shared items",
+ "shared items labelled correctly"
+ );
+ }
+);
+
+// Test outline registers AXDisclosed attr as settable
+addAccessibleTask(
+ `
+ <div role="tree" id="tree" tabindex="0" aria-label="My drive" aria-activedescendant="myfiles">
+ <div id="myfiles" role="treeitem" aria-label="My files" aria-selected="true" aria-expanded="false">My files</div>
+ <div role="treeitem" aria-label="Shared items" aria-selected="false" aria-expanded="true">Shared items</div>
+ </div>
+ `,
+ async (browser, accDoc) => {
+ const tree = getNativeInterface(accDoc, "tree");
+ const treeItems = tree.getAttributeValue("AXChildren");
+
+ is(treeItems.length, 2, "Outline has two direct children");
+ is(treeItems[0].getAttributeValue("AXDisclosing"), 0);
+ is(treeItems[1].getAttributeValue("AXDisclosing"), 1);
+
+ is(treeItems[0].isAttributeSettable("AXDisclosing"), true);
+ is(treeItems[1].isAttributeSettable("AXDisclosing"), true);
+
+ // attempt to change attribute values
+ treeItems[0].setAttributeValue("AXDisclosing", 1);
+ treeItems[0].setAttributeValue("AXDisclosing", 0);
+
+ // verify they're unchanged
+ is(treeItems[0].getAttributeValue("AXDisclosing"), 0);
+ is(treeItems[1].getAttributeValue("AXDisclosing"), 1);
+ }
+);
+
+// Test outline rows correctly expose checkable, checked/unchecked/mixed status
+addAccessibleTask(
+ `
+ <div role="tree" id="tree">
+ <div role="treeitem" aria-checked="false" id="l1">
+ Leaf 1
+ </div>
+ <div role="treeitem" aria-checked="true" id="l2">
+ Leaf 2
+ </div>
+ <div role="treeitem" id="l3">
+ Leaf 3
+ </div>
+ <div role="treeitem" aria-checked="mixed" id="l4">
+ Leaf 4
+ </div>
+ </div>
+
+ `,
+ async (browser, accDoc) => {
+ const tree = getNativeInterface(accDoc, "tree");
+ const treeItems = tree.getAttributeValue("AXChildren");
+
+ is(treeItems.length, 4, "Outline has four direct children");
+ is(
+ treeItems[0].getAttributeValue("AXValue"),
+ 0,
+ "Child one is not checked"
+ );
+ is(treeItems[1].getAttributeValue("AXValue"), 1, "Child two is checked");
+ is(
+ treeItems[2].getAttributeValue("AXValue"),
+ null,
+ "Child three is not checkable and has no val"
+ );
+ is(treeItems[3].getAttributeValue("AXValue"), 2, "Child four is mixed");
+
+ let stateChanged = Promise.all([
+ waitForMacEvent("AXValueChanged", "l1"),
+ waitForStateChange("l1", STATE_CHECKED, true),
+ ]);
+ // We should get a state change event for checked.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("l1")
+ .setAttribute("aria-checked", "true");
+ });
+ await stateChanged;
+ is(treeItems[0].getAttributeValue("AXValue"), 1, "Child one is checked");
+
+ stateChanged = Promise.all([
+ waitForMacEvent("AXValueChanged", "l2"),
+ waitForMacEvent("AXValueChanged", "l2"),
+ waitForStateChange("l2", STATE_CHECKED, false),
+ waitForStateChange("l2", STATE_CHECKABLE, false),
+ ]);
+ // We should get a state change event for both checked and checkable,
+ // and value changes for both.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("l2").removeAttribute("aria-checked");
+ });
+ await stateChanged;
+ is(
+ treeItems[1].getAttributeValue("AXValue"),
+ null,
+ "Child two is not checkable and has no val"
+ );
+
+ stateChanged = Promise.all([
+ waitForMacEvent("AXValueChanged", "l3"),
+ waitForMacEvent("AXValueChanged", "l3"),
+ waitForStateChange("l3", STATE_CHECKED, true),
+ waitForStateChange("l3", STATE_CHECKABLE, true),
+ ]);
+ // We should get a state change event for both checked and checkable,
+ // and value changes for each.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("l3")
+ .setAttribute("aria-checked", "true");
+ });
+ await stateChanged;
+ is(treeItems[2].getAttributeValue("AXValue"), 1, "Child three is checked");
+
+ stateChanged = Promise.all([
+ waitForMacEvent("AXValueChanged", "l4"),
+ waitForMacEvent("AXValueChanged", "l4"),
+ waitForStateChange("l4", STATE_MIXED, false),
+ waitForStateChange("l4", STATE_CHECKABLE, false),
+ ]);
+ // We should get a state change event for both mixed and checkable,
+ // and value changes for each.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("l4").removeAttribute("aria-checked");
+ });
+ await stateChanged;
+ is(
+ treeItems[3].getAttributeValue("AXValue"),
+ null,
+ "Child four is not checkable and has no value"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_outline_xul.js b/accessible/tests/browser/mac/browser_outline_xul.js
new file mode 100644
index 0000000000..66eebebf50
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_outline_xul.js
@@ -0,0 +1,274 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ "mac/doc_tree.xhtml",
+ async (browser, accDoc) => {
+ const tree = getNativeInterface(accDoc, "tree");
+ is(
+ tree.getAttributeValue("AXRole"),
+ "AXOutline",
+ "Found tree with role outline"
+ );
+ // XUL trees store all rows as direct children of the outline,
+ // so we should see nine here instead of just three:
+ // (Groceries, Fruits, Veggies)
+ const treeChildren = tree.getAttributeValue("AXChildren");
+ is(treeChildren.length, 9, "Found nine direct children");
+
+ const treeCols = tree.getAttributeValue("AXColumns");
+ is(treeCols.length, 1, "Found one column in tree");
+
+ // Here, we should get only outline rows, not the title
+ const treeRows = tree.getAttributeValue("AXRows");
+ is(treeRows.length, 8, "Found 8 total rows");
+
+ is(
+ treeRows[0].getAttributeValue("AXDescription"),
+ "Fruits",
+ "Located correct first row, row has correct desc"
+ );
+ is(
+ treeRows[0].getAttributeValue("AXDisclosing"),
+ 1,
+ "Fruits is disclosing"
+ );
+ is(
+ treeRows[0].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Fruits is disclosed by outline"
+ );
+ is(
+ treeRows[0].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Fruits is level zero"
+ );
+ let disclosedRows = treeRows[0].getAttributeValue("AXDisclosedRows");
+ is(disclosedRows.length, 2, "Fruits discloses two rows");
+ is(
+ disclosedRows[0].getAttributeValue("AXDescription"),
+ "Apple",
+ "fruits discloses apple"
+ );
+ is(
+ disclosedRows[1].getAttributeValue("AXDescription"),
+ "Orange",
+ "fruits discloses orange"
+ );
+
+ is(
+ treeRows[1].getAttributeValue("AXDescription"),
+ "Apple",
+ "Located correct second row, row has correct desc"
+ );
+ is(
+ treeRows[1].getAttributeValue("AXDisclosing"),
+ 0,
+ "Apple is not disclosing"
+ );
+ is(
+ treeRows[1]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ "Fruits",
+ "Apple is disclosed by fruits"
+ );
+ is(
+ treeRows[1].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Apple is level one"
+ );
+ is(
+ treeRows[1].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Apple does not disclose rows"
+ );
+
+ is(
+ treeRows[2].getAttributeValue("AXDescription"),
+ "Orange",
+ "Located correct third row, row has correct desc"
+ );
+ is(
+ treeRows[2].getAttributeValue("AXDisclosing"),
+ 0,
+ "Orange is not disclosing"
+ );
+ is(
+ treeRows[2]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ "Fruits",
+ "Orange is disclosed by fruits"
+ );
+ is(
+ treeRows[2].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Orange is level one"
+ );
+ is(
+ treeRows[2].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Orange does not disclose rows"
+ );
+
+ is(
+ treeRows[3].getAttributeValue("AXDescription"),
+ "Veggies",
+ "Located correct fourth row, row has correct desc"
+ );
+ is(
+ treeRows[3].getAttributeValue("AXDisclosing"),
+ 1,
+ "Veggies is disclosing"
+ );
+ is(
+ treeRows[3].getAttributeValue("AXDisclosedByRow"),
+ null,
+ "Veggies is disclosed by outline"
+ );
+ is(
+ treeRows[3].getAttributeValue("AXDisclosureLevel"),
+ 0,
+ "Veggies is level zero"
+ );
+ disclosedRows = treeRows[3].getAttributeValue("AXDisclosedRows");
+ is(disclosedRows.length, 2, "Veggies discloses two rows");
+ is(
+ disclosedRows[0].getAttributeValue("AXDescription"),
+ "Green Veggies",
+ "Veggies discloses green veggies"
+ );
+ is(
+ disclosedRows[1].getAttributeValue("AXDescription"),
+ "Squash",
+ "Veggies discloses squash"
+ );
+
+ is(
+ treeRows[4].getAttributeValue("AXDescription"),
+ "Green Veggies",
+ "Located correct fifth row, row has correct desc"
+ );
+ is(
+ treeRows[4].getAttributeValue("AXDisclosing"),
+ 1,
+ "Green veggies is disclosing"
+ );
+ is(
+ treeRows[4]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ "Veggies",
+ "Green Veggies is disclosed by veggies"
+ );
+ is(
+ treeRows[4].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Green veggies is level one"
+ );
+ disclosedRows = treeRows[4].getAttributeValue("AXDisclosedRows");
+ is(disclosedRows.length, 2, "Green veggies has two rows");
+ is(
+ disclosedRows[0].getAttributeValue("AXDescription"),
+ "Spinach",
+ "Green veggies discloses spinach"
+ );
+ is(
+ disclosedRows[1].getAttributeValue("AXDescription"),
+ "Peas",
+ "Green veggies discloses peas"
+ );
+
+ is(
+ treeRows[5].getAttributeValue("AXDescription"),
+ "Spinach",
+ "Located correct sixth row, row has correct desc"
+ );
+ is(
+ treeRows[5].getAttributeValue("AXDisclosing"),
+ 0,
+ "Spinach is not disclosing"
+ );
+ is(
+ treeRows[5]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ "Green Veggies",
+ "Spinach is disclosed by green veggies"
+ );
+ is(
+ treeRows[5].getAttributeValue("AXDisclosureLevel"),
+ 2,
+ "Spinach is level two"
+ );
+ is(
+ treeRows[5].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Spinach does not disclose rows"
+ );
+
+ is(
+ treeRows[6].getAttributeValue("AXDescription"),
+ "Peas",
+ "Located correct seventh row, row has correct desc"
+ );
+ is(
+ treeRows[6].getAttributeValue("AXDisclosing"),
+ 0,
+ "Peas is not disclosing"
+ );
+ is(
+ treeRows[6]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ "Green Veggies",
+ "Peas is disclosed by green veggies"
+ );
+ is(
+ treeRows[6].getAttributeValue("AXDisclosureLevel"),
+ 2,
+ "Peas is level two"
+ );
+ is(
+ treeRows[6].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Peas does not disclose rows"
+ );
+
+ is(
+ treeRows[7].getAttributeValue("AXDescription"),
+ "Squash",
+ "Located correct eighth row, row has correct desc"
+ );
+ is(
+ treeRows[7].getAttributeValue("AXDisclosing"),
+ 0,
+ "Squash is not disclosing"
+ );
+ is(
+ treeRows[7]
+ .getAttributeValue("AXDisclosedByRow")
+ .getAttributeValue("AXDescription"),
+ "Veggies",
+ "Squash is disclosed by veggies"
+ );
+ is(
+ treeRows[7].getAttributeValue("AXDisclosureLevel"),
+ 1,
+ "Squash is level one"
+ );
+ is(
+ treeRows[7].getAttributeValue("AXDisclosedRows").length,
+ 0,
+ "Squash does not disclose rows"
+ );
+ },
+ { topLevel: false, chrome: true }
+);
diff --git a/accessible/tests/browser/mac/browser_popupbutton.js b/accessible/tests/browser/mac/browser_popupbutton.js
new file mode 100644
index 0000000000..2d5ff1ac35
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_popupbutton.js
@@ -0,0 +1,166 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+// Test dropdown select element
+addAccessibleTask(
+ `<select id="select" aria-label="Choose a number">
+ <option id="one" selected>One</option>
+ <option id="two">Two</option>
+ <option id="three">Three</option>
+ <option id="four" disabled>Four</option>
+ </select>`,
+ async (browser, accDoc) => {
+ // Test combobox
+ let select = getNativeInterface(accDoc, "select");
+ is(
+ select.getAttributeValue("AXRole"),
+ "AXPopUpButton",
+ "select has AXPopupButton role"
+ );
+ ok(select.attributeNames.includes("AXValue"), "select advertises AXValue");
+ is(
+ select.getAttributeValue("AXValue"),
+ "One",
+ "select has correctt initial value"
+ );
+ ok(
+ !select.attributeNames.includes("AXHasPopup"),
+ "select does not advertise AXHasPopup"
+ );
+ is(
+ select.getAttributeValue("AXHasPopup"),
+ null,
+ "select does not provide value for AXHasPopup"
+ );
+
+ ok(select.actionNames.includes("AXPress"), "Selectt has press action");
+ // These four events happen in quick succession when select is pressed
+ let events = Promise.all([
+ waitForMacEvent("AXMenuOpened"),
+ waitForMacEvent("AXSelectedChildrenChanged"),
+ waitForMacEvent(
+ "AXFocusedUIElementChanged",
+ e => e.getAttributeValue("AXRole") == "AXPopUpButton"
+ ),
+ waitForMacEvent(
+ "AXFocusedUIElementChanged",
+ e => e.getAttributeValue("AXRole") == "AXMenuItem"
+ ),
+ ]);
+ select.performAction("AXPress");
+ // Only capture the target of AXMenuOpened (first element)
+ let [menu] = await events;
+
+ is(menu.getAttributeValue("AXRole"), "AXMenu", "dropdown has AXMenu role");
+ is(
+ menu.getAttributeValue("AXSelectedChildren").length,
+ 1,
+ "dropdown has single selected child"
+ );
+
+ let selectedChildren = menu.getAttributeValue("AXSelectedChildren");
+ is(selectedChildren.length, 1, "Only one child is selected");
+ is(selectedChildren[0].getAttributeValue("AXRole"), "AXMenuItem");
+ is(selectedChildren[0].getAttributeValue("AXTitle"), "One");
+
+ let menuParent = menu.getAttributeValue("AXParent");
+ is(
+ menuParent.getAttributeValue("AXRole"),
+ "AXPopUpButton",
+ "dropdown parent is a popup button"
+ );
+
+ let menuItems = menu.getAttributeValue("AXChildren").map(c => {
+ return [
+ c.getAttributeValue("AXMenuItemMarkChar"),
+ c.getAttributeValue("AXRole"),
+ c.getAttributeValue("AXTitle"),
+ c.getAttributeValue("AXEnabled"),
+ ];
+ });
+
+ Assert.deepEqual(
+ menuItems,
+ [
+ ["✓", "AXMenuItem", "One", true],
+ [null, "AXMenuItem", "Two", true],
+ [null, "AXMenuItem", "Three", true],
+ [null, "AXMenuItem", "Four", false],
+ ],
+ "Menu items have correct checkmark on current value, correctt roles, correct titles, and correct AXEnabled value"
+ );
+
+ events = Promise.all([
+ waitForMacEvent("AXSelectedChildrenChanged"),
+ waitForMacEvent("AXFocusedUIElementChanged"),
+ ]);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ let [, menuItem] = await events;
+ is(
+ menuItem.getAttributeValue("AXTitle"),
+ "Two",
+ "Focused menu item has correct title"
+ );
+
+ selectedChildren = menu.getAttributeValue("AXSelectedChildren");
+ is(selectedChildren.length, 1, "Only one child is selected");
+ is(
+ selectedChildren[0].getAttributeValue("AXTitle"),
+ "Two",
+ "Selected child matches focused item"
+ );
+
+ events = Promise.all([
+ waitForMacEvent("AXSelectedChildrenChanged"),
+ waitForMacEvent("AXFocusedUIElementChanged"),
+ ]);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ [, menuItem] = await events;
+ is(
+ menuItem.getAttributeValue("AXTitle"),
+ "Three",
+ "Focused menu item has correct title"
+ );
+
+ selectedChildren = menu.getAttributeValue("AXSelectedChildren");
+ is(selectedChildren.length, 1, "Only one child is selected");
+ is(
+ selectedChildren[0].getAttributeValue("AXTitle"),
+ "Three",
+ "Selected child matches focused item"
+ );
+
+ events = Promise.all([
+ waitForMacEvent("AXMenuClosed"),
+ waitForMacEvent("AXFocusedUIElementChanged"),
+ waitForMacEvent("AXSelectedChildrenChanged"),
+ ]);
+ menuItem.performAction("AXPress");
+ let [, newFocus] = await events;
+ is(
+ newFocus.getAttributeValue("AXRole"),
+ "AXPopUpButton",
+ "Newly focused element is AXPopupButton"
+ );
+ is(
+ newFocus.getAttributeValue("AXDOMIdentifier"),
+ "select",
+ "Should return focus to select"
+ );
+ is(
+ newFocus.getAttributeValue("AXValue"),
+ "Three",
+ "select has correct new value"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_radio_position.js b/accessible/tests/browser/mac/browser_radio_position.js
new file mode 100644
index 0000000000..76f518a91e
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_radio_position.js
@@ -0,0 +1,321 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+function getChildRoles(parent) {
+ return parent
+ .getAttributeValue("AXChildren")
+ .map(c => c.getAttributeValue("AXRole"));
+}
+
+function getLinkedTitles(element) {
+ return element
+ .getAttributeValue("AXLinkedUIElements")
+ .map(c => c.getAttributeValue("AXTitle"));
+}
+
+/**
+ * Test radio group
+ */
+addAccessibleTask(
+ `<div role="radiogroup" id="radioGroup">
+ <div role="radio"
+ id="radioGroupItem1">
+ Regular crust
+ </div>
+ <div role="radio"
+ id="radioGroupItem2">
+ Deep dish
+ </div>
+ <div role="radio"
+ id="radioGroupItem3">
+ Thin crust
+ </div>
+ </div>`,
+ async (browser, accDoc) => {
+ let item1 = getNativeInterface(accDoc, "radioGroupItem1");
+ let item2 = getNativeInterface(accDoc, "radioGroupItem2");
+ let item3 = getNativeInterface(accDoc, "radioGroupItem3");
+ let titleList = ["Regular crust", "Deep dish", "Thin crust"];
+
+ Assert.deepEqual(
+ titleList,
+ [item1, item2, item3].map(c => c.getAttributeValue("AXTitle")),
+ "Title list matches"
+ );
+
+ let linkedElems = item1.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Item 1 has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(item1),
+ titleList,
+ "Item one has correctly ordered linked elements"
+ );
+
+ linkedElems = item2.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Item 2 has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(item2),
+ titleList,
+ "Item two has correctly ordered linked elements"
+ );
+
+ linkedElems = item3.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Item 3 has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(item3),
+ titleList,
+ "Item three has correctly ordered linked elements"
+ );
+ }
+);
+
+/**
+ * Test dynamic add to a radio group
+ */
+addAccessibleTask(
+ `<div role="radiogroup" id="radioGroup">
+ <div role="radio"
+ id="radioGroupItem1">
+ Option One
+ </div>
+ </div>`,
+ async (browser, accDoc) => {
+ let item1 = getNativeInterface(accDoc, "radioGroupItem1");
+ let linkedElems = item1.getAttributeValue("AXLinkedUIElements");
+
+ is(linkedElems.length, 1, "Item 1 has one linked UI elem");
+ is(
+ linkedElems[0].getAttributeValue("AXTitle"),
+ item1.getAttributeValue("AXTitle"),
+ "Item 1 is first element"
+ );
+
+ let reorder = waitForEvent(EVENT_REORDER, "radioGroup");
+ await SpecialPowers.spawn(browser, [], () => {
+ let d = content.document.createElement("div");
+ d.setAttribute("role", "radio");
+ content.document.getElementById("radioGroup").appendChild(d);
+ });
+ await reorder;
+
+ let radioGroup = getNativeInterface(accDoc, "radioGroup");
+ let groupMembers = radioGroup.getAttributeValue("AXChildren");
+ is(groupMembers.length, 2, "Radio group has two members");
+ let item2 = groupMembers[1];
+ item1 = getNativeInterface(accDoc, "radioGroupItem1");
+ let titleList = ["Option One", ""];
+
+ Assert.deepEqual(
+ titleList,
+ [item1, item2].map(c => c.getAttributeValue("AXTitle")),
+ "Title list matches"
+ );
+
+ linkedElems = item1.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 2, "Item 1 has two linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(item1),
+ titleList,
+ "Item one has correctly ordered linked elements"
+ );
+
+ linkedElems = item2.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 2, "Item 2 has two linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(item2),
+ titleList,
+ "Item two has correctly ordered linked elements"
+ );
+ }
+);
+
+/**
+ * Test input[type=radio] for single group
+ */
+addAccessibleTask(
+ `<input type="radio" id="cat" name="animal"><label for="cat">Cat</label>
+ <input type="radio" id="dog" name="animal"><label for="dog">Dog</label>
+ <input type="radio" id="catdog" name="animal"><label for="catdog">CatDog</label>`,
+ async (browser, accDoc) => {
+ let cat = getNativeInterface(accDoc, "cat");
+ let dog = getNativeInterface(accDoc, "dog");
+ let catdog = getNativeInterface(accDoc, "catdog");
+ let titleList = ["Cat", "Dog", "CatDog"];
+
+ Assert.deepEqual(
+ titleList,
+ [cat, dog, catdog].map(x => x.getAttributeValue("AXTitle")),
+ "Title list matches"
+ );
+
+ let linkedElems = cat.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Cat has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(cat),
+ titleList,
+ "Cat has correctly ordered linked elements"
+ );
+
+ linkedElems = dog.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Dog has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(dog),
+ titleList,
+ "Dog has correctly ordered linked elements"
+ );
+
+ linkedElems = catdog.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Catdog has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(catdog),
+ titleList,
+ "catdog has correctly ordered linked elements"
+ );
+ }
+);
+
+/**
+ * Test input[type=radio] for different groups
+ */
+addAccessibleTask(
+ `<input type="radio" id="cat" name="one"><label for="cat">Cat</label>
+ <input type="radio" id="dog" name="two"><label for="dog">Dog</label>
+ <input type="radio" id="catdog"><label for="catdog">CatDog</label>`,
+ async (browser, accDoc) => {
+ let cat = getNativeInterface(accDoc, "cat");
+ let dog = getNativeInterface(accDoc, "dog");
+ let catdog = getNativeInterface(accDoc, "catdog");
+
+ let linkedElems = cat.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 1, "Cat has one linked UI elem");
+ is(
+ linkedElems[0].getAttributeValue("AXTitle"),
+ cat.getAttributeValue("AXTitle"),
+ "Cat is only element"
+ );
+
+ linkedElems = dog.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 1, "Dog has one linked UI elem");
+ is(
+ linkedElems[0].getAttributeValue("AXTitle"),
+ dog.getAttributeValue("AXTitle"),
+ "Dog is only element"
+ );
+
+ linkedElems = catdog.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 0, "Catdog has no linked UI elem");
+ }
+);
+
+/**
+ * Test input[type=radio] for single group across DOM
+ */
+addAccessibleTask(
+ `<input type="radio" id="cat" name="animal"><label for="cat">Cat</label>
+ <div>
+ <span>
+ <input type="radio" id="dog" name="animal"><label for="dog">Dog</label>
+ </span>
+ </div>
+ <div>
+ <input type="radio" id="catdog" name="animal"><label for="catdog">CatDog</label>
+ </div>`,
+ async (browser, accDoc) => {
+ let cat = getNativeInterface(accDoc, "cat");
+ let dog = getNativeInterface(accDoc, "dog");
+ let catdog = getNativeInterface(accDoc, "catdog");
+ let titleList = ["Cat", "Dog", "CatDog"];
+
+ Assert.deepEqual(
+ titleList,
+ [cat, dog, catdog].map(x => x.getAttributeValue("AXTitle")),
+ "Title list matches"
+ );
+
+ let linkedElems = cat.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Cat has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(cat),
+ titleList,
+ "cat has correctly ordered linked elements"
+ );
+
+ linkedElems = dog.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Dog has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(dog),
+ titleList,
+ "dog has correctly ordered linked elements"
+ );
+
+ linkedElems = catdog.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 3, "Catdog has three linked UI elems");
+ Assert.deepEqual(
+ getLinkedTitles(catdog),
+ titleList,
+ "catdog has correctly ordered linked elements"
+ );
+ }
+);
+
+/**
+ * Test dynamic add of input[type=radio] in a single group
+ */
+addAccessibleTask(
+ `<div id="container"><input type="radio" id="cat" name="animal"></div>`,
+ async (browser, accDoc) => {
+ let cat = getNativeInterface(accDoc, "cat");
+ let container = getNativeInterface(accDoc, "container");
+
+ let containerChildren = container.getAttributeValue("AXChildren");
+ is(containerChildren.length, 1, "container has one button");
+ is(
+ containerChildren[0].getAttributeValue("AXRole"),
+ "AXRadioButton",
+ "Container child is radio button"
+ );
+
+ let linkedElems = cat.getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 1, "Cat has 1 linked UI elem");
+ is(
+ linkedElems[0].getAttributeValue("AXTitle"),
+ cat.getAttributeValue("AXTitle"),
+ "Cat is first element"
+ );
+ let reorder = waitForEvent(EVENT_REORDER, "container");
+ await SpecialPowers.spawn(browser, [], () => {
+ let input = content.document.createElement("input");
+ input.setAttribute("type", "radio");
+ input.setAttribute("name", "animal");
+ content.document.getElementById("container").appendChild(input);
+ });
+ await reorder;
+
+ container = getNativeInterface(accDoc, "container");
+ containerChildren = container.getAttributeValue("AXChildren");
+
+ is(containerChildren.length, 2, "container has two children");
+
+ Assert.deepEqual(
+ getChildRoles(container),
+ ["AXRadioButton", "AXRadioButton"],
+ "Both children are radio buttons"
+ );
+
+ linkedElems = containerChildren[0].getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 2, "Cat has 2 linked elements");
+
+ linkedElems = containerChildren[1].getAttributeValue("AXLinkedUIElements");
+ is(linkedElems.length, 2, "New button has 2 linked elements");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_range.js b/accessible/tests/browser/mac/browser_range.js
new file mode 100644
index 0000000000..430e41d6ea
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_range.js
@@ -0,0 +1,190 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Verify that the value of a slider input can be incremented/decremented
+ * Test input[type=range]
+ */
+addAccessibleTask(
+ `<input id="range" type="range" min="1" max="100" value="1" step="10">`,
+ async (browser, accDoc) => {
+ let range = getNativeInterface(accDoc, "range");
+ is(range.getAttributeValue("AXRole"), "AXSlider", "Correct AXSlider role");
+ is(range.getAttributeValue("AXValue"), 1, "Correct initial value");
+
+ let actions = range.actionNames;
+ ok(actions.includes("AXDecrement"), "Has decrement action");
+ ok(actions.includes("AXIncrement"), "Has increment action");
+
+ let evt = waitForMacEvent("AXValueChanged");
+ range.performAction("AXIncrement");
+ await evt;
+ is(range.getAttributeValue("AXValue"), 11, "Correct increment value");
+
+ evt = waitForMacEvent("AXValueChanged");
+ range.performAction("AXDecrement");
+ await evt;
+ is(range.getAttributeValue("AXValue"), 1, "Correct decrement value");
+
+ evt = waitForMacEvent("AXValueChanged");
+ // Adjust value via script in content
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("range").value = 41;
+ });
+ await evt;
+ is(
+ range.getAttributeValue("AXValue"),
+ 41,
+ "Correct value from content change"
+ );
+ }
+);
+
+/**
+ * Verify that the value of a slider input can be set directly
+ * Test input[type=range]
+ */
+addAccessibleTask(
+ `<input id="range" type="range" min="1" max="100" value="1" step="10">`,
+ async (browser, accDoc) => {
+ let nextValue = 21;
+ let range = getNativeInterface(accDoc, "range");
+ is(range.getAttributeValue("AXRole"), "AXSlider", "Correct AXSlider role");
+ is(range.getAttributeValue("AXValue"), 1, "Correct initial value");
+
+ ok(range.isAttributeSettable("AXValue"), "Range AXValue is settable.");
+
+ let evt = waitForMacEvent("AXValueChanged");
+ range.setAttributeValue("AXValue", nextValue);
+ await evt;
+ is(range.getAttributeValue("AXValue"), nextValue, "Correct updated value");
+ }
+);
+
+/**
+ * Verify that the value of a number input can be incremented/decremented
+ * Test input[type=number]
+ */
+addAccessibleTask(
+ `<input type="number" value="11" id="number" step=".05">`,
+ async (browser, accDoc) => {
+ let number = getNativeInterface(accDoc, "number");
+ is(
+ number.getAttributeValue("AXRole"),
+ "AXIncrementor",
+ "Correct AXIncrementor role"
+ );
+ is(number.getAttributeValue("AXValue"), 11, "Correct initial value");
+
+ let actions = number.actionNames;
+ ok(actions.includes("AXDecrement"), "Has decrement action");
+ ok(actions.includes("AXIncrement"), "Has increment action");
+
+ let evt = waitForMacEvent("AXValueChanged");
+ number.performAction("AXIncrement");
+ await evt;
+ is(number.getAttributeValue("AXValue"), 11.05, "Correct increment value");
+
+ evt = waitForMacEvent("AXValueChanged");
+ number.performAction("AXDecrement");
+ await evt;
+ is(number.getAttributeValue("AXValue"), 11, "Correct decrement value");
+
+ evt = waitForMacEvent("AXValueChanged");
+ // Adjust value via script in content
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("number").value = 42;
+ });
+ await evt;
+ is(
+ number.getAttributeValue("AXValue"),
+ 42,
+ "Correct value from content change"
+ );
+ }
+);
+
+/**
+ * Test Min, Max, Orientation, ValueDescription
+ */
+addAccessibleTask(
+ `<input type="number" value="11" id="number">`,
+ async (browser, accDoc) => {
+ let nextValue = 21;
+ let number = getNativeInterface(accDoc, "number");
+ is(
+ number.getAttributeValue("AXRole"),
+ "AXIncrementor",
+ "Correct AXIncrementor role"
+ );
+ is(number.getAttributeValue("AXValue"), 11, "Correct initial value");
+
+ ok(number.isAttributeSettable("AXValue"), "Range AXValue is settable.");
+
+ let evt = waitForMacEvent("AXValueChanged");
+ number.setAttributeValue("AXValue", nextValue);
+ await evt;
+ is(number.getAttributeValue("AXValue"), nextValue, "Correct updated value");
+ }
+);
+
+/**
+ * Verify that the value of a number input can be set directly
+ * Test input[type=number]
+ */
+addAccessibleTask(
+ `<div aria-valuetext="High" id="slider" aria-orientation="horizontal" role="slider" aria-valuenow="2" aria-valuemin="0" aria-valuemax="3"></div>`,
+ async (browser, accDoc) => {
+ let slider = getNativeInterface(accDoc, "slider");
+ is(
+ slider.getAttributeValue("AXValueDescription"),
+ "High",
+ "Correct value description"
+ );
+ is(
+ slider.getAttributeValue("AXOrientation"),
+ "AXHorizontalOrientation",
+ "Correct orientation"
+ );
+ is(slider.getAttributeValue("AXMinValue"), 0, "Correct min value");
+ is(slider.getAttributeValue("AXMaxValue"), 3, "Correct max value");
+
+ let evt = waitForMacEvent("AXValueChanged");
+ await invokeContentTask(browser, [], () => {
+ const s = content.document.getElementById("slider");
+ s.setAttribute("aria-valuetext", "Low");
+ });
+ await evt;
+ is(
+ slider.getAttributeValue("AXValueDescription"),
+ "Low",
+ "Correct value description"
+ );
+
+ evt = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, "slider");
+ await invokeContentTask(browser, [], () => {
+ const s = content.document.getElementById("slider");
+ s.setAttribute("aria-orientation", "vertical");
+ s.setAttribute("aria-valuemin", "-1");
+ s.setAttribute("aria-valuemax", "5");
+ });
+ await evt;
+ is(
+ slider.getAttributeValue("AXOrientation"),
+ "AXVerticalOrientation",
+ "Correct orientation"
+ );
+ is(slider.getAttributeValue("AXMinValue"), -1, "Correct min value");
+ is(slider.getAttributeValue("AXMaxValue"), 5, "Correct max value");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_required.js b/accessible/tests/browser/mac/browser_required.js
new file mode 100644
index 0000000000..2109d265ab
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_required.js
@@ -0,0 +1,175 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test required and aria-required attributes on checkboxes
+ * and radio buttons.
+ */
+addAccessibleTask(
+ `
+ <form>
+ <input type="checkbox" id="checkbox" required>
+ <br>
+ <input type="radio" id="radio" required>
+ <br>
+ <input type="checkbox" id="ariaCheckbox" aria-required="true">
+ <br>
+ <input type="radio" id="ariaRadio" aria-required="true">
+ </form>
+ `,
+ async (browser, accDoc) => {
+ // Check initial AXRequired values are correct
+ let radio = getNativeInterface(accDoc, "radio");
+ is(
+ radio.getAttributeValue("AXRequired"),
+ 1,
+ "Correct required val for radio"
+ );
+
+ let ariaRadio = getNativeInterface(accDoc, "ariaRadio");
+ is(
+ ariaRadio.getAttributeValue("AXRequired"),
+ 1,
+ "Correct required val for ariaRadio"
+ );
+
+ let checkbox = getNativeInterface(accDoc, "checkbox");
+ is(
+ checkbox.getAttributeValue("AXRequired"),
+ 1,
+ "Correct required val for checkbox"
+ );
+
+ let ariaCheckbox = getNativeInterface(accDoc, "ariaCheckbox");
+ is(
+ ariaCheckbox.getAttributeValue("AXRequired"),
+ 1,
+ "Correct required val for ariaCheckbox"
+ );
+
+ // Change aria-required, verify AXRequired is updated
+ let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaCheckbox");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("ariaCheckbox")
+ .setAttribute("aria-required", "false");
+ });
+ await stateChanged;
+
+ is(
+ ariaCheckbox.getAttributeValue("AXRequired"),
+ 0,
+ "Correct required after false set for ariaCheckbox"
+ );
+
+ // Change aria-required, verify AXRequired is updated
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaCheckbox");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("ariaCheckbox")
+ .setAttribute("aria-required", "true");
+ });
+ await stateChanged;
+
+ is(
+ ariaCheckbox.getAttributeValue("AXRequired"),
+ 1,
+ "Correct required after true set for ariaCheckbox"
+ );
+
+ // Remove aria-required, verify AXRequired is updated
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaCheckbox");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("ariaCheckbox")
+ .removeAttribute("aria-required");
+ });
+ await stateChanged;
+
+ is(
+ ariaCheckbox.getAttributeValue("AXRequired"),
+ 0,
+ "Correct required after removal for ariaCheckbox"
+ );
+
+ // Change aria-required, verify AXRequired is updated
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaRadio");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("ariaRadio")
+ .setAttribute("aria-required", "false");
+ });
+ await stateChanged;
+
+ is(
+ ariaRadio.getAttributeValue("AXRequired"),
+ 0,
+ "Correct required after false set for ariaRadio"
+ );
+
+ // Change aria-required, verify AXRequired is updated
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaRadio");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("ariaRadio")
+ .setAttribute("aria-required", "true");
+ });
+ await stateChanged;
+
+ is(
+ ariaRadio.getAttributeValue("AXRequired"),
+ 1,
+ "Correct required after true set for ariaRadio"
+ );
+
+ // Remove aria-required, verify AXRequired is updated
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaRadio");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("ariaRadio")
+ .removeAttribute("aria-required");
+ });
+ await stateChanged;
+
+ is(
+ ariaRadio.getAttributeValue("AXRequired"),
+ 0,
+ "Correct required after removal for ariaRadio"
+ );
+
+ // Remove required, verify AXRequired is updated
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "checkbox");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("checkbox").removeAttribute("required");
+ });
+ await stateChanged;
+
+ is(
+ checkbox.getAttributeValue("AXRequired"),
+ 0,
+ "Correct required after removal for checkbox"
+ );
+
+ stateChanged = waitForEvent(EVENT_STATE_CHANGE, "radio");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("radio").removeAttribute("required");
+ });
+ await stateChanged;
+
+ is(
+ checkbox.getAttributeValue("AXRequired"),
+ 0,
+ "Correct required after removal for radio"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_rich_listbox.js b/accessible/tests/browser/mac/browser_rich_listbox.js
new file mode 100644
index 0000000000..97dd6785bb
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_rich_listbox.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
+
+addAccessibleTask(
+ "mac/doc_rich_listbox.xhtml",
+ async (browser, accDoc) => {
+ const categories = getNativeInterface(accDoc, "categories");
+ const categoriesChildren = categories.getAttributeValue("AXChildren");
+ is(categoriesChildren.length, 4, "Found listbox and 4 items");
+
+ const general = getNativeInterface(accDoc, "general");
+ is(
+ general.getAttributeValue("AXTitle"),
+ "general",
+ "general has appropriate title"
+ );
+ is(
+ categoriesChildren[0].getAttributeValue("AXTitle"),
+ general.getAttributeValue("AXTitle"),
+ "Found general listitem"
+ );
+ is(
+ general.getAttributeValue("AXEnabled"),
+ 1,
+ "general is enabled, not dimmed"
+ );
+
+ const home = getNativeInterface(accDoc, "home");
+ is(home.getAttributeValue("AXTitle"), "home", "home has appropriate title");
+ is(
+ categoriesChildren[1].getAttributeValue("AXTitle"),
+ home.getAttributeValue("AXTitle"),
+ "Found home listitem"
+ );
+ is(home.getAttributeValue("AXEnabled"), 1, "Home is enabled, not dimmed");
+
+ const search = getNativeInterface(accDoc, "search");
+ is(
+ search.getAttributeValue("AXTitle"),
+ "search",
+ "search has appropriate title"
+ );
+ is(
+ categoriesChildren[2].getAttributeValue("AXTitle"),
+ search.getAttributeValue("AXTitle"),
+ "Found search listitem"
+ );
+ is(
+ search.getAttributeValue("AXEnabled"),
+ 1,
+ "search is enabled, not dimmed"
+ );
+
+ const privacy = getNativeInterface(accDoc, "privacy");
+ is(
+ privacy.getAttributeValue("AXTitle"),
+ "privacy",
+ "privacy has appropriate title"
+ );
+ is(
+ categoriesChildren[3].getAttributeValue("AXTitle"),
+ privacy.getAttributeValue("AXTitle"),
+ "Found privacy listitem"
+ );
+ },
+ { topLevel: false, chrome: true }
+);
diff --git a/accessible/tests/browser/mac/browser_roles_elements.js b/accessible/tests/browser/mac/browser_roles_elements.js
new file mode 100644
index 0000000000..791598fed6
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_roles_elements.js
@@ -0,0 +1,334 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test different HTML elements for their roles and subroles
+ */
+function testRoleAndSubRole(accDoc, id, axRole, axSubRole, axRoleDescription) {
+ let el = getNativeInterface(accDoc, id);
+ if (axRole) {
+ is(
+ el.getAttributeValue("AXRole"),
+ axRole,
+ "AXRole for " + id + " is " + axRole
+ );
+ }
+ if (axSubRole) {
+ is(
+ el.getAttributeValue("AXSubrole"),
+ axSubRole,
+ "Subrole for " + id + " is " + axSubRole
+ );
+ }
+ if (axRoleDescription) {
+ is(
+ el.getAttributeValue("AXRoleDescription"),
+ axRoleDescription,
+ "Subrole for " + id + " is " + axRoleDescription
+ );
+ }
+}
+
+addAccessibleTask(
+ `
+ <!-- WAI-ARIA landmark roles -->
+ <div id="application" role="application"></div>
+ <div id="banner" role="banner"></div>
+ <div id="complementary" role="complementary"></div>
+ <div id="contentinfo" role="contentinfo"></div>
+ <div id="form" role="form"></div>
+ <div id="main" role="main"></div>
+ <div id="navigation" role="navigation"></div>
+ <div id="search" role="search"></div>
+ <div id="searchbox" role="searchbox"></div>
+
+ <!-- DPub landmarks -->
+ <div id="dPubNavigation" role="doc-index"></div>
+ <div id="dPubRegion" role="doc-introduction"></div>
+
+ <!-- Other WAI-ARIA widget roles -->
+ <div id="alert" role="alert"></div>
+ <div id="alertdialog" role="alertdialog"></div>
+ <div id="article" role="article"></div>
+ <div id="code" role="code"></div>
+ <div id="dialog" role="dialog"></div>
+ <div id="ariaDocument" role="document"></div>
+ <div id="log" role="log"></div>
+ <div id="marquee" role="marquee"></div>
+ <div id="ariaMath" role="math"></div>
+ <div id="note" role="note"></div>
+ <div id="ariaRegion" aria-label="region" role="region"></div>
+ <div id="ariaStatus" role="status"></div>
+ <div id="switch" role="switch"></div>
+ <div id="timer" role="timer"></div>
+ <div id="tooltip" role="tooltip"></div>
+ <input type="radio" role="menuitemradio" id="menuitemradio">
+ <input type="checkbox" role="menuitemcheckbox" id="menuitemcheckbox">
+ <input type="datetime-local" id="datetime">
+
+ <!-- text entries -->
+ <div id="textbox_multiline" role="textbox" aria-multiline="true"></div>
+ <div id="textbox_singleline" role="textbox" aria-multiline="false"></div>
+ <textarea id="textArea"></textarea>
+ <input id="textInput">
+
+ <!-- True HTML5 search box -->
+ <input type="search" id="htmlSearch" />
+
+ <!-- A button morphed into a toggle via ARIA -->
+ <button id="toggle" aria-pressed="false"></button>
+
+ <!-- A button with a 'banana' role description -->
+ <button id="banana" aria-roledescription="banana"></button>
+
+ <!-- Other elements -->
+ <del id="deletion">Deleted text</del>
+ <dl id="dl"><dt id="dt">term</dt><dd id="dd">definition</dd></dl>
+ <hr id="hr" />
+ <ins id="insertion">Inserted text</ins>
+ <meter id="meter" min="0" max="100" value="24">meter text here</meter>
+ <sub id="sub">sub text here</sub>
+ <sup id="sup">sup text here</sup>
+
+ <!-- Some SVG stuff -->
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g id="g">
+ <title>g</title>
+ </g>
+ <rect width="300" height="100" id="rect"
+ style="fill:rgb(0,0,255);stroke-width:1;stroke:rgb(0,0,0)">
+ <title>rect</title>
+ </rect>
+ <circle cx="100" cy="50" r="40" stroke="black" id="circle"
+ stroke-width="2" fill="red">
+ <title>circle</title>
+ </circle>
+ <ellipse cx="300" cy="80" rx="100" ry="50" id="ellipse"
+ style="fill:yellow;stroke:purple;stroke-width:2">
+ <title>ellipse</title>
+ </ellipse>
+ <line x1="0" y1="0" x2="200" y2="200" id="line"
+ style="stroke:rgb(255,0,0);stroke-width:2">
+ <title>line</title>
+ </line>
+ <polygon points="200,10 250,190 160,210" id="polygon"
+ style="fill:lime;stroke:purple;stroke-width:1">
+ <title>polygon</title>
+ </polygon>
+ <polyline points="20,20 40,25 60,40 80,120 120,140 200,180" id="polyline"
+ style="fill:none;stroke:black;stroke-width:3" >
+ <title>polyline</title>
+ </polyline>
+ <path d="M150 0 L75 200 L225 200 Z" id="path">
+ <title>path</title>
+ </path>
+ <image x1="25" y1="80" width="50" height="20" id="image"
+ xlink:href="../moz.png">
+ <title>image</title>
+ </image>
+ </svg>`,
+ (browser, accDoc) => {
+ // WAI-ARIA landmark subroles, regardless of AXRole
+ testRoleAndSubRole(accDoc, "application", null, "AXLandmarkApplication");
+ testRoleAndSubRole(accDoc, "banner", null, "AXLandmarkBanner");
+ testRoleAndSubRole(
+ accDoc,
+ "complementary",
+ null,
+ "AXLandmarkComplementary"
+ );
+ testRoleAndSubRole(accDoc, "contentinfo", null, "AXLandmarkContentInfo");
+ testRoleAndSubRole(accDoc, "form", null, "AXLandmarkForm");
+ testRoleAndSubRole(accDoc, "main", null, "AXLandmarkMain");
+ testRoleAndSubRole(accDoc, "navigation", null, "AXLandmarkNavigation");
+ testRoleAndSubRole(accDoc, "search", null, "AXLandmarkSearch");
+ testRoleAndSubRole(accDoc, "searchbox", null, "AXSearchField");
+
+ // DPub roles map into two categories, sample one of each
+ testRoleAndSubRole(
+ accDoc,
+ "dPubNavigation",
+ "AXGroup",
+ "AXLandmarkNavigation"
+ );
+ testRoleAndSubRole(accDoc, "dPubRegion", "AXGroup", "AXLandmarkRegion");
+
+ // ARIA widget roles
+ testRoleAndSubRole(accDoc, "alert", null, "AXApplicationAlert");
+ testRoleAndSubRole(
+ accDoc,
+ "alertdialog",
+ "AXGroup",
+ "AXApplicationAlertDialog",
+ "alert dialog"
+ );
+ testRoleAndSubRole(accDoc, "article", null, "AXDocumentArticle");
+ testRoleAndSubRole(accDoc, "code", "AXGroup", "AXCodeStyleGroup");
+ testRoleAndSubRole(accDoc, "dialog", null, "AXApplicationDialog", "dialog");
+ testRoleAndSubRole(accDoc, "ariaDocument", null, "AXDocument");
+ testRoleAndSubRole(accDoc, "log", null, "AXApplicationLog");
+ testRoleAndSubRole(accDoc, "marquee", null, "AXApplicationMarquee");
+ testRoleAndSubRole(accDoc, "ariaMath", null, "AXDocumentMath");
+ testRoleAndSubRole(accDoc, "note", null, "AXDocumentNote");
+ testRoleAndSubRole(accDoc, "ariaRegion", null, "AXLandmarkRegion");
+ testRoleAndSubRole(accDoc, "ariaStatus", "AXGroup", "AXApplicationStatus");
+ testRoleAndSubRole(accDoc, "switch", "AXCheckBox", "AXSwitch");
+ testRoleAndSubRole(accDoc, "timer", null, "AXApplicationTimer");
+ testRoleAndSubRole(accDoc, "tooltip", "AXGroup", "AXUserInterfaceTooltip");
+ testRoleAndSubRole(accDoc, "menuitemradio", "AXMenuItem", null);
+ testRoleAndSubRole(accDoc, "menuitemcheckbox", "AXMenuItem", null);
+ testRoleAndSubRole(accDoc, "datetime", "AXGroup", null);
+ // XXX for datetime elements, we spoof the role via the title, since
+ // providing the correct role results in the internal elements being
+ // unreachable by VO
+ is(
+ getNativeInterface(accDoc, "datetime").getAttributeValue("AXTitle"),
+ "date field"
+ );
+
+ // Text boxes
+ testRoleAndSubRole(accDoc, "textbox_multiline", "AXTextArea");
+ testRoleAndSubRole(accDoc, "textbox_singleline", "AXTextField");
+ testRoleAndSubRole(accDoc, "textArea", "AXTextArea");
+ testRoleAndSubRole(accDoc, "textInput", "AXTextField");
+
+ // True HTML5 search field
+ testRoleAndSubRole(accDoc, "htmlSearch", "AXTextField", "AXSearchField");
+
+ // A button morphed into a toggle by ARIA
+ testRoleAndSubRole(accDoc, "toggle", "AXCheckBox", "AXToggle");
+
+ // A banana button
+ testRoleAndSubRole(accDoc, "banana", "AXButton", null, "banana");
+
+ // Other elements
+ testRoleAndSubRole(accDoc, "deletion", "AXGroup", "AXDeleteStyleGroup");
+ testRoleAndSubRole(accDoc, "dl", "AXList", "AXDescriptionList");
+ testRoleAndSubRole(accDoc, "dt", "AXGroup", "AXTerm");
+ testRoleAndSubRole(accDoc, "dd", "AXGroup", "AXDescription");
+ testRoleAndSubRole(accDoc, "hr", "AXSplitter", "AXContentSeparator");
+ testRoleAndSubRole(accDoc, "insertion", "AXGroup", "AXInsertStyleGroup");
+ testRoleAndSubRole(
+ accDoc,
+ "meter",
+ "AXLevelIndicator",
+ "AXMeter",
+ "level indicator"
+ );
+ testRoleAndSubRole(accDoc, "sub", "AXGroup", "AXSubscriptStyleGroup");
+ testRoleAndSubRole(accDoc, "sup", "AXGroup", "AXSuperscriptStyleGroup");
+
+ // Some SVG stuff
+ testRoleAndSubRole(accDoc, "svg", "AXImage");
+ testRoleAndSubRole(accDoc, "g", "AXGroup");
+ testRoleAndSubRole(accDoc, "rect", "AXImage");
+ testRoleAndSubRole(accDoc, "circle", "AXImage");
+ testRoleAndSubRole(accDoc, "ellipse", "AXImage");
+ testRoleAndSubRole(accDoc, "line", "AXImage");
+ testRoleAndSubRole(accDoc, "polygon", "AXImage");
+ testRoleAndSubRole(accDoc, "polyline", "AXImage");
+ testRoleAndSubRole(accDoc, "path", "AXImage");
+ testRoleAndSubRole(accDoc, "image", "AXImage");
+ }
+);
+
+addAccessibleTask(
+ `
+ <figure id="figure">
+ <img id="img" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" alt="Logo">
+ <p>Non-image figure content</p>
+ <figcaption id="figcaption">Old Mozilla logo</figcaption>
+ </figure>`,
+ (browser, accDoc) => {
+ let figure = getNativeInterface(accDoc, "figure");
+ ok(!figure.getAttributeValue("AXTitle"), "Figure should not have a title");
+ is(
+ figure.getAttributeValue("AXDescription"),
+ "Old Mozilla logo",
+ "Correct figure label"
+ );
+ is(figure.getAttributeValue("AXRole"), "AXGroup", "Correct figure role");
+ is(
+ figure.getAttributeValue("AXRoleDescription"),
+ "figure",
+ "Correct figure role description"
+ );
+
+ let img = getNativeInterface(accDoc, "img");
+ ok(!img.getAttributeValue("AXTitle"), "img should not have a title");
+ is(img.getAttributeValue("AXDescription"), "Logo", "Correct img label");
+ is(img.getAttributeValue("AXRole"), "AXImage", "Correct img role");
+ is(
+ img.getAttributeValue("AXRoleDescription"),
+ "image",
+ "Correct img role description"
+ );
+
+ let figcaption = getNativeInterface(accDoc, "figcaption");
+ ok(
+ !figcaption.getAttributeValue("AXTitle"),
+ "figcaption should not have a title"
+ );
+ ok(
+ !figcaption.getAttributeValue("AXDescription"),
+ "figcaption should not have a label"
+ );
+ is(
+ figcaption.getAttributeValue("AXRole"),
+ "AXGroup",
+ "Correct figcaption role"
+ );
+ is(
+ figcaption.getAttributeValue("AXRoleDescription"),
+ "group",
+ "Correct figcaption role description"
+ );
+ }
+);
+
+addAccessibleTask(`<button>hello world</button>`, async (browser, accDoc) => {
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "web area should be an AXWebArea"
+ );
+ ok(
+ !webArea.attributeNames.includes("AXSubrole"),
+ "AXWebArea should not have a subrole"
+ );
+
+ let roleChanged = waitForMacEvent("AXMozRoleChanged");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.setAttribute("role", "application");
+ });
+ await roleChanged;
+
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "web area should retain AXWebArea role"
+ );
+ ok(
+ !webArea.attributeNames.includes("AXSubrole"),
+ "AXWebArea should not have a subrole"
+ );
+
+ let rootGroup = webArea.getAttributeValue("AXChildren")[0];
+ is(rootGroup.getAttributeValue("AXRole"), "AXGroup");
+ is(rootGroup.getAttributeValue("AXSubrole"), "AXLandmarkApplication");
+});
diff --git a/accessible/tests/browser/mac/browser_rootgroup.js b/accessible/tests/browser/mac/browser_rootgroup.js
new file mode 100644
index 0000000000..a8f4297d64
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_rootgroup.js
@@ -0,0 +1,246 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test document with no single group child
+ */
+addAccessibleTask(
+ `<p id="p1">hello</p><p>world</p>`,
+ async (browser, accDoc) => {
+ let doc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ let docChildren = doc.getAttributeValue("AXChildren");
+ is(docChildren.length, 1, "The document contains a root group");
+
+ let rootGroup = docChildren[0];
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+
+ is(
+ rootGroup.getAttributeValue("AXChildren").length,
+ 2,
+ "Root group has two children"
+ );
+
+ // From bottom-up
+ let p1 = getNativeInterface(accDoc, "p1");
+ rootGroup = p1.getAttributeValue("AXParent");
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+ }
+);
+
+/**
+ * Test document with a top-level group
+ */
+addAccessibleTask(
+ `<div role="grouping" id="group"><p>hello</p><p>world</p></div>`,
+ async (browser, accDoc) => {
+ let doc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ let docChildren = doc.getAttributeValue("AXChildren");
+ is(docChildren.length, 1, "The document contains a root group");
+
+ let rootGroup = docChildren[0];
+ is(
+ rootGroup.getAttributeValue("AXDOMIdentifier"),
+ "group",
+ "Root group is a document element"
+ );
+
+ // Adding an 'application' role to the body should
+ // create a root group with an application subrole.
+ let evt = waitForMacEvent("AXMozRoleChanged");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.body.setAttribute("role", "application");
+ });
+ await evt;
+
+ is(
+ doc.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "doc still has web area role"
+ );
+ is(
+ doc.getAttributeValue("AXRoleDescription"),
+ "HTML Content",
+ "doc has correct role description"
+ );
+ ok(
+ !doc.attributeNames.includes("AXSubrole"),
+ "sub role not available on web area"
+ );
+
+ rootGroup = doc.getAttributeValue("AXChildren")[0];
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+ is(
+ rootGroup.getAttributeValue("AXRole"),
+ "AXGroup",
+ "root group has AXGroup role"
+ );
+ is(
+ rootGroup.getAttributeValue("AXSubrole"),
+ "AXLandmarkApplication",
+ "root group has application subrole"
+ );
+ is(
+ rootGroup.getAttributeValue("AXRoleDescription"),
+ "application",
+ "root group has application role description"
+ );
+ }
+);
+
+/**
+ * Test document with body[role=application] and a top-level group
+ */
+addAccessibleTask(
+ `<div role="grouping" id="group"><p>hello</p><p>world</p></div>`,
+ async (browser, accDoc) => {
+ let doc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ is(
+ doc.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "doc still has web area role"
+ );
+ is(
+ doc.getAttributeValue("AXRoleDescription"),
+ "HTML Content",
+ "doc has correct role description"
+ );
+ ok(
+ !doc.attributeNames.includes("AXSubrole"),
+ "sub role not available on web area"
+ );
+
+ let rootGroup = doc.getAttributeValue("AXChildren")[0];
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+ is(
+ rootGroup.getAttributeValue("AXRole"),
+ "AXGroup",
+ "root group has AXGroup role"
+ );
+ is(
+ rootGroup.getAttributeValue("AXSubrole"),
+ "AXLandmarkApplication",
+ "root group has application subrole"
+ );
+ is(
+ rootGroup.getAttributeValue("AXRoleDescription"),
+ "application",
+ "root group has application role description"
+ );
+ },
+ { contentDocBodyAttrs: { role: "application" } }
+);
+
+/**
+ * Test document with a single button
+ */
+addAccessibleTask(
+ `<button id="button">I am a button</button>`,
+ async (browser, accDoc) => {
+ let doc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ let docChildren = doc.getAttributeValue("AXChildren");
+ is(docChildren.length, 1, "The document contains a root group");
+
+ let rootGroup = docChildren[0];
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+
+ let rootGroupChildren = rootGroup.getAttributeValue("AXChildren");
+ is(rootGroupChildren.length, 1, "Root group has one children");
+
+ is(
+ rootGroupChildren[0].getAttributeValue("AXRole"),
+ "AXButton",
+ "Button is child of root group"
+ );
+
+ // From bottom-up
+ let button = getNativeInterface(accDoc, "button");
+ rootGroup = button.getAttributeValue("AXParent");
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+ }
+);
+
+/**
+ * Test document with dialog role and heading
+ */
+addAccessibleTask(
+ `<body role="dialog" aria-labelledby="h">
+ <h1 id="h">
+ We're building a richer search experience
+ </h1>
+ </body>`,
+ async (browser, accDoc) => {
+ let doc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ let docChildren = doc.getAttributeValue("AXChildren");
+ is(docChildren.length, 1, "The document contains a root group");
+
+ let rootGroup = docChildren[0];
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+
+ is(rootGroup.getAttributeValue("AXRole"), "AXGroup", "Inherits role");
+
+ is(
+ rootGroup.getAttributeValue("AXSubrole"),
+ "AXApplicationDialog",
+ "Inherits subrole"
+ );
+ let rootGroupChildren = rootGroup.getAttributeValue("AXChildren");
+ is(rootGroupChildren.length, 1, "Root group has one child");
+
+ is(
+ rootGroupChildren[0].getAttributeValue("AXRole"),
+ "AXHeading",
+ "Heading is child of root group"
+ );
+
+ // From bottom-up
+ let heading = getNativeInterface(accDoc, "h");
+ rootGroup = heading.getAttributeValue("AXParent");
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Parent is generated root group"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_rotor.js b/accessible/tests/browser/mac/browser_rotor.js
new file mode 100644
index 0000000000..3f13506757
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_rotor.js
@@ -0,0 +1,1752 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/states.js */
+loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+/**
+ * Test rotor with heading
+ */
+addAccessibleTask(
+ `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>goodbye`,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXHeadingSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const headingCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(2, headingCount, "Found two headings");
+
+ const headings = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const hello = getNativeInterface(accDoc, "hello");
+ const world = getNativeInterface(accDoc, "world");
+ is(
+ hello.getAttributeValue("AXTitle"),
+ headings[0].getAttributeValue("AXTitle"),
+ "Found correct first heading"
+ );
+ is(
+ world.getAttributeValue("AXTitle"),
+ headings[1].getAttributeValue("AXTitle"),
+ "Found correct second heading"
+ );
+ }
+);
+
+/**
+ * Test rotor with heading and empty search text
+ */
+addAccessibleTask(
+ `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>goodbye`,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXHeadingSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ AXSearchText: "",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const headingCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(headingCount, 2, "Found two headings");
+
+ const headings = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const hello = getNativeInterface(accDoc, "hello");
+ const world = getNativeInterface(accDoc, "world");
+ is(
+ headings[0].getAttributeValue("AXTitle"),
+ hello.getAttributeValue("AXTitle"),
+ "Found correct first heading"
+ );
+ is(
+ headings[1].getAttributeValue("AXTitle"),
+ world.getAttributeValue("AXTitle"),
+ "Found correct second heading"
+ );
+ }
+);
+
+/**
+ * Test rotor with articles
+ */
+addAccessibleTask(
+ `<article id="google">
+ <h2>Google Chrome</h2>
+ <p>Google Chrome is a web browser developed by Google, released in 2008. Chrome is the world's most popular web browser today!</p>
+ </article>
+
+ <article id="moz">
+ <h2>Mozilla Firefox</h2>
+ <p>Mozilla Firefox is an open-source web browser developed by Mozilla. Firefox has been the second most popular web browser since January, 2018.</p>
+ </article>
+
+ <article id="microsoft">
+ <h2>Microsoft Edge</h2>
+ <p>Microsoft Edge is a web browser developed by Microsoft, released in 2015. Microsoft Edge replaced Internet Explorer.</p>
+ </article> `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXArticleSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const articleCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(3, articleCount, "Found three articles");
+
+ const articles = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const google = getNativeInterface(accDoc, "google");
+ const moz = getNativeInterface(accDoc, "moz");
+ const microsoft = getNativeInterface(accDoc, "microsoft");
+
+ is(
+ google.getAttributeValue("AXTitle"),
+ articles[0].getAttributeValue("AXTitle"),
+ "Found correct first article"
+ );
+ is(
+ moz.getAttributeValue("AXTitle"),
+ articles[1].getAttributeValue("AXTitle"),
+ "Found correct second article"
+ );
+ is(
+ microsoft.getAttributeValue("AXTitle"),
+ articles[2].getAttributeValue("AXTitle"),
+ "Found correct third article"
+ );
+ }
+);
+
+/**
+ * Test rotor with tables
+ */
+addAccessibleTask(
+ `
+ <table id="shapes">
+ <tr>
+ <th>Shape</th>
+ <th>Color</th>
+ <th>Do I like it?</th>
+ </tr>
+ <tr>
+ <td>Triangle</td>
+ <td>Green</td>
+ <td>No</td>
+ </tr>
+ <tr>
+ <td>Square</td>
+ <td>Red</td>
+ <td>Yes</td>
+ </tr>
+ </table>
+ <br>
+ <table id="food">
+ <tr>
+ <th>Grocery Item</th>
+ <th>Quantity</th>
+ </tr>
+ <tr>
+ <td>Onions</td>
+ <td>2</td>
+ </tr>
+ <tr>
+ <td>Yogurt</td>
+ <td>1</td>
+ </tr>
+ <tr>
+ <td>Spinach</td>
+ <td>1</td>
+ </tr>
+ <tr>
+ <td>Cherries</td>
+ <td>12</td>
+ </tr>
+ <tr>
+ <td>Carrots</td>
+ <td>5</td>
+ </tr>
+ </table>
+ <br>
+ <div role="table" id="ariaTable">
+ <div role="row">
+ <div role="cell">
+ I am a tiny aria table
+ </div>
+ </div>
+ </div>
+ <br>
+ <table role="grid" id="grid">
+ <tr>
+ <th>A</th>
+ <th>B</th>
+ <th>C</th>
+ <th>D</th>
+ <th>E</th>
+ </tr>
+ <tr>
+ <th>F</th>
+ <th>G</th>
+ <th>H</th>
+ <th>I</th>
+ <th>J</th>
+ </tr>
+ </table>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXTableSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const tableCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(4, tableCount, "Found four tables");
+
+ const tables = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const shapes = getNativeInterface(accDoc, "shapes");
+ const food = getNativeInterface(accDoc, "food");
+ const ariaTable = getNativeInterface(accDoc, "ariaTable");
+ const grid = getNativeInterface(accDoc, "grid");
+
+ is(
+ shapes.getAttributeValue("AXColumnCount"),
+ tables[0].getAttributeValue("AXColumnCount"),
+ "Found correct first table"
+ );
+ is(
+ food.getAttributeValue("AXColumnCount"),
+ tables[1].getAttributeValue("AXColumnCount"),
+ "Found correct second table"
+ );
+ is(
+ ariaTable.getAttributeValue("AXColumnCount"),
+ tables[2].getAttributeValue("AXColumnCount"),
+ "Found correct third table"
+ );
+ is(
+ grid.getAttributeValue("AXColumnCount"),
+ tables[3].getAttributeValue("AXColumnCount"),
+ "Found correct fourth table"
+ );
+ }
+);
+
+/**
+ * Test rotor with landmarks
+ */
+addAccessibleTask(
+ `
+ <header id="header">
+ <h1>This is a heading within a header</h1>
+ </header>
+
+ <nav id="nav">
+ <a href="example.com">I am a link in a nav</a>
+ </nav>
+
+ <main id="main">
+ I am some text in a main element
+ </main>
+
+ <footer id="footer">
+ <h2>Heading in footer</h2>
+ </footer>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXLandmarkSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const landmarkCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(4, landmarkCount, "Found four landmarks");
+
+ const landmarks = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const header = getNativeInterface(accDoc, "header");
+ const nav = getNativeInterface(accDoc, "nav");
+ const main = getNativeInterface(accDoc, "main");
+ const footer = getNativeInterface(accDoc, "footer");
+
+ is(
+ header.getAttributeValue("AXSubrole"),
+ landmarks[0].getAttributeValue("AXSubrole"),
+ "Found correct first landmark"
+ );
+ is(
+ nav.getAttributeValue("AXSubrole"),
+ landmarks[1].getAttributeValue("AXSubrole"),
+ "Found correct second landmark"
+ );
+ is(
+ main.getAttributeValue("AXSubrole"),
+ landmarks[2].getAttributeValue("AXSubrole"),
+ "Found correct third landmark"
+ );
+ is(
+ footer.getAttributeValue("AXSubrole"),
+ landmarks[3].getAttributeValue("AXSubrole"),
+ "Found correct fourth landmark"
+ );
+ }
+);
+
+/**
+ * Test rotor with aria landmarks
+ */
+addAccessibleTask(
+ `
+ <div id="banner" role="banner">
+ <h1>This is a heading within a banner</h1>
+ </div>
+
+ <div id="nav" role="navigation">
+ <a href="example.com">I am a link in a nav</a>
+ </div>
+
+ <div id="main" role="main">
+ I am some text in a main element
+ </div>
+
+ <div id="contentinfo" role="contentinfo">
+ <h2>Heading in contentinfo</h2>
+ </div>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXLandmarkSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const landmarkCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(4, landmarkCount, "Found four landmarks");
+
+ const landmarks = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const banner = getNativeInterface(accDoc, "banner");
+ const nav = getNativeInterface(accDoc, "nav");
+ const main = getNativeInterface(accDoc, "main");
+ const contentinfo = getNativeInterface(accDoc, "contentinfo");
+
+ is(
+ banner.getAttributeValue("AXSubrole"),
+ landmarks[0].getAttributeValue("AXSubrole"),
+ "Found correct first landmark"
+ );
+ is(
+ nav.getAttributeValue("AXSubrole"),
+ landmarks[1].getAttributeValue("AXSubrole"),
+ "Found correct second landmark"
+ );
+ is(
+ main.getAttributeValue("AXSubrole"),
+ landmarks[2].getAttributeValue("AXSubrole"),
+ "Found correct third landmark"
+ );
+ is(
+ contentinfo.getAttributeValue("AXSubrole"),
+ landmarks[3].getAttributeValue("AXSubrole"),
+ "Found correct fourth landmark"
+ );
+ }
+);
+
+/**
+ * Test rotor with buttons
+ */
+addAccessibleTask(
+ `
+ <button id="button">hello world</button><br>
+
+ <input type="button" value="another kinda button" id="input"><br>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXButtonSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const buttonCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(2, buttonCount, "Found two buttons");
+
+ const buttons = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const button = getNativeInterface(accDoc, "button");
+ const input = getNativeInterface(accDoc, "input");
+
+ is(
+ button.getAttributeValue("AXRole"),
+ buttons[0].getAttributeValue("AXRole"),
+ "Found correct button"
+ );
+ is(
+ input.getAttributeValue("AXRole"),
+ buttons[1].getAttributeValue("AXRole"),
+ "Found correct input button"
+ );
+ }
+);
+
+/**
+ * Test rotor with heading
+ */
+addAccessibleTask(
+ `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>goodbye`,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXHeadingSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const headingCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(2, headingCount, "Found two headings");
+
+ const headings = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const hello = getNativeInterface(accDoc, "hello");
+ const world = getNativeInterface(accDoc, "world");
+ is(
+ hello.getAttributeValue("AXTitle"),
+ headings[0].getAttributeValue("AXTitle"),
+ "Found correct first heading"
+ );
+ is(
+ world.getAttributeValue("AXTitle"),
+ headings[1].getAttributeValue("AXTitle"),
+ "Found correct second heading"
+ );
+ }
+);
+
+/**
+ * Test rotor with buttons
+ */
+addAccessibleTask(
+ `
+ <form>
+ <h2>input[type=button]</h2>
+ <input type="button" value="apply" id="button1">
+
+ <h2>input[type=submit]</h2>
+ <input type="submit" value="submit now" id="submit">
+
+ <h2>input[type=image]</h2>
+ <input type="image" src="sample.jpg" alt="submit image" id="image">
+
+ <h2>input[type=reset]</h2>
+ <input type="reset" value="reset now" id="reset">
+
+ <h2>button element</h2>
+ <button id="button2">Submit button</button>
+ </form>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXControlSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const controlsCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(5, controlsCount, "Found 5 controls");
+
+ const controls = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const button1 = getNativeInterface(accDoc, "button1");
+ const submit = getNativeInterface(accDoc, "submit");
+ const image = getNativeInterface(accDoc, "image");
+ const reset = getNativeInterface(accDoc, "reset");
+ const button2 = getNativeInterface(accDoc, "button2");
+
+ is(
+ button1.getAttributeValue("AXTitle"),
+ controls[0].getAttributeValue("AXTitle"),
+ "Found correct first control"
+ );
+ is(
+ submit.getAttributeValue("AXTitle"),
+ controls[1].getAttributeValue("AXTitle"),
+ "Found correct second control"
+ );
+ is(
+ image.getAttributeValue("AXTitle"),
+ controls[2].getAttributeValue("AXTitle"),
+ "Found correct third control"
+ );
+ is(
+ reset.getAttributeValue("AXTitle"),
+ controls[3].getAttributeValue("AXTitle"),
+ "Found correct third control"
+ );
+ is(
+ button2.getAttributeValue("AXTitle"),
+ controls[4].getAttributeValue("AXTitle"),
+ "Found correct third control"
+ );
+ }
+);
+
+/**
+ * Test rotor with inputs
+ */
+addAccessibleTask(
+ `
+ <input type="text" value="I'm a text field." id="text"><br>
+ <input type="text" value="me too" id="implText"><br>
+ <textarea id="textarea">this is some text in a text area</textarea><br>
+ <input type="tel" value="0000000000" id="tel"><br>
+ <input type="url" value="https://example.com" id="url"><br>
+ <input type="email" value="hi@example.com" id="email"><br>
+ <input type="password" value="blah" id="password"><br>
+ <input type="month" value="2020-01" id="month"><br>
+ <input type="week" value="2020-W01" id="week"><br>
+ <input type="number" value="12" id="number"><br>
+ <input type="range" value="12" min="0" max="20" id="range"><br>
+ <input type="date" value="2020-01-01" id="date"><br>
+ <input type="time" value="10:10:10" id="time"><br>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXControlSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const controlsCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(13, controlsCount, "Found 13 controls");
+ // the extra controls here come from our time control
+ // we can't filter out its internal buttons/incrementors
+ // like we do with the date entry because the time entry
+ // doesn't have its own specific role -- its just a grouping.
+
+ const controls = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const text = getNativeInterface(accDoc, "text");
+ const implText = getNativeInterface(accDoc, "implText");
+ const textarea = getNativeInterface(accDoc, "textarea");
+ const tel = getNativeInterface(accDoc, "tel");
+ const url = getNativeInterface(accDoc, "url");
+ const email = getNativeInterface(accDoc, "email");
+ const password = getNativeInterface(accDoc, "password");
+ const month = getNativeInterface(accDoc, "month");
+ const week = getNativeInterface(accDoc, "week");
+ const number = getNativeInterface(accDoc, "number");
+ const range = getNativeInterface(accDoc, "range");
+
+ const toCheck = [
+ text,
+ implText,
+ textarea,
+ tel,
+ url,
+ email,
+ password,
+ month,
+ week,
+ number,
+ range,
+ ];
+
+ for (let i = 0; i < toCheck.length; i++) {
+ is(
+ toCheck[i].getAttributeValue("AXValue"),
+ controls[i].getAttributeValue("AXValue"),
+ "Found correct input control"
+ );
+ }
+
+ const date = getNativeInterface(accDoc, "date");
+ const time = getNativeInterface(accDoc, "time");
+
+ is(
+ date.getAttributeValue("AXRole"),
+ controls[11].getAttributeValue("AXRole"),
+ "Found corrent date editor"
+ );
+
+ is(
+ time.getAttributeValue("AXRole"),
+ controls[12].getAttributeValue("AXRole"),
+ "Found corrent time editor"
+ );
+ }
+);
+
+/**
+ * Test rotor with groupings
+ */
+addAccessibleTask(
+ `
+ <fieldset>
+ <legend>Radios</legend>
+ <div role="radiogroup" id="radios">
+ <input id="radio1" type="radio" name="g1" checked="checked"> Radio 1
+ <input id="radio2" type="radio" name="g1"> Radio 2
+ </div>
+ </fieldset>
+
+ <fieldset id="checkboxes">
+ <legend>Checkboxes</legend>
+ <input id="checkbox1" type="checkbox" name="g2"> Checkbox 1
+ <input id="checkbox2" type="checkbox" name="g2" checked="checked">Checkbox 2
+ </fieldset>
+
+ <fieldset id="switches">
+ <legend>Switches</legend>
+ <input id="switch1" name="g3" role="switch" type="checkbox">Switch 1
+ <input checked="checked" id="switch2" name="g3" role="switch" type="checkbox">Switch 2
+ </fieldset>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXControlSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const controlsCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(9, controlsCount, "Found 9 controls");
+
+ const controls = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const radios = getNativeInterface(accDoc, "radios");
+ const radio1 = getNativeInterface(accDoc, "radio1");
+ const radio2 = getNativeInterface(accDoc, "radio2");
+
+ is(
+ radios.getAttributeValue("AXRole"),
+ controls[0].getAttributeValue("AXRole"),
+ "Found correct group of radios"
+ );
+ is(
+ radio1.getAttributeValue("AXRole"),
+ controls[1].getAttributeValue("AXRole"),
+ "Found correct radio 1"
+ );
+ is(
+ radio2.getAttributeValue("AXRole"),
+ controls[2].getAttributeValue("AXRole"),
+ "Found correct radio 2"
+ );
+
+ const checkboxes = getNativeInterface(accDoc, "checkboxes");
+ const checkbox1 = getNativeInterface(accDoc, "checkbox1");
+ const checkbox2 = getNativeInterface(accDoc, "checkbox2");
+
+ is(
+ checkboxes.getAttributeValue("AXRole"),
+ controls[3].getAttributeValue("AXRole"),
+ "Found correct group of checkboxes"
+ );
+ is(
+ checkbox1.getAttributeValue("AXRole"),
+ controls[4].getAttributeValue("AXRole"),
+ "Found correct checkbox 1"
+ );
+ is(
+ checkbox2.getAttributeValue("AXRole"),
+ controls[5].getAttributeValue("AXRole"),
+ "Found correct checkbox 2"
+ );
+
+ const switches = getNativeInterface(accDoc, "switches");
+ const switch1 = getNativeInterface(accDoc, "switch1");
+ const switch2 = getNativeInterface(accDoc, "switch2");
+
+ is(
+ switches.getAttributeValue("AXRole"),
+ controls[6].getAttributeValue("AXRole"),
+ "Found correct group of switches"
+ );
+ is(
+ switch1.getAttributeValue("AXRole"),
+ controls[7].getAttributeValue("AXRole"),
+ "Found correct switch 1"
+ );
+ is(
+ switch2.getAttributeValue("AXRole"),
+ controls[8].getAttributeValue("AXRole"),
+ "Found correct switch 2"
+ );
+ }
+);
+
+/**
+ * Test rotor with misc controls
+ */
+addAccessibleTask(
+ `
+ <input role="spinbutton" id="spinbutton" type="number" value="25">
+
+ <details id="details">
+ <summary>Hello</summary>
+ world
+ </details>
+
+ <ul role="tree" id="tree">
+ <li role="treeitem">item1</li>
+ <li role="treeitem">item1</li>
+ </ul>
+
+ <a id="buttonMenu" role="button">Click Me</a>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXControlSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const controlsCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(4, controlsCount, "Found 4 controls");
+
+ const controls = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const spin = getNativeInterface(accDoc, "spinbutton");
+ const details = getNativeInterface(accDoc, "details");
+ const tree = getNativeInterface(accDoc, "tree");
+ const buttonMenu = getNativeInterface(accDoc, "buttonMenu");
+
+ is(
+ spin.getAttributeValue("AXRole"),
+ controls[0].getAttributeValue("AXRole"),
+ "Found correct spinbutton"
+ );
+ is(
+ details.getAttributeValue("AXRole"),
+ controls[1].getAttributeValue("AXRole"),
+ "Found correct details element"
+ );
+ is(
+ tree.getAttributeValue("AXRole"),
+ controls[2].getAttributeValue("AXRole"),
+ "Found correct tree"
+ );
+ is(
+ buttonMenu.getAttributeValue("AXRole"),
+ controls[3].getAttributeValue("AXRole"),
+ "Found correct button menu"
+ );
+ }
+);
+
+/**
+ * Test rotor with links
+ */
+addAccessibleTask(
+ `
+ <a href="" id="empty">empty link</a>
+ <a href="http://www.example.com/" id="href">Example link</a>
+ <a id="noHref">link without href</a>
+ `,
+ async (browser, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXLinkSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ let linkCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(2, linkCount, "Found two links");
+
+ let links = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ const empty = getNativeInterface(accDoc, "empty");
+ const href = getNativeInterface(accDoc, "href");
+
+ is(
+ empty.getAttributeValue("AXTitle"),
+ links[0].getAttributeValue("AXTitle"),
+ "Found correct first link"
+ );
+ is(
+ href.getAttributeValue("AXTitle"),
+ links[1].getAttributeValue("AXTitle"),
+ "Found correct second link"
+ );
+
+ // unvisited links
+
+ searchPred = {
+ AXSearchKey: "AXUnvisitedLinkSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ linkCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, linkCount, "Found two links");
+
+ links = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(
+ empty.getAttributeValue("AXTitle"),
+ links[0].getAttributeValue("AXTitle"),
+ "Found correct first link"
+ );
+ is(
+ href.getAttributeValue("AXTitle"),
+ links[1].getAttributeValue("AXTitle"),
+ "Found correct second link"
+ );
+
+ // visited links
+
+ let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "href");
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await PlacesTestUtils.addVisits(["http://www.example.com/"]);
+
+ await stateChanged;
+
+ searchPred = {
+ AXSearchKey: "AXVisitedLinkSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ linkCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(1, linkCount, "Found one link");
+
+ links = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(
+ href.getAttributeValue("AXTitle"),
+ links[0].getAttributeValue("AXTitle"),
+ "Found correct visited link"
+ );
+
+ // Ensure history is cleared before running again
+ await PlacesUtils.history.clear();
+ }
+);
+
+/*
+ * Test AXAnyTypeSearchKey with root group
+ */
+addAccessibleTask(
+ `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>goodbye`,
+ (browser, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXAnyTypeSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: 1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ let results = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(results.length, 1, "One result for root group");
+ is(
+ results[0].getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+
+ searchPred.AXStartElement = results[0];
+ results = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(results.length, 0, "No more results past root group");
+
+ searchPred.AXDirection = "AXDirectionPrevious";
+ results = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(
+ results.length,
+ 0,
+ "Searching backwards from root group should yield no results"
+ );
+
+ const rootGroup = webArea.getAttributeValue("AXChildren")[0];
+ is(
+ rootGroup.getAttributeValue("AXIdentifier"),
+ "root-group",
+ "Is generated root group"
+ );
+
+ searchPred = {
+ AXSearchKey: "AXAnyTypeSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: 1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ results = rootGroup.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(
+ results[0].getAttributeValue("AXRole"),
+ "AXHeading",
+ "Is first heading child"
+ );
+ }
+);
+
+/**
+ * Test rotor with checkboxes
+ */
+addAccessibleTask(
+ `
+ <fieldset id="checkboxes">
+ <legend>Checkboxes</legend>
+ <input id="checkbox1" type="checkbox" name="g2"> Checkbox 1
+ <input id="checkbox2" type="checkbox" name="g2" checked="checked">Checkbox 2
+ <div id="checkbox3" role="checkbox">Checkbox 3</div>
+ <div id="checkbox4" role="checkbox" aria-checked="true">Checkbox 4</div>
+ </fieldset>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXCheckBoxSearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const checkboxCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(4, checkboxCount, "Found 4 checkboxes");
+
+ const checkboxes = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const checkbox1 = getNativeInterface(accDoc, "checkbox1");
+ const checkbox2 = getNativeInterface(accDoc, "checkbox2");
+ const checkbox3 = getNativeInterface(accDoc, "checkbox3");
+ const checkbox4 = getNativeInterface(accDoc, "checkbox4");
+
+ is(
+ checkbox1.getAttributeValue("AXValue"),
+ checkboxes[0].getAttributeValue("AXValue"),
+ "Found correct checkbox 1"
+ );
+ is(
+ checkbox2.getAttributeValue("AXValue"),
+ checkboxes[1].getAttributeValue("AXValue"),
+ "Found correct checkbox 2"
+ );
+ is(
+ checkbox3.getAttributeValue("AXValue"),
+ checkboxes[2].getAttributeValue("AXValue"),
+ "Found correct checkbox 3"
+ );
+ is(
+ checkbox4.getAttributeValue("AXValue"),
+ checkboxes[3].getAttributeValue("AXValue"),
+ "Found correct checkbox 4"
+ );
+ }
+);
+
+/**
+ * Test rotor with radiogroups
+ */
+addAccessibleTask(
+ `
+ <div role="radiogroup" id="radios" aria-labelledby="desc">
+ <h1 id="desc">some radio buttons</h1>
+ <div id="radio1" role="radio"> Radio 1</div>
+ <div id="radio2" role="radio"> Radio 2</div>
+ </div>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXRadioGroupSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const radiogroupCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(1, radiogroupCount, "Found 1 radio group");
+
+ const controls = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const radios = getNativeInterface(accDoc, "radios");
+
+ is(
+ radios.getAttributeValue("AXDescription"),
+ controls[0].getAttributeValue("AXDescription"),
+ "Found correct group of radios"
+ );
+ }
+);
+
+/*
+ * Test rotor with inputs
+ */
+addAccessibleTask(
+ `
+ <input type="text" value="I'm a text field." id="text"><br>
+ <input type="text" value="me too" id="implText"><br>
+ <textarea id="textarea">this is some text in a text area</textarea><br>
+ <input type="tel" value="0000000000" id="tel"><br>
+ <input type="url" value="https://example.com" id="url"><br>
+ <input type="email" value="hi@example.com" id="email"><br>
+ <input type="password" value="blah" id="password"><br>
+ <input type="month" value="2020-01" id="month"><br>
+ <input type="week" value="2020-W01" id="week"><br>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXTextFieldSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const textfieldCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(9, textfieldCount, "Found 9 fields");
+
+ const fields = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const text = getNativeInterface(accDoc, "text");
+ const implText = getNativeInterface(accDoc, "implText");
+ const textarea = getNativeInterface(accDoc, "textarea");
+ const tel = getNativeInterface(accDoc, "tel");
+ const url = getNativeInterface(accDoc, "url");
+ const email = getNativeInterface(accDoc, "email");
+ const password = getNativeInterface(accDoc, "password");
+ const month = getNativeInterface(accDoc, "month");
+ const week = getNativeInterface(accDoc, "week");
+
+ const toCheck = [
+ text,
+ implText,
+ textarea,
+ tel,
+ url,
+ email,
+ password,
+ month,
+ week,
+ ];
+
+ for (let i = 0; i < toCheck.length; i++) {
+ is(
+ toCheck[i].getAttributeValue("AXValue"),
+ fields[i].getAttributeValue("AXValue"),
+ "Found correct input control"
+ );
+ }
+ }
+);
+
+/**
+ * Test rotor with static text
+ */
+addAccessibleTask(
+ `
+ <h1>Hello I am a heading</h1>
+ This is some regular text.<p>this is some paragraph text</p><br>
+ This is a list:<ul>
+ <li>List item one</li>
+ <li>List item two</li>
+ </ul>
+
+ <a href="http://example.com">This is a link</a>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXStaticTextSearchKey",
+ AXImmediateDescendants: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const textCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(7, textCount, "Found 7 pieces of text");
+
+ const text = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(
+ "Hello I am a heading",
+ text[0].getAttributeValue("AXValue"),
+ "Found correct text node for heading"
+ );
+ is(
+ "This is some regular text.",
+ text[1].getAttributeValue("AXValue"),
+ "Found correct text node"
+ );
+ is(
+ "this is some paragraph text",
+ text[2].getAttributeValue("AXValue"),
+ "Found correct text node for paragraph"
+ );
+ is(
+ "This is a list:",
+ text[3].getAttributeValue("AXValue"),
+ "Found correct text node for pre-list text node"
+ );
+ is(
+ "List item one",
+ text[4].getAttributeValue("AXValue"),
+ "Found correct text node for list item one"
+ );
+ is(
+ "List item two",
+ text[5].getAttributeValue("AXValue"),
+ "Found correct text node for list item two"
+ );
+ is(
+ "This is a link",
+ text[6].getAttributeValue("AXValue"),
+ "Found correct text node for link"
+ );
+ }
+);
+
+/**
+ * Test rotor with lists
+ */
+addAccessibleTask(
+ `
+ <ul id="unordered">
+ <li>hello</li>
+ <li>world</li>
+ </ul>
+
+ <ol id="ordered">
+ <li>item one</li>
+ <li>item two</li>
+ </ol>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXListSearchKey",
+ AXImmediateDescendants: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const listCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(2, listCount, "Found 2 lists");
+
+ const lists = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ const ordered = getNativeInterface(accDoc, "ordered");
+ const unordered = getNativeInterface(accDoc, "unordered");
+
+ is(
+ unordered.getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"),
+ lists[0].getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"),
+ "Found correct unordered list"
+ );
+ is(
+ ordered.getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"),
+ lists[1].getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"),
+ "Found correct ordered list"
+ );
+ }
+);
+
+/*
+ * Test rotor with images
+ */
+addAccessibleTask(
+ `
+ <img id="img1" alt="image one" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"><br>
+ <a href="http://example.com">
+ <img id="img2" alt="image two" src="http://example.com/a11y/accessible/tests/mochitest/moz.png">
+ </a>
+ <img src="" id="img3">
+ `,
+ (browser, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXImageSearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ let images = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(images.length, 3, "Found three images");
+
+ const img1 = getNativeInterface(accDoc, "img1");
+ const img2 = getNativeInterface(accDoc, "img2");
+ const img3 = getNativeInterface(accDoc, "img3");
+
+ is(
+ img1.getAttributeValue("AXDescription"),
+ images[0].getAttributeValue("AXDescription"),
+ "Found correct image"
+ );
+
+ is(
+ img2.getAttributeValue("AXDescription"),
+ images[1].getAttributeValue("AXDescription"),
+ "Found correct image"
+ );
+
+ is(
+ img3.getAttributeValue("AXDescription"),
+ images[2].getAttributeValue("AXDescription"),
+ "Found correct image"
+ );
+ }
+);
+
+/**
+ * Test rotor with frames
+ */
+addAccessibleTask(
+ `
+ <iframe id="frame1" src="data:text/html,<h1>hello</h1>world"></iframe>
+ <iframe id="frame2" src="data:text/html,<iframe id='frame3' src='data:text/html,<h1>goodbye</h1>'>"></iframe>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXFrameSearchKey",
+ AXImmediateDescendantsOnly: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const frameCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(3, frameCount, "Found 3 frames");
+ }
+);
+
+/**
+ * Test rotor with static text
+ */
+addAccessibleTask(
+ `
+ <h1>Hello I am a heading</h1>
+ This is some regular text.<p>this is some paragraph text</p><br>
+ This is a list:<ul>
+ <li>List item one</li>
+ <li>List item two</li>
+ </ul>
+
+ <a href="http://example.com">This is a link</a>
+ `,
+ async (browser, accDoc) => {
+ const searchPred = {
+ AXSearchKey: "AXStaticTextSearchKey",
+ AXImmediateDescendants: 0,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const textCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(7, textCount, "Found 7 pieces of text");
+
+ const text = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ is(
+ "Hello I am a heading",
+ text[0].getAttributeValue("AXValue"),
+ "Found correct text node for heading"
+ );
+ is(
+ "This is some regular text.",
+ text[1].getAttributeValue("AXValue"),
+ "Found correct text node"
+ );
+ is(
+ "this is some paragraph text",
+ text[2].getAttributeValue("AXValue"),
+ "Found correct text node for paragraph"
+ );
+ is(
+ "This is a list:",
+ text[3].getAttributeValue("AXValue"),
+ "Found correct text node for pre-list text node"
+ );
+ is(
+ "List item one",
+ text[4].getAttributeValue("AXValue"),
+ "Found correct text node for list item one"
+ );
+ is(
+ "List item two",
+ text[5].getAttributeValue("AXValue"),
+ "Found correct text node for list item two"
+ );
+ is(
+ "This is a link",
+ text[6].getAttributeValue("AXValue"),
+ "Found correct text node for link"
+ );
+ }
+);
+
+/**
+ * Test search with non-webarea root
+ */
+addAccessibleTask(
+ `
+ <div id="searchroot"><p id="p1">hello</p><p id="p2">world</p></div>
+ <div><p>goodybe</p></div>
+ `,
+ async (browser, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXAnyTypeSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ };
+
+ const searchRoot = getNativeInterface(accDoc, "searchroot");
+ const resultCount = searchRoot.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(resultCount, 2, "Found 2 items");
+
+ const p1 = getNativeInterface(accDoc, "p1");
+ searchPred = {
+ AXSearchKey: "AXAnyTypeSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ AXStartElement: p1,
+ };
+
+ let results = searchRoot.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ Assert.deepEqual(
+ results.map(r => r.getAttributeValue("AXDOMIdentifier")),
+ ["p2"],
+ "Result is next group sibling"
+ );
+
+ searchPred = {
+ AXSearchKey: "AXAnyTypeSearchKey",
+ AXImmediateDescendantsOnly: 1,
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionPrevious",
+ };
+
+ results = searchRoot.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ Assert.deepEqual(
+ results.map(r => r.getAttributeValue("AXDOMIdentifier")),
+ ["p2", "p1"],
+ "A reverse search should return groups in reverse"
+ );
+ }
+);
+
+/**
+ * Test search text
+ */
+addAccessibleTask(
+ `
+ <p>It's about the future, isn't it?</p>
+ <p>Okay, alright, Saturday is good, Saturday's good, I could spend a week in 1955.</p>
+ <ul>
+ <li>I could hang out, you could show me around.</li>
+ <li>There's that word again, heavy.</li>
+ </ul>
+ `,
+ async (browser, f, accDoc) => {
+ let searchPred = {
+ AXSearchKey: "AXAnyTypeSearchKey",
+ AXResultsLimit: -1,
+ AXDirection: "AXDirectionNext",
+ AXSearchText: "could",
+ };
+
+ const webArea = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ is(
+ webArea.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "Got web area accessible"
+ );
+
+ const textSearchCount = webArea.getParameterizedAttributeValue(
+ "AXUIElementCountForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+ is(textSearchCount, 2, "Found 2 matching items in text search");
+
+ const results = webArea.getParameterizedAttributeValue(
+ "AXUIElementsForSearchPredicate",
+ NSDictionary(searchPred)
+ );
+
+ info(results.map(r => r.getAttributeValue("AXMozDebugDescription")));
+
+ Assert.deepEqual(
+ results.map(r => r.getAttributeValue("AXValue")),
+ [
+ "Okay, alright, Saturday is good, Saturday's good, I could spend a week in 1955.",
+ "I could hang out, you could show me around.",
+ ],
+ "Correct text search results"
+ );
+ },
+ { topLevel: false, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/mac/browser_selectables.js b/accessible/tests/browser/mac/browser_selectables.js
new file mode 100644
index 0000000000..af88c2e136
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_selectables.js
@@ -0,0 +1,363 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+function getSelectedIds(selectable) {
+ return selectable
+ .getAttributeValue("AXSelectedChildren")
+ .map(c => c.getAttributeValue("AXDOMIdentifier"));
+}
+
+/**
+ * Test aria tabs
+ */
+addAccessibleTask("mac/doc_aria_tabs.html", async (browser, accDoc) => {
+ let tablist = getNativeInterface(accDoc, "tablist");
+ is(
+ tablist.getAttributeValue("AXRole"),
+ "AXTabGroup",
+ "Correct role for tablist"
+ );
+
+ let tabMacAccs = tablist.getAttributeValue("AXTabs");
+ is(tabMacAccs.length, 3, "3 items in AXTabs");
+
+ let selectedTabs = tablist.getAttributeValue("AXSelectedChildren");
+ is(selectedTabs.length, 1, "one selected tab");
+
+ let tab = selectedTabs[0];
+ is(tab.getAttributeValue("AXRole"), "AXRadioButton", "Correct role for tab");
+ is(
+ tab.getAttributeValue("AXSubrole"),
+ "AXTabButton",
+ "Correct subrole for tab"
+ );
+ is(tab.getAttributeValue("AXTitle"), "First Tab", "Correct title for tab");
+
+ let tabToSelect = tabMacAccs[1];
+ is(
+ tabToSelect.getAttributeValue("AXTitle"),
+ "Second Tab",
+ "Correct title for tab"
+ );
+
+ let actions = tabToSelect.actionNames;
+ ok(true, actions);
+ ok(actions.includes("AXPress"), "Has switch action");
+
+ let evt = waitForMacEvent("AXSelectedChildrenChanged");
+ tabToSelect.performAction("AXPress");
+ await evt;
+
+ selectedTabs = tablist.getAttributeValue("AXSelectedChildren");
+ is(selectedTabs.length, 1, "one selected tab");
+ is(
+ selectedTabs[0].getAttributeValue("AXTitle"),
+ "Second Tab",
+ "Correct title for tab"
+ );
+});
+
+addAccessibleTask('<p id="p">hello</p>', async (browser, accDoc) => {
+ let p = getNativeInterface(accDoc, "p");
+ ok(
+ p.attributeNames.includes("AXSelected"),
+ "html element includes 'AXSelected' attribute"
+ );
+ is(p.getAttributeValue("AXSelected"), 0, "AX selected is 'false'");
+});
+
+addAccessibleTask(
+ `<select id="select" aria-label="Choose a number" multiple>
+ <option id="one" selected>One</option>
+ <option id="two">Two</option>
+ <option id="three">Three</option>
+ <option id="four" disabled>Four</option>
+ </select>`,
+ async (browser, accDoc) => {
+ let select = getNativeInterface(accDoc, "select");
+ let one = getNativeInterface(accDoc, "one");
+ let two = getNativeInterface(accDoc, "two");
+ let three = getNativeInterface(accDoc, "three");
+ let four = getNativeInterface(accDoc, "four");
+
+ is(
+ select.getAttributeValue("AXTitle"),
+ "Choose a number",
+ "Select titled correctly"
+ );
+ ok(
+ select.attributeNames.includes("AXOrientation"),
+ "Have orientation attribute"
+ );
+ ok(
+ select.isAttributeSettable("AXSelectedChildren"),
+ "Select can have AXSelectedChildren set"
+ );
+
+ is(one.getAttributeValue("AXTitle"), "", "Option should not have a title");
+ is(
+ one.getAttributeValue("AXValue"),
+ "One",
+ "Option should have correct value"
+ );
+ is(
+ one.getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Options should have AXStaticText role"
+ );
+ ok(one.isAttributeSettable("AXSelected"), "Option can have AXSelected set");
+
+ is(select.getAttributeValue("AXSelectedChildren").length, 1);
+ let evt = waitForMacEvent("AXSelectedChildrenChanged");
+ one.setAttributeValue("AXSelected", false);
+ await evt;
+ is(select.getAttributeValue("AXSelectedChildren").length, 0);
+ evt = waitForMacEvent("AXSelectedChildrenChanged");
+ three.setAttributeValue("AXSelected", true);
+ await evt;
+ is(select.getAttributeValue("AXSelectedChildren").length, 1);
+ ok(getSelectedIds(select).includes("three"), "'three' is selected");
+ evt = waitForMacEvent("AXSelectedChildrenChanged");
+ select.setAttributeValue("AXSelectedChildren", [one, two]);
+ await evt;
+ await untilCacheOk(() => {
+ let ids = getSelectedIds(select);
+ return ids[0] == "one" && ids[1] == "two";
+ }, "Got correct selected children");
+
+ evt = waitForMacEvent("AXSelectedChildrenChanged");
+ select.setAttributeValue("AXSelectedChildren", [three, two, four]);
+ await evt;
+ await untilCacheOk(() => {
+ let ids = getSelectedIds(select);
+ return ids[0] == "two" && ids[1] == "three";
+ }, "Got correct selected children");
+
+ ok(!four.getAttributeValue("AXEnabled"), "Disabled option is disabled");
+ }
+);
+
+addAccessibleTask(
+ `<select id="select" aria-label="Choose a thing" multiple>
+ <optgroup label="Fruits">
+ <option id="banana" selected>Banana</option>
+ <option id="apple">Apple</option>
+ <option id="orange">Orange</option>
+ </optgroup>
+ <optgroup label="Vegetables">
+ <option id="lettuce" selected>Lettuce</option>
+ <option id="tomato">Tomato</option>
+ <option id="onion">Onion</option>
+ </optgroup>
+ <optgroup label="Spices">
+ <option id="cumin">Cumin</option>
+ <option id="coriander">Coriander</option>
+ <option id="allspice" selected>Allspice</option>
+ </optgroup>
+ <option id="everything">Everything</option>
+ </select>`,
+ async (browser, accDoc) => {
+ let select = getNativeInterface(accDoc, "select");
+
+ is(
+ select.getAttributeValue("AXTitle"),
+ "Choose a thing",
+ "Select titled correctly"
+ );
+ ok(
+ select.attributeNames.includes("AXOrientation"),
+ "Have orientation attribute"
+ );
+ ok(
+ select.isAttributeSettable("AXSelectedChildren"),
+ "Select can have AXSelectedChildren set"
+ );
+ let childValueSelectablePairs = select
+ .getAttributeValue("AXChildren")
+ .map(c => [
+ c.getAttributeValue("AXValue"),
+ c.isAttributeSettable("AXSelected"),
+ c.getAttributeValue("AXEnabled"),
+ ]);
+ [
+ ["​", false, 0],
+ ["Fruits", false, 0],
+ ["Banana", true, 1],
+ ["Apple", true, 1],
+ ["Orange", true, 1],
+ ["​", false, 0],
+ ["Vegetables", false, 0],
+ ["Lettuce", true, 1],
+ ["Tomato", true, 1],
+ ["Onion", true, 1],
+ ["​", false, 0],
+ ["Spices", false, 0],
+ ["Cumin", true, 1],
+ ["Coriander", true, 1],
+ ["Allspice", true, 1],
+ ["Everything", true, 1],
+ ];
+ Assert.deepEqual(
+ childValueSelectablePairs,
+ [
+ ["​", false, false],
+ ["Fruits", false, false],
+ ["Banana", true, true],
+ ["Apple", true, true],
+ ["Orange", true, true],
+ ["​", false, false],
+ ["Vegetables", false, false],
+ ["Lettuce", true, true],
+ ["Tomato", true, true],
+ ["Onion", true, true],
+ ["​", false, false],
+ ["Spices", false, false],
+ ["Cumin", true, true],
+ ["Coriander", true, true],
+ ["Allspice", true, true],
+ ["Everything", true, true],
+ ],
+ "Options are selectable, group labels are not"
+ );
+
+ let allspice = getNativeInterface(accDoc, "allspice");
+ is(
+ allspice.getAttributeValue("AXTitle"),
+ "",
+ "Option should not have a title"
+ );
+ is(
+ allspice.getAttributeValue("AXValue"),
+ "Allspice",
+ "Option should have a value"
+ );
+ is(
+ allspice.getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Options should have AXStaticText role"
+ );
+ ok(
+ allspice.isAttributeSettable("AXSelected"),
+ "Option can have AXSelected set"
+ );
+ is(
+ allspice
+ .getAttributeValue("AXParent")
+ .getAttributeValue("AXDOMIdentifier"),
+ "select",
+ "Select is direct parent of nested option"
+ );
+
+ let groupLabel = select.getAttributeValue("AXChildren")[1];
+ ok(
+ !groupLabel.isAttributeSettable("AXSelected"),
+ "Group label should not be selectable"
+ );
+ is(
+ groupLabel.getAttributeValue("AXValue"),
+ "Fruits",
+ "Group label should have a value"
+ );
+ is(
+ groupLabel.getAttributeValue("AXTitle"),
+ null,
+ "Group label should not have a title"
+ );
+ is(
+ groupLabel.getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Group label should have AXStaticText role"
+ );
+ is(
+ groupLabel
+ .getAttributeValue("AXParent")
+ .getAttributeValue("AXDOMIdentifier"),
+ "select",
+ "Select is direct parent of group label"
+ );
+
+ Assert.deepEqual(getSelectedIds(select), ["banana", "lettuce", "allspice"]);
+ }
+);
+
+addAccessibleTask(
+ `<div role="listbox" id="select" aria-label="Choose a number" aria-multiselectable="true">
+ <div role="option" id="one" aria-selected="true">One</div>
+ <div role="option" id="two">Two</div>
+ <div role="option" id="three">Three</div>
+ <div role="option" id="four" aria-disabled="true">Four</div>
+</div>`,
+ async (browser, accDoc) => {
+ let select = getNativeInterface(accDoc, "select");
+ let one = getNativeInterface(accDoc, "one");
+ let two = getNativeInterface(accDoc, "two");
+ let three = getNativeInterface(accDoc, "three");
+ let four = getNativeInterface(accDoc, "four");
+
+ is(
+ select.getAttributeValue("AXTitle"),
+ "Choose a number",
+ "Select titled correctly"
+ );
+ ok(
+ select.attributeNames.includes("AXOrientation"),
+ "Have orientation attribute"
+ );
+ ok(
+ select.isAttributeSettable("AXSelectedChildren"),
+ "Select can have AXSelectedChildren set"
+ );
+
+ is(one.getAttributeValue("AXTitle"), "", "Option should not have a title");
+ is(
+ one.getAttributeValue("AXValue"),
+ "One",
+ "Option should have correct value"
+ );
+ is(
+ one.getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Options should have AXStaticText role"
+ );
+ ok(one.isAttributeSettable("AXSelected"), "Option can have AXSelected set");
+
+ is(select.getAttributeValue("AXSelectedChildren").length, 1);
+ let evt = waitForMacEvent("AXSelectedChildrenChanged");
+ // Change selection from content.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("one").removeAttribute("aria-selected");
+ });
+ await evt;
+ is(select.getAttributeValue("AXSelectedChildren").length, 0);
+ evt = waitForMacEvent("AXSelectedChildrenChanged");
+ three.setAttributeValue("AXSelected", true);
+ await evt;
+ is(select.getAttributeValue("AXSelectedChildren").length, 1);
+ ok(getSelectedIds(select).includes("three"), "'three' is selected");
+ evt = waitForMacEvent("AXSelectedChildrenChanged");
+ select.setAttributeValue("AXSelectedChildren", [one, two]);
+ await evt;
+ await untilCacheOk(() => {
+ let ids = getSelectedIds(select);
+ return ids[0] == "one" && ids[1] == "two";
+ }, "Got correct selected children");
+
+ evt = waitForMacEvent("AXSelectedChildrenChanged");
+ select.setAttributeValue("AXSelectedChildren", [three, two, four]);
+ await evt;
+ await untilCacheOk(() => {
+ let ids = getSelectedIds(select);
+ return ids[0] == "two" && ids[1] == "three";
+ }, "Got correct selected children");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_table.js b/accessible/tests/browser/mac/browser_table.js
new file mode 100644
index 0000000000..50ae697deb
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_table.js
@@ -0,0 +1,629 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Helper function to test table consistency.
+ */
+function testTableConsistency(table, expectedRowCount, expectedColumnCount) {
+ is(table.getAttributeValue("AXRole"), "AXTable", "Correct role for table");
+
+ let tableChildren = table.getAttributeValue("AXChildren");
+ // XXX: Should be expectedRowCount+ExpectedColumnCount+1 children, rows (incl headers) + cols + headers
+ // if we're trying to match Safari.
+ is(
+ tableChildren.length,
+ expectedRowCount + expectedColumnCount,
+ "Table has children = rows (4) + cols (3)"
+ );
+ for (let i = 0; i < tableChildren.length; i++) {
+ let currChild = tableChildren[i];
+ if (i < expectedRowCount) {
+ is(
+ currChild.getAttributeValue("AXRole"),
+ "AXRow",
+ "Correct role for row"
+ );
+ } else {
+ is(
+ currChild.getAttributeValue("AXRole"),
+ "AXColumn",
+ "Correct role for col"
+ );
+ is(
+ currChild.getAttributeValue("AXRoleDescription"),
+ "column",
+ "Correct role desc for col"
+ );
+ }
+ }
+
+ is(
+ table.getAttributeValue("AXColumnCount"),
+ expectedColumnCount,
+ "Table has correct column count."
+ );
+ is(
+ table.getAttributeValue("AXRowCount"),
+ expectedRowCount,
+ "Table has correct row count."
+ );
+
+ let cols = table.getAttributeValue("AXColumns");
+ is(cols.length, expectedColumnCount, "Table has col list of correct length");
+ for (let i = 0; i < cols.length; i++) {
+ let currCol = cols[i];
+ let currChildren = currCol.getAttributeValue("AXChildren");
+ is(
+ currChildren.length,
+ expectedRowCount,
+ "Column has correct number of cells"
+ );
+ for (let j = 0; j < currChildren.length; j++) {
+ let currChild = currChildren[j];
+ is(
+ currChild.getAttributeValue("AXRole"),
+ "AXCell",
+ "Column child is cell"
+ );
+ }
+ }
+
+ let rows = table.getAttributeValue("AXRows");
+ is(rows.length, expectedRowCount, "Table has row list of correct length");
+ for (let i = 0; i < rows.length; i++) {
+ let currRow = rows[i];
+ let currChildren = currRow.getAttributeValue("AXChildren");
+ is(
+ currChildren.length,
+ expectedColumnCount,
+ "Row has correct number of cells"
+ );
+ for (let j = 0; j < currChildren.length; j++) {
+ let currChild = currChildren[j];
+ is(currChild.getAttributeValue("AXRole"), "AXCell", "Row child is cell");
+ }
+ }
+}
+
+/**
+ * Test table, columns, rows
+ */
+addAccessibleTask(
+ `<table id="customers">
+ <tbody>
+ <tr id="firstrow"><th>Company</th><th>Contact</th><th>Country</th></tr>
+ <tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr>
+ <tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr>
+ <tr><td>Ernst Handel</td><td>Roland Mendel</td><td>Austria</td></tr>
+ </tbody>
+ </table>`,
+ async (browser, accDoc) => {
+ let table = getNativeInterface(accDoc, "customers");
+ testTableConsistency(table, 4, 3);
+
+ const rowText = [
+ "Madrigal Electromotive GmbH",
+ "Lydia Rodarte-Quayle",
+ "Germany",
+ ];
+ let reorder = waitForEvent(EVENT_REORDER, "customers");
+ await SpecialPowers.spawn(browser, [rowText], _rowText => {
+ let tr = content.document.createElement("tr");
+ for (let t of _rowText) {
+ let td = content.document.createElement("td");
+ td.textContent = t;
+ tr.appendChild(td);
+ }
+ content.document.getElementById("customers").appendChild(tr);
+ });
+ await reorder;
+
+ let cols = table.getAttributeValue("AXColumns");
+ is(cols.length, 3, "Table has col list of correct length");
+ for (let i = 0; i < cols.length; i++) {
+ let currCol = cols[i];
+ let currChildren = currCol.getAttributeValue("AXChildren");
+ is(currChildren.length, 5, "Column has correct number of cells");
+ let lastCell = currChildren[currChildren.length - 1];
+ let cellChildren = lastCell.getAttributeValue("AXChildren");
+ is(cellChildren.length, 1, "Cell has a single text child");
+ is(
+ cellChildren[0].getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Correct role for cell child"
+ );
+ is(
+ cellChildren[0].getAttributeValue("AXValue"),
+ rowText[i],
+ "Correct text for cell"
+ );
+ }
+
+ reorder = waitForEvent(EVENT_REORDER, "firstrow");
+ await SpecialPowers.spawn(browser, [], () => {
+ let td = content.document.createElement("td");
+ td.textContent = "Ticker";
+ content.document.getElementById("firstrow").appendChild(td);
+ });
+ await reorder;
+
+ cols = table.getAttributeValue("AXColumns");
+ is(cols.length, 4, "Table has col list of correct length");
+ is(
+ cols[cols.length - 1].getAttributeValue("AXChildren").length,
+ 1,
+ "Last column has single child"
+ );
+
+ reorder = waitForEvent(
+ EVENT_REORDER,
+ e => e.accessible.role == ROLE_DOCUMENT
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("customers").remove();
+ });
+ await reorder;
+
+ try {
+ cols[0].getAttributeValue("AXChildren");
+ ok(false, "Getting children from column of expired table should fail");
+ } catch (e) {
+ ok(true, "Getting children from column of expired table should fail");
+ }
+ }
+);
+
+addAccessibleTask(
+ `<table id="table">
+ <tr>
+ <th colspan="2" id="header1">Header 1</th>
+ <th id="header2">Header 2</th>
+ </tr>
+ <tr>
+ <td id="cell1">one</td>
+ <td id="cell2" rowspan="2">two</td>
+ <td id="cell3">three</td>
+ </tr>
+ <tr>
+ <td id="cell4">four</td>
+ <td id="cell5">five</td>
+ </tr>
+ </table>`,
+ (browser, accDoc) => {
+ let table = getNativeInterface(accDoc, "table");
+
+ let getCellAt = (col, row) =>
+ table.getParameterizedAttributeValue("AXCellForColumnAndRow", [col, row]);
+
+ function testCell(cell, expectedId, expectedColRange, expectedRowRange) {
+ is(
+ cell.getAttributeValue("AXDOMIdentifier"),
+ expectedId,
+ "Correct DOM Identifier"
+ );
+ Assert.deepEqual(
+ cell.getAttributeValue("AXColumnIndexRange"),
+ expectedColRange,
+ "Correct column range"
+ );
+ Assert.deepEqual(
+ cell.getAttributeValue("AXRowIndexRange"),
+ expectedRowRange,
+ "Correct row range"
+ );
+ }
+
+ testCell(getCellAt(0, 0), "header1", [0, 2], [0, 1]);
+ testCell(getCellAt(1, 0), "header1", [0, 2], [0, 1]);
+ testCell(getCellAt(2, 0), "header2", [2, 1], [0, 1]);
+
+ testCell(getCellAt(0, 1), "cell1", [0, 1], [1, 1]);
+ testCell(getCellAt(1, 1), "cell2", [1, 1], [1, 2]);
+ testCell(getCellAt(2, 1), "cell3", [2, 1], [1, 1]);
+
+ testCell(getCellAt(0, 2), "cell4", [0, 1], [2, 1]);
+ testCell(getCellAt(1, 2), "cell2", [1, 1], [1, 2]);
+ testCell(getCellAt(2, 2), "cell5", [2, 1], [2, 1]);
+
+ let colHeaders = table.getAttributeValue("AXColumnHeaderUIElements");
+ Assert.deepEqual(
+ colHeaders.map(c => c.getAttributeValue("AXDOMIdentifier")),
+ ["header1", "header1", "header2"],
+ "Correct column headers"
+ );
+ }
+);
+
+addAccessibleTask(
+ `<table id="table">
+ <tr>
+ <td>Foo</td>
+ </tr>
+ </table>`,
+ (browser, accDoc) => {
+ // Make sure we guess this table to be a layout table.
+ testAttrs(
+ findAccessibleChildByID(accDoc, "table"),
+ { "layout-guess": "true" },
+ true
+ );
+
+ let table = getNativeInterface(accDoc, "table");
+ is(
+ table.getAttributeValue("AXRole"),
+ "AXGroup",
+ "Correct role (AXGroup) for layout table"
+ );
+
+ let children = table.getAttributeValue("AXChildren");
+ is(
+ children.length,
+ 1,
+ "Layout table has single child (no additional columns)"
+ );
+ }
+);
+
+addAccessibleTask(
+ `<div id="table" role="table">
+ <span style="display: block;">
+ <div role="row">
+ <div role="cell">Cell 1</div>
+ <div role="cell">Cell 2</div>
+ </div>
+ </span>
+ <span style="display: block;">
+ <div role="row">
+ <span style="display: block;">
+ <div role="cell">Cell 3</div>
+ <div role="cell">Cell 4</div>
+ </span>
+ </div>
+ </span>
+ </div>`,
+ async (browser, accDoc) => {
+ let table = getNativeInterface(accDoc, "table");
+ testTableConsistency(table, 2, 2);
+ }
+);
+
+/*
+ * After executing function 'change' which operates on 'elem', verify the specified
+ * 'event' (if not null) is fired on elem. After the event, check if the given
+ * native accessible 'table' is a layout or data table by role using 'isLayout'.
+ */
+async function testIsLayout(table, elem, event, change, isLayout) {
+ info(
+ "Changing " +
+ elem +
+ ", expecting table change to " +
+ (isLayout ? "AXGroup" : "AXTable")
+ );
+ const toWait = event ? waitForEvent(event, elem) : null;
+ await change();
+ if (toWait) {
+ await toWait;
+ }
+ let intendedRole = isLayout ? "AXGroup" : "AXTable";
+ await untilCacheIs(
+ () => table.getAttributeValue("AXRole"),
+ intendedRole,
+ "Table role correct after change"
+ );
+}
+
+/*
+ * The following attributes should fire an attribute changed
+ * event, which in turn invalidates the layout-table cache
+ * associated with the given table. After adding and removing
+ * each attr, verify the table is a data or layout table,
+ * appropriately. Attrs: summary, abbr, scope, headers
+ */
+addAccessibleTask(
+ `<table id="table" summary="example summary">
+ <tr role="presentation">
+ <td id="cellOne">cell1</td>
+ <td>cell2</td>
+ </tr>
+ <tr>
+ <td id="cellThree">cell3</td>
+ <td>cell4</td>
+ </tr>
+ </table>`,
+ async (browser, accDoc) => {
+ let table = getNativeInterface(accDoc, "table");
+ // summary attr should take precedence over role="presentation" to make this
+ // a data table
+ is(table.getAttributeValue("AXRole"), "AXTable", "Table is data table");
+
+ info("Removing summary attr");
+ // after summary is removed, we should have a layout table
+ await testIsLayout(
+ table,
+ "table",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("table").removeAttribute("summary");
+ });
+ },
+ true
+ );
+
+ info("Setting abbr attr");
+ // after abbr is set we should have a data table again
+ await testIsLayout(
+ table,
+ "cellThree",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("cellThree")
+ .setAttribute("abbr", "hello world");
+ });
+ },
+ false
+ );
+
+ info("Removing abbr attr");
+ // after abbr is removed we should have a layout table again
+ await testIsLayout(
+ table,
+ "cellThree",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("cellThree").removeAttribute("abbr");
+ });
+ },
+ true
+ );
+
+ info("Setting scope attr");
+ // after scope is set we should have a data table again
+ await testIsLayout(
+ table,
+ "cellThree",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("cellThree")
+ .setAttribute("scope", "col");
+ });
+ },
+ false
+ );
+
+ info("Removing scope attr");
+ // remove scope should give layout
+ await testIsLayout(
+ table,
+ "cellThree",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("cellThree").removeAttribute("scope");
+ });
+ },
+ true
+ );
+
+ info("Setting headers attr");
+ // add headers attr should give data
+ await testIsLayout(
+ table,
+ "cellThree",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("cellThree")
+ .setAttribute("headers", "cellOne");
+ });
+ },
+ false
+ );
+
+ info("Removing headers attr");
+ // remove headers attr should give layout
+ await testIsLayout(
+ table,
+ "cellThree",
+ EVENT_OBJECT_ATTRIBUTE_CHANGED,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("cellThree")
+ .removeAttribute("headers");
+ });
+ },
+ true
+ );
+ }
+);
+
+/*
+ * The following style changes should fire a table style changed
+ * event, which in turn invalidates the layout-table cache
+ * associated with the given table.
+ */
+addAccessibleTask(
+ `<table id="table">
+ <tr id="rowOne">
+ <td id="cellOne">cell1</td>
+ <td>cell2</td>
+ </tr>
+ <tr>
+ <td>cell3</td>
+ <td>cell4</td>
+ </tr>
+ </table>`,
+ async (browser, accDoc) => {
+ let table = getNativeInterface(accDoc, "table");
+ // we should start as a layout table
+ is(table.getAttributeValue("AXRole"), "AXGroup", "Table is layout table");
+
+ info("Adding cell border");
+ // after cell border added, we should have a data table
+ await testIsLayout(
+ table,
+ "cellOne",
+ null,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("cellOne")
+ .style.setProperty("border", "5px solid green");
+ });
+ },
+ false
+ );
+
+ info("Removing cell border");
+ // after cell border removed, we should have a layout table
+ await testIsLayout(
+ table,
+ "cellOne",
+ null,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("cellOne")
+ .style.removeProperty("border");
+ });
+ },
+ true
+ );
+
+ info("Adding row background");
+ // after row background added, we should have a data table
+ await testIsLayout(
+ table,
+ "rowOne",
+ null,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("rowOne")
+ .style.setProperty("background-color", "green");
+ });
+ },
+ false
+ );
+
+ info("Removing row background");
+ // after row background removed, we should have a layout table
+ await testIsLayout(
+ table,
+ "rowOne",
+ null,
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("rowOne")
+ .style.removeProperty("background-color");
+ });
+ },
+ true
+ );
+ }
+);
+
+/*
+ * thead/tbody elements with click handlers should:
+ * (a) render as AXGroup elements
+ * (b) expose their rows as part of their parent table's AXRows array
+ */
+addAccessibleTask(
+ `<table id="table">
+ <thead id="thead">
+ <tr><td>head row</td></tr>
+ </thead>
+ <tbody id="tbody">
+ <tr><td>body row</td></tr>
+ <tr><td>another body row</td></tr>
+ </tbody>
+ </table>`,
+ async (browser, accDoc) => {
+ let table = getNativeInterface(accDoc, "table");
+
+ // No click handlers present on thead/tbody
+ let tableChildren = table.getAttributeValue("AXChildren");
+ let tableRows = table.getAttributeValue("AXRows");
+
+ is(tableChildren.length, 4, "Table has four children (3 row + 1 col)");
+ is(tableRows.length, 3, "Table has three rows");
+
+ for (let i = 0; i < tableChildren.length; i++) {
+ const child = tableChildren[i];
+ if (i < 3) {
+ is(
+ child.getAttributeValue("AXRole"),
+ "AXRow",
+ "Table's first 3 children are rows"
+ );
+ } else {
+ is(
+ child.getAttributeValue("AXRole"),
+ "AXColumn",
+ "Table's last child is a column"
+ );
+ }
+ }
+ const reorder = waitForEvent(EVENT_REORDER);
+ await invokeContentTask(browser, [], () => {
+ const head = content.document.getElementById("thead");
+ const body = content.document.getElementById("tbody");
+
+ head.addEventListener("click", function () {});
+ body.addEventListener("click", function () {});
+ });
+ await reorder;
+
+ // Click handlers present
+ tableChildren = table.getAttributeValue("AXChildren");
+
+ is(tableChildren.length, 3, "Table has three children (2 groups + 1 col)");
+ is(
+ tableChildren[0].getAttributeValue("AXRole"),
+ "AXGroup",
+ "Child one is a group"
+ );
+ is(
+ tableChildren[0].getAttributeValue("AXChildren").length,
+ 1,
+ "Child one has one child"
+ );
+
+ is(
+ tableChildren[1].getAttributeValue("AXRole"),
+ "AXGroup",
+ "Child two is a group"
+ );
+ is(
+ tableChildren[1].getAttributeValue("AXChildren").length,
+ 2,
+ "Child two has two children"
+ );
+
+ is(
+ tableChildren[2].getAttributeValue("AXRole"),
+ "AXColumn",
+ "Child three is a col"
+ );
+
+ tableRows = table.getAttributeValue("AXRows");
+ is(tableRows.length, 3, "Table has three rows");
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_text_basics.js b/accessible/tests/browser/mac/browser_text_basics.js
new file mode 100644
index 0000000000..21637d0524
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_text_basics.js
@@ -0,0 +1,426 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function testRangeAtMarker(macDoc, marker, attribute, expected, msg) {
+ let range = macDoc.getParameterizedAttributeValue(attribute, marker);
+ is(stringForRange(macDoc, range), expected, msg);
+}
+
+function testUIElement(
+ macDoc,
+ marker,
+ msg,
+ expectedRole,
+ expectedValue,
+ expectedRange
+) {
+ let elem = macDoc.getParameterizedAttributeValue(
+ "AXUIElementForTextMarker",
+ marker
+ );
+ is(
+ elem.getAttributeValue("AXRole"),
+ expectedRole,
+ `${msg}: element role matches`
+ );
+ is(elem.getAttributeValue("AXValue"), expectedValue, `${msg}: element value`);
+ let elemRange = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUIElement",
+ elem
+ );
+ is(
+ stringForRange(macDoc, elemRange),
+ expectedRange,
+ `${msg}: element range matches element value`
+ );
+}
+
+function testStyleRun(macDoc, marker, msg, expectedStyleRun) {
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXStyleTextMarkerRangeForTextMarker",
+ expectedStyleRun,
+ `${msg}: style run matches`
+ );
+}
+
+function testParagraph(macDoc, marker, msg, expectedParagraph) {
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXParagraphTextMarkerRangeForTextMarker",
+ expectedParagraph,
+ `${msg}: paragraph matches`
+ );
+}
+
+function testWords(macDoc, marker, msg, expectedLeft, expectedRight) {
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXLeftWordTextMarkerRangeForTextMarker",
+ expectedLeft,
+ `${msg}: left word matches`
+ );
+
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXRightWordTextMarkerRangeForTextMarker",
+ expectedRight,
+ `${msg}: right word matches`
+ );
+}
+
+function testLines(
+ macDoc,
+ marker,
+ msg,
+ expectedLine,
+ expectedLeft,
+ expectedRight
+) {
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXLineTextMarkerRangeForTextMarker",
+ expectedLine,
+ `${msg}: line matches`
+ );
+
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXLeftLineTextMarkerRangeForTextMarker",
+ expectedLeft,
+ `${msg}: left line matches`
+ );
+
+ testRangeAtMarker(
+ macDoc,
+ marker,
+ "AXRightLineTextMarkerRangeForTextMarker",
+ expectedRight,
+ `${msg}: right line matches`
+ );
+}
+
+function* markerIterator(macDoc, reverse = false) {
+ let m = macDoc.getAttributeValue(
+ reverse ? "AXEndTextMarker" : "AXStartTextMarker"
+ );
+ let c = 0;
+ while (m) {
+ yield [m, c++];
+ m = macDoc.getParameterizedAttributeValue(
+ reverse
+ ? "AXPreviousTextMarkerForTextMarker"
+ : "AXNextTextMarkerForTextMarker",
+ m
+ );
+ }
+}
+
+// Tests consistency in text markers between:
+// 1. "Linked list" forward navagation
+// 2. Getting markers by index
+// 3. "Linked list" reverse navagation
+// For each iteration method check that the returned index is consistent
+function testMarkerIntegrity(accDoc, expectedMarkerValues) {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ // Iterate forward with "AXNextTextMarkerForTextMarker"
+ let prevMarker;
+ let count = 0;
+ for (let [marker, index] of markerIterator(macDoc)) {
+ count++;
+ let markerIndex = macDoc.getParameterizedAttributeValue(
+ "AXIndexForTextMarker",
+ marker
+ );
+ is(
+ markerIndex,
+ index,
+ `Correct index in "AXNextTextMarkerForTextMarker": ${index}`
+ );
+ if (prevMarker) {
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUnorderedTextMarkers",
+ [prevMarker, marker]
+ );
+ is(
+ macDoc.getParameterizedAttributeValue(
+ "AXLengthForTextMarkerRange",
+ range
+ ),
+ 1,
+ `[${index}] marker moved one character`
+ );
+ }
+ prevMarker = marker;
+
+ testWords(
+ macDoc,
+ marker,
+ `At index ${index}`,
+ ...expectedMarkerValues[index].words
+ );
+ testLines(
+ macDoc,
+ marker,
+ `At index ${index}`,
+ ...expectedMarkerValues[index].lines
+ );
+ testUIElement(
+ macDoc,
+ marker,
+ `At index ${index}`,
+ ...expectedMarkerValues[index].element
+ );
+ testParagraph(
+ macDoc,
+ marker,
+ `At index ${index}`,
+ expectedMarkerValues[index].paragraph
+ );
+ testStyleRun(
+ macDoc,
+ marker,
+ `At index ${index}`,
+ expectedMarkerValues[index].style
+ );
+ }
+
+ is(expectedMarkerValues.length, count, `Correct marker count: ${count}`);
+
+ // Use "AXTextMarkerForIndex" to retrieve all text markers
+ for (let i = 0; i < count; i++) {
+ let marker = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerForIndex",
+ i
+ );
+ let index = macDoc.getParameterizedAttributeValue(
+ "AXIndexForTextMarker",
+ marker
+ );
+ is(index, i, `Correct index in "AXTextMarkerForIndex": ${i}`);
+
+ if (i == count - 1) {
+ ok(
+ !macDoc.getParameterizedAttributeValue(
+ "AXNextTextMarkerForTextMarker",
+ marker
+ ),
+ "Iterated through all markers"
+ );
+ }
+ }
+
+ count = expectedMarkerValues.length;
+
+ // Iterate backward with "AXPreviousTextMarkerForTextMarker"
+ for (let [marker] of markerIterator(macDoc, true)) {
+ if (count <= 0) {
+ ok(false, "Exceeding marker count");
+ break;
+ }
+ count--;
+ let index = macDoc.getParameterizedAttributeValue(
+ "AXIndexForTextMarker",
+ marker
+ );
+ is(
+ index,
+ count,
+ `Correct index in "AXPreviousTextMarkerForTextMarker": ${count}`
+ );
+ }
+
+ is(count, 0, "Iterated backward through all text markers");
+}
+
+// Run tests with old word segmenter
+addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.icu4x.segmenter.enabled", false],
+ ["layout.word_select.stop_at_punctuation", true], // This is default
+ ],
+ });
+
+ const expectedValues = await SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.getExpected(false, true);
+ });
+
+ testMarkerIntegrity(accDoc, expectedValues);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// new UAX#14 segmenter without stop_at_punctuation.
+addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.icu4x.segmenter.enabled", true],
+ ["layout.word_select.stop_at_punctuation", false],
+ ],
+ });
+
+ const expectedValues = await SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.getExpected(true, false);
+ });
+
+ testMarkerIntegrity(accDoc, expectedValues);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// new UAX#14 segmenter with stop_at_punctuation
+addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["intl.icu4x.segmenter.enabled", true],
+ ["layout.word_select.stop_at_punctuation", true], // this is default
+ ],
+ });
+
+ const expectedValues = await SpecialPowers.spawn(browser, [], async () => {
+ return content.wrappedJSObject.getExpected(true, true);
+ });
+
+ testMarkerIntegrity(accDoc, expectedValues);
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Test text marker lesser-than operator
+addAccessibleTask(
+ `<p id="p">hello <a id="a" href="#">goodbye</a> world</p>`,
+ async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let start = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerForIndex",
+ 1
+ );
+ let end = macDoc.getParameterizedAttributeValue("AXTextMarkerForIndex", 10);
+
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUnorderedTextMarkers",
+ [end, start]
+ );
+ is(stringForRange(macDoc, range), "ello good");
+ }
+);
+
+addAccessibleTask(
+ `<input id="input" value=""><a href="#">goodbye</a>`,
+ async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let input = getNativeInterface(accDoc, "input");
+
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUIElement",
+ input
+ );
+
+ is(stringForRange(macDoc, range), "", "string value is correct");
+ }
+);
+
+addAccessibleTask(
+ `<div role="listbox" id="box">
+ <input type="radio" name="test" role="option" title="First item"/>
+ <input type="radio" name="test" role="option" title="Second item"/>
+ </div>`,
+ async (browser, accDoc) => {
+ let box = getNativeInterface(accDoc, "box");
+ const children = box.getAttributeValue("AXChildren");
+ is(children.length, 2, "Listbox contains two items");
+ is(children[0].getAttributeValue("AXValue"), "First item");
+ is(children[1].getAttributeValue("AXValue"), "Second item");
+ }
+);
+
+addAccessibleTask(
+ `<div id="t">
+ A link <b>should</b> explain <u>clearly</u> what information the <i>reader</i> will get by clicking on that link.
+ </div>`,
+ async (browser, accDoc) => {
+ let t = getNativeInterface(accDoc, "t");
+ const children = t.getAttributeValue("AXChildren");
+ const expectedTitles = [
+ "A link ",
+ "should",
+ " explain ",
+ "clearly",
+ " what information the ",
+ "reader",
+ " will get by clicking on that link. ",
+ ];
+ is(children.length, 7, "container has seven children");
+ children.forEach((child, index) => {
+ is(child.getAttributeValue("AXValue"), expectedTitles[index]);
+ });
+ }
+);
+
+addAccessibleTask(
+ `<a href="#">link</a> <input id="input" value="hello">`,
+ async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let input = getNativeInterface(accDoc, "input");
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUIElement",
+ input
+ );
+
+ let firstMarkerInInput = macDoc.getParameterizedAttributeValue(
+ "AXStartTextMarkerForTextMarkerRange",
+ range
+ );
+
+ let leftWordRange = macDoc.getParameterizedAttributeValue(
+ "AXLeftWordTextMarkerRangeForTextMarker",
+ firstMarkerInInput
+ );
+ let str = macDoc.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ leftWordRange
+ );
+ is(str, "hello", "Left word at start of input should be right word");
+ }
+);
+
+addAccessibleTask(`<p id="p">hello world</p>`, async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let p = getNativeInterface(accDoc, "p");
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUIElement",
+ p
+ );
+
+ let bounds = macDoc.getParameterizedAttributeValue(
+ "AXBoundsForTextMarkerRange",
+ range
+ );
+
+ ok(bounds.origin && bounds.size, "Returned valid bounds");
+});
diff --git a/accessible/tests/browser/mac/browser_text_input.js b/accessible/tests/browser/mac/browser_text_input.js
new file mode 100644
index 0000000000..11a9dc25f1
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_text_input.js
@@ -0,0 +1,657 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+function testValueChangedEventData(
+ macIface,
+ data,
+ expectedId,
+ expectedChangeValue,
+ expectedEditType,
+ expectedWordAtLeft
+) {
+ is(
+ data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier"),
+ expectedId,
+ "Correct AXTextChangeElement"
+ );
+ is(
+ data.AXTextStateChangeType,
+ AXTextStateChangeTypeEdit,
+ "Correct AXTextStateChangeType"
+ );
+
+ let changeValues = data.AXTextChangeValues;
+ is(changeValues.length, 1, "One element in AXTextChangeValues");
+ is(
+ changeValues[0].AXTextChangeValue,
+ expectedChangeValue,
+ "Correct AXTextChangeValue"
+ );
+ is(
+ changeValues[0].AXTextEditType,
+ expectedEditType,
+ "Correct AXTextEditType"
+ );
+
+ let textMarker = changeValues[0].AXTextChangeValueStartMarker;
+ ok(textMarker, "There is a AXTextChangeValueStartMarker");
+ let range = macIface.getParameterizedAttributeValue(
+ "AXLeftWordTextMarkerRangeForTextMarker",
+ textMarker
+ );
+ let str = macIface.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ range,
+ "correct word before caret"
+ );
+ is(str, expectedWordAtLeft);
+}
+
+// Return true if the first given object a subset of the second
+function isSubset(subset, superset) {
+ if (typeof subset != "object" || typeof superset != "object") {
+ return superset == subset;
+ }
+
+ for (let [prop, val] of Object.entries(subset)) {
+ if (!isSubset(val, superset[prop])) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function matchWebArea(expectedId, expectedInfo) {
+ return (iface, data) => {
+ if (!data) {
+ return false;
+ }
+
+ let textChangeElemID =
+ data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier");
+
+ return (
+ iface.getAttributeValue("AXRole") == "AXWebArea" &&
+ textChangeElemID == expectedId &&
+ isSubset(expectedInfo, data)
+ );
+ };
+}
+
+function matchInput(expectedId, expectedInfo) {
+ return (iface, data) => {
+ if (!data) {
+ return false;
+ }
+
+ return (
+ iface.getAttributeValue("AXDOMIdentifier") == expectedId &&
+ isSubset(expectedInfo, data)
+ );
+ };
+}
+
+async function synthKeyAndTestSelectionChanged(
+ synthKey,
+ synthEvent,
+ expectedId,
+ expectedSelectionString,
+ expectedSelectionInfo
+) {
+ let selectionChangedEvents = Promise.all([
+ waitForMacEventWithInfo(
+ "AXSelectedTextChanged",
+ matchWebArea(expectedId, expectedSelectionInfo)
+ ),
+ waitForMacEventWithInfo(
+ "AXSelectedTextChanged",
+ matchInput(expectedId, expectedSelectionInfo)
+ ),
+ ]);
+
+ EventUtils.synthesizeKey(synthKey, synthEvent);
+ let [webareaEvent, inputEvent] = await selectionChangedEvents;
+ is(
+ inputEvent.data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier"),
+ expectedId,
+ "Correct AXTextChangeElement"
+ );
+
+ let rangeString = inputEvent.macIface.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ inputEvent.data.AXSelectedTextMarkerRange
+ );
+ is(
+ rangeString,
+ expectedSelectionString,
+ `selection has correct value (${expectedSelectionString})`
+ );
+
+ is(
+ webareaEvent.macIface.getAttributeValue("AXDOMIdentifier"),
+ "body",
+ "Input event target is top-level WebArea"
+ );
+ rangeString = webareaEvent.macIface.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ inputEvent.data.AXSelectedTextMarkerRange
+ );
+ is(
+ rangeString,
+ expectedSelectionString,
+ `selection has correct value (${expectedSelectionString}) via top document`
+ );
+
+ return inputEvent;
+}
+
+function testSelectionEventLeftChar(event, expectedChar) {
+ const selStart = event.macIface.getParameterizedAttributeValue(
+ "AXStartTextMarkerForTextMarkerRange",
+ event.data.AXSelectedTextMarkerRange
+ );
+ const selLeft = event.macIface.getParameterizedAttributeValue(
+ "AXPreviousTextMarkerForTextMarker",
+ selStart
+ );
+ const leftCharRange = event.macIface.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUnorderedTextMarkers",
+ [selLeft, selStart]
+ );
+ const leftCharString = event.macIface.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ leftCharRange
+ );
+ is(leftCharString, expectedChar, "Left character is correct");
+}
+
+function testSelectionEventLine(event, expectedLine) {
+ const selStart = event.macIface.getParameterizedAttributeValue(
+ "AXStartTextMarkerForTextMarkerRange",
+ event.data.AXSelectedTextMarkerRange
+ );
+ const lineRange = event.macIface.getParameterizedAttributeValue(
+ "AXLineTextMarkerRangeForTextMarker",
+ selStart
+ );
+ const lineString = event.macIface.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ lineRange
+ );
+ is(lineString, expectedLine, "Line is correct");
+}
+
+async function synthKeyAndTestValueChanged(
+ synthKey,
+ synthEvent,
+ expectedId,
+ expectedTextSelectionId,
+ expectedChangeValue,
+ expectedEditType,
+ expectedWordAtLeft
+) {
+ let valueChangedEvents = Promise.all([
+ waitForMacEvent(
+ "AXSelectedTextChanged",
+ matchWebArea(expectedTextSelectionId, {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ })
+ ),
+ waitForMacEvent(
+ "AXSelectedTextChanged",
+ matchInput(expectedTextSelectionId, {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ })
+ ),
+ waitForMacEventWithInfo(
+ "AXValueChanged",
+ matchWebArea(expectedId, {
+ AXTextStateChangeType: AXTextStateChangeTypeEdit,
+ AXTextChangeValues: [
+ {
+ AXTextChangeValue: expectedChangeValue,
+ AXTextEditType: expectedEditType,
+ },
+ ],
+ })
+ ),
+ waitForMacEventWithInfo(
+ "AXValueChanged",
+ matchInput(expectedId, {
+ AXTextStateChangeType: AXTextStateChangeTypeEdit,
+ AXTextChangeValues: [
+ {
+ AXTextChangeValue: expectedChangeValue,
+ AXTextEditType: expectedEditType,
+ },
+ ],
+ })
+ ),
+ ]);
+
+ EventUtils.synthesizeKey(synthKey, synthEvent);
+ let [, , webareaEvent, inputEvent] = await valueChangedEvents;
+
+ testValueChangedEventData(
+ webareaEvent.macIface,
+ webareaEvent.data,
+ expectedId,
+ expectedChangeValue,
+ expectedEditType,
+ expectedWordAtLeft
+ );
+ testValueChangedEventData(
+ inputEvent.macIface,
+ inputEvent.data,
+ expectedId,
+ expectedChangeValue,
+ expectedEditType,
+ expectedWordAtLeft
+ );
+}
+
+async function focusIntoInput(accDoc, inputId, innerContainerId) {
+ let selectionId = innerContainerId ? innerContainerId : inputId;
+ let input = getNativeInterface(accDoc, inputId);
+ ok(!input.getAttributeValue("AXFocused"), "input is not focused");
+ ok(input.isAttributeSettable("AXFocused"), "input is focusable");
+ let events = Promise.all([
+ waitForMacEvent(
+ "AXFocusedUIElementChanged",
+ iface => iface.getAttributeValue("AXDOMIdentifier") == inputId
+ ),
+ waitForMacEventWithInfo(
+ "AXSelectedTextChanged",
+ matchWebArea(selectionId, {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ })
+ ),
+ waitForMacEventWithInfo(
+ "AXSelectedTextChanged",
+ matchInput(selectionId, {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ })
+ ),
+ ]);
+ input.setAttributeValue("AXFocused", true);
+ await events;
+}
+
+async function focusIntoInputAndType(accDoc, inputId, innerContainerId) {
+ let selectionId = innerContainerId ? innerContainerId : inputId;
+ await focusIntoInput(accDoc, inputId, innerContainerId);
+
+ async function testTextInput(
+ synthKey,
+ expectedChangeValue,
+ expectedWordAtLeft
+ ) {
+ await synthKeyAndTestValueChanged(
+ synthKey,
+ null,
+ inputId,
+ selectionId,
+ expectedChangeValue,
+ AXTextEditTypeTyping,
+ expectedWordAtLeft
+ );
+ }
+
+ await testTextInput("h", "h", "h");
+ await testTextInput("e", "e", "he");
+ await testTextInput("l", "l", "hel");
+ await testTextInput("l", "l", "hell");
+ await testTextInput("o", "o", "hello");
+ await testTextInput(" ", " ", "hello");
+ // You would expect this to be useless but this is what VO
+ // consumes. I guess it concats the inserted text data to the
+ // word to the left of the marker.
+ await testTextInput("w", "w", " ");
+ await testTextInput("o", "o", "wo");
+ await testTextInput("r", "r", "wor");
+ await testTextInput("l", "l", "worl");
+ await testTextInput("d", "d", "world");
+
+ async function testTextDelete(expectedChangeValue, expectedWordAtLeft) {
+ await synthKeyAndTestValueChanged(
+ "KEY_Backspace",
+ null,
+ inputId,
+ selectionId,
+ expectedChangeValue,
+ AXTextEditTypeDelete,
+ expectedWordAtLeft
+ );
+ }
+
+ await testTextDelete("d", "worl");
+ await testTextDelete("l", "wor");
+
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ null,
+ selectionId,
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionPrevious,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ }
+ );
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ { shiftKey: true },
+ selectionId,
+ "o",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend,
+ AXTextSelectionDirection: AXTextSelectionDirectionPrevious,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ }
+ );
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ { shiftKey: true },
+ selectionId,
+ "wo",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend,
+ AXTextSelectionDirection: AXTextSelectionDirectionPrevious,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ }
+ );
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ null,
+ selectionId,
+ "",
+ { AXTextStateChangeType: AXTextStateChangeTypeSelectionMove }
+ );
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ { shiftKey: true, metaKey: true },
+ selectionId,
+ "hello ",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend,
+ AXTextSelectionDirection: AXTextSelectionDirectionBeginning,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ null,
+ selectionId,
+ "",
+ { AXTextStateChangeType: AXTextStateChangeTypeSelectionMove }
+ );
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowRight",
+ { shiftKey: true, altKey: true },
+ selectionId,
+ "hello",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityWord,
+ }
+ );
+}
+
+// Test text input
+addAccessibleTask(
+ `<a href="#">link</a> <input id="input">`,
+ async (browser, accDoc) => {
+ await focusIntoInputAndType(accDoc, "input");
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+// Test content editable
+addAccessibleTask(
+ `<div id="input" contentEditable="true" tabindex="0" role="textbox" aria-multiline="true"><div id="inner"><br /></div></div>`,
+ async (browser, accDoc) => {
+ const inner = getNativeInterface(accDoc, "inner");
+ const editableAncestor = inner.getAttributeValue("AXEditableAncestor");
+ is(
+ editableAncestor.getAttributeValue("AXDOMIdentifier"),
+ "input",
+ "Editable ancestor is input"
+ );
+ await focusIntoInputAndType(accDoc, "input");
+ }
+);
+
+// Test input that gets role::EDITCOMBOBOX
+addAccessibleTask(`<input type="text" id="box">`, async (browser, accDoc) => {
+ const box = getNativeInterface(accDoc, "box");
+ const editableAncestor = box.getAttributeValue("AXEditableAncestor");
+ is(
+ editableAncestor.getAttributeValue("AXDOMIdentifier"),
+ "box",
+ "Editable ancestor is box itself"
+ );
+ await focusIntoInputAndType(accDoc, "box");
+});
+
+// Test multiline caret control in a text area
+addAccessibleTask(
+ `<textarea id="input" cols="15">one two three four five six seven eight</textarea>`,
+ async (browser, accDoc) => {
+ await focusIntoInput(accDoc, "input");
+
+ await synthKeyAndTestSelectionChanged("KEY_ArrowRight", null, "input", "", {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ });
+
+ await synthKeyAndTestSelectionChanged("KEY_ArrowDown", null, "input", "", {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ });
+
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowLeft",
+ { metaKey: true },
+ "input",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionBeginning,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+
+ await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowRight",
+ { metaKey: true },
+ "input",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionEnd,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ },
+ { topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test that the caret returns the correct marker when it is positioned after
+ * the last character (to facilitate appending text).
+ */
+addAccessibleTask(
+ `<input id="input" value="abc">`,
+ async function (browser, docAcc) {
+ await focusIntoInput(docAcc, "input");
+
+ let event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowRight",
+ null,
+ "input",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ }
+ );
+ testSelectionEventLeftChar(event, "a");
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowRight",
+ null,
+ "input",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ }
+ );
+ testSelectionEventLeftChar(event, "b");
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowRight",
+ null,
+ "input",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityCharacter,
+ }
+ );
+ testSelectionEventLeftChar(event, "c");
+ },
+ { chrome: true, topLevel: true }
+);
+
+/**
+ * Test that the caret returns the correct line when the caret is at the start
+ * of the line.
+ */
+addAccessibleTask(
+ `
+<textarea id="hard">ab
+cd
+ef
+
+gh
+</textarea>
+<div role="textbox" id="wrapped" contenteditable style="width: 1ch;">a b c</div>
+ `,
+ async function (browser, docAcc) {
+ let hard = getNativeInterface(docAcc, "hard");
+ await focusIntoInput(docAcc, "hard");
+ is(hard.getAttributeValue("AXInsertionPointLineNumber"), 0);
+ let event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "hard",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "cd");
+ is(hard.getAttributeValue("AXInsertionPointLineNumber"), 1);
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "hard",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "ef");
+ is(hard.getAttributeValue("AXInsertionPointLineNumber"), 2);
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "hard",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "");
+ is(hard.getAttributeValue("AXInsertionPointLineNumber"), 3);
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "hard",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "gh");
+ is(hard.getAttributeValue("AXInsertionPointLineNumber"), 4);
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "hard",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "");
+ is(hard.getAttributeValue("AXInsertionPointLineNumber"), 5);
+
+ let wrapped = getNativeInterface(docAcc, "wrapped");
+ await focusIntoInput(docAcc, "wrapped");
+ is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 0);
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "wrapped",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "b ");
+ is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 1);
+ event = await synthKeyAndTestSelectionChanged(
+ "KEY_ArrowDown",
+ null,
+ "wrapped",
+ "",
+ {
+ AXTextStateChangeType: AXTextStateChangeTypeSelectionMove,
+ AXTextSelectionDirection: AXTextSelectionDirectionNext,
+ AXTextSelectionGranularity: AXTextSelectionGranularityLine,
+ }
+ );
+ testSelectionEventLine(event, "c");
+ is(wrapped.getAttributeValue("AXInsertionPointLineNumber"), 2);
+ },
+ { chrome: true, topLevel: true }
+);
diff --git a/accessible/tests/browser/mac/browser_text_leaf.js b/accessible/tests/browser/mac/browser_text_leaf.js
new file mode 100644
index 0000000000..21deed6212
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_text_leaf.js
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test accessibles aren't created for linebreaks.
+ */
+addAccessibleTask(
+ `hello<br>world`,
+ async (browser, accDoc) => {
+ let doc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+ let docChildren = doc.getAttributeValue("AXChildren");
+ is(docChildren.length, 1, "The document contains a root group");
+
+ let rootGroup = docChildren[0];
+ let children = rootGroup.getAttributeValue("AXChildren");
+ is(docChildren.length, 1, "The root group contains 2 children");
+
+ // verify first child is correct
+ is(
+ children[0].getAttributeValue("AXRole"),
+ "AXStaticText",
+ "First child is a text node"
+ );
+ is(
+ children[0].getAttributeValue("AXValue"),
+ "hello",
+ "First child is hello text"
+ );
+
+ // verify second child is correct
+ is(
+ children[1].getAttributeValue("AXRole"),
+ "AXStaticText",
+ "Second child is a text node"
+ );
+
+ is(
+ children[1].getAttributeValue("AXValue"),
+ gIsIframe && !gIsRemoteIframe ? "world" : "world ",
+ "Second child is world text"
+ );
+ // we have a trailing space in here due to bug 1577028
+ // but this appears fixed in non-remote iframes
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+addAccessibleTask(
+ `<p id="p">hello, this is a test</p>`,
+ async (browser, accDoc) => {
+ let p = getNativeInterface(accDoc, "p");
+ let textLeaf = p.getAttributeValue("AXChildren")[0];
+ ok(textLeaf, "paragraph has a text leaf");
+
+ let str = textLeaf.getParameterizedAttributeValue(
+ "AXStringForRange",
+ NSRange(3, 6)
+ );
+
+ is(str, "lo, th", "AXStringForRange matches.");
+
+ let smallBounds = textLeaf.getParameterizedAttributeValue(
+ "AXBoundsForRange",
+ NSRange(3, 6)
+ );
+
+ let largeBounds = textLeaf.getParameterizedAttributeValue(
+ "AXBoundsForRange",
+ NSRange(3, 8)
+ );
+
+ ok(smallBounds.size[0] < largeBounds.size[0], "longer range is wider");
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/mac/browser_text_selection.js b/accessible/tests/browser/mac/browser_text_selection.js
new file mode 100644
index 0000000000..a914adba8e
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_text_selection.js
@@ -0,0 +1,187 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test simple text selection
+ */
+addAccessibleTask(`<p id="p">Hello World</p>`, async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let startMarker = macDoc.getAttributeValue("AXStartTextMarker");
+ let endMarker = macDoc.getAttributeValue("AXEndTextMarker");
+ let range = macDoc.getParameterizedAttributeValue(
+ "AXTextMarkerRangeForUnorderedTextMarkers",
+ [startMarker, endMarker]
+ );
+ is(stringForRange(macDoc, range), "Hello World");
+
+ let evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => {
+ return (
+ !info.AXTextStateSync &&
+ info.AXTextStateChangeType == AXTextStateChangeTypeSelectionExtend &&
+ elem.getAttributeValue("AXRole") == "AXWebArea"
+ );
+ });
+ await SpecialPowers.spawn(browser, [], () => {
+ let p = content.document.getElementById("p");
+ let r = new content.Range();
+ r.setStart(p.firstChild, 1);
+ r.setEnd(p.firstChild, 8);
+
+ let s = content.getSelection();
+ s.addRange(r);
+ });
+ await evt;
+
+ range = macDoc.getAttributeValue("AXSelectedTextMarkerRange");
+ is(stringForRange(macDoc, range), "ello Wo");
+
+ let firstWordRange = macDoc.getParameterizedAttributeValue(
+ "AXRightWordTextMarkerRangeForTextMarker",
+ startMarker
+ );
+ is(stringForRange(macDoc, firstWordRange), "Hello");
+
+ evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => {
+ return (
+ !info.AXTextStateSync &&
+ info.AXTextStateChangeType == AXTextStateChangeTypeSelectionExtend &&
+ elem.getAttributeValue("AXRole") == "AXWebArea"
+ );
+ });
+ macDoc.setAttributeValue("AXSelectedTextMarkerRange", firstWordRange);
+ await evt;
+ range = macDoc.getAttributeValue("AXSelectedTextMarkerRange");
+ is(stringForRange(macDoc, range), "Hello");
+
+ // Collapse selection
+ evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => {
+ return (
+ info.AXTextStateSync &&
+ info.AXTextStateChangeType == AXTextStateChangeTypeSelectionMove &&
+ elem.getAttributeValue("AXRole") == "AXWebArea"
+ );
+ });
+ await SpecialPowers.spawn(browser, [], () => {
+ let s = content.getSelection();
+ s.collapseToEnd();
+ });
+ await evt;
+});
+
+/**
+ * Test text selection events caused by focus change
+ */
+addAccessibleTask(
+ `<p>
+ Hello <a href="#" id="link">World</a>,
+ I <a href="#" style="user-select: none;" id="unselectable_link">love</a>
+ <button id="button">you</button></p>`,
+ async (browser, accDoc) => {
+ // Set up an AXSelectedTextChanged listener here. It will get resolved
+ // on the first non-root event it encounters, so if we test its data at the end
+ // of this test it will show us the first text-selectable object that was focused,
+ // which is "link".
+ let selTextChanged = waitForMacEvent(
+ "AXSelectedTextChanged",
+ e => e.getAttributeValue("AXDOMIdentifier") != "body"
+ );
+
+ let focusChanged = waitForMacEvent("AXFocusedUIElementChanged");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("unselectable_link").focus();
+ });
+ let focusChangedTarget = await focusChanged;
+ is(
+ focusChangedTarget.getAttributeValue("AXDOMIdentifier"),
+ "unselectable_link",
+ "Correct event target"
+ );
+
+ focusChanged = waitForMacEvent("AXFocusedUIElementChanged");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("button").focus();
+ });
+ focusChangedTarget = await focusChanged;
+ is(
+ focusChangedTarget.getAttributeValue("AXDOMIdentifier"),
+ "button",
+ "Correct event target"
+ );
+
+ focusChanged = waitForMacEvent("AXFocusedUIElementChanged");
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("link").focus();
+ });
+ focusChangedTarget = await focusChanged;
+ is(
+ focusChangedTarget.getAttributeValue("AXDOMIdentifier"),
+ "link",
+ "Correct event target"
+ );
+
+ let selTextChangedTarget = await selTextChanged;
+ is(
+ selTextChangedTarget.getAttributeValue("AXDOMIdentifier"),
+ "link",
+ "Correct event target"
+ );
+ }
+);
+
+/**
+ * Test text selection with focus change
+ */
+addAccessibleTask(
+ `<p id="p">Hello <input id="input"></p>`,
+ async (browser, accDoc) => {
+ let macDoc = accDoc.nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+
+ let evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => {
+ return (
+ !info.AXTextStateSync &&
+ info.AXTextStateChangeType == AXTextStateChangeTypeSelectionExtend &&
+ elem.getAttributeValue("AXRole") == "AXWebArea"
+ );
+ });
+ await SpecialPowers.spawn(browser, [], () => {
+ let p = content.document.getElementById("p");
+ let r = new content.Range();
+ r.setStart(p.firstChild, 1);
+ r.setEnd(p.firstChild, 3);
+
+ let s = content.getSelection();
+ s.addRange(r);
+ });
+ await evt;
+
+ let range = macDoc.getAttributeValue("AXSelectedTextMarkerRange");
+ is(stringForRange(macDoc, range), "el");
+
+ let events = Promise.all([
+ waitForMacEvent("AXFocusedUIElementChanged"),
+ waitForMacEventWithInfo("AXSelectedTextChanged"),
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("input").focus();
+ });
+ let [, { data }] = await events;
+ ok(
+ data.AXTextSelectionChangedFocus,
+ "have AXTextSelectionChangedFocus in event info"
+ );
+ ok(!data.AXTextStateSync, "no AXTextStateSync in editables");
+ is(
+ data.AXTextSelectionDirection,
+ AXTextSelectionDirectionDiscontiguous,
+ "discontigous direction"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_toggle_radio_check.js b/accessible/tests/browser/mac/browser_toggle_radio_check.js
new file mode 100644
index 0000000000..1695d73b0d
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_toggle_radio_check.js
@@ -0,0 +1,304 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+/**
+ * Test input[type=checkbox]
+ */
+addAccessibleTask(
+ `<input type="checkbox" id="vehicle"><label for="vehicle"> Bike</label>`,
+ async (browser, accDoc) => {
+ let checkbox = getNativeInterface(accDoc, "vehicle");
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 0,
+ "Correct initial value"
+ );
+
+ let actions = checkbox.actionNames;
+ ok(actions.includes("AXPress"), "Has press action");
+
+ let evt = waitForMacEvent("AXValueChanged", "vehicle");
+ checkbox.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 1,
+ "Correct checked value"
+ );
+
+ evt = waitForMacEvent("AXValueChanged", "vehicle");
+ checkbox.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 0,
+ "Correct checked value"
+ );
+ }
+);
+
+/**
+ * Test aria-pressed toggle buttons
+ */
+addAccessibleTask(
+ `<button id="toggle" aria-pressed="false">toggle</button>`,
+ async (browser, accDoc) => {
+ // Set up a callback to change the toggle value
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("toggle").onclick = e => {
+ let curVal = e.target.getAttribute("aria-pressed");
+ let nextVal = curVal == "false" ? "true" : "false";
+ e.target.setAttribute("aria-pressed", nextVal);
+ };
+ });
+
+ let toggle = getNativeInterface(accDoc, "toggle");
+ await untilCacheIs(
+ () => toggle.getAttributeValue("AXValue"),
+ 0,
+ "Correct initial value"
+ );
+
+ let actions = toggle.actionNames;
+ ok(actions.includes("AXPress"), "Has press action");
+
+ let evt = waitForMacEvent("AXValueChanged", "toggle");
+ toggle.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => toggle.getAttributeValue("AXValue"),
+ 1,
+ "Correct checked value"
+ );
+
+ evt = waitForMacEvent("AXValueChanged", "toggle");
+ toggle.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => toggle.getAttributeValue("AXValue"),
+ 0,
+ "Correct checked value"
+ );
+ }
+);
+
+/**
+ * Test aria-checked with tri state
+ */
+addAccessibleTask(
+ `<button role="checkbox" id="checkbox" aria-checked="false">toggle</button>`,
+ async (browser, accDoc) => {
+ // Set up a callback to change the toggle value
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.getElementById("checkbox").onclick = e => {
+ const states = ["false", "true", "mixed"];
+ let currState = e.target.getAttribute("aria-checked");
+ let nextState = states[(states.indexOf(currState) + 1) % states.length];
+ e.target.setAttribute("aria-checked", nextState);
+ };
+ });
+ let checkbox = getNativeInterface(accDoc, "checkbox");
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 0,
+ "Correct initial value"
+ );
+
+ let actions = checkbox.actionNames;
+ ok(actions.includes("AXPress"), "Has press action");
+
+ let evt = waitForMacEvent("AXValueChanged", "checkbox");
+ checkbox.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 1,
+ "Correct checked value"
+ );
+
+ // Changing from checked to mixed fires two events. Make sure we wait until
+ // the second so we're asserting based on the latest state.
+ evt = waitForMacEvent("AXValueChanged", (iface, data) => {
+ return (
+ iface.getAttributeValue("AXDOMIdentifier") == "checkbox" &&
+ iface.getAttributeValue("AXValue") == 2
+ );
+ });
+ checkbox.performAction("AXPress");
+ await evt;
+ is(checkbox.getAttributeValue("AXValue"), 2, "Correct checked value");
+ }
+);
+
+/**
+ * Test input[type=radio]
+ */
+addAccessibleTask(
+ `<input type="radio" id="huey" name="drone" value="huey" checked>
+ <label for="huey">Huey</label>
+ <input type="radio" id="dewey" name="drone" value="dewey">
+ <label for="dewey">Dewey</label>`,
+ async (browser, accDoc) => {
+ let huey = getNativeInterface(accDoc, "huey");
+ await untilCacheIs(
+ () => huey.getAttributeValue("AXValue"),
+ 1,
+ "Correct initial value for huey"
+ );
+
+ let dewey = getNativeInterface(accDoc, "dewey");
+ await untilCacheIs(
+ () => dewey.getAttributeValue("AXValue"),
+ 0,
+ "Correct initial value for dewey"
+ );
+
+ let actions = dewey.actionNames;
+ ok(actions.includes("AXPress"), "Has press action");
+
+ let evt = Promise.all([
+ waitForMacEvent("AXValueChanged", "huey"),
+ waitForMacEvent("AXValueChanged", "dewey"),
+ ]);
+ dewey.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => dewey.getAttributeValue("AXValue"),
+ 1,
+ "Correct checked value for dewey"
+ );
+ await untilCacheIs(
+ () => huey.getAttributeValue("AXValue"),
+ 0,
+ "Correct checked value for huey"
+ );
+ }
+);
+
+/**
+ * Test role=switch
+ */
+addAccessibleTask(
+ `<div role="switch" aria-checked="false" id="sw">hello</div>`,
+ async (browser, accDoc) => {
+ let sw = getNativeInterface(accDoc, "sw");
+ await untilCacheIs(
+ () => sw.getAttributeValue("AXValue"),
+ 0,
+ "Initially switch is off"
+ );
+ is(sw.getAttributeValue("AXRole"), "AXCheckBox", "Has correct role");
+ is(sw.getAttributeValue("AXSubrole"), "AXSwitch", "Has correct subrole");
+
+ let stateChanged = Promise.all([
+ waitForMacEvent("AXValueChanged", "sw"),
+ waitForStateChange("sw", STATE_CHECKED, true),
+ ]);
+
+ // We should get a state change event, and a value change.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document
+ .getElementById("sw")
+ .setAttribute("aria-checked", "true");
+ });
+
+ await stateChanged;
+
+ await untilCacheIs(
+ () => sw.getAttributeValue("AXValue"),
+ 1,
+ "Switch is now on"
+ );
+ }
+);
+
+/**
+ * Test input[type=checkbox] with role=menuitemcheckbox
+ */
+addAccessibleTask(
+ `<input type="checkbox" role="menuitemcheckbox" id="vehicle"><label for="vehicle"> Bike</label>`,
+ async (browser, accDoc) => {
+ let checkbox = getNativeInterface(accDoc, "vehicle");
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 0,
+ "Correct initial value"
+ );
+
+ let actions = checkbox.actionNames;
+ ok(actions.includes("AXPress"), "Has press action");
+
+ let evt = waitForMacEvent("AXValueChanged", "vehicle");
+ checkbox.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 1,
+ "Correct checked value"
+ );
+
+ evt = waitForMacEvent("AXValueChanged", "vehicle");
+ checkbox.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => checkbox.getAttributeValue("AXValue"),
+ 0,
+ "Correct checked value"
+ );
+ }
+);
+
+/**
+ * Test input[type=radio] with role=menuitemradio
+ */
+addAccessibleTask(
+ `<input type="radio" role="menuitemradio" id="huey" name="drone" value="huey" checked>
+ <label for="huey">Huey</label>
+ <input type="radio" role="menuitemradio" id="dewey" name="drone" value="dewey">
+ <label for="dewey">Dewey</label>`,
+ async (browser, accDoc) => {
+ let huey = getNativeInterface(accDoc, "huey");
+ await untilCacheIs(
+ () => huey.getAttributeValue("AXValue"),
+ 1,
+ "Correct initial value for huey"
+ );
+
+ let dewey = getNativeInterface(accDoc, "dewey");
+ await untilCacheIs(
+ () => dewey.getAttributeValue("AXValue"),
+ 0,
+ "Correct initial value for dewey"
+ );
+
+ let actions = dewey.actionNames;
+ ok(actions.includes("AXPress"), "Has press action");
+
+ let evt = Promise.all([
+ waitForMacEvent("AXValueChanged", "huey"),
+ waitForMacEvent("AXValueChanged", "dewey"),
+ ]);
+ dewey.performAction("AXPress");
+ await evt;
+ await untilCacheIs(
+ () => dewey.getAttributeValue("AXValue"),
+ 1,
+ "Correct checked value for dewey"
+ );
+ await untilCacheIs(
+ () => huey.getAttributeValue("AXValue"),
+ 0,
+ "Correct checked value for huey"
+ );
+ }
+);
diff --git a/accessible/tests/browser/mac/browser_webarea.js b/accessible/tests/browser/mac/browser_webarea.js
new file mode 100644
index 0000000000..ac6122de14
--- /dev/null
+++ b/accessible/tests/browser/mac/browser_webarea.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+// Test web area role and AXLoadComplete event
+addAccessibleTask(``, async (browser, accDoc) => {
+ let evt = waitForMacEvent("AXLoadComplete", (iface, data) => {
+ return iface.getAttributeValue("AXDescription") == "webarea test";
+ });
+ await SpecialPowers.spawn(browser, [], () => {
+ content.location = "data:text/html,<title>webarea test</title>";
+ });
+ let doc = await evt;
+
+ is(
+ doc.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "document has AXWebArea role"
+ );
+ is(doc.getAttributeValue("AXValue"), "", "document has no AXValue");
+ is(doc.getAttributeValue("AXTitle"), null, "document has no AXTitle");
+
+ is(doc.getAttributeValue("AXLoaded"), 1, "document has finished loading");
+});
+
+// Test iframe web area role and AXLayoutComplete event
+addAccessibleTask(`<title>webarea test</title>`, async (browser, accDoc) => {
+ // If the iframe loads before the top level document finishes loading, we'll
+ // get both an AXLayoutComplete event for the iframe and an AXLoadComplete
+ // event for the document. Otherwise, if the iframe loads after the
+ // document, we'll get one AXLoadComplete event.
+ let eventPromise = Promise.race([
+ waitForMacEvent("AXLayoutComplete", (iface, data) => {
+ return iface.getAttributeValue("AXDescription") == "iframe document";
+ }),
+ waitForMacEvent("AXLoadComplete", (iface, data) => {
+ return iface.getAttributeValue("AXDescription") == "webarea test";
+ }),
+ ]);
+ await SpecialPowers.spawn(browser, [], () => {
+ const iframe = content.document.createElement("iframe");
+ iframe.src = "data:text/html,<title>iframe document</title>hello world";
+ content.document.body.appendChild(iframe);
+ });
+ let doc = await eventPromise;
+
+ if (doc.getAttributeValue("AXTitle")) {
+ // iframe should have no title, so if we get a title here
+ // we've got the main document and need to get the iframe from
+ // the main doc
+ doc = doc.getAttributeValue("AXChildren")[0];
+ }
+
+ is(
+ doc.getAttributeValue("AXRole"),
+ "AXWebArea",
+ "iframe document has AXWebArea role"
+ );
+ is(doc.getAttributeValue("AXValue"), "", "iframe document has no AXValue");
+ is(doc.getAttributeValue("AXTitle"), null, "iframe document has no AXTitle");
+ is(
+ doc.getAttributeValue("AXDescription"),
+ "iframe document",
+ "test has correct label"
+ );
+
+ is(
+ doc.getAttributeValue("AXLoaded"),
+ 1,
+ "iframe document has finished loading"
+ );
+});
diff --git a/accessible/tests/browser/mac/doc_aria_tabs.html b/accessible/tests/browser/mac/doc_aria_tabs.html
new file mode 100644
index 0000000000..0c8f2afd6f
--- /dev/null
+++ b/accessible/tests/browser/mac/doc_aria_tabs.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html><head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta charset="utf-8">
+
+ <style type="text/css">
+ .tabs {
+ padding: 1em;
+ }
+
+ [role="tablist"] {
+ margin-bottom: -1px;
+ }
+
+ [role="tab"] {
+ position: relative;
+ z-index: 1;
+ background: white;
+ border-radius: 5px 5px 0 0;
+ border: 1px solid grey;
+ border-bottom: 0;
+ padding: 0.2em;
+ }
+
+ [role="tab"][aria-selected="true"] {
+ z-index: 3;
+ }
+
+ [role="tabpanel"] {
+ position: relative;
+ padding: 0 0.5em 0.5em 0.7em;
+ border: 1px solid grey;
+ border-radius: 0 0 5px 5px;
+ background: white;
+ z-index: 2;
+ }
+
+ [role="tabpanel"]:focus {
+ border-color: orange;
+ outline: 1px solid orange;
+ }
+ </style>
+ <script>
+ 'use strict';
+ /* exported changeTabs */
+ function changeTabs(target) {
+ const parent = target.parentNode;
+ const grandparent = parent.parentNode;
+
+ // Remove all current selected tabs
+ parent
+ .querySelectorAll('[aria-selected="true"]')
+ .forEach(t => t.setAttribute("aria-selected", false));
+
+ // Set this tab as selected
+ target.setAttribute("aria-selected", true);
+
+ // Hide all tab panels
+ grandparent
+ .querySelectorAll('[role="tabpanel"]')
+ .forEach(p => (p.hidden = true));
+
+ // Show the selected panel
+ grandparent.parentNode
+ .querySelector(`#${target.getAttribute("aria-controls")}`)
+ .removeAttribute("hidden");
+ }
+ </script>
+ <title>ARIA: tab role - Example - code sample</title>
+</head>
+<body id="body">
+
+ <div class="tabs">
+ <div id="tablist" role="tablist" aria-label="Sample Tabs">
+ <button onclick="changeTabs(this)" role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
+ First Tab
+ </button>
+ <button onclick="changeTabs(this)" role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2">
+ Second Tab
+ </button>
+ <button onclick="changeTabs(this)" role="tab" aria-selected="false" aria-controls="panel-3" id="tab-3">
+ Third Tab
+ </button>
+ </div>
+ <div id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1">
+ <p>Content for the first panel</p>
+ </div>
+ <div id="panel-2" role="tabpanel" tabindex="0" aria-labelledby="tab-2" hidden="">
+ <p>Content for the second panel</p>
+ </div>
+ <div id="panel-3" role="tabpanel" tabindex="0" aria-labelledby="tab-3" hidden="">
+ <p>Content for the third panel</p>
+ </div>
+ </div>
+</body></html>
diff --git a/accessible/tests/browser/mac/doc_menulist.xhtml b/accessible/tests/browser/mac/doc_menulist.xhtml
new file mode 100644
index 0000000000..d6751bc8f4
--- /dev/null
+++ b/accessible/tests/browser/mac/doc_menulist.xhtml
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <hbox>
+ <label control="defaultZoom" value="Zoom"/>
+ <hbox>
+ <menulist id="defaultZoom">
+ <menupopup>
+ <menuitem label="50%" value="50"/>
+ <menuitem label="100%" value="100"/>
+ <menuitem label="150%" value="150"/>
+ <menuitem label="200%" value="200"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/browser/mac/doc_rich_listbox.xhtml b/accessible/tests/browser/mac/doc_rich_listbox.xhtml
new file mode 100644
index 0000000000..3acaf3bff8
--- /dev/null
+++ b/accessible/tests/browser/mac/doc_rich_listbox.xhtml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <richlistbox id="categories">
+ <richlistitem id="general">
+ <label value="general"/>
+ </richlistitem>
+
+ <richlistitem id="home">
+ <label value="home"/>
+ </richlistitem>
+
+ <richlistitem id="search">
+ <label value="search"/>
+ </richlistitem>
+
+ <richlistitem id="privacy">
+ <label value="privacy"/>
+ </richlistitem>
+ </richlistbox>
+</window>
diff --git a/accessible/tests/browser/mac/doc_textmarker_test.html b/accessible/tests/browser/mac/doc_textmarker_test.html
new file mode 100644
index 0000000000..9584d28e09
--- /dev/null
+++ b/accessible/tests/browser/mac/doc_textmarker_test.html
@@ -0,0 +1,2427 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <meta charset="utf-8">
+ </head>
+ <body id="body">
+ <p>Bob Loblaw Lobs Law Bomb</p>
+ <p>I love all of my <a href="#">children</a> equally</p>
+ <p>This is the <b>best</b> free scr<a href="#">apbook</a>ing class I have ever taken</p>
+ <ul>
+ <li>Fried cheese with club sauce</li>
+ <li>Popcorn shrimp with club sauce</li>
+ <li>Chicken fingers with <i>spicy</i> club sauce</li>
+ </ul>
+ <ul style="list-style: none;"><li>Do not order the Skip's Scramble</li></ul>
+ <p style="width: 1rem">These are my awards, Mother. From Army.</p>
+ <p>I <input value="deceived you">, mom.</p>
+ <script>
+ "use strict";
+ // eslint-disable-next-line no-unused-vars
+ function getExpected(useNewSegmenter, stopAtPunctuation) {
+ return [
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bob", "Bob"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bob", "Bob"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bob", "Bob"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bob", " "],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: [" ", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", "Loblaw"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Loblaw", " "],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: [" ", "Lobs"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Lobs", "Lobs"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Lobs", "Lobs"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Lobs", "Lobs"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Lobs", " "],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: [" ", "Law"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Law", "Law"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Law", "Law"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Law", " "],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: [" ", "Bomb"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bomb", "Bomb"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bomb", "Bomb"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "Bob Loblaw Lobs Law Bomb",
+ lines: ["Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"],
+ words: ["Bomb", "Bomb"],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "Bob Loblaw Lobs Law Bomb",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "Bob Loblaw Lobs Law Bomb",
+ "I love all of my children equally"],
+ words: ["Bomb", ""],
+ element: ["AXStaticText",
+ "Bob Loblaw Lobs Law Bomb",
+ "Bob Loblaw Lobs Law Bomb"] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["I", " "],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: [" ", "love"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["love", "love"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["love", "love"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["love", "love"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["love", " "],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: [" ", "all"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["all", "all"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["all", "all"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["all", " "],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: [" ", "of"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["of", "of"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["of", " "],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: [" ", "my"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["my", "my"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["my", " "],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "I love all of my ",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: [" ", "children"],
+ element: ["AXStaticText", "I love all of my ", "I love all of my "] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", "children"],
+ element: ["AXStaticText", "children", "children"] },
+ { style: "children",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["children", " "],
+ element: ["AXStaticText", "children", "children"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: [" ", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["equally", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["equally", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["equally", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["equally", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["equally", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "I love all of my children equally",
+ lines: ["I love all of my children equally",
+ "I love all of my children equally",
+ "I love all of my children equally"],
+ words: ["equally", "equally"],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: " equally",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "I love all of my children equally",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["equally", ""],
+ element: ["AXStaticText", " equally", " equally"] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["This", "This"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["This", "This"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["This", "This"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["This", " "],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "is"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["is", "is"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["is", " "],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "the"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["the", "the"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["the", "the"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["the", " "],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "This is the ",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "best"],
+ element: ["AXStaticText", "This is the ", "This is the "] },
+ { style: "best",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["best", "best"],
+ element: ["AXStaticText", "best", "best"] },
+ { style: "best",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["best", "best"],
+ element: ["AXStaticText", "best", "best"] },
+ { style: "best",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["best", "best"],
+ element: ["AXStaticText", "best", "best"] },
+ { style: "best",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["best", " "],
+ element: ["AXStaticText", "best", "best"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "free"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["free", "free"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["free", "free"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["free", "free"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["free", " "],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "scrapbooking"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: " free scr",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", " free scr", " free scr"] },
+ { style: "apbook",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", "apbook", "apbook"] },
+ { style: "apbook",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", "apbook", "apbook"] },
+ { style: "apbook",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", "apbook", "apbook"] },
+ { style: "apbook",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", "apbook", "apbook"] },
+ { style: "apbook",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", "apbook", "apbook"] },
+ { style: "apbook",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText", "apbook", "apbook"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", "scrapbooking"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["scrapbooking", " "],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "class"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["class", "class"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["class", "class"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["class", "class"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["class", "class"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["class", " "],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "I"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["I", " "],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "have"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["have", "have"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["have", "have"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["have", "have"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["have", " "],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "ever"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["ever", "ever"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["ever", "ever"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["ever", "ever"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["ever", " "],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: [" ", "taken"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["taken", "taken"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["taken", "taken"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["taken", "taken"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "This is the best free scrapbooking class I have ever taken",
+ lines: ["This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken",
+ "This is the best free scrapbooking class I have ever taken"],
+ words: ["taken", "taken"],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "ing class I have ever taken",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "This is the best free scrapbooking class I have ever taken",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["taken", ""],
+ element: ["AXStaticText",
+ "ing class I have ever taken",
+ "ing class I have ever taken"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["\u2022 Fried", "\u2022 Fried"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["\u2022 Fried", "\u2022 Fried"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["\u2022 Fried", "\u2022 Fried"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["\u2022 Fried", "\u2022 Fried"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["\u2022 Fried", " "],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: [" ", "cheese"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["cheese", "cheese"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["cheese", "cheese"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["cheese", "cheese"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["cheese", "cheese"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["cheese", "cheese"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["cheese", " "],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: [" ", "with"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["with", " "],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: [" ", "club"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["club", " "],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: [" ", "sauce"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Fried cheese with club sauce",
+ lines: ["\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Fried cheese with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Fried cheese with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["sauce", ""],
+ element: ["AXStaticText",
+ "Fried cheese with club sauce",
+ "\u2022 Fried cheese with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", "\u2022 Popcorn"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", "\u2022 Popcorn"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", "\u2022 Popcorn"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", "\u2022 Popcorn"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", "\u2022 Popcorn"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", "\u2022 Popcorn"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["\u2022 Popcorn", " "],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: [" ", "shrimp"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["shrimp", "shrimp"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["shrimp", "shrimp"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["shrimp", "shrimp"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["shrimp", "shrimp"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["shrimp", "shrimp"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["shrimp", " "],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: [" ", "with"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["with", " "],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: [" ", "club"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["club", " "],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: [" ", "sauce"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Popcorn shrimp with club sauce",
+ lines: ["\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Popcorn shrimp with club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Popcorn shrimp with club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["sauce", ""],
+ element: ["AXStaticText",
+ "Popcorn shrimp with club sauce",
+ "\u2022 Popcorn shrimp with club sauce"] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", "\u2022 Chicken"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", "\u2022 Chicken"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", "\u2022 Chicken"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", "\u2022 Chicken"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", "\u2022 Chicken"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", "\u2022 Chicken"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["\u2022 Chicken", " "],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: [" ", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", "fingers"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["fingers", " "],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: [" ", "with"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["with", "with"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["with", " "],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "\u2022 Chicken fingers with ",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: [" ", "spicy"],
+ element: ["AXStaticText",
+ "Chicken fingers with ",
+ "\u2022 Chicken fingers with "] },
+ { style: "spicy",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["spicy", "spicy"],
+ element: ["AXStaticText", "spicy", "spicy"] },
+ { style: "spicy",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["spicy", "spicy"],
+ element: ["AXStaticText", "spicy", "spicy"] },
+ { style: "spicy",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["spicy", "spicy"],
+ element: ["AXStaticText", "spicy", "spicy"] },
+ { style: "spicy",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["spicy", "spicy"],
+ element: ["AXStaticText", "spicy", "spicy"] },
+ { style: "spicy",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["spicy", " "],
+ element: ["AXStaticText", "spicy", "spicy"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: [" ", "club"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["club", "club"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["club", " "],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: [" ", "sauce"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "\u2022 Chicken fingers with spicy club sauce",
+ lines: ["\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "\u2022 Chicken fingers with spicy club sauce"],
+ words: ["sauce", "sauce"],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: " club sauce",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "\u2022 Chicken fingers with spicy club sauce",
+ "Do not order the Skip's Scramble"],
+ words: ["sauce", ""],
+ element: ["AXStaticText", " club sauce", " club sauce"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Do", "Do"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Do", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: [" ", "not"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["not", "not"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["not", "not"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["not", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: [" ", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", "order"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["order", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: [" ", "the"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["the", "the"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["the", "the"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["the", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? [" ", "Skip's"] : [" ", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["Skip'", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["Skip'", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["Skip'", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["Skip'", "Skip'"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? ["Skip's", "Skip's"] : ["Skip'", "s"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: useNewSegmenter && !stopAtPunctuation ? ["Skip's", " "] : ["s", " "],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: [" ", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "Do not order the Skip's Scramble",
+ lines: ["Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"],
+ words: ["Scramble", "Scramble"],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "Do not order the Skip's Scramble",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["These ",
+ "Do not order the Skip's Scramble",
+ "These "],
+ words: ["Scramble", ""],
+ element: ["AXStaticText",
+ "Do not order the Skip's Scramble",
+ "Do not order the Skip's Scramble"] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["These ", "These ", "These "],
+ words: ["These", "These"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["These ", "These ", "These "],
+ words: ["These", "These"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["These ", "These ", "These "],
+ words: ["These", "These"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["These ", "These ", "These "],
+ words: ["These", "These"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["These ", "These ", "These "],
+ words: ["These", " "],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["are ", "These ", "are "],
+ words: [" ", "are"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["are ", "are ", "are "],
+ words: ["are", "are"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["are ", "are ", "are "],
+ words: ["are", "are"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["are ", "are ", "are "],
+ words: ["are", " "],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["my ", "are ", "my "],
+ words: [" ", "my"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["my ", "my ", "my "],
+ words: ["my", "my"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["my ", "my ", "my "],
+ words: ["my", " "],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "my ", "awards, "],
+ words: [" ", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", "awards,"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["awards, ", "awards, ", "awards, "],
+ words: ["awards,", " "],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "awards, ", "Mother. "],
+ words: [" ", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", "Mother."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Mother. ", "Mother. ", "Mother. "],
+ words: ["Mother.", " "],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["From ", "Mother. ", "From "],
+ words: [" ", "From"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["From ", "From ", "From "],
+ words: ["From", "From"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["From ", "From ", "From "],
+ words: ["From", "From"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["From ", "From ", "From "],
+ words: ["From", "From"],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["From ", "From ", "From "],
+ words: ["From", " "],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Army.", "From ", "Army."],
+ words: [" ", "Army."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Army.", "Army.", "Army."],
+ words: ["Army.", "Army."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Army.", "Army.", "Army."],
+ words: ["Army.", "Army."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Army.", "Army.", "Army."],
+ words: ["Army.", "Army."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "These are my awards, Mother. From Army.",
+ lines: ["Army.", "Army.", "Army."],
+ words: ["Army.", "Army."],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "These are my awards, Mother. From Army.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "Army.", "I deceived you, mom."],
+ words: ["Army.", ""],
+ element: ["AXStaticText",
+ "These are my awards, Mother. From Army.",
+ "These are my awards, Mother. From Army."] },
+ { style: "I ",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: ["I", " "],
+ element: ["AXStaticText", "I ", "I "] },
+ { style: "I ",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXStaticText", "I ", "I "] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", "deceived"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["deceived", " "],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: [" ", "you"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["you", "you"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "deceived you",
+ lines: ["deceived you", "deceived you", "deceived you"],
+ words: ["you", "you"],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: "deceived you",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: ["", ""],
+ element: ["AXTextField", "deceived you", "deceived you"] },
+ { style: ", mom.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: [",", " "],
+ element: ["AXStaticText", ", mom.", ", mom."] },
+ { style: ", mom.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: [" ", "mom."],
+ element: ["AXStaticText", ", mom.", ", mom."] },
+ { style: ", mom.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: ["mom.", "mom."],
+ element: ["AXStaticText", ", mom.", ", mom."] },
+ { style: ", mom.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: ["mom.", "mom."],
+ element: ["AXStaticText", ", mom.", ", mom."] },
+ { style: ", mom.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: ["mom.", "mom."],
+ element: ["AXStaticText", ", mom.", ", mom."] },
+ { style: ", mom.",
+ paragraph: "I deceived you, mom.",
+ lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."],
+ words: ["mom.", ""],
+ element: ["AXStaticText", ", mom.", ", mom."] }];
+ }
+ </script>
+ </body>
+</html>
diff --git a/accessible/tests/browser/mac/doc_tree.xhtml b/accessible/tests/browser/mac/doc_tree.xhtml
new file mode 100644
index 0000000000..d043fa8923
--- /dev/null
+++ b/accessible/tests/browser/mac/doc_tree.xhtml
@@ -0,0 +1,59 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <tree id="tree" hidecolumnpicker="true">
+ <treecols>
+ <treecol primary="true" label="Groceries"/>
+ </treecols>
+ <treechildren id="internalTree">
+ <treeitem id="fruits" container="true" open="true">
+ <treerow>
+ <treecell label="Fruits"/>
+ </treerow>
+ <treechildren>
+ <treeitem id="apple">
+ <treerow>
+ <treecell label="Apple"/>
+ </treerow>
+ </treeitem>
+ <treeitem id="orange">
+ <treerow>
+ <treecell label="Orange"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </treeitem>
+ <treeitem id="veggies" container="true" open="true">
+ <treerow>
+ <treecell label="Veggies"/>
+ </treerow>
+ <treechildren>
+ <treeitem id="greenVeggies" container="true" open="true">
+ <treerow>
+ <treecell label="Green Veggies"/>
+ </treerow>
+ <treechildren>
+ <treeitem id="spinach">
+ <treerow>
+ <treecell label="Spinach"/>
+ </treerow>
+ </treeitem>
+ <treeitem id="peas">
+ <treerow>
+ <treecell label="Peas"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </treeitem>
+ <treeitem id="squash">
+ <treerow>
+ <treecell label="Squash"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </treeitem>
+ </treechildren>
+ </tree>
+</window>
diff --git a/accessible/tests/browser/mac/head.js b/accessible/tests/browser/mac/head.js
new file mode 100644
index 0000000000..f33f86288b
--- /dev/null
+++ b/accessible/tests/browser/mac/head.js
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* exported getNativeInterface, waitForMacEventWithInfo, waitForMacEvent, waitForStateChange,
+ NSRange, NSDictionary, stringForRange, AXTextStateChangeTypeEdit,
+ AXTextEditTypeDelete, AXTextEditTypeTyping, AXTextStateChangeTypeSelectionMove,
+ AXTextStateChangeTypeSelectionExtend, AXTextSelectionDirectionUnknown,
+ AXTextSelectionDirectionPrevious, AXTextSelectionDirectionNext,
+ AXTextSelectionDirectionDiscontiguous, AXTextSelectionGranularityUnknown,
+ AXTextSelectionDirectionBeginning, AXTextSelectionDirectionEnd,
+ AXTextSelectionGranularityCharacter, AXTextSelectionGranularityWord,
+ AXTextSelectionGranularityLine */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
+
+// AXTextStateChangeType enum values
+const AXTextStateChangeTypeEdit = 1;
+const AXTextStateChangeTypeSelectionMove = 2;
+const AXTextStateChangeTypeSelectionExtend = 3;
+
+// AXTextEditType enum values
+const AXTextEditTypeDelete = 1;
+const AXTextEditTypeTyping = 3;
+
+// AXTextSelectionDirection enum values
+const AXTextSelectionDirectionUnknown = 0;
+const AXTextSelectionDirectionBeginning = 1;
+const AXTextSelectionDirectionEnd = 2;
+const AXTextSelectionDirectionPrevious = 3;
+const AXTextSelectionDirectionNext = 4;
+const AXTextSelectionDirectionDiscontiguous = 5;
+
+// AXTextSelectionGranularity enum values
+const AXTextSelectionGranularityUnknown = 0;
+const AXTextSelectionGranularityCharacter = 1;
+const AXTextSelectionGranularityWord = 2;
+const AXTextSelectionGranularityLine = 3;
+
+function getNativeInterface(accDoc, id) {
+ return findAccessibleChildByID(accDoc, id).nativeInterface.QueryInterface(
+ Ci.nsIAccessibleMacInterface
+ );
+}
+
+function waitForMacEventWithInfo(notificationType, filter) {
+ let filterFunc = (macIface, data) => {
+ if (!filter) {
+ return true;
+ }
+
+ if (typeof filter == "function") {
+ return filter(macIface, data);
+ }
+
+ return macIface.getAttributeValue("AXDOMIdentifier") == filter;
+ };
+
+ return new Promise(resolve => {
+ let eventObserver = {
+ observe(subject, topic, data) {
+ let macEvent = subject.QueryInterface(Ci.nsIAccessibleMacEvent);
+ if (
+ data === notificationType &&
+ filterFunc(macEvent.macIface, macEvent.data)
+ ) {
+ Services.obs.removeObserver(this, "accessible-mac-event");
+ resolve(macEvent);
+ }
+ },
+ };
+ Services.obs.addObserver(eventObserver, "accessible-mac-event");
+ });
+}
+
+function waitForMacEvent(notificationType, filter) {
+ return waitForMacEventWithInfo(notificationType, filter).then(
+ e => e.macIface
+ );
+}
+
+function NSRange(location, length) {
+ return {
+ valueType: "NSRange",
+ value: [location, length],
+ };
+}
+
+function NSDictionary(dict) {
+ return {
+ objectType: "NSDictionary",
+ object: dict,
+ };
+}
+
+function stringForRange(macDoc, range) {
+ if (!range) {
+ return "";
+ }
+
+ let str = macDoc.getParameterizedAttributeValue(
+ "AXStringForTextMarkerRange",
+ range
+ );
+
+ let attrStr = macDoc.getParameterizedAttributeValue(
+ "AXAttributedStringForTextMarkerRange",
+ range
+ );
+
+ // This is a fly-by test to make sure our attributed strings
+ // always match our flat strings.
+ is(
+ attrStr.map(({ string }) => string).join(""),
+ str,
+ "attributed text matches non-attributed text"
+ );
+
+ return str;
+}
diff --git a/accessible/tests/browser/pivot/browser.toml b/accessible/tests/browser/pivot/browser.toml
new file mode 100644
index 0000000000..f6552741e5
--- /dev/null
+++ b/accessible/tests/browser/pivot/browser.toml
@@ -0,0 +1,10 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "!/accessible/tests/browser/shared-head.js",
+ "head.js",
+ "!/accessible/tests/mochitest/*.js",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_pivot.js"]
diff --git a/accessible/tests/browser/pivot/browser_pivot.js b/accessible/tests/browser/pivot/browser_pivot.js
new file mode 100644
index 0000000000..bd46ae4933
--- /dev/null
+++ b/accessible/tests/browser/pivot/browser_pivot.js
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Tests Pivot API
+ */
+addAccessibleTask(
+ `
+ <h1 id="heading-1-1">Main Title</h1>
+ <h2 id="heading-2-1" aria-hidden="true">First Section Title</h2>
+ <p id="paragraph-1">
+ Lorem ipsum <strong>dolor</strong> sit amet. Integer vitae urna
+ leo, id <a href="#">semper</a> nulla.
+ </p>
+ <h2 id="heading-2-2" aria-hidden="undefined">Second Section Title</h2>
+ <p id="paragraph-2" aria-hidden="">
+ Sed accumsan luctus lacus, vitae mollis arcu tristique vulputate.</p>
+ <p id="paragraph-3" aria-hidden="true">
+ <a href="#" id="hidden-link">Maybe</a> it was the other <i>George Michael</i>.
+ You know, the <a href="#">singer-songwriter</a>.
+ </p>
+ <p style="opacity: 0;" id="paragraph-4">
+ This is completely transparent
+ </p>
+ <iframe
+ src="data:text/html,<html><body>An <i>embedded</i> document.</body></html>">
+ </iframe>
+ <div id="hide-me">Hide me</div>
+ <p id="links" aria-hidden="false">
+ <a href="http://mozilla.org" title="Link 1 title">Link 1</a>
+ <a href="http://mozilla.org" title="Link 2 title">Link 2</a>
+ <a href="http://mozilla.org" title="Link 3 title">Link 3</a>
+ </p>
+ <ul>
+ <li>Hello<span> </span></li>
+ <li>World</li>
+ </ul>
+ `,
+ async function (browser, docAcc) {
+ let pivot = gAccService.createAccessiblePivot(docAcc);
+ testPivotSequence(pivot, HeadersTraversalRule, [
+ "heading-1-1",
+ "heading-2-2",
+ ]);
+
+ testPivotSequence(pivot, ObjectTraversalRule, [
+ "Main Title",
+ "Lorem ipsum ",
+ "dolor",
+ " sit amet. Integer vitae urna leo, id ",
+ "semper",
+ " nulla. ",
+ "Second Section Title",
+ "Sed accumsan luctus lacus, vitae mollis arcu tristique vulputate.",
+ "An ",
+ "embedded",
+ " document.",
+ "Hide me",
+ "Link 1",
+ "Link 2",
+ "Link 3",
+ "Hello",
+ "World",
+ ]);
+
+ let hideMeAcc = findAccessibleChildByID(docAcc, "hide-me");
+ let onHide = waitForEvent(EVENT_HIDE, hideMeAcc);
+ invokeContentTask(browser, [], () => {
+ content.document.getElementById("hide-me").remove();
+ });
+
+ await onHide;
+ testFailsWithNotInTree(
+ () => pivot.next(hideMeAcc, ObjectTraversalRule),
+ "moveNext from defunct accessible should fail"
+ );
+
+ let linksAcc = findAccessibleChildByID(docAcc, "links");
+
+ let removedRootPivot = gAccService.createAccessiblePivot(linksAcc);
+ onHide = waitForEvent(EVENT_HIDE, linksAcc);
+ invokeContentTask(browser, [], () => {
+ content.document.getElementById("links").remove();
+ });
+
+ await onHide;
+ testFailsWithNotInTree(
+ () => removedRootPivot.last(ObjectTraversalRule),
+ "moveLast with pivot with defunct root should fail"
+ );
+
+ let [x, y] = getBounds(findAccessibleChildByID(docAcc, "heading-1-1"));
+ let hitacc = pivot.atPoint(x + 1, y + 1, HeadersTraversalRule);
+ is(getIdOrName(hitacc), "heading-1-1", "Matching accessible at point");
+
+ hitacc = pivot.atPoint(x - 1, y - 1, HeadersTraversalRule);
+ ok(!hitacc, "No heading at given point");
+ },
+ { iframe: true, remoteIframe: true, topLevel: true, chrome: true }
+);
diff --git a/accessible/tests/browser/pivot/head.js b/accessible/tests/browser/pivot/head.js
new file mode 100644
index 0000000000..8389190a69
--- /dev/null
+++ b/accessible/tests/browser/pivot/head.js
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* exported HeadersTraversalRule, ObjectTraversalRule, testPivotSequence, testFailsWithNotInTree */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+/* import-globals-from ../../mochitest/layout.js */
+/* import-globals-from ../../mochitest/role.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "layout.js", dir: MOCHITESTS_DIR }
+);
+
+const FILTER_MATCH = nsIAccessibleTraversalRule.FILTER_MATCH;
+const FILTER_IGNORE = nsIAccessibleTraversalRule.FILTER_IGNORE;
+const FILTER_IGNORE_SUBTREE = nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE;
+
+const NS_ERROR_NOT_IN_TREE = 0x80780026;
+
+// //////////////////////////////////////////////////////////////////////////////
+// Traversal rules
+
+/**
+ * Rule object to traverse all focusable nodes and text nodes.
+ */
+const HeadersTraversalRule = {
+ match(acc) {
+ return acc.role == ROLE_HEADING ? FILTER_MATCH : FILTER_IGNORE;
+ },
+
+ QueryInterface: ChromeUtils.generateQI([nsIAccessibleTraversalRule]),
+};
+
+/**
+ * Traversal rule for all focusable nodes or leafs.
+ */
+const ObjectTraversalRule = {
+ match(acc) {
+ let [state, extstate] = getStates(acc);
+ if (state & STATE_INVISIBLE) {
+ return FILTER_IGNORE;
+ }
+
+ if ((extstate & EXT_STATE_OPAQUE) == 0) {
+ return FILTER_IGNORE | FILTER_IGNORE_SUBTREE;
+ }
+
+ let rv = FILTER_IGNORE;
+ let role = acc.role;
+ if (
+ hasState(acc, STATE_FOCUSABLE) &&
+ role != ROLE_DOCUMENT &&
+ role != ROLE_INTERNAL_FRAME
+ ) {
+ rv = FILTER_IGNORE_SUBTREE | FILTER_MATCH;
+ } else if (
+ acc.childCount == 0 &&
+ role != ROLE_LISTITEM_MARKER &&
+ acc.name.trim()
+ ) {
+ rv = FILTER_MATCH;
+ }
+
+ return rv;
+ },
+
+ QueryInterface: ChromeUtils.generateQI([nsIAccessibleTraversalRule]),
+};
+
+function getIdOrName(acc) {
+ let id = getAccessibleDOMNodeID(acc);
+ if (id) {
+ return id;
+ }
+ return acc.name;
+}
+
+function* pivotNextGenerator(pivot, rule) {
+ for (let acc = pivot.first(rule); acc; acc = pivot.next(acc, rule)) {
+ yield acc;
+ }
+}
+
+function* pivotPreviousGenerator(pivot, rule) {
+ for (let acc = pivot.last(rule); acc; acc = pivot.prev(acc, rule)) {
+ yield acc;
+ }
+}
+
+function testPivotSequence(pivot, rule, expectedSequence) {
+ is(
+ JSON.stringify([...pivotNextGenerator(pivot, rule)].map(getIdOrName)),
+ JSON.stringify(expectedSequence),
+ "Forward pivot sequence is correct"
+ );
+ is(
+ JSON.stringify([...pivotPreviousGenerator(pivot, rule)].map(getIdOrName)),
+ JSON.stringify([...expectedSequence].reverse()),
+ "Reverse pivot sequence is correct"
+ );
+}
+
+function testFailsWithNotInTree(func, msg) {
+ try {
+ func();
+ ok(false, msg);
+ } catch (x) {
+ is(x.result, NS_ERROR_NOT_IN_TREE, `Expecting NOT_IN_TREE: ${msg}`);
+ }
+}
diff --git a/accessible/tests/browser/python_runner_wsh.py b/accessible/tests/browser/python_runner_wsh.py
new file mode 100644
index 0000000000..488051240f
--- /dev/null
+++ b/accessible/tests/browser/python_runner_wsh.py
@@ -0,0 +1,88 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""A pywebsocket3 handler which runs arbitrary Python code and returns the
+result.
+This is used to test OS specific accessibility APIs which can't be tested in JS.
+It is intended to be called from JS browser tests.
+"""
+
+import json
+import os
+import sys
+import traceback
+
+from mod_pywebsocket import msgutil
+
+
+def web_socket_do_extra_handshake(request):
+ pass
+
+
+def web_socket_transfer_data(request):
+ def send(*args):
+ """Send a response to the client as a JSON array."""
+ msgutil.send_message(request, json.dumps(args))
+
+ cleanNamespace = {}
+ testDir = None
+ if sys.platform == "win32":
+ testDir = "windows"
+ elif sys.platform == "linux":
+ testDir = "atk"
+ if testDir:
+ sys.path.append(
+ os.path.join(
+ os.getcwd(), "browser", "accessible", "tests", "browser", testDir
+ )
+ )
+ try:
+ import a11y_setup
+
+ cleanNamespace = a11y_setup.__dict__
+ setupExc = None
+ except Exception:
+ setupExc = traceback.format_exc()
+ sys.path.pop()
+
+ def info(message):
+ """Log an info message."""
+ send("info", str(message))
+
+ cleanNamespace["info"] = info
+ namespace = cleanNamespace.copy()
+
+ # Keep handling messages until the WebSocket is closed.
+ while True:
+ code = msgutil.receive_message(request)
+ if not code:
+ return
+ if code == "__reset__":
+ namespace = cleanNamespace.copy()
+ continue
+
+ if setupExc:
+ # a11y_setup failed. Report an exception immediately.
+ send("exception", setupExc)
+ continue
+
+ # Wrap the code in a function called run(). This allows the code to
+ # return a result by simply using the return statement.
+ if "\n" not in code and not code.lstrip().startswith("return "):
+ # Single line without return. Assume this is an expression. We use
+ # a lambda to return the result.
+ code = f"run = lambda: {code}"
+ else:
+ lines = ["def run():"]
+ # Indent each line inside the function.
+ lines.extend(f" {line}" for line in code.splitlines())
+ code = "\n".join(lines)
+ try:
+ # Execute this Python code, which will define the run() function.
+ exec(code, namespace)
+ # Run the function we just defined.
+ ret = namespace["run"]()
+ send("return", ret)
+ except Exception:
+ send("exception", traceback.format_exc())
diff --git a/accessible/tests/browser/role/browser.toml b/accessible/tests/browser/role/browser.toml
new file mode 100644
index 0000000000..ac593eb0e6
--- /dev/null
+++ b/accessible/tests/browser/role/browser.toml
@@ -0,0 +1,15 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/browser/*.mjs",
+]
+prefs = [
+ "javascript.options.asyncstack_capture_debuggee_only=false",
+ "dom.element.popover.enabled=true"
+]
+
+["browser_computedARIARole.js"]
+["browser_minimumRole.js"]
diff --git a/accessible/tests/browser/role/browser_computedARIARole.js b/accessible/tests/browser/role/browser_computedARIARole.js
new file mode 100644
index 0000000000..50cfe43c98
--- /dev/null
+++ b/accessible/tests/browser/role/browser_computedARIARole.js
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+addAccessibleTask(
+ `
+<div id="ariaButton" role="button">ARIA button</div>
+<div id="ariaLog" role="log">ARIA log</div>
+<div id="ariaMain" role="main">ARIA main</div>
+<div id="ariaRegion" role="region" aria-label="ARIA region">ARIA region</div>
+<nav id="ariaUnnamedRegion" role="region">ARIA unnamed region</nav>
+<div id="ariaDirectory" role="directory">ARIA directory</div>
+<div id="ariaAlertdialog" role="alertdialog">ARIA alertdialog</div>
+<div id="ariaFeed" role="feed">ARIA feed</div>
+<div id="ariaRowgroup" role="rowgroup">ARIA rowgroup</div>
+<div id="ariaSearchbox" role="searchbox">ARIA searchbox</div>
+<div id="ariaUnknown" role="unknown">unknown ARIA role</div>
+<button id="htmlButton">HTML button</button>
+<button id="toggleButton" aria-pressed="true">toggle button</button>
+<main id="htmlMain">HTML main</main>
+<header id="htmlHeader">HTML header</header>
+<section id="htmlSection">
+ <header id="htmlSectionHeader">HTML header inside section</header>
+</section>
+<section id="htmlRegion" aria-label="HTML region">HTML region</section>
+<fieldset id="htmlFieldset">HTML fieldset</fieldset>
+<table>
+ <tbody id="htmlTbody" tabindex="-1"><tr><th>HTML tbody</th></tr></tbody>
+</table>
+<table role="grid">
+ <tr>
+ <td id="htmlGridcell">HTML implicit gridcell</td>
+ </tr>
+</table>
+<div id="htmlDiv">HTML div</div>
+<span id="htmlSpan" aria-label="HTML span">HTML span</span>
+<iframe id="iframe"></iframe>
+ `,
+ async function (browser, docAcc) {
+ function testComputedARIARole(id, role) {
+ const acc = findAccessibleChildByID(docAcc, id);
+ is(acc.computedARIARole, role, `computedARIARole for ${id} is correct`);
+ }
+
+ testComputedARIARole("ariaButton", "button");
+ testComputedARIARole("ariaLog", "log");
+ // Landmarks map to a single Gecko role.
+ testComputedARIARole("ariaMain", "main");
+ testComputedARIARole("ariaRegion", "region");
+ // Unnamed ARIA regions should ignore the ARIA role.
+ testComputedARIARole("ariaUnnamedRegion", "navigation");
+ // The directory ARIA role is an alias of list.
+ testComputedARIARole("ariaDirectory", "list");
+ // alertdialog, feed, rowgroup and searchbox map to a Gecko role, but it
+ // isn't unique.
+ testComputedARIARole("ariaAlertdialog", "alertdialog");
+ testComputedARIARole("ariaFeed", "feed");
+ testComputedARIARole("ariaRowgroup", "rowgroup");
+ testComputedARIARole("ariaSearchbox", "searchbox");
+ testComputedARIARole("ariaUnknown", "generic");
+ testComputedARIARole("htmlButton", "button");
+ // There is only a single ARIA role for buttons, but Gecko uses different
+ // roles depending on states.
+ testComputedARIARole("toggleButton", "button");
+ testComputedARIARole("htmlMain", "main");
+ testComputedARIARole("htmlHeader", "banner");
+ // <section> only maps to the region ARIA role if it has a label.
+ testComputedARIARole("htmlSection", "generic");
+ // <header> only maps to the banner role if it is not a child of a
+ // sectioning element.
+ testComputedARIARole("htmlSectionHeader", "generic");
+ testComputedARIARole("htmlRegion", "region");
+ // Gecko doesn't have a rowgroup role. Ensure we differentiate for
+ // computedARIARole.
+ testComputedARIARole("htmlFieldset", "group");
+ testComputedARIARole("htmlTbody", "rowgroup");
+ // <td> inside <table role="grid"> implicitly maps to ARIA gridcell.
+ testComputedARIARole("htmlGridcell", "gridcell");
+ // Test generics.
+ testComputedARIARole("htmlDiv", "generic");
+ testComputedARIARole("htmlSpan", "generic");
+ // Some roles can't be mapped to ARIA role tokens.
+ testComputedARIARole("iframe", "");
+ },
+ { chrome: true, topLevel: true }
+);
diff --git a/accessible/tests/browser/role/browser_minimumRole.js b/accessible/tests/browser/role/browser_minimumRole.js
new file mode 100644
index 0000000000..c02c35bc9c
--- /dev/null
+++ b/accessible/tests/browser/role/browser_minimumRole.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test that popover gets a minimum role.
+ */
+addAccessibleTask(
+ `
+<div id="generic" popover>generic</div>
+<div id="alert" role="alert" popover>alert</div>
+<blockquote id="blockquote" popover>blockquote</div>
+ `,
+ async function testPopover(browser, docAcc) {
+ let generic = findAccessibleChildByID(docAcc, "generic");
+ ok(!generic, "generic doesn't have an Accessible");
+ info("Showing generic");
+ let shown = waitForEvent(EVENT_SHOW, "generic");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("generic").showPopover();
+ });
+ generic = (await shown).accessible;
+ testRole(generic, ROLE_GROUPING, "generic has minimum role group");
+ info("Setting popover to null on generic");
+ // Setting popover to null causes the Accessible to be recreated.
+ shown = waitForEvent(EVENT_SHOW, "generic");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("generic").popover = null;
+ });
+ generic = (await shown).accessible;
+ testRole(generic, ROLE_SECTION, "generic has generic role");
+
+ let alert = findAccessibleChildByID(docAcc, "alert");
+ ok(!alert, "alert doesn't have an Accessible");
+ info("Showing alert");
+ shown = waitForEvent(EVENT_SHOW, "alert");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("alert").showPopover();
+ });
+ alert = (await shown).accessible;
+ testRole(alert, ROLE_ALERT, "alert has role alert");
+
+ let blockquote = findAccessibleChildByID(docAcc, "blockquote");
+ ok(!blockquote, "blockquote doesn't have an Accessible");
+ info("Showing blockquote");
+ shown = waitForEvent(EVENT_SHOW, "blockquote");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("blockquote").showPopover();
+ });
+ blockquote = (await shown).accessible;
+ testRole(blockquote, ROLE_BLOCKQUOTE, "blockquote has role blockquote");
+ },
+ { chrome: true, topLevel: true }
+);
diff --git a/accessible/tests/browser/role/head.js b/accessible/tests/browser/role/head.js
new file mode 100644
index 0000000000..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/role/head.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
diff --git a/accessible/tests/browser/scroll/browser.toml b/accessible/tests/browser/scroll/browser.toml
new file mode 100644
index 0000000000..ef637fe9a5
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser.toml
@@ -0,0 +1,19 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_scrollToPoint.js"]
+
+["browser_test_scrollTo.js"]
+
+["browser_test_scroll_bounds.js"]
+
+["browser_test_scroll_substring.js"]
+
+["browser_test_zoom_text.js"]
diff --git a/accessible/tests/browser/scroll/browser_scrollToPoint.js b/accessible/tests/browser/scroll/browser_scrollToPoint.js
new file mode 100644
index 0000000000..810478fa76
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser_scrollToPoint.js
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/layout.js */
+loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test nsIAccessible::scrollToPoint.
+ */
+addAccessibleTask(
+ `
+<hr style="height: 100vh;">
+<p id="p">hi</p>
+<hr style="height: 100vh;">
+ `,
+ async function (browser, docAcc) {
+ const [docX, docY] = getPos(docAcc);
+ const p = findAccessibleChildByID(docAcc, "p");
+ const [pX] = getPos(p);
+ info("Scrolling p");
+ let scrolled = waitForEvent(EVENT_SCROLLING_END, docAcc);
+ p.scrollToPoint(COORDTYPE_SCREEN_RELATIVE, docX, docY);
+ await scrolled;
+ // We can only scroll this vertically.
+ testPos(p, [pX, docY]);
+ },
+ { chrome: true, topLevel: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/scroll/browser_test_scrollTo.js b/accessible/tests/browser/scroll/browser_test_scrollTo.js
new file mode 100644
index 0000000000..c007c62d82
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser_test_scrollTo.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function getCenterY(acc) {
+ const y = {};
+ const h = {};
+ acc.getBounds({}, y, {}, h);
+ return y.value + h.value / 2;
+}
+
+/**
+ * Test nsIAccessible::scrollTo.
+ */
+addAccessibleTask(
+ `
+<div id="scroller" style="height: 100vh; overflow: scroll;">
+ <hr style="height: 100vh;">
+ <p id="p1" style="height: 10vh;">a</p>
+ <hr style="height: 100vh;">
+ <p id="p2" style="height: 10vh;">b</p>
+ <hr style="height: 100vh;">
+</div>
+ `,
+ async function (browser, docAcc) {
+ const scroller = findAccessibleChildByID(docAcc, "scroller");
+ const scrollerY = getCenterY(scroller);
+ // scroller can only show one of p1 or p2, not both.
+ const p1 = findAccessibleChildByID(docAcc, "p1");
+ info("scrollTo p1");
+ let scrolled = waitForEvent(
+ nsIAccessibleEvent.EVENT_SCROLLING_END,
+ scroller
+ );
+ p1.scrollTo(SCROLL_TYPE_ANYWHERE);
+ await scrolled;
+ isWithin(getCenterY(p1), scrollerY, 10, "p1 scrolled to center");
+ const p2 = findAccessibleChildByID(docAcc, "p2");
+ info("scrollTo p2");
+ scrolled = waitForEvent(nsIAccessibleEvent.EVENT_SCROLLING_END, scroller);
+ p2.scrollTo(SCROLL_TYPE_ANYWHERE);
+ await scrolled;
+ isWithin(getCenterY(p2), scrollerY, 10, "p2 scrolled to center");
+ info("scrollTo p1");
+ scrolled = waitForEvent(nsIAccessibleEvent.EVENT_SCROLLING_END, scroller);
+ p1.scrollTo(SCROLL_TYPE_ANYWHERE);
+ await scrolled;
+ isWithin(getCenterY(p1), scrollerY, 10, "p1 scrolled to center");
+ },
+ { topLevel: true, iframe: true, remoteIframe: true, chrome: true }
+);
diff --git a/accessible/tests/browser/scroll/browser_test_scroll_bounds.js b/accessible/tests/browser/scroll/browser_test_scroll_bounds.js
new file mode 100644
index 0000000000..31de002cda
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser_test_scroll_bounds.js
@@ -0,0 +1,662 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts(
+ { name: "layout.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR }
+);
+requestLongerTimeout(2);
+
+const appUnitsPerDevPixel = 60;
+
+function testCachedScrollPosition(
+ acc,
+ expectedX,
+ expectedY,
+ shouldBeEmpty = false
+) {
+ let cachedPosition = "";
+ try {
+ cachedPosition = acc.cache.getStringProperty("scroll-position");
+ } catch (e) {
+ info("Cache was not populated");
+ // If the key doesn't exist, this frame is not scrollable.
+ return shouldBeEmpty;
+ }
+
+ // The value we retrieve from the cache is in app units, but the values
+ // passed in are in pixels. Since the retrieved value is a string,
+ // and harder to modify, adjust our expected x and y values to match its units.
+ return (
+ cachedPosition ==
+ `${expectedX * appUnitsPerDevPixel}, ${expectedY * appUnitsPerDevPixel}`
+ );
+}
+
+function getCachedBounds(acc) {
+ let cachedBounds = "";
+ try {
+ cachedBounds = acc.cache.getStringProperty("relative-bounds");
+ } catch (e) {
+ ok(false, "Unable to fetch cached bounds from cache!");
+ }
+ return cachedBounds;
+}
+
+/**
+ * Test bounds of accessibles after scrolling
+ */
+addAccessibleTask(
+ `
+ <div id='square' style='height:100px; width:100px; background:green; margin-top:3000px; margin-bottom:4000px;'>
+ </div>
+
+ <div id='rect' style='height:40px; width:200px; background:blue; margin-bottom:3400px'>
+ </div>
+ `,
+ async function (browser, docAcc) {
+ ok(docAcc, "iframe document acc is present");
+ await testBoundsWithContent(docAcc, "square", browser);
+ await testBoundsWithContent(docAcc, "rect", browser);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("square").scrollIntoView();
+ });
+
+ await waitForContentPaint(browser);
+
+ await testBoundsWithContent(docAcc, "square", browser);
+ await testBoundsWithContent(docAcc, "rect", browser);
+
+ // Scroll rect into view, but also make it reflow so we can be sure the
+ // bounds are correct for reflowed frames.
+ await invokeContentTask(browser, [], () => {
+ const rect = content.document.getElementById("rect");
+ rect.scrollIntoView();
+ rect.style.width = "300px";
+ rect.offsetTop; // Flush layout.
+ rect.style.width = "200px";
+ rect.offsetTop; // Flush layout.
+ });
+
+ await waitForContentPaint(browser);
+ await testBoundsWithContent(docAcc, "square", browser);
+ await testBoundsWithContent(docAcc, "rect", browser);
+ },
+ { iframe: true, remoteIframe: true, chrome: true }
+);
+
+/**
+ * Test scroll offset on cached accessibles
+ */
+addAccessibleTask(
+ `
+ <div id='square' style='height:100px; width:100px; background:green; margin-top:3000px; margin-bottom:4000px;'>
+ </div>
+
+ <div id='rect' style='height:40px; width:200px; background:blue; margin-bottom:3400px'>
+ </div>
+ `,
+ async function (browser, docAcc) {
+ ok(docAcc, "iframe document acc is present");
+ await untilCacheOk(
+ () => testCachedScrollPosition(docAcc, 0, 0),
+ "Correct initial scroll position."
+ );
+ const rectAcc = findAccessibleChildByID(docAcc, "rect");
+ const rectInitialBounds = getCachedBounds(rectAcc);
+
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("square").scrollIntoView();
+ });
+
+ await waitForContentPaint(browser);
+
+ // The only content to scroll over is `square`'s top margin
+ // so our scroll offset here should be 3000px
+ await untilCacheOk(
+ () => testCachedScrollPosition(docAcc, 0, 3000),
+ "Correct scroll position after first scroll."
+ );
+
+ // Scroll rect into view, but also make it reflow so we can be sure the
+ // bounds are correct for reflowed frames.
+ await invokeContentTask(browser, [], () => {
+ const rect = content.document.getElementById("rect");
+ rect.scrollIntoView();
+ rect.style.width = "300px";
+ rect.offsetTop;
+ rect.style.width = "200px";
+ });
+
+ await waitForContentPaint(browser);
+ // We have to scroll over `square`'s top margin (3000px),
+ // `square` itself (100px), and `square`'s bottom margin (4000px).
+ // This should give us a 7100px offset.
+ await untilCacheOk(
+ () => testCachedScrollPosition(docAcc, 0, 7100),
+ "Correct final scroll position."
+ );
+ await untilCacheIs(
+ () => getCachedBounds(rectAcc),
+ rectInitialBounds,
+ "Cached relative bounds don't change when scrolling"
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test scroll offset fixed-pos acc accs
+ */
+addAccessibleTask(
+ `
+ <div style="margin-top: 100px; margin-left: 75px; border: 1px solid;">
+ <div id="d" style="position:fixed;">
+ <button id="top">top</button>
+ </div>
+ </div>
+ `,
+ async function (browser, docAcc) {
+ const origTopBounds = await testBoundsWithContent(docAcc, "top", browser);
+ const origDBounds = await testBoundsWithContent(docAcc, "d", browser);
+ const e = waitForEvent(EVENT_REORDER, docAcc);
+ await invokeContentTask(browser, [], () => {
+ for (let i = 0; i < 1000; ++i) {
+ const div = content.document.createElement("div");
+ div.innerHTML = "<button>${i}</button>";
+ content.document.body.append(div);
+ }
+ });
+ await e;
+
+ await invokeContentTask(browser, [], () => {
+ // scroll to the bottom of the page
+ content.window.scrollTo(0, content.document.body.scrollHeight);
+ });
+
+ await waitForContentPaint(browser);
+
+ let newTopBounds = await testBoundsWithContent(docAcc, "top", browser);
+ let newDBounds = await testBoundsWithContent(docAcc, "d", browser);
+ is(
+ origTopBounds[0],
+ newTopBounds[0],
+ "x of fixed elem is unaffected by scrolling"
+ );
+ is(
+ origTopBounds[1],
+ newTopBounds[1],
+ "y of fixed elem is unaffected by scrolling"
+ );
+ is(
+ origTopBounds[2],
+ newTopBounds[2],
+ "width of fixed elem is unaffected by scrolling"
+ );
+ is(
+ origTopBounds[3],
+ newTopBounds[3],
+ "height of fixed elem is unaffected by scrolling"
+ );
+ is(
+ origDBounds[0],
+ newTopBounds[0],
+ "x of fixed elem container is unaffected by scrolling"
+ );
+ is(
+ origDBounds[1],
+ newDBounds[1],
+ "y of fixed elem container is unaffected by scrolling"
+ );
+ is(
+ origDBounds[2],
+ newDBounds[2],
+ "width of fixed container elem is unaffected by scrolling"
+ );
+ is(
+ origDBounds[3],
+ newDBounds[3],
+ "height of fixed container elem is unaffected by scrolling"
+ );
+
+ await invokeContentTask(browser, [], () => {
+ // remove position styling
+ content.document.getElementById("d").style = "";
+ });
+
+ await waitForContentPaint(browser);
+
+ newTopBounds = await testBoundsWithContent(docAcc, "top", browser);
+ newDBounds = await testBoundsWithContent(docAcc, "d", browser);
+ is(
+ origTopBounds[0],
+ newTopBounds[0],
+ "x of non-fixed element remains accurate."
+ );
+ ok(newTopBounds[1] < 0, "y coordinate shows item scrolled off page");
+ is(
+ origTopBounds[2],
+ newTopBounds[2],
+ "width of non-fixed element remains accurate."
+ );
+ is(
+ origTopBounds[3],
+ newTopBounds[3],
+ "height of non-fixed element remains accurate."
+ );
+ is(
+ origDBounds[0],
+ newDBounds[0],
+ "x of non-fixed container element remains accurate."
+ );
+ ok(newDBounds[1] < 0, "y coordinate shows container scrolled off page");
+ // Removing the position styling on this acc causes it to be bound by
+ // its parent's bounding box, which alters its width as a block element.
+ // We don't particularly care about width in this test, so skip it.
+ is(
+ origDBounds[3],
+ newDBounds[3],
+ "height of non-fixed container element remains accurate."
+ );
+
+ await invokeContentTask(browser, [], () => {
+ // re-add position styling
+ content.document.getElementById("d").style = "position:fixed;";
+ });
+
+ await waitForContentPaint(browser);
+
+ newTopBounds = await testBoundsWithContent(docAcc, "top", browser);
+ newDBounds = await testBoundsWithContent(docAcc, "d", browser);
+ is(
+ origTopBounds[0],
+ newTopBounds[0],
+ "x correct when position:fixed is added."
+ );
+ is(
+ origTopBounds[1],
+ newTopBounds[1],
+ "y correct when position:fixed is added."
+ );
+ is(
+ origTopBounds[2],
+ newTopBounds[2],
+ "width correct when position:fixed is added."
+ );
+ is(
+ origTopBounds[3],
+ newTopBounds[3],
+ "height correct when position:fixed is added."
+ );
+ is(
+ origDBounds[0],
+ newDBounds[0],
+ "x of container correct when position:fixed is added."
+ );
+ is(
+ origDBounds[1],
+ newDBounds[1],
+ "y of container correct when position:fixed is added."
+ );
+ is(
+ origDBounds[2],
+ newDBounds[2],
+ "width of container correct when position:fixed is added."
+ );
+ is(
+ origDBounds[3],
+ newDBounds[3],
+ "height of container correct when position:fixed is added."
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test position: fixed for containers that would otherwise be pruned from the
+ * a11y tree.
+ */
+addAccessibleTask(
+ `
+<table id="fixed" role="presentation" style="position: fixed;">
+ <tr><th>fixed</th></tr>
+</table>
+<div id="mutate" role="presentation">mutate</div>
+<hr style="height: 200vh;">
+<p>bottom</p>
+ `,
+ async function (browser, docAcc) {
+ const fixed = findAccessibleChildByID(docAcc, "fixed");
+ ok(fixed, "fixed is accessible");
+ isnot(fixed.role, ROLE_TABLE, "fixed doesn't have ROLE_TABLE");
+ ok(!findAccessibleChildByID(docAcc, "mutate"), "mutate inaccessible");
+ info("Setting position: fixed on mutate");
+ let shown = waitForEvent(EVENT_SHOW, "mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").style.position = "fixed";
+ });
+ await shown;
+ const origFixedBounds = await testBoundsWithContent(
+ docAcc,
+ "fixed",
+ browser
+ );
+ const origMutateBounds = await testBoundsWithContent(
+ docAcc,
+ "mutate",
+ browser
+ );
+ info("Scrolling to bottom of page");
+ await invokeContentTask(browser, [], () => {
+ content.window.scrollTo(0, content.document.body.scrollHeight);
+ });
+ await waitForContentPaint(browser);
+ const newFixedBounds = await testBoundsWithContent(
+ docAcc,
+ "fixed",
+ browser
+ );
+ Assert.deepEqual(
+ newFixedBounds,
+ origFixedBounds,
+ "fixed bounds are unchanged"
+ );
+ const newMutateBounds = await testBoundsWithContent(
+ docAcc,
+ "mutate",
+ browser
+ );
+ Assert.deepEqual(
+ newMutateBounds,
+ origMutateBounds,
+ "mutate bounds are unchanged"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test scroll offset on sticky-pos acc
+ */
+addAccessibleTask(
+ `
+ <div id="d" style="margin-top: 100px; margin-left: 75px; position:sticky; top:0px;">
+ <button id="top">top</button>
+ </div>
+ `,
+ async function (browser, docAcc) {
+ const containerBounds = await testBoundsWithContent(docAcc, "d", browser);
+ const e = waitForEvent(EVENT_REORDER, docAcc);
+ await invokeContentTask(browser, [], () => {
+ for (let i = 0; i < 1000; ++i) {
+ const div = content.document.createElement("div");
+ div.innerHTML = "<button>${i}</button>";
+ content.document.body.append(div);
+ }
+ });
+ await e;
+ for (let id of ["d", "top"]) {
+ info(`Verifying bounds for acc with ID ${id}`);
+ const origBounds = await testBoundsWithContent(docAcc, id, browser);
+
+ info("Scrolling partially");
+ await invokeContentTask(browser, [], () => {
+ // scroll some of the window
+ content.window.scrollTo(0, 50);
+ });
+
+ await waitForContentPaint(browser);
+
+ let newBounds = await testBoundsWithContent(docAcc, id, browser);
+ is(
+ origBounds[0],
+ newBounds[0],
+ `x coord of sticky element is unaffected by scrolling`
+ );
+ ok(
+ origBounds[1] > newBounds[1] && newBounds[1] >= 0,
+ "sticky element scrolled, but not off the page"
+ );
+ is(
+ origBounds[2],
+ newBounds[2],
+ `width of sticky element is unaffected by scrolling`
+ );
+ is(
+ origBounds[3],
+ newBounds[3],
+ `height of sticky element is unaffected by scrolling`
+ );
+
+ info("Scrolling to bottom");
+ await invokeContentTask(browser, [], () => {
+ // scroll to the bottom of the page
+ content.window.scrollTo(0, content.document.body.scrollHeight);
+ });
+
+ await waitForContentPaint(browser);
+
+ newBounds = await testBoundsWithContent(docAcc, id, browser);
+ is(
+ origBounds[0],
+ newBounds[0],
+ `x coord of sticky element is unaffected by scrolling`
+ );
+ // Subtract margin from container screen coords to get chrome height
+ // which is where our y pos should be
+ is(
+ newBounds[1],
+ containerBounds[1] - 100,
+ "Sticky element is top of screen"
+ );
+ is(
+ origBounds[2],
+ newBounds[2],
+ `width of sticky element is unaffected by scrolling`
+ );
+ is(
+ origBounds[3],
+ newBounds[3],
+ `height of sticky element is unaffected by scrolling`
+ );
+
+ info("Removing position style on container");
+ await invokeContentTask(browser, [], () => {
+ // remove position styling
+ content.document.getElementById("d").style =
+ "margin-top: 100px; margin-left: 75px;";
+ });
+
+ await waitForContentPaint(browser);
+
+ newBounds = await testBoundsWithContent(docAcc, id, browser);
+
+ is(
+ origBounds[0],
+ newBounds[0],
+ `x coord of non-sticky element remains accurate.`
+ );
+ ok(newBounds[1] < 0, "y coordinate shows item scrolled off page");
+
+ // Removing the position styling on this acc causes it to be bound by
+ // its parent's bounding box, which alters its width as a block element.
+ // We don't particularly care about width in this test, so skip it.
+ is(
+ origBounds[3],
+ newBounds[3],
+ `height of non-sticky element remains accurate.`
+ );
+
+ info("Adding position style on container");
+ await invokeContentTask(browser, [], () => {
+ // re-add position styling
+ content.document.getElementById("d").style =
+ "margin-top: 100px; margin-left: 75px; position:sticky; top:0px;";
+ });
+
+ await waitForContentPaint(browser);
+
+ newBounds = await testBoundsWithContent(docAcc, id, browser);
+ is(
+ origBounds[0],
+ newBounds[0],
+ `x coord of sticky element is unaffected by scrolling`
+ );
+ is(
+ newBounds[1],
+ containerBounds[1] - 100,
+ "Sticky element is top of screen"
+ );
+ is(
+ origBounds[2],
+ newBounds[2],
+ `width of sticky element is unaffected by scrolling`
+ );
+ is(
+ origBounds[3],
+ newBounds[3],
+ `height of sticky element is unaffected by scrolling`
+ );
+
+ info("Scrolling back up to test next ID");
+ await invokeContentTask(browser, [], () => {
+ // scroll some of the window
+ content.window.scrollTo(0, 0);
+ });
+ }
+ },
+ { chrome: false, iframe: false, remoteIframe: false }
+);
+
+/**
+ * Test position: sticky for containers that would otherwise be pruned from the
+ * a11y tree.
+ */
+addAccessibleTask(
+ `
+<hr style="height: 100vh;">
+<div id="stickyContainer">
+ <div id="sticky" role="presentation" style="position: sticky; top: 0px;">sticky</div>
+ <hr style="height: 100vh;">
+ <p id="stickyEnd">stickyEnd</p>
+</div>
+<div id="mutateContainer">
+ <div id="mutate" role="presentation" style="top: 0px;">mutate</div>
+ <hr style="height: 100vh;">
+ <p id="mutateEnd">mutateEnd</p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ ok(findAccessibleChildByID(docAcc, "sticky"), "sticky is accessible");
+ info("Scrolling to sticky");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("sticky").scrollIntoView();
+ });
+ await waitForContentPaint(browser);
+ const origStickyBounds = await testBoundsWithContent(
+ docAcc,
+ "sticky",
+ browser
+ );
+ info("Scrolling to stickyEnd");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("stickyEnd").scrollIntoView();
+ });
+ await waitForContentPaint(browser);
+ const newStickyBounds = await testBoundsWithContent(
+ docAcc,
+ "sticky",
+ browser
+ );
+ Assert.deepEqual(
+ newStickyBounds,
+ origStickyBounds,
+ "sticky bounds are unchanged"
+ );
+
+ ok(!findAccessibleChildByID(docAcc, "mutate"), "mutate inaccessible");
+ info("Setting position: sticky on mutate");
+ let shown = waitForEvent(EVENT_SHOW, "mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").style.position = "sticky";
+ });
+ await shown;
+ info("Scrolling to mutate");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutate").scrollIntoView();
+ });
+ await waitForContentPaint(browser);
+ const origMutateBounds = await testBoundsWithContent(
+ docAcc,
+ "mutate",
+ browser
+ );
+ info("Scrolling to mutateEnd");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("mutateEnd").scrollIntoView();
+ });
+ await waitForContentPaint(browser);
+ const newMutateBounds = await testBoundsWithContent(
+ docAcc,
+ "mutate",
+ browser
+ );
+ assertBoundsFuzzyEqual(newMutateBounds, origMutateBounds);
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test scroll offset on non-scrollable accs
+ */
+addAccessibleTask(
+ `
+ <div id='square' style='height:100px; width: 100px; background:green;'>hello world
+ </div>
+ `,
+ async function (browser, docAcc) {
+ const square = findAccessibleChildByID(docAcc, "square");
+ await untilCacheOk(
+ () => testCachedScrollPosition(square, 0, 0, true),
+ "Square is not scrollable."
+ );
+
+ info("Adding more text content to square");
+ await invokeContentTask(browser, [], () => {
+ const s = content.document.getElementById("square");
+ s.textContent =
+ "hello world I am some text and I should overflow this container because I am very long";
+ s.offsetTop; // Flush layout.
+ });
+
+ await waitForContentPaint(browser);
+
+ await untilCacheOk(
+ () => testCachedScrollPosition(square, 0, 0, true),
+ "Square is not scrollable (still has overflow:visible)."
+ );
+
+ info("Adding overflow:auto; styling");
+ await invokeContentTask(browser, [], () => {
+ const s = content.document.getElementById("square");
+ s.setAttribute(
+ "style",
+ "overflow:auto; height:100px; width: 100px; background:green;"
+ );
+ s.offsetTop; // Flush layout.
+ });
+
+ await waitForContentPaint(browser);
+
+ await untilCacheOk(
+ () => testCachedScrollPosition(square, 0, 0),
+ "Square is scrollable."
+ );
+ },
+ { iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/scroll/browser_test_scroll_substring.js b/accessible/tests/browser/scroll/browser_test_scroll_substring.js
new file mode 100644
index 0000000000..e8426d00ca
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser_test_scroll_substring.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/layout.js */
+loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test nsIAccessibleText::scrollSubstringTo.
+ */
+addAccessibleTask(
+ `
+ <style>
+ @font-face {
+ font-family: Ahem;
+ src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
+ }
+ pre {
+ font: 20px/20px Ahem;
+ height: 40px;
+ overflow-y: scroll;
+ }
+ </style>
+ <pre id="text">
+
+
+
+
+
+It's a jetpack, Michael. What could possibly go wrong?
+
+
+
+
+
+The only thing I found in the fridge was a dead dove in a bag.
+</pre>`,
+ async function (browser, docAcc) {
+ let text = findAccessibleChildByID(docAcc, "text", [nsIAccessibleText]);
+ let [, containerY, , containerHeight] = getBounds(text);
+ let getCharY = () => {
+ let objY = {};
+ text.getCharacterExtents(7, {}, objY, {}, {}, COORDTYPE_SCREEN_RELATIVE);
+ return objY.value;
+ };
+ ok(
+ containerHeight < getCharY(),
+ "Character is outside of container bounds"
+ );
+ text.scrollSubstringTo(7, 8, SCROLL_TYPE_TOP_EDGE);
+
+ await waitForContentPaint(browser);
+ await untilCacheIs(
+ getCharY,
+ containerY,
+ "Character is scrolled to top of container"
+ );
+ },
+ {
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ chrome: true,
+ }
+);
diff --git a/accessible/tests/browser/scroll/browser_test_zoom_text.js b/accessible/tests/browser/scroll/browser_test_zoom_text.js
new file mode 100644
index 0000000000..4fc0a56b43
--- /dev/null
+++ b/accessible/tests/browser/scroll/browser_test_zoom_text.js
@@ -0,0 +1,145 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/layout.js */
+loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR });
+
+async function runTests(browser, accDoc) {
+ await loadContentScripts(browser, {
+ script: "Layout.sys.mjs",
+ symbol: "Layout",
+ });
+
+ let paragraph = findAccessibleChildByID(accDoc, "paragraph", [
+ nsIAccessibleText,
+ ]);
+ let offset = 64; // beginning of 4th stanza
+
+ let [x /* ,y*/] = getPos(paragraph);
+ let [docX, docY] = getPos(accDoc);
+
+ paragraph.scrollSubstringToPoint(
+ offset,
+ offset,
+ COORDTYPE_SCREEN_RELATIVE,
+ docX,
+ docY
+ );
+
+ await waitForContentPaint(browser);
+ testTextPos(paragraph, offset, [x, docY], COORDTYPE_SCREEN_RELATIVE);
+
+ await SpecialPowers.spawn(browser, [], () => {
+ content.Layout.zoomDocument(content.document, 2.0);
+ });
+
+ paragraph = findAccessibleChildByID(accDoc, "paragraph2", [
+ nsIAccessibleText,
+ ]);
+ offset = 52; // // beginning of 4th stanza
+ [x /* ,y*/] = getPos(paragraph);
+ paragraph.scrollSubstringToPoint(
+ offset,
+ offset,
+ COORDTYPE_SCREEN_RELATIVE,
+ docX,
+ docY
+ );
+
+ await waitForContentPaint(browser);
+ testTextPos(paragraph, offset, [x, docY], COORDTYPE_SCREEN_RELATIVE);
+}
+
+/**
+ * Test caching of accessible object states
+ */
+addAccessibleTask(
+ `
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br><hr>
+ <p id='paragraph'>
+ Пошел котик на торжок<br>
+ Купил котик пирожок<br>
+ Пошел котик на улочку<br>
+ Купил котик булочку<br>
+ </p>
+ <hr><br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br><hr>
+ <p id='paragraph2'>
+ Самому ли съесть<br>
+ Либо Сашеньке снесть<br>
+ Я и сам укушу<br>
+ Я и Сашеньке снесу<br>
+ </p>
+ <hr><br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>`,
+ runTests
+);
diff --git a/accessible/tests/browser/scroll/head.js b/accessible/tests/browser/scroll/head.js
new file mode 100644
index 0000000000..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/scroll/head.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
diff --git a/accessible/tests/browser/selectable/browser.toml b/accessible/tests/browser/selectable/browser.toml
new file mode 100644
index 0000000000..38a13c25f4
--- /dev/null
+++ b/accessible/tests/browser/selectable/browser.toml
@@ -0,0 +1,13 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_test_aria_select.js"]
+
+["browser_test_select.js"]
diff --git a/accessible/tests/browser/selectable/browser_test_aria_select.js b/accessible/tests/browser/selectable/browser_test_aria_select.js
new file mode 100644
index 0000000000..f52603d1cb
--- /dev/null
+++ b/accessible/tests/browser/selectable/browser_test_aria_select.js
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/selectable.js */
+
+// ////////////////////////////////////////////////////////////////////////
+// role="tablist" role="listbox" role="grid" role="tree" role="treegrid"
+addAccessibleTask(
+ `<div role="tablist" id="tablist">
+ <div role="tab">tab1</div>
+ <div role="tab">tab2</div>
+ </div>
+ <div role="listbox" id="listbox">
+ <div role="option">item1</div>
+ <div role="option">item2</div>
+ </div>
+ <div role="grid" id="grid">
+ <div role="row">
+ <span role="gridcell">cell</span>
+ <span role="gridcell">cell</span>
+ </div>
+ <div role="row">
+ <span role="gridcell">cell</span>
+ <span role="gridcell">cell</span>
+ </div>
+ </div>
+ <div role="tree" id="tree">
+ <div role="treeitem">
+ item1
+ <div role="group">
+ <div role="treeitem">item1.1</div>
+ </div>
+ </div>
+ <div>item2</div>
+ </div>
+ <div role="treegrid" id="treegrid">
+ <div role="row" aria-level="1">
+ <span role="gridcell">cell</span>
+ <span role="gridcell">cell</span>
+ </div>
+ <div role="row" aria-level="2">
+ <span role="gridcell">cell</span>
+ <span role="gridcell">cell</span>
+ </div>
+ <div role="row" aria-level="1">
+ <span role="gridcell">cell</span>
+ <span role="gridcell">cell</span>
+ </div>
+ </div>`,
+ async function (browser, docAcc) {
+ info(
+ 'role="tablist" role="listbox" role="grid" role="tree" role="treegrid"'
+ );
+ testSelectableSelection(findAccessibleChildByID(docAcc, "tablist"), []);
+ testSelectableSelection(findAccessibleChildByID(docAcc, "listbox"), []);
+ testSelectableSelection(findAccessibleChildByID(docAcc, "grid"), []);
+ testSelectableSelection(findAccessibleChildByID(docAcc, "tree"), []);
+ testSelectableSelection(findAccessibleChildByID(docAcc, "treegrid"), []);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// role="tablist" aria-multiselectable
+addAccessibleTask(
+ `<div role="tablist" id="tablist" aria-multiselectable="true">
+ <div role="tab" id="tab_multi1">tab1</div>
+ <div role="tab" id="tab_multi2">tab2</div>
+ </div>`,
+ async function (browser, docAcc) {
+ info('role="tablist" aria-multiselectable');
+ let tablist = findAccessibleChildByID(docAcc, "tablist", [
+ nsIAccessibleSelectable,
+ ]);
+
+ await testMultiSelectable(tablist, ["tab_multi1", "tab_multi2"]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// role="listbox" aria-multiselectable
+addAccessibleTask(
+ `<div role="listbox" id="listbox" aria-multiselectable="true">
+ <div role="option" id="listbox2_item1">item1</div>
+ <div role="option" id="listbox2_item2">item2</div>
+ </div>`,
+ async function (browser, docAcc) {
+ info('role="listbox" aria-multiselectable');
+ let listbox = findAccessibleChildByID(docAcc, "listbox", [
+ nsIAccessibleSelectable,
+ ]);
+
+ await testMultiSelectable(listbox, ["listbox2_item1", "listbox2_item2"]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// role="grid" aria-multiselectable, selectable children in subtree
+addAccessibleTask(
+ `<table tabindex="0" border="2" cellspacing="0" id="grid" role="grid"
+ aria-multiselectable="true">
+ <thead>
+ <tr>
+ <th tabindex="-1" role="columnheader" id="grid_colhead1"
+ style="width:6em">Entry #</th>
+ <th tabindex="-1" role="columnheader" id="grid_colhead2"
+ style="width:10em">Date</th>
+ <th tabindex="-1" role="columnheader" id="grid_colhead3"
+ style="width:20em">Expense</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td tabindex="-1" role="rowheader" id="grid_rowhead"
+ aria-readonly="true">1</td>
+ <td tabindex="-1" role="gridcell" id="grid_cell1"
+ aria-selected="false">03/14/05</td>
+ <td tabindex="-1" role="gridcell" id="grid_cell2"
+ aria-selected="false">Conference Fee</td>
+ </tr>
+ </tobdy>
+ </table>`,
+ async function (browser, docAcc) {
+ info('role="grid" aria-multiselectable, selectable children in subtree');
+ let grid = findAccessibleChildByID(docAcc, "grid", [
+ nsIAccessibleSelectable,
+ ]);
+
+ await testMultiSelectable(grid, [
+ "grid_colhead1",
+ "grid_colhead2",
+ "grid_colhead3",
+ "grid_rowhead",
+ "grid_cell1",
+ "grid_cell2",
+ ]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/selectable/browser_test_select.js b/accessible/tests/browser/selectable/browser_test_select.js
new file mode 100644
index 0000000000..f86a371d81
--- /dev/null
+++ b/accessible/tests/browser/selectable/browser_test_select.js
@@ -0,0 +1,329 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/selectable.js */
+/* import-globals-from ../../mochitest/states.js */
+
+// ////////////////////////////////////////////////////////////////////////
+// select@size="1" aka combobox
+addAccessibleTask(
+ `<select id="combobox">
+ <option id="item1">option1</option>
+ <option id="item2">option2</option>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='1' aka combobox");
+ let combobox = findAccessibleChildByID(docAcc, "combobox");
+ let comboboxList = combobox.firstChild;
+ ok(
+ isAccessible(comboboxList, [nsIAccessibleSelectable]),
+ "No selectable accessible for combobox"
+ );
+
+ let select = getAccessible(comboboxList, [nsIAccessibleSelectable]);
+ testSelectableSelection(select, ["item1"]);
+
+ // select 2nd item
+ let promise = Promise.all([
+ waitForStateChange("item2", STATE_SELECTED, true),
+ waitForStateChange("item1", STATE_SELECTED, false),
+ ]);
+ select.addItemToSelection(1);
+ await promise;
+ testSelectableSelection(select, ["item2"], "addItemToSelection(1): ");
+
+ // unselect 2nd item, 1st item gets selected automatically
+ promise = Promise.all([
+ waitForStateChange("item2", STATE_SELECTED, false),
+ waitForStateChange("item1", STATE_SELECTED, true),
+ ]);
+ select.removeItemFromSelection(1);
+ await promise;
+ testSelectableSelection(select, ["item1"], "removeItemFromSelection(1): ");
+
+ // doesn't change selection
+ is(select.selectAll(), false, "No way to select all items in combobox");
+ testSelectableSelection(select, ["item1"], "selectAll: ");
+
+ // doesn't change selection
+ select.unselectAll();
+ testSelectableSelection(select, ["item1"], "unselectAll: ");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// select@size="1" with optgroups
+addAccessibleTask(
+ `<select id="combobox">
+ <option id="item1">option1</option>
+ <optgroup>optgroup
+ <option id="item2">option2</option>
+ </optgroup>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='1' with optgroups");
+ let combobox = findAccessibleChildByID(docAcc, "combobox");
+ let comboboxList = combobox.firstChild;
+ ok(
+ isAccessible(comboboxList, [nsIAccessibleSelectable]),
+ "No selectable accessible for combobox"
+ );
+
+ let select = getAccessible(comboboxList, [nsIAccessibleSelectable]);
+ testSelectableSelection(select, ["item1"]);
+
+ let promise = Promise.all([
+ waitForStateChange("item2", STATE_SELECTED, true),
+ waitForStateChange("item1", STATE_SELECTED, false),
+ ]);
+ select.addItemToSelection(1);
+ await promise;
+ testSelectableSelection(select, ["item2"], "addItemToSelection(1): ");
+
+ promise = Promise.all([
+ waitForStateChange("item2", STATE_SELECTED, false),
+ waitForStateChange("item1", STATE_SELECTED, true),
+ ]);
+ select.removeItemFromSelection(1);
+ await promise;
+ testSelectableSelection(select, ["item1"], "removeItemFromSelection(1): ");
+
+ is(select.selectAll(), false, "No way to select all items in combobox");
+ testSelectableSelection(select, ["item1"]);
+
+ select.unselectAll();
+ testSelectableSelection(select, ["item1"]);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// select@size="4" aka single selectable listbox
+addAccessibleTask(
+ `<select id="listbox" size="4">
+ <option id="item1">option1</option>
+ <option id="item2">option2</option>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='4' aka single selectable listbox");
+ let select = findAccessibleChildByID(docAcc, "listbox", [
+ nsIAccessibleSelectable,
+ ]);
+ testSelectableSelection(select, []);
+
+ // select 2nd item
+ let promise = waitForStateChange("item2", STATE_SELECTED, true);
+ select.addItemToSelection(1);
+ await promise;
+ testSelectableSelection(select, ["item2"], "addItemToSelection(1): ");
+
+ // unselect 2nd item, 1st item gets selected automatically
+ promise = waitForStateChange("item2", STATE_SELECTED, false);
+ select.removeItemFromSelection(1);
+ await promise;
+ testSelectableSelection(select, [], "removeItemFromSelection(1): ");
+
+ // doesn't change selection
+ is(
+ select.selectAll(),
+ false,
+ "No way to select all items in single selectable listbox"
+ );
+ testSelectableSelection(select, [], "selectAll: ");
+
+ // doesn't change selection
+ select.unselectAll();
+ testSelectableSelection(select, [], "unselectAll: ");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// select@size="4" with optgroups, single selectable
+addAccessibleTask(
+ `<select id="listbox" size="4">
+ <option id="item1">option1</option>
+ <optgroup>optgroup>
+ <option id="item2">option2</option>
+ </optgroup>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='4' with optgroups, single selectable");
+ let select = findAccessibleChildByID(docAcc, "listbox", [
+ nsIAccessibleSelectable,
+ ]);
+ testSelectableSelection(select, []);
+
+ let promise = waitForStateChange("item2", STATE_SELECTED, true);
+ select.addItemToSelection(1);
+ await promise;
+ testSelectableSelection(select, ["item2"]);
+
+ promise = waitForStateChange("item2", STATE_SELECTED, false);
+ select.removeItemFromSelection(1);
+ await promise;
+ testSelectableSelection(select, []);
+
+ is(
+ select.selectAll(),
+ false,
+ "No way to select all items in single selectable listbox"
+ );
+ testSelectableSelection(select, []);
+
+ select.unselectAll();
+ testSelectableSelection(select, []);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// select@size="4" multiselect aka listbox
+addAccessibleTask(
+ `<select id="listbox" size="4" multiple="true">
+ <option id="item1">option1</option>
+ <option id="item2">option2</option>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='4' multiselect aka listbox");
+ let select = findAccessibleChildByID(docAcc, "listbox", [
+ nsIAccessibleSelectable,
+ ]);
+ await testMultiSelectable(
+ select,
+ ["item1", "item2"],
+ "select@size='4' multiselect aka listbox "
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// select@size="4" multiselect with optgroups
+addAccessibleTask(
+ `<select id="listbox" size="4" multiple="true">
+ <option id="item1">option1</option>
+ <optgroup>optgroup>
+ <option id="item2">option2</option>
+ </optgroup>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='4' multiselect with optgroups");
+ let select = findAccessibleChildByID(docAcc, "listbox", [
+ nsIAccessibleSelectable,
+ ]);
+ await testMultiSelectable(
+ select,
+ ["item1", "item2"],
+ "select@size='4' multiselect aka listbox "
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+// ////////////////////////////////////////////////////////////////////////
+// multiselect with coalesced selection event
+addAccessibleTask(
+ `<select id="listbox" size="4" multiple="true">
+ <option id="item1">option1</option>
+ <option id="item2">option2</option>
+ <option id="item3">option3</option>
+ <option id="item4">option4</option>
+ <option id="item5">option5</option>
+ <option id="item6">option6</option>
+ <option id="item7">option7</option>
+ <option id="item8">option8</option>
+ <option id="item9">option9</option>
+ </select>`,
+ async function (browser, docAcc) {
+ info("select@size='4' multiselect with coalesced selection event");
+ let select = findAccessibleChildByID(docAcc, "listbox", [
+ nsIAccessibleSelectable,
+ ]);
+ await testMultiSelectable(
+ select,
+ [
+ "item1",
+ "item2",
+ "item3",
+ "item4",
+ "item5",
+ "item6",
+ "item7",
+ "item8",
+ "item9",
+ ],
+ "select@size='4' multiselect with coalesced selection event "
+ );
+ },
+ {
+ chrome: false,
+ topLevel: true,
+ iframe: false,
+ remoteIframe: false,
+ }
+);
+
+/**
+ * Ensure that we don't assert when dealing with defunct items in selection
+ * events dropped due to coalescence (bug 1800755).
+ */
+addAccessibleTask(
+ `
+<form id="form">
+ <select id="select">
+ <option>
+ <optgroup id="optgroup">
+ <option>
+ </optgroup>
+ </select>
+</form>
+ `,
+ async function (browser, docAcc) {
+ let selected = waitForEvent(EVENT_SELECTION_WITHIN, "select");
+ await invokeContentTask(browser, [], () => {
+ const form = content.document.getElementById("form");
+ const select = content.document.getElementById("select");
+ const optgroup = content.document.getElementById("optgroup");
+ form.reset();
+ select.selectedIndex = 1;
+ select.add(optgroup);
+ select.item(0).remove();
+ });
+ await selected;
+ }
+);
diff --git a/accessible/tests/browser/selectable/head.js b/accessible/tests/browser/selectable/head.js
new file mode 100644
index 0000000000..ccf9e86f77
--- /dev/null
+++ b/accessible/tests/browser/selectable/head.js
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* exported testMultiSelectable */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+/* import-globals-from ../../mochitest/selectable.js */
+/* import-globals-from ../../mochitest/states.js */
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR },
+ { name: "selectable.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR }
+);
+
+// Handle case where multiple selection change events are coalesced into
+// a SELECTION_WITHIN event. Promise resolves to true in that case.
+function multipleSelectionChanged(widget, changedChildren, selected) {
+ return Promise.race([
+ Promise.all(
+ changedChildren.map(id =>
+ waitForStateChange(id, STATE_SELECTED, selected)
+ )
+ ).then(() => false),
+ waitForEvent(EVENT_SELECTION_WITHIN, widget).then(() => true),
+ ]);
+}
+
+async function testMultiSelectable(widget, selectableChildren, msg = "") {
+ let isRemote = false;
+ try {
+ widget.DOMNode;
+ } catch (e) {
+ isRemote = true;
+ }
+
+ testSelectableSelection(widget, [], `${msg}: initial`);
+
+ let promise = waitForStateChange(selectableChildren[0], STATE_SELECTED, true);
+ widget.addItemToSelection(0);
+ await promise;
+ testSelectableSelection(
+ widget,
+ [selectableChildren[0]],
+ `${msg}: addItemToSelection(0)`
+ );
+
+ promise = waitForStateChange(selectableChildren[0], STATE_SELECTED, false);
+ widget.removeItemFromSelection(0);
+ await promise;
+ testSelectableSelection(widget, [], `${msg}: removeItemFromSelection(0)`);
+
+ promise = multipleSelectionChanged(widget, selectableChildren, true);
+ let success = widget.selectAll();
+ ok(success, `${msg}: selectAll success`);
+ await promise;
+ if (isRemote) {
+ await untilCacheIs(
+ () => widget.selectedItemCount,
+ selectableChildren.length,
+ "Selection cache updated"
+ );
+ }
+ testSelectableSelection(widget, selectableChildren, `${msg}: selectAll`);
+
+ promise = multipleSelectionChanged(widget, selectableChildren, false);
+ widget.unselectAll();
+ await promise;
+ if (isRemote) {
+ await untilCacheIs(
+ () => widget.selectedItemCount,
+ 0,
+ "Selection cache updated"
+ );
+ }
+ testSelectableSelection(widget, [], `${msg}: selectAll`);
+}
diff --git a/accessible/tests/browser/shared-head.js b/accessible/tests/browser/shared-head.js
new file mode 100644
index 0000000000..fe87a77765
--- /dev/null
+++ b/accessible/tests/browser/shared-head.js
@@ -0,0 +1,973 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../mochitest/common.js */
+/* import-globals-from ../mochitest/layout.js */
+/* import-globals-from ../mochitest/promisified-events.js */
+
+/* exported Logger, MOCHITESTS_DIR, invokeSetAttribute, invokeFocus,
+ invokeSetStyle, getAccessibleDOMNodeID, getAccessibleTagName,
+ addAccessibleTask, findAccessibleChildByID, isDefunct,
+ CURRENT_CONTENT_DIR, loadScripts, loadContentScripts, snippetToURL,
+ Cc, Cu, arrayFromChildren, forceGC, contentSpawnMutation,
+ DEFAULT_IFRAME_ID, DEFAULT_IFRAME_DOC_BODY_ID, invokeContentTask,
+ matchContentDoc, currentContentDoc, getContentDPR,
+ waitForImageMap, getContentBoundsForDOMElm, untilCacheIs,
+ untilCacheOk, testBoundsWithContent, waitForContentPaint,
+ runPython */
+
+const CURRENT_FILE_DIR = "/browser/accessible/tests/browser/";
+
+/**
+ * Current browser test directory path used to load subscripts.
+ */
+const CURRENT_DIR = `chrome://mochitests/content${CURRENT_FILE_DIR}`;
+/**
+ * A11y mochitest directory where we find common files used in both browser and
+ * plain tests.
+ */
+const MOCHITESTS_DIR =
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/";
+/**
+ * A base URL for test files used in content.
+ */
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const CURRENT_CONTENT_DIR = `http://example.com${CURRENT_FILE_DIR}`;
+
+const LOADED_CONTENT_SCRIPTS = new Map();
+
+const DEFAULT_CONTENT_DOC_BODY_ID = "body";
+const DEFAULT_IFRAME_ID = "default-iframe-id";
+const DEFAULT_IFRAME_DOC_BODY_ID = "default-iframe-body-id";
+
+const HTML_MIME_TYPE = "text/html";
+const XHTML_MIME_TYPE = "application/xhtml+xml";
+
+function loadHTMLFromFile(path) {
+ // Load the HTML to return in the response from file.
+ // Since it's relative to the cwd of the test runner, we start there and
+ // append to get to the actual path of the file.
+ const testHTMLFile = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ const dirs = path.split("/");
+ for (let i = 0; i < dirs.length; i++) {
+ testHTMLFile.append(dirs[i]);
+ }
+
+ const testHTMLFileStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ testHTMLFileStream.init(testHTMLFile, -1, 0, 0);
+ const testHTML = NetUtil.readInputStreamToString(
+ testHTMLFileStream,
+ testHTMLFileStream.available()
+ );
+
+ return testHTML;
+}
+
+let gIsIframe = false;
+let gIsRemoteIframe = false;
+
+function currentContentDoc() {
+ return gIsIframe ? DEFAULT_IFRAME_DOC_BODY_ID : DEFAULT_CONTENT_DOC_BODY_ID;
+}
+
+/**
+ * Accessible event match criteria based on the id of the current document
+ * accessible in test.
+ *
+ * @param {nsIAccessibleEvent} event
+ * Accessible event to be tested for a match.
+ *
+ * @return {Boolean}
+ * True if accessible event's accessible object ID matches current
+ * document accessible ID.
+ */
+function matchContentDoc(event) {
+ return getAccessibleDOMNodeID(event.accessible) === currentContentDoc();
+}
+
+/**
+ * Used to dump debug information.
+ */
+let Logger = {
+ /**
+ * Set up this variable to dump log messages into console.
+ */
+ dumpToConsole: false,
+
+ /**
+ * Set up this variable to dump log messages into error console.
+ */
+ dumpToAppConsole: false,
+
+ /**
+ * Return true if dump is enabled.
+ */
+ get enabled() {
+ return this.dumpToConsole || this.dumpToAppConsole;
+ },
+
+ /**
+ * Dump information into console if applicable.
+ */
+ log(msg) {
+ if (this.enabled) {
+ this.logToConsole(msg);
+ this.logToAppConsole(msg);
+ }
+ },
+
+ /**
+ * Log message to console.
+ */
+ logToConsole(msg) {
+ if (this.dumpToConsole) {
+ dump(`\n${msg}\n`);
+ }
+ },
+
+ /**
+ * Log message to error console.
+ */
+ logToAppConsole(msg) {
+ if (this.dumpToAppConsole) {
+ Services.console.logStringMessage(`${msg}`);
+ }
+ },
+};
+
+/**
+ * Asynchronously set or remove content element's attribute (in content process
+ * if e10s is enabled).
+ * @param {Object} browser current "tabbrowser" element
+ * @param {String} id content element id
+ * @param {String} attr attribute name
+ * @param {String?} value optional attribute value, if not present, remove
+ * attribute
+ * @return {Promise} promise indicating that attribute is set/removed
+ */
+function invokeSetAttribute(browser, id, attr, value) {
+ if (value) {
+ Logger.log(`Setting ${attr} attribute to ${value} for node with id: ${id}`);
+ } else {
+ Logger.log(`Removing ${attr} attribute from node with id: ${id}`);
+ }
+
+ return invokeContentTask(
+ browser,
+ [id, attr, value],
+ (contentId, contentAttr, contentValue) => {
+ let elm = content.document.getElementById(contentId);
+ if (contentValue) {
+ elm.setAttribute(contentAttr, contentValue);
+ } else {
+ elm.removeAttribute(contentAttr);
+ }
+ }
+ );
+}
+
+/**
+ * Asynchronously set or remove content element's style (in content process if
+ * e10s is enabled, or in fission process if fission is enabled and a fission
+ * frame is present).
+ * @param {Object} browser current "tabbrowser" element
+ * @param {String} id content element id
+ * @param {String} aStyle style property name
+ * @param {String?} aValue optional style property value, if not present,
+ * remove style
+ * @return {Promise} promise indicating that style is set/removed
+ */
+function invokeSetStyle(browser, id, style, value) {
+ if (value) {
+ Logger.log(`Setting ${style} style to ${value} for node with id: ${id}`);
+ } else {
+ Logger.log(`Removing ${style} style from node with id: ${id}`);
+ }
+
+ return invokeContentTask(
+ browser,
+ [id, style, value],
+ (contentId, contentStyle, contentValue) => {
+ const elm = content.document.getElementById(contentId);
+ if (contentValue) {
+ elm.style[contentStyle] = contentValue;
+ } else {
+ delete elm.style[contentStyle];
+ }
+ }
+ );
+}
+
+/**
+ * Asynchronously set focus on a content element (in content process if e10s is
+ * enabled, or in fission process if fission is enabled and a fission frame is
+ * present).
+ * @param {Object} browser current "tabbrowser" element
+ * @param {String} id content element id
+ * @return {Promise} promise indicating that focus is set
+ */
+function invokeFocus(browser, id) {
+ Logger.log(`Setting focus on a node with id: ${id}`);
+
+ return invokeContentTask(browser, [id], contentId => {
+ const elm = content.document.getElementById(contentId);
+ if (elm.editor) {
+ elm.selectionStart = elm.selectionEnd = elm.value.length;
+ }
+
+ elm.focus();
+ });
+}
+
+/**
+ * Get DPR for a specific content window.
+ * @param browser
+ * Browser for which we want its content window's DPR reported.
+ *
+ * @return {Promise}
+ * Promise with the value that resolves to the devicePixelRatio of the
+ * content window of a given browser.
+ *
+ */
+function getContentDPR(browser) {
+ return invokeContentTask(browser, [], () => content.window.devicePixelRatio);
+}
+
+/**
+ * Asynchronously perform a task in content (in content process if e10s is
+ * enabled, or in fission process if fission is enabled and a fission frame is
+ * present).
+ * @param {Object} browser current "tabbrowser" element
+ * @param {Array} args arguments for the content task
+ * @param {Function} task content task function
+ *
+ * @return {Promise} promise indicating that content task is complete
+ */
+function invokeContentTask(browser, args, task) {
+ return SpecialPowers.spawn(
+ browser,
+ [DEFAULT_IFRAME_ID, task.toString(), ...args],
+ (iframeId, contentTask, ...contentArgs) => {
+ // eslint-disable-next-line no-eval
+ const runnableTask = eval(`
+ (() => {
+ return (${contentTask});
+ })();`);
+ const frame = content.document.getElementById(iframeId);
+
+ return frame
+ ? SpecialPowers.spawn(frame, contentArgs, runnableTask)
+ : runnableTask.call(this, ...contentArgs);
+ }
+ );
+}
+
+/**
+ * Compare process ID's between the top level content process and possible
+ * remote/local iframe proccess.
+ * @param {Object} browser
+ * Top level browser object for a tab.
+ * @param {Boolean} isRemote
+ * Indicates if we expect the iframe content process to be remote or not.
+ */
+async function comparePIDs(browser, isRemote) {
+ function getProcessID() {
+ return Services.appinfo.processID;
+ }
+
+ const contentPID = await SpecialPowers.spawn(browser, [], getProcessID);
+ const iframePID = await invokeContentTask(browser, [], getProcessID);
+ is(
+ isRemote,
+ contentPID !== iframePID,
+ isRemote
+ ? "Remote IFRAME is in a different process."
+ : "IFRAME is in the same process."
+ );
+}
+
+/**
+ * Load a list of scripts into the test
+ * @param {Array} scripts a list of scripts to load
+ */
+function loadScripts(...scripts) {
+ for (let script of scripts) {
+ let path =
+ typeof script === "string"
+ ? `${CURRENT_DIR}${script}`
+ : `${script.dir}${script.name}`;
+ Services.scriptloader.loadSubScript(path, this);
+ }
+}
+
+/**
+ * Load a list of scripts into target's content.
+ * @param {Object} target
+ * target for loading scripts into
+ * @param {Array} scripts
+ * a list of scripts to load into content
+ */
+async function loadContentScripts(target, ...scripts) {
+ for (let { script, symbol } of scripts) {
+ let contentScript = `${CURRENT_DIR}${script}`;
+ let loadedScriptSet = LOADED_CONTENT_SCRIPTS.get(contentScript);
+ if (!loadedScriptSet) {
+ loadedScriptSet = new WeakSet();
+ LOADED_CONTENT_SCRIPTS.set(contentScript, loadedScriptSet);
+ } else if (loadedScriptSet.has(target)) {
+ continue;
+ }
+
+ await SpecialPowers.spawn(
+ target,
+ [contentScript, symbol],
+ async (_contentScript, importSymbol) => {
+ let module = ChromeUtils.importESModule(_contentScript);
+ content.window[importSymbol] = module[importSymbol];
+ }
+ );
+ loadedScriptSet.add(target);
+ }
+}
+
+function attrsToString(attrs) {
+ return Object.entries(attrs)
+ .map(([attr, value]) => `${attr}=${JSON.stringify(value)}`)
+ .join(" ");
+}
+
+function wrapWithIFrame(doc, options = {}) {
+ let src;
+ let { iframeAttrs = {}, iframeDocBodyAttrs = {} } = options;
+ iframeDocBodyAttrs = {
+ id: DEFAULT_IFRAME_DOC_BODY_ID,
+ ...iframeDocBodyAttrs,
+ };
+ if (options.remoteIframe) {
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ const srcURL = new URL(`http://example.net/document-builder.sjs`);
+ if (doc.endsWith("html")) {
+ srcURL.searchParams.append("file", `${CURRENT_FILE_DIR}${doc}`);
+ } else {
+ srcURL.searchParams.append(
+ "html",
+ `<!doctype html>
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Fission Test</title>
+ </head>
+ <body ${attrsToString(iframeDocBodyAttrs)}>${doc}</body>
+ </html>`
+ );
+ }
+ src = srcURL.href;
+ } else {
+ const mimeType = doc.endsWith("xhtml") ? XHTML_MIME_TYPE : HTML_MIME_TYPE;
+ if (doc.endsWith("html")) {
+ doc = loadHTMLFromFile(`${CURRENT_FILE_DIR}${doc}`);
+ doc = doc.replace(
+ /<body[.\s\S]*?>/,
+ `<body ${attrsToString(iframeDocBodyAttrs)}>`
+ );
+ } else {
+ doc = `<!doctype html>
+ <body ${attrsToString(iframeDocBodyAttrs)}>${doc}</body>`;
+ }
+
+ src = `data:${mimeType};charset=utf-8,${encodeURIComponent(doc)}`;
+ }
+
+ iframeAttrs = {
+ id: DEFAULT_IFRAME_ID,
+ src,
+ ...iframeAttrs,
+ };
+
+ return `<iframe ${attrsToString(iframeAttrs)}/>`;
+}
+
+/**
+ * Takes an HTML snippet or HTML doc url and returns an encoded URI for a full
+ * document with the snippet or the URL as a source for the IFRAME.
+ * @param {String} doc
+ * a markup snippet or url.
+ * @param {Object} options (see options in addAccessibleTask).
+ *
+ * @return {String}
+ * a base64 encoded data url of the document container the snippet.
+ **/
+function snippetToURL(doc, options = {}) {
+ const { contentDocBodyAttrs = {} } = options;
+ const attrs = {
+ id: DEFAULT_CONTENT_DOC_BODY_ID,
+ ...contentDocBodyAttrs,
+ };
+
+ if (gIsIframe) {
+ doc = wrapWithIFrame(doc, options);
+ }
+
+ const encodedDoc = encodeURIComponent(
+ `<!doctype html>
+ <html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Test</title>
+ </head>
+ <body ${attrsToString(attrs)}>${doc}</body>
+ </html>`
+ );
+
+ return `data:text/html;charset=utf-8,${encodedDoc}`;
+}
+
+function accessibleTask(doc, task, options = {}) {
+ return async function () {
+ gIsRemoteIframe = options.remoteIframe;
+ gIsIframe = options.iframe || gIsRemoteIframe;
+ let url;
+ if (options.chrome && doc.endsWith("html")) {
+ // Load with a chrome:// URL so this loads as a chrome document in the
+ // parent process.
+ url = `${CURRENT_DIR}${doc}`;
+ } else if (doc.endsWith("html") && !gIsIframe) {
+ url = `${CURRENT_CONTENT_DIR}${doc}`;
+ } else {
+ url = snippetToURL(doc, options);
+ }
+
+ registerCleanupFunction(() => {
+ for (let observer of Services.obs.enumerateObservers(
+ "accessible-event"
+ )) {
+ Services.obs.removeObserver(observer, "accessible-event");
+ }
+ if (gPythonSocket) {
+ // Remove any globals set by Python code run in this test.
+ runPython(`__reset__`);
+ }
+ });
+
+ let onContentDocLoad;
+ if (!options.chrome) {
+ onContentDocLoad = waitForEvent(
+ EVENT_DOCUMENT_LOAD_COMPLETE,
+ DEFAULT_CONTENT_DOC_BODY_ID
+ );
+ }
+
+ let onIframeDocLoad;
+ if (options.remoteIframe && !options.skipFissionDocLoad) {
+ onIframeDocLoad = waitForEvent(
+ EVENT_DOCUMENT_LOAD_COMPLETE,
+ DEFAULT_IFRAME_DOC_BODY_ID
+ );
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ // For chrome, we need a non-remote browser.
+ opening: !options.chrome
+ ? url
+ : () => {
+ // Passing forceNotRemote: true still sets maychangeremoteness,
+ // which will cause data: URIs to load remotely. There's no way to
+ // avoid this with gBrowser or BrowserTestUtils. Therefore, we
+ // load a blank document initially and replace it below.
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ "about:blank",
+ {
+ forceNotRemote: true,
+ }
+ );
+ },
+ },
+ async function (browser) {
+ registerCleanupFunction(() => {
+ if (browser) {
+ let tab = gBrowser.getTabForBrowser(browser);
+ if (tab && !tab.closing && tab.linkedBrowser) {
+ gBrowser.removeTab(tab);
+ }
+ }
+ });
+
+ if (options.chrome) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.allow_unsafe_parent_loads", true]],
+ });
+ // Ensure this never becomes a remote browser.
+ browser.removeAttribute("maychangeremoteness");
+ // Now we can load our page without it becoming remote.
+ browser.setAttribute("src", url);
+ }
+
+ await SimpleTest.promiseFocus(browser);
+
+ if (options.chrome) {
+ ok(!browser.isRemoteBrowser, "Not remote browser");
+ } else if (Services.appinfo.browserTabsRemoteAutostart) {
+ ok(browser.isRemoteBrowser, "Actually remote browser");
+ }
+
+ let docAccessible;
+ if (options.chrome) {
+ // Chrome documents don't fire DOCUMENT_LOAD_COMPLETE. Instead, wait
+ // until we can get the DocAccessible and it doesn't have the busy
+ // state.
+ await BrowserTestUtils.waitForCondition(() => {
+ docAccessible = getAccessible(browser.contentWindow.document);
+ if (!docAccessible) {
+ return false;
+ }
+ const state = {};
+ docAccessible.getState(state, {});
+ return !(state.value & STATE_BUSY);
+ });
+ } else {
+ ({ accessible: docAccessible } = await onContentDocLoad);
+ }
+ let iframeDocAccessible;
+ if (gIsIframe) {
+ if (!options.skipFissionDocLoad) {
+ await comparePIDs(browser, options.remoteIframe);
+ iframeDocAccessible = onIframeDocLoad
+ ? (await onIframeDocLoad).accessible
+ : findAccessibleChildByID(docAccessible, DEFAULT_IFRAME_ID)
+ .firstChild;
+ }
+ }
+
+ await loadContentScripts(browser, {
+ script: "Common.sys.mjs",
+ symbol: "CommonUtils",
+ });
+
+ await task(
+ browser,
+ iframeDocAccessible || docAccessible,
+ iframeDocAccessible && docAccessible
+ );
+ }
+ );
+ };
+}
+
+/**
+ * A wrapper around browser test add_task that triggers an accessible test task
+ * as a new browser test task with given document, data URL or markup snippet.
+ * @param {String} doc
+ * URL (relative to current directory) or data URL or markup snippet
+ * that is used to test content with
+ * @param {Function|AsyncFunction} task
+ * a generator or a function with tests to run
+ * @param {null|Object} options
+ * Options for running accessibility test tasks:
+ * - {Boolean} topLevel
+ * Flag to run the test with content in the top level content process.
+ * Default is true.
+ * - {Boolean} chrome
+ * Flag to run the test with content as a chrome document in the
+ * parent process. Default is false. Although url can be a markup
+ * snippet, a snippet cannot be used for XUL content. To load XUL,
+ * specify a relative URL to a XUL document. In that case, toplevel
+ * should usually be set to false, since XUL documents don't work in
+ * content processes.
+ * - {Boolean} iframe
+ * Flag to run the test with content wrapped in an iframe. Default is
+ * false.
+ * - {Boolean} remoteIframe
+ * Flag to run the test with content wrapped in a remote iframe.
+ * Default is false.
+ * - {Object} iframeAttrs
+ * A map of attribute/value pairs to be applied to IFRAME element.
+ * - {Boolean} skipFissionDocLoad
+ * If true, the test will not wait for iframe document document
+ * loaded event (useful for when IFRAME is initially hidden).
+ * - {Object} contentDocBodyAttrs
+ * a set of attributes to be applied to a top level content document
+ * body
+ * - {Object} iframeDocBodyAttrs
+ * a set of attributes to be applied to a iframe content document body
+ */
+function addAccessibleTask(doc, task, options = {}) {
+ const {
+ topLevel = true,
+ chrome = false,
+ iframe = false,
+ remoteIframe = false,
+ } = options;
+ if (topLevel) {
+ add_task(
+ accessibleTask(doc, task, {
+ ...options,
+ chrome: false,
+ iframe: false,
+ remoteIframe: false,
+ })
+ );
+ }
+
+ if (chrome) {
+ add_task(
+ accessibleTask(doc, task, {
+ ...options,
+ topLevel: false,
+ iframe: false,
+ remoteIframe: false,
+ })
+ );
+ }
+
+ if (iframe) {
+ add_task(
+ accessibleTask(doc, task, {
+ ...options,
+ topLevel: false,
+ chrome: false,
+ remoteIframe: false,
+ })
+ );
+ }
+
+ if (gFissionBrowser && remoteIframe) {
+ add_task(
+ accessibleTask(doc, task, {
+ ...options,
+ topLevel: false,
+ chrome: false,
+ iframe: false,
+ })
+ );
+ }
+}
+
+/**
+ * Check if an accessible object has a defunct test.
+ * @param {nsIAccessible} accessible object to test defunct state for
+ * @return {Boolean} flag indicating defunct state
+ */
+function isDefunct(accessible) {
+ let defunct = false;
+ try {
+ let extState = {};
+ accessible.getState({}, extState);
+ defunct = extState.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT;
+ } catch (x) {
+ defunct = true;
+ } finally {
+ if (defunct) {
+ Logger.log(`Defunct accessible: ${prettyName(accessible)}`);
+ }
+ }
+ return defunct;
+}
+
+/**
+ * Get the DOM tag name for a given accessible.
+ * @param {nsIAccessible} accessible accessible
+ * @return {String?} tag name of associated DOM node, or null.
+ */
+function getAccessibleTagName(acc) {
+ try {
+ return acc.attributes.getStringProperty("tag");
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Traverses the accessible tree starting from a given accessible as a root and
+ * looks for an accessible that matches based on its DOMNode id.
+ * @param {nsIAccessible} accessible root accessible
+ * @param {String} id id to look up accessible for
+ * @param {Array?} interfaces the interface or an array interfaces
+ * to query it/them from obtained accessible
+ * @return {nsIAccessible?} found accessible if any
+ */
+function findAccessibleChildByID(accessible, id, interfaces) {
+ if (getAccessibleDOMNodeID(accessible) === id) {
+ return queryInterfaces(accessible, interfaces);
+ }
+ for (let i = 0; i < accessible.children.length; ++i) {
+ let found = findAccessibleChildByID(accessible.getChildAt(i), id);
+ if (found) {
+ return queryInterfaces(found, interfaces);
+ }
+ }
+ return null;
+}
+
+function queryInterfaces(accessible, interfaces) {
+ if (!interfaces) {
+ return accessible;
+ }
+
+ for (let iface of interfaces.filter(i => !(accessible instanceof i))) {
+ try {
+ accessible.QueryInterface(iface);
+ } catch (e) {
+ ok(false, "Can't query " + iface);
+ }
+ }
+
+ return accessible;
+}
+
+function arrayFromChildren(accessible) {
+ return Array.from({ length: accessible.childCount }, (c, i) =>
+ accessible.getChildAt(i)
+ );
+}
+
+/**
+ * Force garbage collection.
+ */
+function forceGC() {
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+}
+
+/*
+ * This function spawns a content task and awaits expected mutation events from
+ * various content changes. It's good at catching events we did *not* expect. We
+ * do this advancing the layout refresh to flush the relocations/insertions
+ * queue.
+ */
+async function contentSpawnMutation(browser, waitFor, func, args = []) {
+ let onReorders = waitForEvents({ expected: waitFor.expected || [] });
+ let unexpectedListener = new UnexpectedEvents(waitFor.unexpected || []);
+
+ function tick() {
+ // 100ms is an arbitrary positive number to advance the clock.
+ // We don't need to advance the clock for a11y mutations, but other
+ // tick listeners may depend on an advancing clock with each refresh.
+ content.windowUtils.advanceTimeAndRefresh(100);
+ }
+
+ // This stops the refreh driver from doing its regular ticks, and leaves
+ // us in control.
+ await invokeContentTask(browser, [], tick);
+
+ // Perform the tree mutation.
+ await invokeContentTask(browser, args, func);
+
+ // Do one tick to flush our queue (insertions, relocations, etc.)
+ await invokeContentTask(browser, [], tick);
+
+ let events = await onReorders;
+
+ unexpectedListener.stop();
+
+ // Go back to normal refresh driver ticks.
+ await invokeContentTask(browser, [], function () {
+ content.windowUtils.restoreNormalRefresh();
+ });
+
+ return events;
+}
+
+async function waitForImageMap(browser, accDoc, id = "imgmap") {
+ let acc = findAccessibleChildByID(accDoc, id);
+
+ if (!acc) {
+ const onShow = waitForEvent(EVENT_SHOW, id);
+ acc = (await onShow).accessible;
+ }
+
+ if (acc.firstChild) {
+ return;
+ }
+
+ const onReorder = waitForEvent(EVENT_REORDER, id);
+ // Wave over image map
+ await invokeContentTask(browser, [id], contentId => {
+ const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+ );
+ const EventUtils = ContentTaskUtils.getEventUtils(content);
+ EventUtils.synthesizeMouse(
+ content.document.getElementById(contentId),
+ 10,
+ 10,
+ { type: "mousemove" },
+ content
+ );
+ });
+ await onReorder;
+}
+
+async function getContentBoundsForDOMElm(browser, id) {
+ return invokeContentTask(browser, [id], contentId => {
+ const { Layout: LayoutUtils } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+
+ return LayoutUtils.getBoundsForDOMElm(contentId, content.document);
+ });
+}
+
+const CACHE_WAIT_TIMEOUT_MS = 5000;
+
+/**
+ * Wait for a predicate to be true after cache ticks.
+ * This function takes two callbacks, the condition is evaluated
+ * by calling the first callback with the arguments returned by the second.
+ * This allows us to asynchronously return the arguments as a result if the condition
+ * of the first callback is met, or if it times out. The returned arguments can then
+ * be used to record a pass or fail in the test.
+ */
+function untilCacheCondition(conditionFunc, argsFunc) {
+ return new Promise((resolve, reject) => {
+ let args = argsFunc();
+ if (conditionFunc(...args)) {
+ resolve(args);
+ return;
+ }
+
+ let cacheObserver = {
+ observe(subject) {
+ args = argsFunc();
+ if (conditionFunc(...args)) {
+ clearTimeout(this.timer);
+ Services.obs.removeObserver(this, "accessible-cache");
+ resolve(args);
+ }
+ },
+
+ timeout() {
+ ok(false, "Timeout while waiting for cache update");
+ Services.obs.removeObserver(this, "accessible-cache");
+ args = argsFunc();
+ resolve(args);
+ },
+ };
+
+ cacheObserver.timer = setTimeout(
+ cacheObserver.timeout.bind(cacheObserver),
+ CACHE_WAIT_TIMEOUT_MS
+ );
+ Services.obs.addObserver(cacheObserver, "accessible-cache");
+ });
+}
+
+function untilCacheOk(conditionFunc, message) {
+ return untilCacheCondition(
+ (v, _unusedMessage) => v,
+ () => [conditionFunc(), message]
+ ).then(([v, msg]) => ok(v, msg));
+}
+
+function untilCacheIs(retrievalFunc, expected, message) {
+ return untilCacheCondition(
+ (a, b, _unusedMessage) => Object.is(a, b),
+ () => [retrievalFunc(), expected, message]
+ ).then(([got, exp, msg]) => is(got, exp, msg));
+}
+
+async function waitForContentPaint(browser) {
+ await SpecialPowers.spawn(browser, [], () => {
+ return new Promise(function (r) {
+ content.requestAnimationFrame(() => content.setTimeout(r));
+ });
+ });
+}
+
+// Returns true if both number arrays match within `FUZZ`.
+function areBoundsFuzzyEqual(actual, expected) {
+ const FUZZ = 1;
+ return actual
+ .map((val, i) => Math.abs(val - expected[i]) <= FUZZ)
+ .reduce((a, b) => a && b, true);
+}
+
+function assertBoundsFuzzyEqual(actual, expected) {
+ ok(
+ areBoundsFuzzyEqual(actual, expected),
+ `${actual} fuzzily matches expected ${expected}`
+ );
+}
+
+async function testBoundsWithContent(iframeDocAcc, id, browser) {
+ // Retrieve layout bounds from content
+ let expectedBounds = await invokeContentTask(browser, [id], _id => {
+ const { Layout: LayoutUtils } = ChromeUtils.importESModule(
+ "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs"
+ );
+ return LayoutUtils.getBoundsForDOMElm(_id, content.document);
+ });
+
+ function isWithinExpected(bounds) {
+ return areBoundsFuzzyEqual(bounds, expectedBounds);
+ }
+
+ const acc = findAccessibleChildByID(iframeDocAcc, id);
+ let [accBounds] = await untilCacheCondition(isWithinExpected, () => [
+ getBounds(acc),
+ ]);
+
+ assertBoundsFuzzyEqual(accBounds, expectedBounds);
+
+ return accBounds;
+}
+
+let gPythonSocket = null;
+
+/**
+ * Run some Python code. This is useful for testing OS APIs.
+ * This function returns a Promise which is resolved or rejected when the Python
+ * code completes. The Python code can return a result with the return
+ * statement, as long as the result can be serialized to JSON. For convenience,
+ * if the code is a single line which does not begin with return, it will be
+ * treated as an expression and its result will be returned. The JS Promise will
+ * be resolved with the deserialized result. If the Python code raises an
+ * exception, the JS Promise will be rejected with the Python traceback.
+ * An info() function is provided in Python to log an info message.
+ * See windows/a11y_setup.py for other things available in the Python
+ * environment.
+ */
+function runPython(code) {
+ if (!gPythonSocket) {
+ // Keep the socket open across calls to avoid repeated setup overhead.
+ gPythonSocket = new WebSocket(
+ "ws://mochi.test:8888/browser/accessible/tests/browser/python_runner"
+ );
+ if (gPythonSocket.readyState != WebSocket.OPEN) {
+ gPythonSocket.onopen = evt => {
+ gPythonSocket.send(code);
+ gPythonSocket.onopen = null;
+ };
+ }
+ }
+ return new Promise((resolve, reject) => {
+ gPythonSocket.onmessage = evt => {
+ const message = JSON.parse(evt.data);
+ if (message[0] == "return") {
+ gPythonSocket.onmessage = null;
+ resolve(message[1]);
+ } else if (message[0] == "exception") {
+ gPythonSocket.onmessage = null;
+ reject(new Error(message[1]));
+ } else if (message[0] == "info") {
+ info(message[1]);
+ }
+ };
+ // If gPythonSocket isn't open yet, we'll send the message when .onopen is
+ // called. If it's open, we can send it immediately.
+ if (gPythonSocket.readyState == WebSocket.OPEN) {
+ gPythonSocket.send(code);
+ }
+ });
+}
diff --git a/accessible/tests/browser/states/browser.toml b/accessible/tests/browser/states/browser.toml
new file mode 100644
index 0000000000..fe29597d27
--- /dev/null
+++ b/accessible/tests/browser/states/browser.toml
@@ -0,0 +1,22 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/browser/*.jsm",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_test_link.js"]
+https_first_disabled = true
+skip-if = ["verify"]
+
+["browser_test_select_visibility.js"]
+https_first_disabled = true
+
+["browser_test_visibility.js"]
+https_first_disabled = true
+
+["browser_test_visibility_2.js"]
+https_first_disabled = true
diff --git a/accessible/tests/browser/states/browser_test_link.js b/accessible/tests/browser/states/browser_test_link.js
new file mode 100644
index 0000000000..0a3e8a9975
--- /dev/null
+++ b/accessible/tests/browser/states/browser_test_link.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+async function runTests(browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ // a: no traversed state
+ testStates(getAcc("link_traversed"), 0, 0, STATE_TRAVERSED);
+
+ let onStateChanged = waitForEvent(EVENT_STATE_CHANGE, "link_traversed");
+ let newTabOpened = BrowserTestUtils.waitForNewTab(gBrowser);
+
+ await BrowserTestUtils.synthesizeMouse(
+ "#link_traversed",
+ 1,
+ 1,
+ { ctrlKey: !MAC, metaKey: MAC },
+ browser
+ );
+
+ await onStateChanged;
+ testStates(getAcc("link_traversed"), STATE_TRAVERSED);
+
+ let newTab = await newTabOpened;
+ gBrowser.removeTab(newTab);
+}
+
+/**
+ * Test caching of accessible object states
+ */
+addAccessibleTask(
+ // The URL doesn't really matter, just the fact that it isn't in the history
+ // initially. We append ms since epoch to the URL so it will never be visited
+ // initially, regardless of other tests (even this one) that ran before.
+ `
+ <a id="link_traversed"
+ href="https://www.example.com/${Date.now()}" target="_top">
+ example.com
+ </a>`,
+ runTests
+);
diff --git a/accessible/tests/browser/states/browser_test_select_visibility.js b/accessible/tests/browser/states/browser_test_select_visibility.js
new file mode 100644
index 0000000000..89b4df67f7
--- /dev/null
+++ b/accessible/tests/browser/states/browser_test_select_visibility.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// test selects and options
+addAccessibleTask(
+ `<select id="select">
+ <option id="o1">hello</option>
+ <option id="o2">world</option>
+ </select>`,
+ async function (browser, accDoc) {
+ const select = findAccessibleChildByID(accDoc, "select");
+ ok(
+ isAccessible(select.firstChild, [nsIAccessibleSelectable]),
+ "No selectable accessible for combobox"
+ );
+ await untilCacheOk(
+ () => testVisibility(select, false, false),
+ "select should be on screen and visible"
+ );
+
+ if (!browser.isRemoteBrowser) {
+ await untilCacheOk(
+ () => testVisibility(select.firstChild, false, true),
+ "combobox list should be on screen and invisible"
+ );
+ } else {
+ // XXX: When the cache is used, states::INVISIBLE is
+ // incorrect. Test OFFSCREEN anyway.
+ await untilCacheOk(() => {
+ const [states] = getStates(select.firstChild);
+ return (states & STATE_OFFSCREEN) == 0;
+ }, "combobox list should be on screen");
+ }
+
+ const o1 = findAccessibleChildByID(accDoc, "o1");
+ const o2 = findAccessibleChildByID(accDoc, "o2");
+
+ await untilCacheOk(
+ () => testVisibility(o1, false, false),
+ "option one should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(o2, true, false),
+ "option two should be off screen and visible"
+ );
+
+ // Select the second option (drop-down collapsed).
+ const p = waitForEvents({
+ expected: [
+ [EVENT_SELECTION, "o2"],
+ [EVENT_TEXT_VALUE_CHANGE, "select"],
+ ],
+ unexpected: [
+ stateChangeEventArgs("o2", EXT_STATE_ACTIVE, true, true),
+ stateChangeEventArgs("o1", EXT_STATE_ACTIVE, false, true),
+ ],
+ });
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("select").selectedIndex = 1;
+ });
+ await p;
+
+ await untilCacheOk(() => {
+ const [states] = getStates(o1);
+ return (states & STATE_OFFSCREEN) != 0;
+ }, "option 1 should be off screen");
+ await untilCacheOk(() => {
+ const [states] = getStates(o2);
+ return (states & STATE_OFFSCREEN) == 0;
+ }, "option 2 should be on screen");
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/states/browser_test_visibility.js b/accessible/tests/browser/states/browser_test_visibility.js
new file mode 100644
index 0000000000..25bd903ed4
--- /dev/null
+++ b/accessible/tests/browser/states/browser_test_visibility.js
@@ -0,0 +1,181 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+async function runTest(browser, accDoc) {
+ let getAcc = id => findAccessibleChildByID(accDoc, id);
+
+ await untilCacheOk(
+ () => testVisibility(getAcc("div"), false, false),
+ "Div should be on screen"
+ );
+
+ let input = getAcc("input_scrolledoff");
+ await untilCacheOk(
+ () => testVisibility(input, true, false),
+ "Input should be offscreen"
+ );
+
+ // scrolled off item (twice)
+ let lastLi = getAcc("li_last");
+ await untilCacheOk(
+ () => testVisibility(lastLi, true, false),
+ "Last list item should be offscreen"
+ );
+
+ // scroll into view the item
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("li_last").scrollIntoView(true);
+ });
+ await untilCacheOk(
+ () => testVisibility(lastLi, false, false),
+ "Last list item should no longer be offscreen"
+ );
+
+ // first item is scrolled off now (testcase for bug 768786)
+ let firstLi = getAcc("li_first");
+ await untilCacheOk(
+ () => testVisibility(firstLi, true, false),
+ "First listitem should now be offscreen"
+ );
+
+ await untilCacheOk(
+ () => testVisibility(getAcc("frame"), false, false),
+ "iframe should initially be onscreen"
+ );
+
+ let loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, "iframeDoc");
+ await invokeContentTask(browser, [], () => {
+ content.document.querySelector("iframe").src =
+ 'data:text/html,<body id="iframeDoc"><p id="p">hi</p></body>';
+ });
+
+ const iframeDoc = (await loaded).accessible;
+ await untilCacheOk(
+ () => testVisibility(getAcc("frame"), false, false),
+ "iframe outer doc should now be on screen"
+ );
+ await untilCacheOk(
+ () => testVisibility(iframeDoc, false, false),
+ "iframe inner doc should be on screen"
+ );
+ const iframeP = findAccessibleChildByID(iframeDoc, "p");
+ await untilCacheOk(
+ () => testVisibility(iframeP, false, false),
+ "iframe content should also be on screen"
+ );
+
+ // scroll into view the div
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").scrollIntoView(true);
+ });
+
+ await untilCacheOk(
+ () => testVisibility(getAcc("frame"), true, false),
+ "iframe outer doc should now be off screen"
+ );
+ await untilCacheOk(
+ () => testVisibility(iframeDoc, true, false),
+ "iframe inner doc should now be off screen"
+ );
+ await untilCacheOk(
+ () => testVisibility(iframeP, true, false),
+ "iframe content should now be off screen"
+ );
+
+ let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ // Accessibles in background tab should have offscreen state and no
+ // invisible state.
+ await untilCacheOk(
+ () => testVisibility(getAcc("div"), true, false),
+ "Accs in background tab should be offscreen but not invisible."
+ );
+
+ await untilCacheOk(
+ () => testVisibility(getAcc("frame"), true, false),
+ "iframe outer doc should still be off screen"
+ );
+ await untilCacheOk(
+ () => testVisibility(iframeDoc, true, false),
+ "iframe inner doc should still be off screen"
+ );
+ await untilCacheOk(
+ () => testVisibility(iframeP, true, false),
+ "iframe content should still be off screen"
+ );
+
+ BrowserTestUtils.removeTab(newTab);
+}
+
+addAccessibleTask(
+ `
+ <div id="div" style="border:2px solid blue; width: 500px; height: 110vh;"></div>
+ <input id="input_scrolledoff">
+ <ul style="border:2px solid red; width: 100px; height: 50px; overflow: auto;">
+ <li id="li_first">item1</li><li>item2</li><li>item3</li>
+ <li>item4</li><li>item5</li><li id="li_last">item6</li>
+ </ul>
+ <iframe id="frame"></iframe>
+ `,
+ runTest,
+ { iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test div containers are reported as onscreen, even if some of their contents are
+ * offscreen.
+ */
+addAccessibleTask(
+ `
+ <div id="outer" style="width:200vw; background: green; overflow:scroll;"><div id="inner"><div style="display:inline-block; width:100vw; background:red;" id="on">on screen</div><div style="background:blue; display:inline;" id="off">offscreen</div></div></div>
+ `,
+ async function (browser, accDoc) {
+ const outer = findAccessibleChildByID(accDoc, "outer");
+ const inner = findAccessibleChildByID(accDoc, "inner");
+ const on = findAccessibleChildByID(accDoc, "on");
+ const off = findAccessibleChildByID(accDoc, "off");
+
+ await untilCacheOk(
+ () => testVisibility(outer, false, false),
+ "outer should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(inner, false, false),
+ "inner should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(on, false, false),
+ "on should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(off, true, false),
+ "off should be off screen and visible"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+// test dynamic translation
+addAccessibleTask(
+ `<div id="container" style="position: absolute; left: -300px; top: 100px;">Hello</div><button id="b" onclick="container.style.transform = 'translateX(400px)'">Move</button>`,
+ async function (browser, accDoc) {
+ const container = findAccessibleChildByID(accDoc, "container");
+ await untilCacheOk(
+ () => testVisibility(container, true, false),
+ "container should be off screen and visible"
+ );
+ await invokeContentTask(browser, [], () => {
+ let b = content.document.getElementById("b");
+ b.click();
+ });
+
+ await waitForContentPaint(browser);
+ await untilCacheOk(
+ () => testVisibility(container, false, false),
+ "container should be on screen and visible"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/states/browser_test_visibility_2.js b/accessible/tests/browser/states/browser_test_visibility_2.js
new file mode 100644
index 0000000000..ead134069a
--- /dev/null
+++ b/accessible/tests/browser/states/browser_test_visibility_2.js
@@ -0,0 +1,131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test tables, table rows are reported on screen, even if some cells of a given row are
+ * offscreen.
+ */
+addAccessibleTask(
+ `
+ <table id="table" style="width:150vw;" border><tr id="row"><td id="one" style="width:50vw;">one</td><td style="width:50vw;" id="two">two</td><td id="three">three</td></tr></table>
+ `,
+ async function (browser, accDoc) {
+ const table = findAccessibleChildByID(accDoc, "table");
+ const row = findAccessibleChildByID(accDoc, "row");
+ const one = findAccessibleChildByID(accDoc, "one");
+ const two = findAccessibleChildByID(accDoc, "two");
+ const three = findAccessibleChildByID(accDoc, "three");
+
+ await untilCacheOk(
+ () => testVisibility(table, false, false),
+ "table should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(row, false, false),
+ "row should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(one, false, false),
+ "one should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(two, false, false),
+ "two should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(three, true, false),
+ "three should be off screen and visible"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test rows and cells outside of the viewport are reported as offscreen.
+ */
+addAccessibleTask(
+ `
+ <table id="table" style="height:150vh;" border><tr style="height:100vh;" id="rowA"><td id="one">one</td></tr><tr id="rowB"><td id="two">two</td></tr></table>
+ `,
+ async function (browser, accDoc) {
+ const table = findAccessibleChildByID(accDoc, "table");
+ const rowA = findAccessibleChildByID(accDoc, "rowA");
+ const one = findAccessibleChildByID(accDoc, "one");
+ const rowB = findAccessibleChildByID(accDoc, "rowB");
+ const two = findAccessibleChildByID(accDoc, "two");
+
+ await untilCacheOk(
+ () => testVisibility(table, false, false),
+ "table should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(rowA, false, false),
+ "rowA should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(one, false, false),
+ "one should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(rowB, true, false),
+ "rowB should be off screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(two, true, false),
+ "two should be off screen and visible"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+addAccessibleTask(
+ `
+ <div id="div">hello</div>
+ `,
+ async function (browser, accDoc) {
+ let textLeaf = findAccessibleChildByID(accDoc, "div").firstChild;
+ await untilCacheOk(
+ () => testVisibility(textLeaf, false, false),
+ "text should be on screen and visible"
+ );
+ let p = waitForEvent(EVENT_TEXT_INSERTED, "div");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("div").textContent = "goodbye";
+ });
+ await p;
+ textLeaf = findAccessibleChildByID(accDoc, "div").firstChild;
+ await untilCacheOk(
+ () => testVisibility(textLeaf, false, false),
+ "text should be on screen and visible"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Overlapping, opaque divs with the same bounds should not be considered
+ * offscreen.
+ */
+addAccessibleTask(
+ `
+ <style>div { height: 5px; width: 5px; background: green; }</style>
+ <div id="outer" role="group"><div style="background:blue;" id="inner" role="group">hi</div></div>
+ `,
+ async function (browser, accDoc) {
+ const outer = findAccessibleChildByID(accDoc, "outer");
+ const inner = findAccessibleChildByID(accDoc, "inner");
+
+ await untilCacheOk(
+ () => testVisibility(outer, false, false),
+ "outer should be on screen and visible"
+ );
+ await untilCacheOk(
+ () => testVisibility(inner, false, false),
+ "inner should be on screen and visible"
+ );
+ },
+ { chrome: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/states/head.js b/accessible/tests/browser/states/head.js
new file mode 100644
index 0000000000..10c616cb80
--- /dev/null
+++ b/accessible/tests/browser/states/head.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* exported waitForIFrameA11yReady, waitForIFrameUpdates, spawnTestStates, testVisibility */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+/* import-globals-from ../../mochitest/states.js */
+/* import-globals-from ../../mochitest/role.js */
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "states.js", dir: MOCHITESTS_DIR }
+);
+
+// This is another version of addA11yLoadEvent for fission.
+async function waitForIFrameA11yReady(iFrameBrowsingContext) {
+ await SimpleTest.promiseFocus(window);
+
+ await SpecialPowers.spawn(iFrameBrowsingContext, [], () => {
+ return new Promise(resolve => {
+ function waitForDocLoad() {
+ SpecialPowers.executeSoon(() => {
+ const acc = SpecialPowers.Cc[
+ "@mozilla.org/accessibilityService;1"
+ ].getService(SpecialPowers.Ci.nsIAccessibilityService);
+
+ const accDoc = acc.getAccessibleFor(content.document);
+ let state = {};
+ accDoc.getState(state, {});
+ if (state.value & SpecialPowers.Ci.nsIAccessibleStates.STATE_BUSY) {
+ SpecialPowers.executeSoon(waitForDocLoad);
+ return;
+ }
+ resolve();
+ }, 0);
+ }
+ waitForDocLoad();
+ });
+ });
+}
+
+// A utility function to make sure the information of scroll position or visible
+// area changes reach to out-of-process iframes.
+async function waitForIFrameUpdates() {
+ // Wait for two frames since the information is notified via asynchronous IPC
+ // calls.
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ await new Promise(resolve => requestAnimationFrame(resolve));
+}
+
+// A utility function to test the state of |elementId| element in out-of-process
+// |browsingContext|.
+async function spawnTestStates(browsingContext, elementId, expectedStates) {
+ function testStates(id, expected, unexpected) {
+ const acc = SpecialPowers.Cc[
+ "@mozilla.org/accessibilityService;1"
+ ].getService(SpecialPowers.Ci.nsIAccessibilityService);
+ const target = content.document.getElementById(id);
+ let state = {};
+ acc.getAccessibleFor(target).getState(state, {});
+ if (expected === 0) {
+ Assert.equal(state.value, expected);
+ } else {
+ Assert.ok(state.value & expected);
+ }
+ Assert.ok(!(state.value & unexpected));
+ }
+ await SpecialPowers.spawn(
+ browsingContext,
+ [elementId, expectedStates],
+ testStates
+ );
+}
+
+function testVisibility(acc, shouldBeOffscreen, shouldBeInvisible) {
+ const [states] = getStates(acc);
+ let looksGood = shouldBeOffscreen == ((states & STATE_OFFSCREEN) != 0);
+ looksGood &= shouldBeInvisible == ((states & STATE_INVISIBLE) != 0);
+ return looksGood;
+}
diff --git a/accessible/tests/browser/telemetry/browser.toml b/accessible/tests/browser/telemetry/browser.toml
new file mode 100644
index 0000000000..3606d1c099
--- /dev/null
+++ b/accessible/tests/browser/telemetry/browser.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+subsuite = "a11y"
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_HCM_telemetry.js"]
+support-files = ["!/browser/components/preferences/tests/head.js"]
diff --git a/accessible/tests/browser/telemetry/browser_HCM_telemetry.js b/accessible/tests/browser/telemetry/browser_HCM_telemetry.js
new file mode 100644
index 0000000000..60ac3be82f
--- /dev/null
+++ b/accessible/tests/browser/telemetry/browser_HCM_telemetry.js
@@ -0,0 +1,407 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/preferences/tests/head.js",
+ this
+);
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+registerCleanupFunction(() => {
+ reset();
+});
+
+function reset() {
+ // This (manually) runs after every task in this test suite.
+ // We have to add this in because the initial state of
+ // `document_color_use` affects the initial state of
+ // `foreground_color`/`background_color` which can change our
+ // starting telem samples. This ensures each tasks makes no lasting
+ // state changes.
+ Services.prefs.clearUserPref("browser.display.document_color_use");
+ Services.prefs.clearUserPref("browser.display.permit_backplate");
+ Services.prefs.clearUserPref("browser.display.use_system_colors");
+ Services.prefs.clearUserPref("layout.css.always_underline_links");
+ Services.telemetry.clearEvents();
+ TelemetryTestUtils.assertNumberOfEvents(0);
+ Services.prefs.clearUserPref("browser.display.foreground_color");
+ Services.prefs.clearUserPref("browser.display.background_color");
+}
+
+async function openColorsDialog() {
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ const colorsButton =
+ gBrowser.selectedBrowser.contentDocument.getElementById("colors");
+
+ const dialogOpened = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/colors.xhtml"
+ );
+ colorsButton.doCommand();
+
+ return dialogOpened;
+}
+
+async function closeColorsDialog(dialogWin) {
+ const dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload");
+ const button = dialogWin.document
+ .getElementById("ColorsDialog")
+ .getButton("accept");
+ button.focus();
+ button.doCommand();
+ return dialogClosed;
+}
+
+function verifyBackplate(expectedValue) {
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent", false, true),
+ "a11y.backplate",
+ expectedValue,
+ "Backplate scalar is logged as " + expectedValue
+ );
+}
+
+function verifyUseSystemColors(expectedValue) {
+ const snapshot = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ ok("a11y.use_system_colors" in snapshot, "System color usage was logged.");
+ TelemetryTestUtils.assertScalar(
+ snapshot,
+ "a11y.use_system_colors",
+ expectedValue,
+ "System colors scalar is logged as " + expectedValue
+ );
+}
+
+async function verifyAlwaysUnderlineLinks(expectedValue) {
+ let snapshot = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ ok(
+ "a11y.always_underline_links" in snapshot,
+ "Always underline links was logged."
+ );
+ await TestUtils.waitForCondition(() => {
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, false);
+ return snapshot["a11y.always_underline_links"] == expectedValue;
+ }, "Always underline links has expected value " + expectedValue);
+}
+
+// The magic numbers below are the uint32_t values representing RGB white
+// and RGB black respectively. They're directly captured as nsColors and
+// follow the same bit-shift pattern.
+function testIsWhite(pref, snapshot) {
+ ok(pref in snapshot, "Scalar must be present.");
+ is(snapshot[pref], 4294967295, "Scalar is logged as white");
+}
+
+function testIsBlack(pref, snapshot) {
+ ok(pref in snapshot, "Scalar must be present.");
+ is(snapshot[pref], 4278190080, "Scalar is logged as black");
+}
+
+async function setForegroundColor(color) {
+ // Note: we set the foreground and background colors by modifying this pref
+ // instead of setting the value attribute on the color input direclty.
+ // This is because setting the value of the input with setAttribute
+ // doesn't generate the correct event to save the new value to the prefs
+ // store, so we have to do it ourselves.
+ Services.prefs.setStringPref("browser.display.foreground_color", color);
+}
+
+async function setBackgroundColor(color) {
+ Services.prefs.setStringPref("browser.display.background_color", color);
+}
+
+add_task(async function testInit() {
+ const dialogWin = await openColorsDialog();
+ const menulistHCM = dialogWin.document.getElementById("useDocumentColors");
+ if (AppConstants.platform == "win") {
+ is(
+ Services.prefs.getBoolPref("browser.display.use_system_colors"),
+ true,
+ "Use system colours pref is init'd correctly"
+ );
+ verifyUseSystemColors(true);
+
+ is(
+ menulistHCM.value,
+ "0",
+ "HCM menulist should be set to only with HCM theme on startup for windows"
+ );
+
+ // Verify correct default value
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "a11y.theme",
+ "default",
+ false
+ );
+ } else {
+ is(
+ Services.prefs.getBoolPref("browser.display.use_system_colors"),
+ false,
+ "Use system colours pref is init'd correctly"
+ );
+ verifyUseSystemColors(false);
+
+ is(
+ menulistHCM.value,
+ "1",
+ "HCM menulist should be set to never on startup for non-windows platforms"
+ );
+
+ // Verify correct default value
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "a11y.theme",
+ "always",
+ false
+ );
+
+ await closeColorsDialog(dialogWin);
+
+ // We should not have logged any colors
+ let snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ ok(
+ !("a11y.HCM_foreground" in snapshot),
+ "Foreground color shouldn't be present."
+ );
+ ok(
+ !("a11y.HCM_background" in snapshot),
+ "Background color shouldn't be present."
+ );
+
+ // If we change the colors, our probes should not be updated
+ await setForegroundColor("#ffffff"); // white
+ await setBackgroundColor("#000000"); // black
+
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ ok(
+ !("a11y.HCM_foreground" in snapshot),
+ "Foreground color shouldn't be present."
+ );
+ ok(
+ !("a11y.HCM_background" in snapshot),
+ "Background color shouldn't be present."
+ );
+ }
+
+ reset();
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function testSetAlways() {
+ const dialogWin = await openColorsDialog();
+ const menulistHCM = dialogWin.document.getElementById("useDocumentColors");
+
+ menulistHCM.doCommand();
+ const newOption = dialogWin.document.getElementById("documentColorAlways");
+ newOption.click();
+
+ is(menulistHCM.value, "2", "HCM menulist should be set to always");
+
+ await closeColorsDialog(dialogWin);
+
+ // Verify correct initial value
+ let snapshot = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ TelemetryTestUtils.assertKeyedScalar(snapshot, "a11y.theme", "never", false);
+
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ // We should have logged the default foreground and background colors
+ testIsWhite("a11y.HCM_background", snapshot);
+ testIsBlack("a11y.HCM_foreground", snapshot);
+
+ // If we change the colors, our probes update on non-windows platforms.
+ // On windows, useSystemColors is on by default, and so the values we set here
+ // will not be written to our telemetry probes, because they capture
+ // used colors, not the values of browser.foreground/background_color directly.
+
+ setBackgroundColor("#000000");
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ if (AppConstants.platform == "win") {
+ testIsWhite("a11y.HCM_background", snapshot);
+ } else {
+ testIsBlack("a11y.HCM_background", snapshot);
+ }
+
+ setForegroundColor("#ffffff");
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ if (AppConstants.platform == "win") {
+ testIsBlack("a11y.HCM_foreground", snapshot);
+ } else {
+ testIsWhite("a11y.HCM_foreground", snapshot);
+ }
+
+ reset();
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function testSetDefault() {
+ const dialogWin = await openColorsDialog();
+ const menulistHCM = dialogWin.document.getElementById("useDocumentColors");
+
+ menulistHCM.doCommand();
+ const newOption = dialogWin.document.getElementById("documentColorAutomatic");
+ newOption.click();
+
+ is(menulistHCM.value, "0", "HCM menulist should be set to default");
+
+ await closeColorsDialog(dialogWin);
+
+ // Verify correct initial value
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "a11y.theme",
+ "default",
+ false
+ );
+
+ // We should not have logged any colors
+ let snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ ok(
+ !("a11y.HCM_foreground" in snapshot),
+ "Foreground color shouldn't be present."
+ );
+ ok(
+ !("a11y.HCM_background" in snapshot),
+ "Background color shouldn't be present."
+ );
+
+ // If we change the colors, our probes should not be updated anywhere
+ await setForegroundColor("#ffffff"); // white
+ await setBackgroundColor("#000000"); // black
+
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ ok(
+ !("a11y.HCM_foreground" in snapshot),
+ "Foreground color shouldn't be present."
+ );
+ ok(
+ !("a11y.HCM_background" in snapshot),
+ "Background color shouldn't be present."
+ );
+
+ reset();
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function testSetNever() {
+ const dialogWin = await openColorsDialog();
+ const menulistHCM = dialogWin.document.getElementById("useDocumentColors");
+
+ menulistHCM.doCommand();
+ const newOption = dialogWin.document.getElementById("documentColorNever");
+ newOption.click();
+
+ is(menulistHCM.value, "1", "HCM menulist should be set to never");
+
+ await closeColorsDialog(dialogWin);
+
+ // Verify correct initial value
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "a11y.theme",
+ "always",
+ false
+ );
+
+ // We should not have logged any colors
+ let snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ ok(
+ !("a11y.HCM_foreground" in snapshot),
+ "Foreground color shouldn't be present."
+ );
+ ok(
+ !("a11y.HCM_background" in snapshot),
+ "Background color shouldn't be present."
+ );
+
+ // If we change the colors, our probes should not be updated anywhere
+ await setForegroundColor("#ffffff"); // white
+ await setBackgroundColor("#000000"); // black
+
+ snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ ok(
+ !("a11y.HCM_foreground" in snapshot),
+ "Foreground color shouldn't be present."
+ );
+ ok(
+ !("a11y.HCM_background" in snapshot),
+ "Background color shouldn't be present."
+ );
+
+ reset();
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function testBackplate() {
+ is(
+ Services.prefs.getBoolPref("browser.display.permit_backplate"),
+ true,
+ "Backplate is init'd to true"
+ );
+
+ Services.prefs.setBoolPref("browser.display.permit_backplate", false);
+ // Verify correct recorded value
+ verifyBackplate(false);
+
+ Services.prefs.setBoolPref("browser.display.permit_backplate", true);
+ // Verify correct recorded value
+ verifyBackplate(true);
+});
+
+add_task(async function testSystemColors() {
+ let expectedInitVal = false;
+ if (AppConstants.platform == "win") {
+ expectedInitVal = true;
+ }
+
+ const dialogWin = await openColorsDialog();
+ const checkbox = dialogWin.document.getElementById("browserUseSystemColors");
+ checkbox.click();
+
+ is(
+ checkbox.checked,
+ !expectedInitVal,
+ "System colors checkbox should be modified"
+ );
+
+ await closeColorsDialog(dialogWin);
+
+ verifyUseSystemColors(!expectedInitVal);
+
+ reset();
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function testAlwaysUnderlineLinks() {
+ const expectedInitVal = false;
+ await verifyAlwaysUnderlineLinks(expectedInitVal);
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ const checkbox = gBrowser.selectedBrowser.contentDocument.getElementById(
+ "alwaysUnderlineLinks"
+ );
+ is(
+ checkbox.checked,
+ expectedInitVal,
+ "Always underline links checkbox has correct initial state"
+ );
+ checkbox.click();
+
+ is(
+ checkbox.checked,
+ !expectedInitVal,
+ "Always underline links checkbox should be modified"
+ );
+ is(
+ Services.prefs.getBoolPref("layout.css.always_underline_links"),
+ !expectedInitVal,
+ "Always underline links pref reflects new value."
+ );
+ await verifyAlwaysUnderlineLinks(!expectedInitVal);
+ reset();
+ gBrowser.removeCurrentTab();
+});
diff --git a/accessible/tests/browser/text/browser.toml b/accessible/tests/browser/text/browser.toml
new file mode 100644
index 0000000000..c04531b126
--- /dev/null
+++ b/accessible/tests/browser/text/browser.toml
@@ -0,0 +1,24 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/browser/*.jsm",
+ "!/accessible/tests/mochitest/*.js",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_editabletext.js"]
+
+["browser_text.js"]
+
+["browser_text_caret.js"]
+
+["browser_text_paragraph_boundary.js"]
+
+["browser_text_selection.js"]
+
+["browser_text_spelling.js"]
+skip-if = ["true"] # Bug 1800400
+
+["browser_textleafpoint.js"]
diff --git a/accessible/tests/browser/text/browser_editabletext.js b/accessible/tests/browser/text/browser_editabletext.js
new file mode 100644
index 0000000000..0310122deb
--- /dev/null
+++ b/accessible/tests/browser/text/browser_editabletext.js
@@ -0,0 +1,173 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+async function testEditable(browser, acc, aBefore = "", aAfter = "") {
+ async function resetInput() {
+ if (acc.childCount <= 1) {
+ return;
+ }
+
+ let emptyInputEvent = waitForEvent(EVENT_TEXT_VALUE_CHANGE, "input");
+ await invokeContentTask(browser, [], async () => {
+ content.document.getElementById("input").innerHTML = "";
+ });
+
+ await emptyInputEvent;
+ }
+
+ // ////////////////////////////////////////////////////////////////////////
+ // insertText
+ await testInsertText(acc, "hello", 0, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
+ await testInsertText(acc, "ma ", 0, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ma hello", aAfter]);
+ await testInsertText(acc, "ma", 2, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "mama hello", aAfter]);
+ await testInsertText(acc, " hello", 10, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [
+ aBefore,
+ "mama hello hello",
+ aAfter,
+ ]);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // deleteText
+ await testDeleteText(acc, 0, 5, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello hello", aAfter]);
+ await testDeleteText(acc, 5, 6, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hellohello", aAfter]);
+ await testDeleteText(acc, 5, 10, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
+ await testDeleteText(acc, 0, 5, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "", aAfter]);
+
+ // XXX: clipboard operation tests don't work well with editable documents.
+ if (acc.role == ROLE_DOCUMENT) {
+ return;
+ }
+
+ await resetInput();
+
+ // copyText and pasteText
+ await testInsertText(acc, "hello", 0, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
+
+ await testCopyText(acc, 0, 1, aBefore.length, browser, "h");
+ await testPasteText(acc, 1, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hhello", aAfter]);
+
+ await testCopyText(acc, 5, 6, aBefore.length, browser, "o");
+ await testPasteText(acc, 6, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hhelloo", aAfter]);
+
+ await testCopyText(acc, 2, 3, aBefore.length, browser, "e");
+ await testPasteText(acc, 1, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hehelloo", aAfter]);
+
+ // cut & paste
+ await testCutText(acc, 0, 1, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ehelloo", aAfter]);
+ await testPasteText(acc, 2, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ehhelloo", aAfter]);
+
+ await testCutText(acc, 3, 4, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ehhlloo", aAfter]);
+ await testPasteText(acc, 6, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "ehhlloeo", aAfter]);
+
+ await testCutText(acc, 0, 8, aBefore.length);
+ await isFinalValueCorrect(browser, acc, [aBefore, "", aAfter]);
+
+ await resetInput();
+
+ // ////////////////////////////////////////////////////////////////////////
+ // setTextContents
+ await testSetTextContents(acc, "hello", aBefore.length, [
+ EVENT_TEXT_INSERTED,
+ ]);
+ await isFinalValueCorrect(browser, acc, [aBefore, "hello", aAfter]);
+ await testSetTextContents(acc, "katze", aBefore.length, [
+ EVENT_TEXT_REMOVED,
+ EVENT_TEXT_INSERTED,
+ ]);
+ await isFinalValueCorrect(browser, acc, [aBefore, "katze", aAfter]);
+}
+
+addAccessibleTask(
+ `<input id="input"/>`,
+ async function (browser, docAcc) {
+ await testEditable(browser, findAccessibleChildByID(docAcc, "input"));
+ },
+ { chrome: true, topLevel: true }
+);
+
+addAccessibleTask(
+ `<style>
+ #input::after {
+ content: "pseudo element";
+ }
+</style>
+<div id="input" contenteditable="true" role="textbox"></div>`,
+ async function (browser, docAcc) {
+ await testEditable(
+ browser,
+ findAccessibleChildByID(docAcc, "input"),
+ "",
+ "pseudo element"
+ );
+ },
+ { chrome: true, topLevel: false /* bug 1834129 */ }
+);
+
+addAccessibleTask(
+ `<style>
+ #input::before {
+ content: "pseudo element";
+ }
+</style>
+<div id="input" contenteditable="true" role="textbox"></div>`,
+ async function (browser, docAcc) {
+ await testEditable(
+ browser,
+ findAccessibleChildByID(docAcc, "input"),
+ "pseudo element"
+ );
+ },
+ { chrome: true, topLevel: false /* bug 1834129 */ }
+);
+
+addAccessibleTask(
+ `<style>
+ #input::before {
+ content: "before";
+ }
+ #input::after {
+ content: "after";
+ }
+</style>
+<div id="input" contenteditable="true" role="textbox"></div>`,
+ async function (browser, docAcc) {
+ await testEditable(
+ browser,
+ findAccessibleChildByID(docAcc, "input"),
+ "before",
+ "after"
+ );
+ },
+ { chrome: true, topLevel: false /* bug 1834129 */ }
+);
+
+addAccessibleTask(
+ ``,
+ async function (browser, docAcc) {
+ await testEditable(browser, docAcc);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ contentDocBodyAttrs: { contentEditable: "true" },
+ }
+);
diff --git a/accessible/tests/browser/text/browser_text.js b/accessible/tests/browser/text/browser_text.js
new file mode 100644
index 0000000000..79909ee412
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text.js
@@ -0,0 +1,326 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/attributes.js */
+/* import-globals-from ../../mochitest/text.js */
+loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Test line and word offsets for various cases for both local and remote
+ * Accessibles. There is more extensive coverage in ../../mochitest/text. These
+ * tests don't need to duplicate all of that, since much of the underlying code
+ * is unified. They should ensure that the cache works as expected and that
+ * there is consistency between local and remote.
+ */
+addAccessibleTask(
+ `
+<p id="br">ab cd<br>ef gh</p>
+<pre id="pre">ab cd
+ef gh</pre>
+<p id="linksStartEnd"><a href="https://example.com/">a</a>b<a href="https://example.com/">c</a></p>
+<p id="linksBreaking">a<a href="https://example.com/">b<br>c</a>d</p>
+<p id="p">a<br role="presentation">b</p>
+<p id="leafThenWrap" style="font-family: monospace; width: 2ch; word-break: break-word;"><span>a</span>bc</p>
+ `,
+ async function (browser, docAcc) {
+ for (const id of ["br", "pre"]) {
+ const acc = findAccessibleChildByID(docAcc, id);
+ testCharacterCount([acc], 11);
+ testTextAtOffset(acc, BOUNDARY_LINE_START, [
+ [0, 5, "ab cd\n", 0, 6],
+ [6, 11, "ef gh", 6, 11],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_LINE_START, [
+ [0, 5, "", 0, 0],
+ [6, 11, "ab cd\n", 0, 6],
+ ]);
+ testTextAfterOffset(acc, BOUNDARY_LINE_START, [
+ [0, 5, "ef gh", 6, 11],
+ [6, 11, "", 11, 11],
+ ]);
+ testTextAtOffset(acc, BOUNDARY_LINE_END, [
+ [0, 5, "ab cd", 0, 5],
+ [6, 11, "\nef gh", 5, 11],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_LINE_END, [
+ [0, 5, "", 0, 0],
+ [6, 11, "ab cd", 0, 5],
+ ]);
+ testTextAfterOffset(acc, BOUNDARY_LINE_END, [
+ [0, 5, "\nef gh", 5, 11],
+ [6, 11, "", 11, 11],
+ ]);
+ testTextAtOffset(acc, BOUNDARY_WORD_START, [
+ [0, 2, "ab ", 0, 3],
+ [3, 5, "cd\n", 3, 6],
+ [6, 8, "ef ", 6, 9],
+ [9, 11, "gh", 9, 11],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_WORD_START, [
+ [0, 2, "", 0, 0],
+ [3, 5, "ab ", 0, 3],
+ [6, 8, "cd\n", 3, 6],
+ [9, 11, "ef ", 6, 9],
+ ]);
+ testTextAfterOffset(acc, BOUNDARY_WORD_START, [
+ [0, 2, "cd\n", 3, 6],
+ [3, 5, "ef ", 6, 9],
+ [6, 8, "gh", 9, 11],
+ [9, 11, "", 11, 11],
+ ]);
+ testTextAtOffset(acc, BOUNDARY_WORD_END, [
+ [0, 1, "ab", 0, 2],
+ [2, 4, " cd", 2, 5],
+ [5, 7, "\nef", 5, 8],
+ [8, 11, " gh", 8, 11],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_WORD_END, [
+ [0, 2, "", 0, 0],
+ [3, 5, "ab", 0, 2],
+ // See below for offset 6.
+ [7, 8, " cd", 2, 5],
+ [9, 11, "\nef", 5, 8],
+ ]);
+ testTextBeforeOffset(acc, BOUNDARY_WORD_END, [[6, 6, " cd", 2, 5]]);
+ testTextAfterOffset(acc, BOUNDARY_WORD_END, [
+ [0, 2, " cd", 2, 5],
+ [3, 5, "\nef", 5, 8],
+ [6, 8, " gh", 8, 11],
+ [9, 11, "", 11, 11],
+ ]);
+ testTextAtOffset(acc, BOUNDARY_PARAGRAPH, [
+ [0, 5, "ab cd\n", 0, 6],
+ [6, 11, "ef gh", 6, 11],
+ ]);
+ }
+ const linksStartEnd = findAccessibleChildByID(docAcc, "linksStartEnd");
+ testTextAtOffset(linksStartEnd, BOUNDARY_LINE_START, [
+ [0, 3, `${kEmbedChar}b${kEmbedChar}`, 0, 3],
+ ]);
+ testTextAtOffset(linksStartEnd, BOUNDARY_WORD_START, [
+ [0, 3, `${kEmbedChar}b${kEmbedChar}`, 0, 3],
+ ]);
+ const linksBreaking = findAccessibleChildByID(docAcc, "linksBreaking");
+ testTextAtOffset(linksBreaking, BOUNDARY_LINE_START, [
+ [0, 0, `a${kEmbedChar}`, 0, 2],
+ [1, 1, `a${kEmbedChar}d`, 0, 3],
+ [2, 3, `${kEmbedChar}d`, 1, 3],
+ ]);
+ const p = findAccessibleChildByID(docAcc, "p");
+ testTextAtOffset(p, BOUNDARY_LINE_START, [
+ [0, 0, "a", 0, 1],
+ [1, 2, "b", 1, 2],
+ ]);
+ testTextAtOffset(p, BOUNDARY_PARAGRAPH, [[0, 2, "ab", 0, 2]]);
+ const leafThenWrap = findAccessibleChildByID(docAcc, "leafThenWrap");
+ testTextAtOffset(leafThenWrap, BOUNDARY_LINE_START, [
+ [0, 1, "ab", 0, 2],
+ [2, 3, "c", 2, 3],
+ ]);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test line offsets after text mutation.
+ */
+addAccessibleTask(
+ `
+<p id="initBr"><br></p>
+<p id="rewrap" style="font-family: monospace; width: 2ch; word-break: break-word;"><span id="rewrap1">ac</span>def</p>
+ `,
+ async function (browser, docAcc) {
+ const initBr = findAccessibleChildByID(docAcc, "initBr");
+ testTextAtOffset(initBr, BOUNDARY_LINE_START, [
+ [0, 0, "\n", 0, 1],
+ [1, 1, "", 1, 1],
+ ]);
+ info("initBr: Inserting text before br");
+ let reordered = waitForEvent(EVENT_REORDER, initBr);
+ await invokeContentTask(browser, [], () => {
+ const initBrNode = content.document.getElementById("initBr");
+ initBrNode.insertBefore(
+ content.document.createTextNode("a"),
+ initBrNode.firstElementChild
+ );
+ });
+ await reordered;
+ testTextAtOffset(initBr, BOUNDARY_LINE_START, [
+ [0, 1, "a\n", 0, 2],
+ [2, 2, "", 2, 2],
+ ]);
+
+ const rewrap = findAccessibleChildByID(docAcc, "rewrap");
+ testTextAtOffset(rewrap, BOUNDARY_LINE_START, [
+ [0, 1, "ac", 0, 2],
+ [2, 3, "de", 2, 4],
+ [4, 5, "f", 4, 5],
+ ]);
+ info("rewrap: Changing ac to abc");
+ reordered = waitForEvent(EVENT_REORDER, rewrap);
+ await invokeContentTask(browser, [], () => {
+ const rewrap1 = content.document.getElementById("rewrap1");
+ rewrap1.textContent = "abc";
+ });
+ await reordered;
+ testTextAtOffset(rewrap, BOUNDARY_LINE_START, [
+ [0, 1, "ab", 0, 2],
+ [2, 3, "cd", 2, 4],
+ [4, 6, "ef", 4, 6],
+ ]);
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test retrieval of text offsets when an invalid offset is given.
+ */
+addAccessibleTask(
+ `<p id="p">test</p>`,
+ async function (browser, docAcc) {
+ const p = findAccessibleChildByID(docAcc, "p");
+ testTextAtOffset(p, BOUNDARY_LINE_START, [[5, 5, "", 0, 0]]);
+ testTextBeforeOffset(p, BOUNDARY_LINE_START, [[5, 5, "", 0, 0]]);
+ testTextAfterOffset(p, BOUNDARY_LINE_START, [[5, 5, "", 0, 0]]);
+ },
+ {
+ // The old HyperTextAccessible implementation doesn't crash, but it returns
+ // different offsets. This doesn't matter because they're invalid either
+ // way. Since the new HyperTextAccessibleBase implementation is all we will
+ // have soon, just test that.
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test HyperText embedded object methods.
+ */
+addAccessibleTask(
+ `<div id="container">a<a id="link" href="https://example.com/">b</a>c</div>`,
+ async function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "container", [
+ nsIAccessibleHyperText,
+ ]);
+ is(container.linkCount, 1, "container linkCount is 1");
+ let link = container.getLinkAt(0);
+ queryInterfaces(link, [nsIAccessible, nsIAccessibleHyperText]);
+ is(getAccessibleDOMNodeID(link), "link", "LinkAt 0 is the link");
+ is(container.getLinkIndex(link), 0, "getLinkIndex for link is 0");
+ is(link.startIndex, 1, "link's startIndex is 1");
+ is(link.endIndex, 2, "link's endIndex is 2");
+ is(container.getLinkIndexAtOffset(1), 0, "getLinkIndexAtOffset(1) is 0");
+ is(container.getLinkIndexAtOffset(0), -1, "getLinkIndexAtOffset(0) is -1");
+ is(link.linkCount, 0, "link linkCount is 0");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test HyperText embedded object methods near a list bullet.
+ */
+addAccessibleTask(
+ `<ul><li id="li"><a id="link" href="https://example.com/">a</a></li></ul>`,
+ async function (browser, docAcc) {
+ const li = findAccessibleChildByID(docAcc, "li", [nsIAccessibleHyperText]);
+ let link = li.getLinkAt(0);
+ queryInterfaces(link, [nsIAccessible]);
+ is(getAccessibleDOMNodeID(link), "link", "LinkAt 0 is the link");
+ is(li.getLinkIndex(link), 0, "getLinkIndex for link is 0");
+ is(link.startIndex, 2, "link's startIndex is 2");
+ is(li.getLinkIndexAtOffset(2), 0, "getLinkIndexAtOffset(2) is 0");
+ is(li.getLinkIndexAtOffset(0), -1, "getLinkIndexAtOffset(0) is -1");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+const boldAttrs = { "font-weight": "700" };
+
+/**
+ * Test text attribute methods.
+ */
+addAccessibleTask(
+ `
+<p id="plain">ab</p>
+<p id="bold" style="font-weight: bold;">ab</p>
+<p id="partialBold">ab<b>cd</b>ef</p>
+<p id="consecutiveBold">ab<b>cd</b><b>ef</b>gh</p>
+<p id="embeddedObjs">ab<a href="https://example.com/">cd</a><a href="https://example.com/">ef</a><a href="https://example.com/">gh</a>ij</p>
+<p id="empty"></p>
+<p id="fontFamilies" style="font-family: sans-serif;">ab<span style="font-family: monospace;">cd</span><span style="font-family: monospace;">ef</span>gh</p>
+ `,
+ async function (browser, docAcc) {
+ let defAttrs = {
+ "text-position": "baseline",
+ "font-style": "normal",
+ "font-weight": "400",
+ };
+
+ const plain = findAccessibleChildByID(docAcc, "plain");
+ testDefaultTextAttrs(plain, defAttrs, true);
+ for (let offset = 0; offset <= 2; ++offset) {
+ testTextAttrs(plain, offset, {}, defAttrs, 0, 2, true);
+ }
+
+ const bold = findAccessibleChildByID(docAcc, "bold");
+ defAttrs["font-weight"] = "700";
+ testDefaultTextAttrs(bold, defAttrs, true);
+ testTextAttrs(bold, 0, {}, defAttrs, 0, 2, true);
+
+ const partialBold = findAccessibleChildByID(docAcc, "partialBold");
+ defAttrs["font-weight"] = "400";
+ testDefaultTextAttrs(partialBold, defAttrs, true);
+ testTextAttrs(partialBold, 0, {}, defAttrs, 0, 2, true);
+ testTextAttrs(partialBold, 2, boldAttrs, defAttrs, 2, 4, true);
+ testTextAttrs(partialBold, 4, {}, defAttrs, 4, 6, true);
+
+ const consecutiveBold = findAccessibleChildByID(docAcc, "consecutiveBold");
+ testDefaultTextAttrs(consecutiveBold, defAttrs, true);
+ testTextAttrs(consecutiveBold, 0, {}, defAttrs, 0, 2, true);
+ testTextAttrs(consecutiveBold, 2, boldAttrs, defAttrs, 2, 6, true);
+ testTextAttrs(consecutiveBold, 6, {}, defAttrs, 6, 8, true);
+
+ const embeddedObjs = findAccessibleChildByID(docAcc, "embeddedObjs");
+ testDefaultTextAttrs(embeddedObjs, defAttrs, true);
+ testTextAttrs(embeddedObjs, 0, {}, defAttrs, 0, 2, true);
+ for (let offset = 2; offset <= 4; ++offset) {
+ // attrs and defAttrs should be completely empty, so we pass
+ // false for aSkipUnexpectedAttrs.
+ testTextAttrs(embeddedObjs, offset, {}, {}, 2, 5, false);
+ }
+ testTextAttrs(embeddedObjs, 5, {}, defAttrs, 5, 7, true);
+
+ const empty = findAccessibleChildByID(docAcc, "empty");
+ testDefaultTextAttrs(empty, defAttrs, true);
+ testTextAttrs(empty, 0, {}, defAttrs, 0, 0, true);
+
+ const fontFamilies = findAccessibleChildByID(docAcc, "fontFamilies", [
+ nsIAccessibleHyperText,
+ ]);
+ testDefaultTextAttrs(fontFamilies, defAttrs, true);
+ testTextAttrs(fontFamilies, 0, {}, defAttrs, 0, 2, true);
+ testTextAttrs(fontFamilies, 2, {}, defAttrs, 2, 6, true);
+ testTextAttrs(fontFamilies, 6, {}, defAttrs, 6, 8, true);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/text/browser_text_caret.js b/accessible/tests/browser/text/browser_text_caret.js
new file mode 100644
index 0000000000..e0cea334d6
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text_caret.js
@@ -0,0 +1,452 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/text.js */
+
+/**
+ * Test caret retrieval.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea"
+ spellcheck="false"
+ style="scrollbar-width: none; font-family: 'Liberation Mono', monospace;"
+ cols="6">ab cd e</textarea>
+<textarea id="empty"></textarea>
+ `,
+ async function (browser, docAcc) {
+ const textarea = findAccessibleChildByID(docAcc, "textarea", [
+ nsIAccessibleText,
+ ]);
+ let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.takeFocus();
+ let evt = await caretMoved;
+ is(textarea.caretOffset, 0, "Initial caret offset is 0");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "a",
+ 0,
+ 1,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "ab ",
+ 0,
+ 3,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 1, "Caret offset is 1 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "b",
+ 1,
+ 2,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "ab ",
+ 0,
+ 3,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 2, "Caret offset is 2 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ " ",
+ 2,
+ 3,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "ab ",
+ 0,
+ 3,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 3, "Caret offset is 3 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "c",
+ 3,
+ 4,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "cd ",
+ 3,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 4, "Caret offset is 4 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "d",
+ 4,
+ 5,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "cd ",
+ 3,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 5, "Caret offset is 5 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ " ",
+ 5,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "cd ",
+ 3,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 6, "Caret offset is 6 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(evt.isAtEndOfLine, "Caret is at end of line");
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "",
+ 6,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "cd ",
+ 3,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "ab cd ",
+ 0,
+ 6,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 6, "Caret offset remains 6 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ // Caret is at start of second line.
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ evt = await caretMoved;
+ is(textarea.caretOffset, 7, "Caret offset is 7 after ArrowRight");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(evt.isAtEndOfLine, "Caret is at end of line");
+ // Caret is at end of textarea.
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_CHAR,
+ "",
+ 7,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_WORD_START,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+ testTextAtOffset(
+ kCaretOffset,
+ BOUNDARY_LINE_START,
+ "e",
+ 6,
+ 7,
+ textarea,
+ kOk,
+ kOk,
+ kOk
+ );
+
+ const empty = findAccessibleChildByID(docAcc, "empty", [nsIAccessibleText]);
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, empty);
+ empty.takeFocus();
+ evt = await caretMoved;
+ is(empty.caretOffset, 0, "Caret offset in empty textarea is 0");
+ evt.QueryInterface(nsIAccessibleCaretMoveEvent);
+ ok(!evt.isAtEndOfLine, "Caret is not at end of line");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
+
+/**
+ * Test setting the caret.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea">ab\nc</textarea>
+<div id="editable" contenteditable>
+ <p id="p">a<a id="link" href="https://example.com/">b</a></p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ const textarea = findAccessibleChildByID(docAcc, "textarea", [
+ nsIAccessibleText,
+ ]);
+ info("textarea: Set caret offset to 0");
+ let focused = waitForEvent(EVENT_FOCUS, textarea);
+ let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.caretOffset = 0;
+ await focused;
+ await caretMoved;
+ is(textarea.caretOffset, 0, "textarea caret correct");
+ // Test setting caret to another line.
+ info("textarea: Set caret offset to 3");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.caretOffset = 3;
+ await caretMoved;
+ is(textarea.caretOffset, 3, "textarea caret correct");
+ // Test setting caret to the end.
+ info("textarea: Set caret offset to 4 (end)");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.caretOffset = 4;
+ await caretMoved;
+ is(textarea.caretOffset, 4, "textarea caret correct");
+
+ const editable = findAccessibleChildByID(docAcc, "editable", [
+ nsIAccessibleText,
+ ]);
+ focused = waitForEvent(EVENT_FOCUS, editable);
+ editable.takeFocus();
+ await focused;
+ const p = findAccessibleChildByID(docAcc, "p", [nsIAccessibleText]);
+ info("p: Set caret offset to 0");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, p);
+ p.caretOffset = 0;
+ await focused;
+ await caretMoved;
+ is(p.caretOffset, 0, "p caret correct");
+ const link = findAccessibleChildByID(docAcc, "link", [nsIAccessibleText]);
+ info("link: Set caret offset to 0");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, link);
+ link.caretOffset = 0;
+ await caretMoved;
+ is(link.caretOffset, 0, "link caret correct");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/text/browser_text_paragraph_boundary.js b/accessible/tests/browser/text/browser_text_paragraph_boundary.js
new file mode 100644
index 0000000000..04e64520e8
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text_paragraph_boundary.js
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Test that we don't crash the parent process when querying the paragraph
+// boundary on an Accessible which has remote ProxyAccessible descendants.
+addAccessibleTask(
+ `test`,
+ async function testParagraphBoundaryWithRemoteDescendants(browser, accDoc) {
+ const root = getRootAccessible(document).QueryInterface(
+ Ci.nsIAccessibleText
+ );
+ let start = {};
+ let end = {};
+ // The offsets will change as the Firefox UI changes. We don't really care
+ // what they are, just that we don't crash.
+ root.getTextAtOffset(0, nsIAccessibleText.BOUNDARY_PARAGRAPH, start, end);
+ ok(true, "Getting paragraph boundary succeeded");
+ }
+);
diff --git a/accessible/tests/browser/text/browser_text_selection.js b/accessible/tests/browser/text/browser_text_selection.js
new file mode 100644
index 0000000000..3b47d5f36e
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text_selection.js
@@ -0,0 +1,344 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/text.js */
+
+function waitForSelectionChange(selectionAcc, caretAcc) {
+ if (!caretAcc) {
+ caretAcc = selectionAcc;
+ }
+ return waitForEvents(
+ [
+ [EVENT_TEXT_SELECTION_CHANGED, selectionAcc],
+ // We must swallow the caret events as well to avoid confusion with later,
+ // unrelated caret events.
+ [EVENT_TEXT_CARET_MOVED, caretAcc],
+ ],
+ true
+ );
+}
+
+function changeDomSelection(
+ browser,
+ anchorId,
+ anchorOffset,
+ focusId,
+ focusOffset
+) {
+ return invokeContentTask(
+ browser,
+ [anchorId, anchorOffset, focusId, focusOffset],
+ (
+ contentAnchorId,
+ contentAnchorOffset,
+ contentFocusId,
+ contentFocusOffset
+ ) => {
+ // We want the text node, so we use firstChild.
+ content.window
+ .getSelection()
+ .setBaseAndExtent(
+ content.document.getElementById(contentAnchorId).firstChild,
+ contentAnchorOffset,
+ content.document.getElementById(contentFocusId).firstChild,
+ contentFocusOffset
+ );
+ }
+ );
+}
+
+function testSelectionRange(
+ browser,
+ root,
+ startContainer,
+ startOffset,
+ endContainer,
+ endOffset
+) {
+ let selRange = root.selectionRanges.queryElementAt(0, nsIAccessibleTextRange);
+ testTextRange(
+ selRange,
+ getAccessibleDOMNodeID(root),
+ startContainer,
+ startOffset,
+ endContainer,
+ endOffset
+ );
+}
+
+/**
+ * Test text selection via keyboard.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea">ab</textarea>
+<div id="editable" contenteditable>
+ <p id="p1">a</p>
+ <p id="p2">bc</p>
+ <p id="pWithLink">d<a id="link" href="https://example.com/">e</a><span id="textAfterLink">f</span></p>
+</div>
+ `,
+ async function (browser, docAcc) {
+ queryInterfaces(docAcc, [nsIAccessibleText]);
+
+ const textarea = findAccessibleChildByID(docAcc, "textarea", [
+ nsIAccessibleText,
+ ]);
+ info("Focusing textarea");
+ let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ textarea.takeFocus();
+ await caretMoved;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 0);
+ is(textarea.selectionCount, 0, "textarea selectionCount is 0");
+ is(docAcc.selectionCount, 0, "document selectionCount is 0");
+
+ info("Selecting a in textarea");
+ let selChanged = waitForSelectionChange(textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
+ await selChanged;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 1);
+ testTextGetSelection(textarea, 0, 1, 0);
+
+ info("Selecting b in textarea");
+ selChanged = waitForSelectionChange(textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
+ await selChanged;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 2);
+ testTextGetSelection(textarea, 0, 2, 0);
+
+ info("Unselecting b in textarea");
+ selChanged = waitForSelectionChange(textarea);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ await selChanged;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 1);
+ testTextGetSelection(textarea, 0, 1, 0);
+
+ info("Unselecting a in textarea");
+ // We don't fire selection changed when the selection collapses.
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ await caretMoved;
+ testSelectionRange(browser, textarea, textarea, 0, textarea, 0);
+ is(textarea.selectionCount, 0, "textarea selectionCount is 0");
+
+ const editable = findAccessibleChildByID(docAcc, "editable", [
+ nsIAccessibleText,
+ ]);
+ const p1 = findAccessibleChildByID(docAcc, "p1", [nsIAccessibleText]);
+ info("Focusing editable, caret to start");
+ caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, p1);
+ await changeDomSelection(browser, "p1", 0, "p1", 0);
+ await caretMoved;
+ testSelectionRange(browser, editable, p1, 0, p1, 0);
+ is(editable.selectionCount, 0, "editable selectionCount is 0");
+ is(p1.selectionCount, 0, "p1 selectionCount is 0");
+ is(docAcc.selectionCount, 0, "document selectionCount is 0");
+
+ info("Selecting a in editable");
+ selChanged = waitForSelectionChange(p1);
+ await changeDomSelection(browser, "p1", 0, "p1", 1);
+ await selChanged;
+ testSelectionRange(browser, editable, p1, 0, p1, 1);
+ testTextGetSelection(editable, 0, 1, 0);
+ testTextGetSelection(p1, 0, 1, 0);
+ const p2 = findAccessibleChildByID(docAcc, "p2", [nsIAccessibleText]);
+ if (browser.isRemoteBrowser) {
+ is(p2.selectionCount, 0, "p2 selectionCount is 0");
+ } else {
+ todo(
+ false,
+ "Siblings report wrong selection in non-cache implementation"
+ );
+ }
+
+ // Selecting across two Accessibles with only a partial selection in the
+ // second.
+ info("Selecting ab in editable");
+ selChanged = waitForSelectionChange(editable, p2);
+ await changeDomSelection(browser, "p1", 0, "p2", 1);
+ await selChanged;
+ testSelectionRange(browser, editable, p1, 0, p2, 1);
+ testTextGetSelection(editable, 0, 2, 0);
+ testTextGetSelection(p1, 0, 1, 0);
+ testTextGetSelection(p2, 0, 1, 0);
+
+ const pWithLink = findAccessibleChildByID(docAcc, "pWithLink", [
+ nsIAccessibleText,
+ ]);
+ const link = findAccessibleChildByID(docAcc, "link", [nsIAccessibleText]);
+ // Selecting both text and a link.
+ info("Selecting de in editable");
+ selChanged = waitForSelectionChange(pWithLink, link);
+ await changeDomSelection(browser, "pWithLink", 0, "link", 1);
+ await selChanged;
+ testSelectionRange(browser, editable, pWithLink, 0, link, 1);
+ testTextGetSelection(editable, 2, 3, 0);
+ testTextGetSelection(pWithLink, 0, 2, 0);
+ testTextGetSelection(link, 0, 1, 0);
+
+ // Selecting a link and text on either side.
+ info("Selecting def in editable");
+ selChanged = waitForSelectionChange(pWithLink, pWithLink);
+ await changeDomSelection(browser, "pWithLink", 0, "textAfterLink", 1);
+ await selChanged;
+ testSelectionRange(browser, editable, pWithLink, 0, pWithLink, 3);
+ testTextGetSelection(editable, 2, 3, 0);
+ testTextGetSelection(pWithLink, 0, 3, 0);
+ testTextGetSelection(link, 0, 1, 0);
+
+ // Noncontiguous selection.
+ info("Selecting a in editable");
+ selChanged = waitForSelectionChange(p1);
+ await changeDomSelection(browser, "p1", 0, "p1", 1);
+ await selChanged;
+ info("Adding c to selection in editable");
+ selChanged = waitForSelectionChange(p2);
+ await invokeContentTask(browser, [], () => {
+ const r = content.document.createRange();
+ const p2text = content.document.getElementById("p2").firstChild;
+ r.setStart(p2text, 0);
+ r.setEnd(p2text, 1);
+ content.window.getSelection().addRange(r);
+ });
+ await selChanged;
+ let selRanges = editable.selectionRanges;
+ is(selRanges.length, 2, "2 selection ranges");
+ testTextRange(
+ selRanges.queryElementAt(0, nsIAccessibleTextRange),
+ "range 0",
+ p1,
+ 0,
+ p1,
+ 1
+ );
+ testTextRange(
+ selRanges.queryElementAt(1, nsIAccessibleTextRange),
+ "range 1",
+ p2,
+ 0,
+ p2,
+ 1
+ );
+ is(editable.selectionCount, 2, "editable selectionCount is 2");
+ testTextGetSelection(editable, 0, 1, 0);
+ testTextGetSelection(editable, 1, 2, 1);
+ if (browser.isRemoteBrowser) {
+ is(p1.selectionCount, 1, "p1 selectionCount is 1");
+ testTextGetSelection(p1, 0, 1, 0);
+ is(p2.selectionCount, 1, "p2 selectionCount is 1");
+ testTextGetSelection(p2, 0, 1, 0);
+ } else {
+ todo(
+ false,
+ "Siblings report wrong selection in non-cache implementation"
+ );
+ }
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Tabbing to an input selects all its text. Test that the cached selection
+ *reflects this. This has to be done separately from the other selection tests
+ * because prior contentEditable selection changes the events that get fired.
+ */
+addAccessibleTask(
+ `
+<button id="before">Before</button>
+<input id="input" value="test">
+ `,
+ async function (browser, docAcc) {
+ // The tab order is different when there's an iframe, so focus a control
+ // before the input to make tab consistent.
+ info("Focusing before");
+ const before = findAccessibleChildByID(docAcc, "before");
+ // Focusing a button fires a selection event. We must swallow this to
+ // avoid confusing the later test.
+ let events = waitForOrderedEvents([
+ [EVENT_FOCUS, before],
+ [EVENT_TEXT_SELECTION_CHANGED, docAcc],
+ ]);
+ before.takeFocus();
+ await events;
+
+ const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]);
+ info("Tabbing to input");
+ events = waitForEvents(
+ {
+ expected: [
+ [EVENT_FOCUS, input],
+ [EVENT_TEXT_SELECTION_CHANGED, input],
+ ],
+ unexpected: [[EVENT_TEXT_SELECTION_CHANGED, docAcc]],
+ },
+ "input",
+ false,
+ (args, task) => invokeContentTask(browser, args, task)
+ );
+ EventUtils.synthesizeKey("KEY_Tab");
+ await events;
+ testSelectionRange(browser, input, input, 0, input, 4);
+ testTextGetSelection(input, 0, 4, 0);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Test text selection via API.
+ */
+addAccessibleTask(
+ `
+ <p id="paragraph">hello world</p>
+ <ol>
+ <li id="li">Number one</li>
+ </ol>
+ `,
+ async function (browser, docAcc) {
+ const paragraph = findAccessibleChildByID(docAcc, "paragraph", [
+ nsIAccessibleText,
+ ]);
+
+ let selChanged = waitForSelectionChange(paragraph);
+ paragraph.setSelectionBounds(0, 2, 4);
+ await selChanged;
+ testTextGetSelection(paragraph, 2, 4, 0);
+
+ selChanged = waitForSelectionChange(paragraph);
+ paragraph.addSelection(6, 10);
+ await selChanged;
+ testTextGetSelection(paragraph, 6, 10, 1);
+ is(paragraph.selectionCount, 2, "paragraph selectionCount is 2");
+
+ selChanged = waitForSelectionChange(paragraph);
+ paragraph.removeSelection(0);
+ await selChanged;
+ testTextGetSelection(paragraph, 6, 10, 0);
+ is(paragraph.selectionCount, 1, "paragraph selectionCount is 1");
+
+ const li = findAccessibleChildByID(docAcc, "li", [nsIAccessibleText]);
+
+ selChanged = waitForSelectionChange(li);
+ li.setSelectionBounds(0, 1, 8);
+ await selChanged;
+ testTextGetSelection(li, 3, 8, 0);
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/text/browser_text_spelling.js b/accessible/tests/browser/text/browser_text_spelling.js
new file mode 100644
index 0000000000..14c5c16be4
--- /dev/null
+++ b/accessible/tests/browser/text/browser_text_spelling.js
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/text.js */
+/* import-globals-from ../../mochitest/attributes.js */
+loadScripts(
+ { name: "text.js", dir: MOCHITESTS_DIR },
+ { name: "attributes.js", dir: MOCHITESTS_DIR }
+);
+
+const boldAttrs = { "font-weight": "700" };
+
+/*
+ * Given a text accessible and a list of ranges
+ * check if those ranges match the misspelled ranges in the accessible.
+ */
+function misspelledRangesMatch(acc, ranges) {
+ let offset = 0;
+ let expectedRanges = [...ranges];
+ let charCount = acc.characterCount;
+ while (offset < charCount) {
+ let start = {};
+ let end = {};
+ let attributes = acc.getTextAttributes(false, offset, start, end);
+ offset = end.value;
+ try {
+ if (attributes.getStringProperty("invalid") == "spelling") {
+ let expected = expectedRanges.shift();
+ if (
+ !expected ||
+ expected[0] != start.value ||
+ expected[1] != end.value
+ ) {
+ return false;
+ }
+ }
+ } catch (err) {}
+ }
+
+ return !expectedRanges.length;
+}
+
+/*
+ * Returns a promise that resolves after a text attribute changed event
+ * brings us to a state where the misspelled ranges match.
+ */
+async function waitForMisspelledRanges(acc, ranges) {
+ await waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED);
+ await untilCacheOk(
+ () => misspelledRangesMatch(acc, ranges),
+ `Misspelled ranges match: ${JSON.stringify(ranges)}`
+ );
+}
+
+/**
+ * Test spelling errors.
+ */
+addAccessibleTask(
+ `
+<textarea id="textarea" spellcheck="true">test tset tset test</textarea>
+<div contenteditable id="editable" spellcheck="true">plain<span> ts</span>et <b>bold</b></div>
+ `,
+ async function (browser, docAcc) {
+ const textarea = findAccessibleChildByID(docAcc, "textarea", [
+ nsIAccessibleText,
+ ]);
+ info("Focusing textarea");
+ let spellingChanged = waitForMisspelledRanges(textarea, [
+ [5, 9],
+ [10, 14],
+ ]);
+ textarea.takeFocus();
+ await spellingChanged;
+
+ // Test removal of a spelling error.
+ info('textarea: Changing first "tset" to "test"');
+ // setTextRange fires multiple EVENT_TEXT_ATTRIBUTE_CHANGED, so replace by
+ // selecting and typing instead.
+ spellingChanged = waitForMisspelledRanges(textarea, [[10, 14]]);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("textarea").setSelectionRange(5, 9);
+ });
+ EventUtils.sendString("test");
+ // Move the cursor to trigger spell check.
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ await spellingChanged;
+
+ // Test addition of a spelling error.
+ info('textarea: Changing it back to "tset"');
+ spellingChanged = waitForMisspelledRanges(textarea, [
+ [5, 9],
+ [10, 14],
+ ]);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("textarea").setSelectionRange(5, 9);
+ });
+ EventUtils.sendString("tset");
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ await spellingChanged;
+
+ // Ensure that changing the text without changing any spelling errors
+ // correctly updates offsets.
+ info('textarea: Changing first "test" to "the"');
+ // Spelling errors don't change, so we won't get
+ // EVENT_TEXT_ATTRIBUTE_CHANGED. We change the text, wait for the insertion
+ // and then select a character so we know when the change is done.
+ let inserted = waitForEvent(EVENT_TEXT_INSERTED, textarea);
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("textarea").setSelectionRange(0, 4);
+ });
+ EventUtils.sendString("the");
+ await inserted;
+ let selected = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, textarea);
+ EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true });
+ await selected;
+ const expectedRanges = [
+ [4, 8],
+ [9, 13],
+ ];
+ await untilCacheOk(
+ () => misspelledRangesMatch(textarea, expectedRanges),
+ `Misspelled ranges match: ${JSON.stringify(expectedRanges)}`
+ );
+
+ const editable = findAccessibleChildByID(docAcc, "editable", [
+ nsIAccessibleText,
+ ]);
+ info("Focusing editable");
+ spellingChanged = waitForMisspelledRanges(editable, [[6, 10]]);
+ editable.takeFocus();
+ await spellingChanged;
+ // Test normal text and spelling errors crossing text nodes.
+ testTextAttrs(editable, 0, {}, {}, 0, 6, true); // "plain "
+ // Ensure we detect the spelling error even though there is a style change
+ // after it.
+ testTextAttrs(editable, 6, { invalid: "spelling" }, {}, 6, 10, true); // "tset"
+ testTextAttrs(editable, 10, {}, {}, 10, 11, true); // " "
+ // Ensure a style change is still detected in the presence of a spelling
+ // error.
+ testTextAttrs(editable, 11, boldAttrs, {}, 11, 15, true); // "bold"
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/text/browser_textleafpoint.js b/accessible/tests/browser/text/browser_textleafpoint.js
new file mode 100644
index 0000000000..344a01e2d3
--- /dev/null
+++ b/accessible/tests/browser/text/browser_textleafpoint.js
@@ -0,0 +1,524 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/text.js */
+
+addAccessibleTask(
+ `
+ <p id="p" style="white-space: pre-line;">A bug
+h<a href="#">id i</a>n
+the <strong>big</strong> rug.</p>
+`,
+ function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "p");
+ const firstPoint = createTextLeafPoint(container, 0);
+ const lastPoint = createTextLeafPoint(container, kTextEndOffset);
+
+ let charSequence = [
+ ...textBoundaryGenerator(firstPoint, BOUNDARY_CHAR, DIRECTION_NEXT),
+ ];
+
+ testPointEqual(
+ firstPoint,
+ charSequence[0],
+ "Point constructed via container and offset 0 is first character point."
+ );
+ testPointEqual(
+ lastPoint,
+ charSequence[charSequence.length - 1],
+ "Point constructed via container and kTextEndOffset is last character point."
+ );
+
+ const expectedCharSequence = [
+ ["A bug\nh", 0],
+ ["A bug\nh", 1],
+ ["A bug\nh", 2],
+ ["A bug\nh", 3],
+ ["A bug\nh", 4],
+ ["A bug\nh", 5],
+ ["A bug\nh", 6],
+ ["id i", 0],
+ ["id i", 1],
+ ["id i", 2],
+ ["id i", 3],
+ ["n\nthe ", 0],
+ ["n\nthe ", 1],
+ ["n\nthe ", 2],
+ ["n\nthe ", 3],
+ ["n\nthe ", 4],
+ ["n\nthe ", 5],
+ ["big", 0],
+ ["big", 1],
+ ["big", 2],
+ [" rug.", 0],
+ [" rug.", 1],
+ [" rug.", 2],
+ [" rug.", 3],
+ [" rug.", 4],
+ [" rug.", 5],
+ ];
+
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_NEXT,
+ expectedCharSequence,
+ "Forward BOUNDARY_CHAR sequence is correct"
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_PREVIOUS,
+ [...expectedCharSequence].reverse(),
+ "Backward BOUNDARY_CHAR sequence is correct"
+ );
+
+ const expectedWordStartSequence = [
+ ["A bug\nh", 0],
+ ["A bug\nh", 2],
+ ["A bug\nh", 6],
+ ["id i", 3],
+ ["n\nthe ", 2],
+ ["big", 0],
+ [" rug.", 1],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_NEXT,
+ // Add last point in doc
+ [...expectedWordStartSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_WORD_START sequence is correct"
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_PREVIOUS,
+ [...expectedWordStartSequence].reverse(),
+ "Backward BOUNDARY_WORD_START sequence is correct"
+ );
+
+ const expectedWordEndSequence = [
+ ["A bug\nh", 1],
+ ["A bug\nh", 5],
+ ["id i", 2],
+ ["n\nthe ", 1],
+ ["n\nthe ", 5],
+ [" rug.", 0],
+ [" rug.", 5],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_NEXT,
+ expectedWordEndSequence,
+ "Forward BOUNDARY_WORD_END sequence is correct"
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_PREVIOUS,
+ [readablePoint(firstPoint), ...expectedWordEndSequence].reverse(),
+ "Backward BOUNDARY_WORD_END sequence is correct"
+ );
+
+ const expectedLineStartSequence = [
+ ["A bug\nh", 0],
+ ["A bug\nh", 6],
+ ["n\nthe ", 2],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_LINE_START,
+ DIRECTION_NEXT,
+ // Add last point in doc
+ [...expectedLineStartSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_LINE_START sequence is correct"
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_LINE_START,
+ DIRECTION_PREVIOUS,
+ [...expectedLineStartSequence].reverse(),
+ "Backward BOUNDARY_LINE_START sequence is correct"
+ );
+
+ const expectedLineEndSequence = [
+ ["A bug\nh", 5],
+ ["n\nthe ", 1],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_LINE_END,
+ DIRECTION_NEXT,
+ // Add last point in doc
+ [...expectedLineEndSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_LINE_END sequence is correct",
+ { todo: true }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_LINE_END,
+ DIRECTION_PREVIOUS,
+ [readablePoint(firstPoint), ...expectedLineEndSequence].reverse(),
+ "Backward BOUNDARY_LINE_END sequence is correct"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+addAccessibleTask(
+ `<p id="p">
+ Rob ca<input id="i1" value="n m">op up.
+ </p>`,
+ function (browser, docAcc) {
+ const container = findAccessibleChildByID(docAcc, "p");
+ const firstPoint = createTextLeafPoint(container, 0);
+ const lastPoint = createTextLeafPoint(container, kTextEndOffset);
+
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_NEXT,
+ [
+ ["Rob ca", 0],
+ ["Rob ca", 1],
+ ["Rob ca", 2],
+ ["Rob ca", 3],
+ ["Rob ca", 4],
+ ["Rob ca", 5],
+ ["n m", 0],
+ ["n m", 1],
+ ["n m", 2],
+ ["n m", 3],
+ ],
+ "Forward BOUNDARY_CHAR sequence when stopping in editable is correct",
+ { flags: BOUNDARY_FLAG_STOP_IN_EDITABLE }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_PREVIOUS,
+ [
+ ["op up. ", 7],
+ ["op up. ", 6],
+ ["op up. ", 5],
+ ["op up. ", 4],
+ ["op up. ", 3],
+ ["op up. ", 2],
+ ["op up. ", 1],
+ ["op up. ", 0],
+ ["n m", 2],
+ ["n m", 1],
+ ["n m", 0],
+ ],
+ "Backward BOUNDARY_CHAR sequence when stopping in editable is correct",
+ { flags: BOUNDARY_FLAG_STOP_IN_EDITABLE }
+ );
+
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_NEXT,
+ [
+ ["Rob ca", 0],
+ ["Rob ca", 4],
+ ["n m", 2],
+ ],
+ "Forward BOUNDARY_WORD_START sequence when stopping in editable is correct",
+ {
+ flags: BOUNDARY_FLAG_STOP_IN_EDITABLE,
+ todo: true, // Shouldn't consider end of input a word start
+ }
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+addAccessibleTask(
+ `
+ <p id="p" style="white-space: pre-line;">A bug
+on a <span style="display: block;">rug</span></p>
+ <p id="p2">
+ Numbers:
+ </p>
+ <ul>
+ <li>One</li>
+ <li>Two</li>
+ <li>Three</li>
+ </ul>`,
+ function (browser, docAcc) {
+ const firstPoint = createTextLeafPoint(docAcc, 0);
+ const lastPoint = createTextLeafPoint(docAcc, kTextEndOffset);
+
+ const expectedParagraphStart = [
+ ["A bug\non a ", 0],
+ ["A bug\non a ", 6],
+ ["rug", 0],
+ ["Numbers: ", 0],
+ ["• ", 0],
+ ["• ", 0],
+ ["• ", 0],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_PARAGRAPH,
+ DIRECTION_NEXT,
+ [...expectedParagraphStart, readablePoint(lastPoint)],
+ "Forward BOUNDARY_PARAGRAPH sequence is correct"
+ );
+
+ const paragraphStart = createTextLeafPoint(
+ findAccessibleChildByID(docAcc, "p2").firstChild,
+ 0
+ );
+ const wordEnd = paragraphStart.findBoundary(
+ BOUNDARY_WORD_END,
+ DIRECTION_NEXT,
+ BOUNDARY_FLAG_INCLUDE_ORIGIN
+ );
+ testPointEqual(
+ wordEnd,
+ paragraphStart,
+ "The word end from the previous block is the first point in this block"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+// Test for skipping list item bullets.
+addAccessibleTask(
+ `<ul>
+ <li>One</li>
+ <li>Two</li>
+ <li style="white-space: pre-line;">Three
+Four</li>
+ </ul>`,
+ function (browser, docAcc) {
+ const firstPoint = createTextLeafPoint(docAcc, 0);
+ const lastPoint = createTextLeafPoint(docAcc, kTextEndOffset);
+
+ const firstNonMarkerPoint = firstPoint.findBoundary(
+ BOUNDARY_CHAR,
+ DIRECTION_NEXT,
+ BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER | BOUNDARY_FLAG_INCLUDE_ORIGIN
+ );
+ Assert.deepEqual(
+ readablePoint(firstNonMarkerPoint),
+ ["One", 0],
+ "First non-marker point is correct"
+ );
+
+ const expectedParagraphStart = [
+ ["One", 0],
+ ["Two", 0],
+ ["Three\nFour", 0],
+ ["Three\nFour", 6],
+ ];
+
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_PARAGRAPH,
+ DIRECTION_NEXT,
+ [...expectedParagraphStart, readablePoint(lastPoint)],
+ "Forward BOUNDARY_PARAGRAPH skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_PARAGRAPH,
+ DIRECTION_PREVIOUS,
+ [...expectedParagraphStart].reverse(),
+ "Backward BOUNDARY_PARAGRAPH skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ const expectedCharSequence = [
+ ["One", 0],
+ ["One", 1],
+ ["One", 2],
+ ["Two", 0],
+ ["Two", 1],
+ ["Two", 2],
+ ["Three\nFour", 0],
+ ["Three\nFour", 1],
+ ["Three\nFour", 2],
+ ["Three\nFour", 3],
+ ["Three\nFour", 4],
+ ["Three\nFour", 5],
+ ["Three\nFour", 6],
+ ["Three\nFour", 7],
+ ["Three\nFour", 8],
+ ["Three\nFour", 9],
+ ["Three\nFour", 10],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_NEXT,
+ expectedCharSequence,
+ "Forward BOUNDARY_CHAR skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_CHAR,
+ DIRECTION_PREVIOUS,
+ [...expectedCharSequence].reverse(),
+ "Backward BOUNDARY_CHAR skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ const expectedWordStartSequence = [
+ ["One", 0],
+ ["Two", 0],
+ ["Three\nFour", 0],
+ ["Three\nFour", 6],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_NEXT,
+ [...expectedWordStartSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_WORD_START skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_PREVIOUS,
+ [...expectedWordStartSequence].reverse(),
+ "Backward BOUNDARY_WORD_START skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ const expectedWordEndSequence = [
+ ["Two", 0],
+ ["Three\nFour", 0],
+ ["Three\nFour", 5],
+ ["Three\nFour", 10],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_NEXT,
+ expectedWordEndSequence,
+ "Forward BOUNDARY_WORD_END skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_PREVIOUS,
+ [
+ readablePoint(firstNonMarkerPoint),
+ ...expectedWordEndSequence,
+ ].reverse(),
+ "Backward BOUNDARY_WORD_END skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ const expectedLineStartSequence = [
+ ["One", 0],
+ ["Two", 0],
+ ["Three\nFour", 0],
+ ["Three\nFour", 6],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_LINE_START,
+ DIRECTION_NEXT,
+ // Add last point in doc
+ [...expectedLineStartSequence, readablePoint(lastPoint)],
+ "Forward BOUNDARY_LINE_START skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_LINE_START,
+ DIRECTION_PREVIOUS,
+ // Add last point in doc
+ [...expectedLineStartSequence].reverse(),
+ "Backward BOUNDARY_LINE_START skipping list item markers sequence is correct",
+ { flags: BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER }
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+/**
+ * Test the paragraph boundary on tables.
+ */
+addAccessibleTask(
+ `
+<table id="table">
+ <tr><th>a</th><td>b</td></tr>
+ <tr><td>c</td><td>d</td></tr>
+</table>
+ `,
+ async function (browser, docAcc) {
+ const firstPoint = createTextLeafPoint(docAcc, 0);
+ const lastPoint = createTextLeafPoint(docAcc, kTextEndOffset);
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_PARAGRAPH,
+ DIRECTION_NEXT,
+ [["a", 0], ["b", 0], ["c", 0], ["d", 0], readablePoint(lastPoint)],
+ "Forward BOUNDARY_PARAGRAPH sequence is correct"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
+
+/*
+ * Test the word boundary with punctuation character
+ */
+addAccessibleTask(
+ `
+<p>ab'cd</p>
+ `,
+ async function (browser, docAcc) {
+ const firstPoint = createTextLeafPoint(docAcc, 0);
+ const lastPoint = createTextLeafPoint(docAcc, kTextEndOffset);
+
+ const expectedWordStartSequence = [
+ ["ab'cd", 0],
+ ["ab'cd", 3],
+ ["ab'cd", 5],
+ ];
+ testBoundarySequence(
+ firstPoint,
+ BOUNDARY_WORD_START,
+ DIRECTION_NEXT,
+ expectedWordStartSequence,
+ "Forward BOUNDARY_WORD_START sequence is correct"
+ );
+ const expectedWordEndSequence = [
+ ["ab'cd", 5],
+ ["ab'cd", 3],
+ ["ab'cd", 0],
+ ];
+ testBoundarySequence(
+ lastPoint,
+ BOUNDARY_WORD_END,
+ DIRECTION_PREVIOUS,
+ expectedWordEndSequence,
+ "Backward BOUNDARY_WORD_END sequence is correct"
+ );
+ },
+ { chrome: true, topLevel: true, iframe: false, remoteIframe: false }
+);
diff --git a/accessible/tests/browser/text/head.js b/accessible/tests/browser/text/head.js
new file mode 100644
index 0000000000..fa4b095892
--- /dev/null
+++ b/accessible/tests/browser/text/head.js
@@ -0,0 +1,276 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* exported createTextLeafPoint, DIRECTION_NEXT, DIRECTION_PREVIOUS,
+ BOUNDARY_FLAG_DEFAULT, BOUNDARY_FLAG_INCLUDE_ORIGIN,
+ BOUNDARY_FLAG_STOP_IN_EDITABLE, BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER,
+ readablePoint, testPointEqual, textBoundaryGenerator, testBoundarySequence,
+ isFinalValueCorrect, isFinalValueCorrect, testInsertText, testDeleteText,
+ testCopyText, testPasteText, testCutText, testSetTextContents */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+
+/* import-globals-from ../../mochitest/role.js */
+
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "text.js", dir: MOCHITESTS_DIR },
+ { name: "role.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
+
+const DIRECTION_NEXT = Ci.nsIAccessibleTextLeafPoint.DIRECTION_NEXT;
+const DIRECTION_PREVIOUS = Ci.nsIAccessibleTextLeafPoint.DIRECTION_PREVIOUS;
+
+const BOUNDARY_FLAG_DEFAULT =
+ Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_DEFAULT;
+const BOUNDARY_FLAG_INCLUDE_ORIGIN =
+ Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_INCLUDE_ORIGIN;
+const BOUNDARY_FLAG_STOP_IN_EDITABLE =
+ Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_STOP_IN_EDITABLE;
+const BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER =
+ Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER;
+
+function createTextLeafPoint(acc, offset) {
+ let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ nsIAccessibilityService
+ );
+
+ return accService.createTextLeafPoint(acc, offset);
+}
+
+// Converts an nsIAccessibleTextLeafPoint into a human/machine
+// readable tuple with a readable accessible and the offset within it.
+// For a point text leaf it would look like this: ["hello", 2],
+// For a point in an empty input it would look like this ["input#name", 0]
+function readablePoint(point) {
+ const readableLeaf = acc => {
+ let tagName = getAccessibleTagName(acc);
+ if (tagName && !tagName.startsWith("_moz_generated")) {
+ let domNodeID = getAccessibleDOMNodeID(acc);
+ if (domNodeID) {
+ return `${tagName}#${domNodeID}`;
+ }
+ return tagName;
+ }
+
+ return acc.name;
+ };
+
+ return [readableLeaf(point.accessible), point.offset];
+}
+
+function sequenceEqual(val, expected, msg) {
+ Assert.deepEqual(val, expected, msg);
+}
+
+// eslint-disable-next-line camelcase
+function sequenceEqualTodo(val, expected, msg) {
+ todo_is(JSON.stringify(val), JSON.stringify(expected), msg);
+}
+
+function pointsEqual(pointA, pointB) {
+ return (
+ pointA.offset == pointB.offset && pointA.accessible == pointB.accessible
+ );
+}
+
+function testPointEqual(pointA, pointB, msg) {
+ is(pointA.offset, pointB.offset, `Offset mismatch - ${msg}`);
+ is(pointA.accessible, pointB.accessible, `Accessible mismatch - ${msg}`);
+}
+
+function* textBoundaryGenerator(
+ firstPoint,
+ boundaryType,
+ direction,
+ flags = BOUNDARY_FLAG_DEFAULT
+) {
+ // Our start point should be inclusive of the given point.
+ let nextLeafPoint = firstPoint.findBoundary(
+ boundaryType,
+ direction,
+ flags | BOUNDARY_FLAG_INCLUDE_ORIGIN
+ );
+ let textLeafPoint = null;
+
+ do {
+ textLeafPoint = nextLeafPoint;
+ yield textLeafPoint;
+ nextLeafPoint = textLeafPoint.findBoundary(boundaryType, direction, flags);
+ } while (!pointsEqual(textLeafPoint, nextLeafPoint));
+}
+
+// This function takes FindBoundary arguments and an expected sequence
+// of boundary points formatted with readablePoint.
+// For example, word starts would look like this:
+// [["one two", 0], ["one two", 4], ["one two", 7]]
+function testBoundarySequence(
+ startPoint,
+ boundaryType,
+ direction,
+ expectedSequence,
+ msg,
+ options = {}
+) {
+ let sequence = [
+ ...textBoundaryGenerator(
+ startPoint,
+ boundaryType,
+ direction,
+ options.flags ? options.flags : BOUNDARY_FLAG_DEFAULT
+ ),
+ ];
+ (options.todo ? sequenceEqualTodo : sequenceEqual)(
+ sequence.map(readablePoint),
+ expectedSequence,
+ msg
+ );
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Editable text
+
+async function waitForCopy(browser) {
+ await BrowserTestUtils.waitForContentEvent(browser, "copy", false, evt => {
+ return true;
+ });
+
+ let clipboardData = await invokeContentTask(browser, [], async () => {
+ let text = await content.navigator.clipboard.readText();
+ return text;
+ });
+
+ return clipboardData;
+}
+
+async function isFinalValueCorrect(
+ browser,
+ acc,
+ expectedTextLeafs,
+ msg = "Value is correct"
+) {
+ let value =
+ acc.role == ROLE_ENTRY
+ ? acc.value
+ : await invokeContentTask(browser, [], () => {
+ return content.document.body.textContent;
+ });
+
+ let [before, text, after] = expectedTextLeafs;
+ let finalValue =
+ before && after && !text
+ ? [before, after].join(" ")
+ : [before, text, after].join("");
+
+ is(value.replace("\xa0", " "), finalValue, msg);
+}
+
+function waitForTextChangeEvents(acc, eventSeq) {
+ let events = eventSeq.map(eventType => {
+ return [eventType, acc];
+ });
+
+ if (acc.role == ROLE_ENTRY) {
+ events.push([EVENT_TEXT_VALUE_CHANGE, acc]);
+ }
+
+ return waitForEvents(events);
+}
+
+async function testSetTextContents(acc, text, staticContentOffset, events) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+ let evtPromise = waitForTextChangeEvents(acc, events);
+ acc.setTextContents(text);
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(evt.start, staticContentOffset);
+}
+
+async function testInsertText(
+ acc,
+ textToInsert,
+ insertOffset,
+ staticContentOffset
+) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+
+ let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_INSERTED]);
+ acc.insertText(textToInsert, staticContentOffset + insertOffset);
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(evt.start, staticContentOffset + insertOffset);
+}
+
+async function testDeleteText(
+ acc,
+ startOffset,
+ endOffset,
+ staticContentOffset
+) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+
+ let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_REMOVED]);
+ acc.deleteText(
+ staticContentOffset + startOffset,
+ staticContentOffset + endOffset
+ );
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(evt.start, staticContentOffset + startOffset);
+}
+
+async function testCopyText(
+ acc,
+ startOffset,
+ endOffset,
+ staticContentOffset,
+ browser,
+ aExpectedClipboard = null
+) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+ let copied = waitForCopy(browser);
+ acc.copyText(
+ staticContentOffset + startOffset,
+ staticContentOffset + endOffset
+ );
+ let clipboardText = await copied;
+ if (aExpectedClipboard != null) {
+ is(clipboardText, aExpectedClipboard, "Correct text in clipboard");
+ }
+}
+
+async function testPasteText(acc, insertOffset, staticContentOffset) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+ let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_INSERTED]);
+ acc.pasteText(staticContentOffset + insertOffset);
+
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ // XXX: In non-headless mode pasting text produces several text leaves
+ // and the offset is not what we expect.
+ // is(evt.start, staticContentOffset + insertOffset);
+}
+
+async function testCutText(acc, startOffset, endOffset, staticContentOffset) {
+ acc.QueryInterface(nsIAccessibleEditableText);
+ let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_REMOVED]);
+ acc.cutText(
+ staticContentOffset + startOffset,
+ staticContentOffset + endOffset
+ );
+
+ let evt = (await evtPromise)[0];
+ evt.QueryInterface(nsIAccessibleTextChangeEvent);
+ is(evt.start, staticContentOffset + startOffset);
+}
diff --git a/accessible/tests/browser/tree/browser.toml b/accessible/tests/browser/tree/browser.toml
new file mode 100644
index 0000000000..64be6853d1
--- /dev/null
+++ b/accessible/tests/browser/tree/browser.toml
@@ -0,0 +1,31 @@
+[DEFAULT]
+subsuite = "a11y"
+support-files = [
+ "head.js",
+ "!/accessible/tests/browser/shared-head.js",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/browser/*.jsm",
+]
+prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]
+
+["browser_aria_owns.js"]
+skip-if = [
+ "true", #Bug 1445513
+ "verify && !debug && os == 'linux'",
+]
+
+["browser_browser_element.js"]
+
+["browser_css_content_visibility.js"]
+
+["browser_general.js"]
+
+["browser_lazy_tabs.js"]
+
+["browser_link.js"]
+
+["browser_searchbar.js"]
+
+["browser_shadowdom.js"]
+
+["browser_test_nsIAccessibleDocument_URL.js"]
diff --git a/accessible/tests/browser/tree/browser_aria_owns.js b/accessible/tests/browser/tree/browser_aria_owns.js
new file mode 100644
index 0000000000..0ca55ed357
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_aria_owns.js
@@ -0,0 +1,278 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let NO_MOVE = { unexpected: [[EVENT_REORDER, "container"]] };
+let MOVE = { expected: [[EVENT_REORDER, "container"]] };
+
+// Set last ordinal child as aria-owned, should produce no reorder.
+addAccessibleTask(
+ `<ul id="container"><li id="a">Test</li></ul>`,
+ async function (browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(containerAcc, ["a"]);
+
+ await contentSpawnMutation(browser, NO_MOVE, function () {
+ // aria-own ordinal child in place, should be a no-op.
+ content.document
+ .getElementById("container")
+ .setAttribute("aria-owns", "a");
+ });
+
+ testChildrenIds(containerAcc, ["a"]);
+ }
+);
+
+// Add a new ordinal child to a container with an aria-owned child.
+// Order should respect aria-owns.
+addAccessibleTask(
+ `<ul id="container"><li id="a">Test</li></ul>`,
+ async function (browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(containerAcc, ["a"]);
+
+ await contentSpawnMutation(browser, MOVE, function () {
+ let container = content.document.getElementById("container");
+ container.setAttribute("aria-owns", "a");
+
+ let aa = content.document.createElement("li");
+ aa.id = "aa";
+ container.appendChild(aa);
+ });
+
+ testChildrenIds(containerAcc, ["aa", "a"]);
+
+ await contentSpawnMutation(browser, MOVE, function () {
+ content.document.getElementById("container").removeAttribute("aria-owns");
+ });
+
+ testChildrenIds(containerAcc, ["a", "aa"]);
+ }
+);
+
+// Remove a no-move aria-owns attribute, should result in a no-move.
+addAccessibleTask(
+ `<ul id="container" aria-owns="a"><li id="a">Test</li></ul>`,
+ async function (browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(containerAcc, ["a"]);
+
+ await contentSpawnMutation(browser, NO_MOVE, function () {
+ // remove aria-owned child that is already ordinal, should be no-op.
+ content.document.getElementById("container").removeAttribute("aria-owns");
+ });
+
+ testChildrenIds(containerAcc, ["a"]);
+ }
+);
+
+// Attempt to steal an aria-owned child. The attempt should fail.
+addAccessibleTask(
+ `
+ <ul>
+ <li id="a">Test</li>
+ </ul>
+ <ul aria-owns="a"></ul>
+ <ul id="container"></ul>`,
+ async function (browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(containerAcc, []);
+
+ await contentSpawnMutation(browser, NO_MOVE, function () {
+ content.document
+ .getElementById("container")
+ .setAttribute("aria-owns", "a");
+ });
+
+ testChildrenIds(containerAcc, []);
+ }
+);
+
+// Don't aria-own children of <select>
+addAccessibleTask(
+ `
+ <div id="container" role="group" aria-owns="b"></div>
+ <select id="select">
+ <option id="a"></option>
+ <option id="b"></option>
+ </select>`,
+ async function (browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+ let selectAcc = findAccessibleChildByID(accDoc, "select");
+
+ testChildrenIds(containerAcc, []);
+ testChildrenIds(selectAcc.firstChild, ["a", "b"]);
+ }
+);
+
+// Don't allow <select> to aria-own
+addAccessibleTask(
+ `
+ <div id="container" role="group">
+ <div id="a"></div>
+ <div id="b"></div>
+ </div>
+ <select id="select" aria-owns="a">
+ <option id="c"></option>
+ </select>`,
+ async function (browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+ let selectAcc = findAccessibleChildByID(accDoc, "select");
+
+ testChildrenIds(containerAcc, ["a", "b"]);
+ testChildrenIds(selectAcc.firstChild, ["c"]);
+ }
+);
+
+// Don't allow one <select> to aria-own an <option> from another <select>.
+addAccessibleTask(
+ `
+ <select id="select1" aria-owns="c">
+ <option id="a"></option>
+ <option id="b"></option>
+ </select>
+ <select id="select2">
+ <option id="c"></option>
+ </select>`,
+ async function (browser, accDoc) {
+ let selectAcc1 = findAccessibleChildByID(accDoc, "select1");
+ let selectAcc2 = findAccessibleChildByID(accDoc, "select2");
+
+ testChildrenIds(selectAcc1.firstChild, ["a", "b"]);
+ testChildrenIds(selectAcc2.firstChild, ["c"]);
+ }
+);
+
+// Don't allow a <select> to reorder its children with aria-owns.
+addAccessibleTask(
+ `
+ <select id="container" aria-owns="c b a">
+ <option id="a"></option>
+ <option id="b"></option>
+ <option id="c"></option>
+ </select>`,
+ async function (browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(containerAcc.firstChild, ["a", "b", "c"]);
+
+ await contentSpawnMutation(browser, NO_MOVE, function () {
+ content.document
+ .getElementById("container")
+ .setAttribute("aria-owns", "a c b");
+ });
+
+ testChildrenIds(containerAcc.firstChild, ["a", "b", "c"]);
+ }
+);
+
+// Don't crash if ID in aria-owns does not exist
+addAccessibleTask(
+ `
+ <select id="container" aria-owns="boom" multiple></select>`,
+ async function (browser, accDoc) {
+ ok(true, "Did not crash");
+ }
+);
+
+addAccessibleTask(
+ `
+ <ul id="one">
+ <li id="a">Test</li>
+ <li id="b">Test 2</li>
+ <li id="c">Test 3</li>
+ </ul>
+ <ul id="two"></ul>`,
+ async function (browser, accDoc) {
+ let one = findAccessibleChildByID(accDoc, "one");
+ let two = findAccessibleChildByID(accDoc, "two");
+
+ let waitfor = {
+ expected: [
+ [EVENT_REORDER, "one"],
+ [EVENT_REORDER, "two"],
+ ],
+ };
+
+ await contentSpawnMutation(browser, waitfor, function () {
+ // Put same id twice in aria-owns
+ content.document.getElementById("two").setAttribute("aria-owns", "a a");
+ });
+
+ testChildrenIds(one, ["b", "c"]);
+ testChildrenIds(two, ["a"]);
+
+ await contentSpawnMutation(browser, waitfor, function () {
+ // If the previous double-id aria-owns worked correctly, we should
+ // be in a good state and all is fine..
+ content.document.getElementById("two").setAttribute("aria-owns", "a b");
+ });
+
+ testChildrenIds(one, ["c"]);
+ testChildrenIds(two, ["a", "b"]);
+ }
+);
+
+addAccessibleTask(
+ `<div id="a"></div><div id="b"></div>`,
+ async function (browser, accDoc) {
+ testChildrenIds(accDoc, ["a", "b"]);
+
+ let waitFor = {
+ expected: [[EVENT_REORDER, e => e.accessible == accDoc]],
+ };
+
+ await contentSpawnMutation(browser, waitFor, function () {
+ content.document.documentElement.style.display = "none";
+ content.document.documentElement.getBoundingClientRect();
+ content.document.body.setAttribute("aria-owns", "b a");
+ content.document.documentElement.remove();
+ });
+
+ testChildrenIds(accDoc, []);
+ }
+);
+
+// Don't allow ordinal child to be placed after aria-owned child (bug 1405796)
+addAccessibleTask(
+ `<div id="container"><div id="a">Hello</div></div>
+ <div><div id="c">There</div><div id="d">There</div></div>`,
+ async function (browser, accDoc) {
+ let containerAcc = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(containerAcc, ["a"]);
+
+ await contentSpawnMutation(browser, MOVE, function () {
+ content.document
+ .getElementById("container")
+ .setAttribute("aria-owns", "c");
+ });
+
+ testChildrenIds(containerAcc, ["a", "c"]);
+
+ await contentSpawnMutation(browser, MOVE, function () {
+ let span = content.document.createElement("span");
+ content.document.getElementById("container").appendChild(span);
+
+ let b = content.document.createElement("div");
+ b.id = "b";
+ content.document.getElementById("container").appendChild(b);
+ });
+
+ testChildrenIds(containerAcc, ["a", "b", "c"]);
+
+ await contentSpawnMutation(browser, MOVE, function () {
+ content.document
+ .getElementById("container")
+ .setAttribute("aria-owns", "c d");
+ });
+
+ testChildrenIds(containerAcc, ["a", "b", "c", "d"]);
+ }
+);
diff --git a/accessible/tests/browser/tree/browser_browser_element.js b/accessible/tests/browser/tree/browser_browser_element.js
new file mode 100644
index 0000000000..82be24d93c
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_browser_element.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+// Test that the tree is correct for browser elements containing remote
+// documents.
+addAccessibleTask(`test`, async function (browser, docAcc) {
+ // testAccessibleTree also verifies childCount, indexInParent and parent.
+ testAccessibleTree(browser, {
+ INTERNAL_FRAME: [{ DOCUMENT: [{ TEXT_LEAF: [] }] }],
+ });
+});
diff --git a/accessible/tests/browser/tree/browser_css_content_visibility.js b/accessible/tests/browser/tree/browser_css_content_visibility.js
new file mode 100644
index 0000000000..798e409d86
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_css_content_visibility.js
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+const snippet1 = `
+ <style>
+ #container {
+ width: 150px;
+ height: 150px;
+ background: lightblue;
+ }
+ .hidden {
+ content-visibility: hidden;
+ }
+ .auto {
+ content-visibility: auto;
+ }
+ </style>
+ <div id="container">
+ <div class="hidden" id="hidden-target">
+ hidden target
+ <div id="hidden-child">
+ hidden child
+ </div>
+ </div>
+ <div class="auto" id="auto-target">
+ auto target
+ <div id="auto-child">
+ auto child
+ </div>
+ </div>
+ </div>
+ `;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["layout.css.content-visibility.enabled", true]],
+ });
+});
+
+// Check if the element specified with `content-visibility` property is accessible
+addAccessibleTask(
+ snippet1,
+ async function (browser, accDoc) {
+ const container = findAccessibleChildByID(accDoc, "container");
+ ok(
+ findAccessibleChildByID(container, "hidden-target"),
+ "hidden-target is accessible"
+ );
+ ok(
+ findAccessibleChildByID(container, "auto-target"),
+ "auto-target is accessible"
+ );
+
+ // The test checks if the child element of the element specified with
+ // `content-visibility: hidden` is ignored from a11y tree
+ let target = findAccessibleChildByID(accDoc, "hidden-target");
+ ok(
+ !findAccessibleChildByID(target, "hidden-child"),
+ "Children of hidden-target is not accessible"
+ );
+
+ // The test checks if the child element of the element specified with
+ // `content-visibility: auto` is showen in a11y tree
+ target = findAccessibleChildByID(accDoc, "auto-target");
+ ok(
+ findAccessibleChildByID(target, "auto-child"),
+ "Children of auto-target is accessible"
+ );
+ },
+ { iframe: true, remoteIframe: true, chrome: true }
+);
+
+// Check if the element having `display: contents` and a child of `content-visibility: hidden` element isn't accessible
+const snippet2 = `
+ <style>
+ #target {
+ width: 150px;
+ height: 150px;
+ background-color: lightblue;
+ }
+ #child {
+ width: 100px;
+ height: 100px;
+ background-color: lightgreen;
+ }
+ #grandchild {
+ width: 50px;
+ height: 50px;
+ background-color: red;
+ }
+ .hidden {
+ content-visibility: hidden;
+ }
+ .display-contents {
+ display: contents;
+ }
+ </style>
+ <div id="target" class="hidden">
+ <div id="child" class="display-contents">
+ <div id="grandchild"></div>
+ </div>
+ </div>
+ `;
+
+addAccessibleTask(
+ snippet2,
+ async function (browser, accDoc) {
+ const target = findAccessibleChildByID(accDoc, "target");
+ ok(
+ !findAccessibleChildByID(target, "child"),
+ "Element having `display: contents` and a child of `content-visibility: hidden` element isn't accessible"
+ );
+ testAccessibleTree(target, { SECTION: [] });
+ },
+ { iframe: true, remoteIframe: true, chrome: true }
+);
diff --git a/accessible/tests/browser/tree/browser_general.js b/accessible/tests/browser/tree/browser_general.js
new file mode 100644
index 0000000000..0d16271a36
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_general.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Verify adding `overflow:hidden;` styling to a div causes it to
+ * get an accessible.
+ */
+addAccessibleTask(`<p>hello world</p>`, async function (browser, docAcc) {
+ const originalTree = { DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }] };
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding div element");
+ await contentSpawnMutation(
+ browser,
+ { unexpected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ const d = content.document.createElement("div");
+ content.document.body.appendChild(d);
+ }
+ );
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding overflow:hidden styling to div");
+ await contentSpawnMutation(
+ browser,
+ { expected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ content.document.body.lastElementChild.setAttribute(
+ "style",
+ "overflow:hidden;"
+ );
+ }
+ );
+
+ testAccessibleTree(docAcc, {
+ DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }, { TEXT_CONTAINER: [] }],
+ });
+});
+
+/**
+ * Verify adding `overflow:scroll;` styling to a div causes
+ * it to get an accessible.
+ */
+addAccessibleTask(`<p>hello world</p>`, async function (browser, docAcc) {
+ const originalTree = { DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }] };
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding div element");
+ await contentSpawnMutation(
+ browser,
+ { unexpected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ const d = content.document.createElement("div");
+ content.document.body.appendChild(d);
+ }
+ );
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding overflow:scroll styling to div");
+ await contentSpawnMutation(
+ browser,
+ { expected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ content.document.body.lastElementChild.setAttribute(
+ "style",
+ "overflow:scroll;"
+ );
+ }
+ );
+
+ testAccessibleTree(docAcc, {
+ DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }, { TEXT_CONTAINER: [] }],
+ });
+});
+
+/**
+ * Verify adding `overflow:auto;` styling to a div causes
+ * it to get an accessible, but `overflow:visible` does not.
+ */
+addAccessibleTask(`<p>hello world</p>`, async function (browser, docAcc) {
+ const originalTree = { DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }] };
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding div element");
+ await contentSpawnMutation(
+ browser,
+ { unexpected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ const d = content.document.createElement("div");
+ content.document.body.appendChild(d);
+ }
+ );
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding overflow:visible styling to div");
+ await contentSpawnMutation(
+ browser,
+ { unexpected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ content.document.body.lastElementChild.setAttribute(
+ "style",
+ "overflow:visible;"
+ );
+ }
+ );
+
+ testAccessibleTree(docAcc, originalTree);
+ info("Adding overflow:auto styling to div");
+ await contentSpawnMutation(
+ browser,
+ { expected: [[EVENT_REORDER, docAcc]] },
+ function () {
+ content.document.body.lastElementChild.setAttribute(
+ "style",
+ "overflow:auto;"
+ );
+ }
+ );
+
+ testAccessibleTree(docAcc, {
+ DOCUMENT: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }, { TEXT_CONTAINER: [] }],
+ });
+});
diff --git a/accessible/tests/browser/tree/browser_lazy_tabs.js b/accessible/tests/browser/tree/browser_lazy_tabs.js
new file mode 100644
index 0000000000..f7aa9bdeb2
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_lazy_tabs.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that lazy background tabs aren't unintentionally loaded when building
+// the a11y tree (bug 1700708).
+addAccessibleTask(``, async function (browser, accDoc) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.sessionstore.restore_on_demand", true]],
+ });
+
+ info("Opening a new window");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ // Window is opened with a blank tab.
+ info("Loading second tab");
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ url: "data:text/html,2",
+ });
+ info("Loading third tab");
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ url: "data:text/html,3",
+ });
+ info("Closing the window");
+ await BrowserTestUtils.closeWindow(win);
+
+ is(SessionStore.getClosedWindowCount(), 1, "Should have a window to restore");
+ info("Restoring the window");
+ win = SessionStore.undoCloseWindow(0);
+ await BrowserTestUtils.waitForEvent(win, "SSWindowStateReady");
+ await BrowserTestUtils.waitForEvent(
+ win.gBrowser.tabContainer,
+ "SSTabRestored"
+ );
+ is(win.gBrowser.tabs.length, 3, "3 tabs restored");
+ ok(win.gBrowser.tabs[2].selected, "Third tab selected");
+ ok(getAccessible(win.gBrowser.tabs[1]), "Second tab has accessible");
+ ok(!win.gBrowser.browsers[1].isConnected, "Second tab is lazy");
+ info("Closing the restored window");
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/accessible/tests/browser/tree/browser_link.js b/accessible/tests/browser/tree/browser_link.js
new file mode 100644
index 0000000000..b0ff992365
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_link.js
@@ -0,0 +1,208 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+/**
+ * Verify that an anchor element reports a generic role without an href
+ * attribute and reports a LINK role with it present. Verify that these roles
+ * change as the attribute appears and disappears.
+ */
+addAccessibleTask(
+ `
+<a id="link">test</a>
+ `,
+ async function (browser, accDoc) {
+ let link = findAccessibleChildByID(accDoc, "link");
+ is(link.role, ROLE_TEXT, "Checking role of anchor element without href");
+
+ let onHideAndShow = waitForEvents({
+ expected: [
+ [EVENT_HIDE, link],
+ [EVENT_SHOW, "link"],
+ ],
+ });
+ info("Adding an href to the anchor element");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("link").setAttribute("href", "#");
+ });
+ await onHideAndShow;
+
+ link = findAccessibleChildByID(accDoc, "link");
+ is(link.role, ROLE_LINK, "Checking role of anchor element with href");
+
+ onHideAndShow = waitForEvents({
+ expected: [
+ [EVENT_HIDE, link],
+ [EVENT_SHOW, "link"],
+ ],
+ });
+ info("Removing the href from the anchor element");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("link").removeAttribute("href");
+ });
+ await onHideAndShow;
+ link = findAccessibleChildByID(accDoc, "link");
+ is(link.role, ROLE_TEXT, "Checking role of anchor element without href");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Verify that an anchor element reports a generic role without a click listener
+ * and reports a LINK role with it present. Verify that these roles change as
+ * the click listener appears.
+ */
+addAccessibleTask(
+ `
+<a id="link">test</a>
+ `,
+ async function (browser, accDoc) {
+ let link = findAccessibleChildByID(accDoc, "link");
+ is(
+ link.role,
+ ROLE_TEXT,
+ "Checking role of anchor element without click listener"
+ );
+
+ let onHideAndShow = waitForEvents({
+ expected: [
+ [EVENT_HIDE, link],
+ [EVENT_SHOW, "link"],
+ ],
+ });
+ info("Adding a click listener to the anchor element");
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("link")
+ .addEventListener("click", () => {});
+ });
+ await onHideAndShow;
+
+ link = findAccessibleChildByID(accDoc, "link");
+ is(
+ link.role,
+ ROLE_LINK,
+ "Checking role of anchor element with click listener"
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Verify that an area element reports a generic role without an href
+ * attribute and reports a LINK role with it present. Verify that these roles
+ * change as the attribute appears and disappears.
+ */
+addAccessibleTask(
+ `
+<map name="map">
+ <area id="link">
+</map>
+<img id="img" usemap="#map" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif">
+`,
+ async function (browser, accDoc) {
+ let link = findAccessibleChildByID(accDoc, "link");
+ is(link.role, ROLE_TEXT, "Checking role of area element without href");
+
+ let img = findAccessibleChildByID(accDoc, "img");
+ let onHideAndShow = waitForEvents({
+ expected: [
+ [EVENT_HIDE, img],
+ [EVENT_SHOW, "img"],
+ ],
+ });
+ info("Adding an href to the area element");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("link").setAttribute("href", "#");
+ });
+ await onHideAndShow;
+
+ link = findAccessibleChildByID(accDoc, "link");
+ is(link.role, ROLE_LINK, "Checking role of area element with href");
+
+ img = findAccessibleChildByID(accDoc, "img");
+ onHideAndShow = waitForEvents({
+ expected: [
+ [EVENT_HIDE, img],
+ [EVENT_SHOW, "img"],
+ ],
+ });
+ info("Removing the href from the area element");
+ await invokeContentTask(browser, [], () => {
+ content.document.getElementById("link").removeAttribute("href");
+ });
+ await onHideAndShow;
+ link = findAccessibleChildByID(accDoc, "link");
+ is(link.role, ROLE_TEXT, "Checking role of area element without href");
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
+
+/**
+ * Verify that an area element reports a generic role without a click listener
+ * and reports a LINK role with it present. Verify that these roles change as
+ * the click listener appears.
+ */
+addAccessibleTask(
+ `
+<map name="map">
+ <area id="link">
+</map>
+<img id="img" usemap="#map" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif">
+ `,
+ async function (browser, accDoc) {
+ let link = findAccessibleChildByID(accDoc, "link");
+ is(
+ link.role,
+ ROLE_TEXT,
+ "Checking role of area element without click listener"
+ );
+
+ let img = findAccessibleChildByID(accDoc, "img");
+ let onHideAndShow = waitForEvents({
+ expected: [
+ [EVENT_HIDE, img],
+ [EVENT_SHOW, "img"],
+ ],
+ });
+ info("Adding a click listener to the area element");
+ await invokeContentTask(browser, [], () => {
+ content.document
+ .getElementById("link")
+ .addEventListener("click", () => {});
+ });
+ await onHideAndShow;
+
+ link = findAccessibleChildByID(accDoc, "link");
+ is(
+ link.role,
+ ROLE_LINK,
+ "Checking role of area element with click listener"
+ );
+ },
+ {
+ chrome: true,
+ topLevel: true,
+ iframe: true,
+ remoteIframe: true,
+ }
+);
diff --git a/accessible/tests/browser/tree/browser_searchbar.js b/accessible/tests/browser/tree/browser_searchbar.js
new file mode 100644
index 0000000000..600c14e143
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_searchbar.js
@@ -0,0 +1,96 @@
+"use strict";
+
+/* import-globals-from ../../mochitest/role.js */
+loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
+
+// eslint-disable-next-line camelcase
+add_task(async function test_searchbar_a11y_tree() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.widget.inNavBar", true]],
+ });
+
+ // This used to rely on the implied 100ms initial timer of
+ // TestUtils.waitForCondition. See bug 1700735.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 100));
+ let searchbar = await TestUtils.waitForCondition(
+ () => document.getElementById("searchbar"),
+ "wait for search bar to appear"
+ );
+
+ // Make sure the popup has been rendered so it shows up in the a11y tree.
+ let popup = document.getElementById("PopupSearchAutoComplete");
+ let promise = Promise.all([
+ BrowserTestUtils.waitForEvent(popup, "popupshown", false),
+ waitForEvent(EVENT_SHOW, popup),
+ ]);
+ searchbar.textbox.openPopup();
+ await promise;
+
+ let TREE = {
+ role: ROLE_EDITCOMBOBOX,
+
+ children: [
+ // image button toggling the results list
+ {
+ role: ROLE_BUTTONMENU,
+ children: [],
+ },
+
+ // input element
+ {
+ role: ROLE_ENTRY,
+ children: [],
+ },
+
+ // context menu
+ {
+ role: ROLE_COMBOBOX_LIST,
+ children: [],
+ },
+
+ // result list
+ {
+ role: ROLE_GROUPING,
+ // not testing the structure inside the result list
+ },
+ ],
+ };
+
+ testAccessibleTree(searchbar, TREE);
+
+ promise = Promise.all([
+ BrowserTestUtils.waitForEvent(popup, "popuphidden", false),
+ waitForEvent(EVENT_HIDE, popup),
+ ]);
+ searchbar.textbox.closePopup();
+ await promise;
+
+ TREE = {
+ role: ROLE_EDITCOMBOBOX,
+
+ children: [
+ // image button toggling the results list
+ {
+ role: ROLE_BUTTONMENU,
+ children: [],
+ },
+
+ // input element
+ {
+ role: ROLE_ENTRY,
+ children: [],
+ },
+
+ // context menu
+ {
+ role: ROLE_COMBOBOX_LIST,
+ children: [],
+ },
+
+ // the result list should be removed from the tree on popuphidden
+ ],
+ };
+
+ testAccessibleTree(searchbar, TREE);
+});
diff --git a/accessible/tests/browser/tree/browser_shadowdom.js b/accessible/tests/browser/tree/browser_shadowdom.js
new file mode 100644
index 0000000000..6d9f06f9ff
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_shadowdom.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const REORDER = { expected: [[EVENT_REORDER, "container"]] };
+
+// Dynamically inserted slotted accessible elements should be in
+// the accessible tree.
+const snippet = `
+<script>
+customElements.define("x-el", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+ this.shadowRoot.innerHTML =
+ "<div role='presentation'><slot></slot></div>";
+ }
+});
+</script>
+<x-el id="container" role="group"><label id="l1">label1</label></x-el>
+`;
+
+addAccessibleTask(snippet, async function (browser, accDoc) {
+ let container = findAccessibleChildByID(accDoc, "container");
+
+ testChildrenIds(container, ["l1"]);
+
+ await contentSpawnMutation(browser, REORDER, function () {
+ let labelEl = content.document.createElement("label");
+ labelEl.id = "l2";
+
+ let containerEl = content.document.getElementById("container");
+ containerEl.appendChild(labelEl);
+ });
+
+ testChildrenIds(container, ["l1", "l2"]);
+});
+
+// Dynamically inserted not accessible custom element containing an accessible
+// in its shadow DOM.
+const snippet2 = `
+<script>
+customElements.define("x-el2", class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+ this.shadowRoot.innerHTML = "<input id='input'>";
+ }
+});
+</script>
+<div role="group" id="container"></div>
+`;
+
+addAccessibleTask(snippet2, async function (browser, accDoc) {
+ let container = findAccessibleChildByID(accDoc, "container");
+
+ await contentSpawnMutation(browser, REORDER, function () {
+ content.document.getElementById("container").innerHTML = "<x-el2></x-el2>";
+ });
+
+ testChildrenIds(container, ["input"]);
+});
+
+/**
+ * Ensure that changing the slot on the body while moving the body doesn't
+ * try to remove the DocAccessible. We test this here instead of in
+ * accessible/tests/mochitest/treeupdate/test_shadow_slots.html because this
+ * messes with the body element and we don't want that to impact other tests.
+ */
+addAccessibleTask(
+ `
+<div id="host"></div>
+<script>
+ const host = document.getElementById("host");
+ host.attachShadow({ mode: "open" });
+ const emptyScript = document.createElement("script");
+ emptyScript.id = "emptyScript";
+ document.head.append(emptyScript);
+</script>
+ `,
+ async function (browser, docAcc) {
+ info("Moving body and setting slot on body");
+ let reordered = waitForEvent(EVENT_REORDER, docAcc);
+ await invokeContentTask(browser, [], () => {
+ const host = content.document.getElementById("host");
+ const emptyScript = content.document.getElementById("emptyScript");
+ const body = content.document.body;
+ emptyScript.append(host);
+ host.append(body);
+ body.slot = "";
+ });
+ await reordered;
+ is(docAcc.childCount, 0, "document has no children after body move");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/tree/browser_test_nsIAccessibleDocument_URL.js b/accessible/tests/browser/tree/browser_test_nsIAccessibleDocument_URL.js
new file mode 100644
index 0000000000..623f2640f0
--- /dev/null
+++ b/accessible/tests/browser/tree/browser_test_nsIAccessibleDocument_URL.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function promiseEventDocumentLoadComplete(expectedURL) {
+ return new Promise(resolve => {
+ waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, event => {
+ try {
+ if (
+ event.accessible.QueryInterface(nsIAccessibleDocument).URL ==
+ expectedURL
+ ) {
+ resolve(event.accessible.QueryInterface(nsIAccessibleDocument));
+ return true;
+ }
+ return false;
+ } catch (e) {
+ return false;
+ }
+ });
+ });
+}
+
+add_task(async function testInDataURI() {
+ const kURL = "data:text/html,Some text";
+ const waitForDocumentLoadComplete = promiseEventDocumentLoadComplete("");
+ await BrowserTestUtils.withNewTab(kURL, async browser => {
+ is(
+ (await waitForDocumentLoadComplete).URL,
+ "",
+ "nsIAccessibleDocument.URL shouldn't return data URI"
+ );
+ });
+});
+
+add_task(async function testInHTTPSURIContainingPrivateThings() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.auth.confirmAuth.enabled", false]],
+ });
+ const kURL =
+ "https://username:password@example.com/browser/toolkit/content/tests/browser/file_empty.html?query=some#ref";
+ const kURLWithoutUserPass =
+ "https://example.com/browser/toolkit/content/tests/browser/file_empty.html?query=some#ref";
+ const waitForDocumentLoadComplete =
+ promiseEventDocumentLoadComplete(kURLWithoutUserPass);
+ await BrowserTestUtils.withNewTab(kURL, async browser => {
+ is(
+ (await waitForDocumentLoadComplete).URL,
+ kURLWithoutUserPass,
+ "nsIAccessibleDocument.URL shouldn't contain user/pass section"
+ );
+ });
+});
diff --git a/accessible/tests/browser/tree/head.js b/accessible/tests/browser/tree/head.js
new file mode 100644
index 0000000000..b9c787e9e2
--- /dev/null
+++ b/accessible/tests/browser/tree/head.js
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* exported testChildrenIds */
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
+
+/*
+ * A test function for comparing the IDs of an accessible's children
+ * with an expected array of IDs.
+ */
+function testChildrenIds(acc, expectedIds) {
+ let ids = arrayFromChildren(acc).map(child => getAccessibleDOMNodeID(child));
+ Assert.deepEqual(
+ ids,
+ expectedIds,
+ `Children for ${getAccessibleDOMNodeID(acc)} are wrong.`
+ );
+}
diff --git a/accessible/tests/browser/windows/a11y_setup.py b/accessible/tests/browser/windows/a11y_setup.py
new file mode 100644
index 0000000000..d6dc19f0fb
--- /dev/null
+++ b/accessible/tests/browser/windows/a11y_setup.py
@@ -0,0 +1,145 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""Python environment for Windows a11y browser tests.
+"""
+
+import ctypes
+import os
+from ctypes import POINTER, byref
+from ctypes.wintypes import BOOL, HWND, LPARAM, POINT # noqa: F401
+
+import comtypes.client
+import psutil
+from comtypes import COMError, IServiceProvider
+
+user32 = ctypes.windll.user32
+oleacc = ctypes.oledll.oleacc
+oleaccMod = comtypes.client.GetModule("oleacc.dll")
+IAccessible = oleaccMod.IAccessible
+del oleaccMod
+OBJID_CLIENT = -4
+CHILDID_SELF = 0
+NAVRELATION_EMBEDS = 0x1009
+# This is the path if running locally.
+ia2Tlb = os.path.join(
+ os.getcwd(),
+ "..",
+ "..",
+ "..",
+ "accessible",
+ "interfaces",
+ "ia2",
+ "IA2Typelib.tlb",
+)
+if not os.path.isfile(ia2Tlb):
+ # This is the path if running in CI.
+ ia2Tlb = os.path.join(os.getcwd(), "ia2Typelib.tlb")
+ia2Mod = comtypes.client.GetModule(ia2Tlb)
+del ia2Tlb
+# Shove all the IAccessible* interfaces and IA2_* constants directly
+# into our namespace for convenience.
+globals().update((k, getattr(ia2Mod, k)) for k in ia2Mod.__all__)
+# We use this below. The linter doesn't understand our globals() update hack.
+IAccessible2 = ia2Mod.IAccessible2
+del ia2Mod
+
+uiaMod = comtypes.client.GetModule("UIAutomationCore.dll")
+globals().update((k, getattr(uiaMod, k)) for k in uiaMod.__all__)
+uiaClient = comtypes.CoCreateInstance(
+ uiaMod.CUIAutomation._reg_clsid_,
+ interface=uiaMod.IUIAutomation,
+ clsctx=comtypes.CLSCTX_INPROC_SERVER,
+)
+TreeScope_Descendants = uiaMod.TreeScope_Descendants
+UIA_AutomationIdPropertyId = uiaMod.UIA_AutomationIdPropertyId
+del uiaMod
+
+
+def AccessibleObjectFromWindow(hwnd, objectID=OBJID_CLIENT):
+ p = POINTER(IAccessible)()
+ oleacc.AccessibleObjectFromWindow(
+ hwnd, objectID, byref(IAccessible._iid_), byref(p)
+ )
+ return p
+
+
+def getFirefoxHwnd():
+ """Search all top level windows for the Firefox instance being
+ tested.
+ We search by window class name and window title prefix.
+ """
+ # We can compare the grandparent process ids to find the Firefox started by
+ # the test harness.
+ commonPid = psutil.Process().parent().ppid()
+ # We need something mutable to store the result from the callback.
+ found = []
+
+ @ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
+ def callback(hwnd, lParam):
+ name = ctypes.create_unicode_buffer(100)
+ user32.GetClassNameW(hwnd, name, 100)
+ if name.value != "MozillaWindowClass":
+ return True
+ pid = ctypes.wintypes.DWORD()
+ user32.GetWindowThreadProcessId(hwnd, byref(pid))
+ if psutil.Process(pid.value).parent().ppid() != commonPid:
+ return True # Not the Firefox being tested.
+ found.append(hwnd)
+ return False
+
+ user32.EnumWindows(callback, LPARAM(0))
+ if not found:
+ raise LookupError("Couldn't find Firefox HWND")
+ return found[0]
+
+
+def toIa2(obj):
+ serv = obj.QueryInterface(IServiceProvider)
+ return serv.QueryService(IAccessible2._iid_, IAccessible2)
+
+
+def getDocIa2():
+ """Get the IAccessible2 for the document being tested."""
+ hwnd = getFirefoxHwnd()
+ root = AccessibleObjectFromWindow(hwnd)
+ doc = root.accNavigate(NAVRELATION_EMBEDS, 0)
+ try:
+ child = toIa2(doc.accChild(1))
+ if "id:default-iframe-id;" in child.attributes:
+ # This is an iframe or remoteIframe test.
+ doc = child.accChild(1)
+ except COMError:
+ pass # No child.
+ return toIa2(doc)
+
+
+def findIa2ByDomId(root, id):
+ search = f"id:{id};"
+ # Child ids begin at 1.
+ for i in range(1, root.accChildCount + 1):
+ child = toIa2(root.accChild(i))
+ if search in child.attributes:
+ return child
+ descendant = findIa2ByDomId(child, id)
+ if descendant:
+ return descendant
+
+
+def getDocUia():
+ """Get the IUIAutomationElement for the document being tested."""
+ # We start with IAccessible2 because there's no efficient way to
+ # find the document we want with UIA.
+ ia2 = getDocIa2()
+ return uiaClient.ElementFromIAccessible(ia2, CHILDID_SELF)
+
+
+def findUiaByDomId(root, id):
+ cond = uiaClient.CreatePropertyCondition(UIA_AutomationIdPropertyId, id)
+ # FindFirst ignores elements in the raw tree, so we have to use
+ # FindFirstBuildCache to override that, even though we don't want to cache
+ # anything.
+ request = uiaClient.CreateCacheRequest()
+ request.TreeFilter = uiaClient.RawViewCondition
+ return root.FindFirstBuildCache(TreeScope_Descendants, cond, request)
diff --git a/accessible/tests/browser/windows/a11y_setup_requirements.txt b/accessible/tests/browser/windows/a11y_setup_requirements.txt
new file mode 100644
index 0000000000..ea131a6f92
--- /dev/null
+++ b/accessible/tests/browser/windows/a11y_setup_requirements.txt
@@ -0,0 +1 @@
+comtypes==1.2.0
diff --git a/accessible/tests/browser/windows/ia2/browser.toml b/accessible/tests/browser/windows/ia2/browser.toml
new file mode 100644
index 0000000000..d72b5f8a2d
--- /dev/null
+++ b/accessible/tests/browser/windows/ia2/browser.toml
@@ -0,0 +1,9 @@
+[DEFAULT]
+subsuite = "a11y"
+skip-if = [
+ "os != 'win'",
+ "headless",
+]
+support-files = ["head.js"]
+
+["browser_role.js"]
diff --git a/accessible/tests/browser/windows/ia2/browser_role.js b/accessible/tests/browser/windows/ia2/browser_role.js
new file mode 100644
index 0000000000..08e44c280f
--- /dev/null
+++ b/accessible/tests/browser/windows/ia2/browser_role.js
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const ROLE_SYSTEM_DOCUMENT = 15;
+const ROLE_SYSTEM_GROUPING = 20;
+const IA2_ROLE_PARAGRAPH = 1054;
+
+addAccessibleTask(
+ `
+<p id="p">p</p>
+ `,
+ async function (browser, docAcc) {
+ let role = await runPython(`
+ global doc
+ doc = getDocIa2()
+ return doc.accRole(CHILDID_SELF)
+ `);
+ is(role, ROLE_SYSTEM_DOCUMENT, "doc has correct MSAA role");
+ role = await runPython(`doc.role()`);
+ is(role, ROLE_SYSTEM_DOCUMENT, "doc has correct IA2 role");
+ ok(
+ await runPython(`
+ global p
+ p = findIa2ByDomId(doc, "p")
+ firstChild = toIa2(doc.accChild(1))
+ return p == firstChild
+ `),
+ "doc's first child is p"
+ );
+ role = await runPython(`p.accRole(CHILDID_SELF)`);
+ is(role, ROLE_SYSTEM_GROUPING, "p has correct MSAA role");
+ role = await runPython(`p.role()`);
+ is(role, IA2_ROLE_PARAGRAPH, "p has correct IA2 role");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/windows/ia2/head.js b/accessible/tests/browser/windows/ia2/head.js
new file mode 100644
index 0000000000..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/windows/ia2/head.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
diff --git a/accessible/tests/browser/windows/uia/browser.toml b/accessible/tests/browser/windows/uia/browser.toml
new file mode 100644
index 0000000000..f7974d69c7
--- /dev/null
+++ b/accessible/tests/browser/windows/uia/browser.toml
@@ -0,0 +1,11 @@
+[DEFAULT]
+subsuite = "a11y"
+skip-if = [
+ "os != 'win'",
+ "headless",
+]
+support-files = ["head.js"]
+
+["browser_controlType.js"]
+
+["browser_elementFromPoint.js"]
diff --git a/accessible/tests/browser/windows/uia/browser_controlType.js b/accessible/tests/browser/windows/uia/browser_controlType.js
new file mode 100644
index 0000000000..16db892581
--- /dev/null
+++ b/accessible/tests/browser/windows/uia/browser_controlType.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* eslint-disable camelcase */
+const UIA_ButtonControlTypeId = 50000;
+const UIA_DocumentControlTypeId = 50030;
+/* eslint-enable camelcase */
+
+addAccessibleTask(
+ `
+<button id="button">button</button>
+ `,
+ async function (browser, docAcc) {
+ let controlType = await runPython(`
+ global doc
+ doc = getDocUia()
+ return doc.CurrentControlType
+ `);
+ is(controlType, UIA_DocumentControlTypeId, "doc has correct control type");
+ controlType = await runPython(`
+ button = findUiaByDomId(doc, "button")
+ return button.CurrentControlType
+ `);
+ is(controlType, UIA_ButtonControlTypeId, "button has correct control type");
+ },
+ { chrome: true, topLevel: true, iframe: true, remoteIframe: true }
+);
diff --git a/accessible/tests/browser/windows/uia/browser_elementFromPoint.js b/accessible/tests/browser/windows/uia/browser_elementFromPoint.js
new file mode 100644
index 0000000000..e2fda4ab30
--- /dev/null
+++ b/accessible/tests/browser/windows/uia/browser_elementFromPoint.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+addAccessibleTask(
+ `
+<button id="button">button</p>
+<a id="a" href="#">a</a>
+ `,
+ async function (browser, docAcc) {
+ ok(
+ await runPython(`
+ global doc
+ doc = getDocUia()
+ button = findUiaByDomId(doc, "button")
+ rect = button.CurrentBoundingRectangle
+ found = uiaClient.ElementFromPoint(POINT(rect.left + 1, rect.top + 1))
+ return uiaClient.CompareElements(button, found)
+ `),
+ "ElementFromPoint on button returns button"
+ );
+ ok(
+ await runPython(`
+ a = findUiaByDomId(doc, "a")
+ rect = a.CurrentBoundingRectangle
+ found = uiaClient.ElementFromPoint(POINT(rect.left + 1, rect.top + 1))
+ return uiaClient.CompareElements(a, found)
+ `),
+ "ElementFromPoint on a returns a"
+ );
+ }
+);
diff --git a/accessible/tests/browser/windows/uia/head.js b/accessible/tests/browser/windows/uia/head.js
new file mode 100644
index 0000000000..afc50984bd
--- /dev/null
+++ b/accessible/tests/browser/windows/uia/head.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
+ this
+);
+
+// Loading and common.js from accessible/tests/mochitest/ for all tests, as
+// well as promisified-events.js.
+loadScripts(
+ { name: "common.js", dir: MOCHITESTS_DIR },
+ { name: "promisified-events.js", dir: MOCHITESTS_DIR }
+);
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<body onload="document.body.appendChild(x);">
+<table><tbody><div id="x"></div></tbody></table>
+</body>
+</html>
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 @@
+<script>
+window.onload=function(){
+ let o=document.getElementById('a');
+ let n=document.createElement('plaintext');
+ o.parentNode.replaceChild(n,o);
+ n.setAttribute('id','a');
+ document.getElementById('a').setAttribute('aria-owns','c b');
+ document.getElementById('d').setAttribute('width',1);
+}
+</script>
+>
+<script id='a'></script>
+<hgroup id='b'>
+<h6 id='c'>
+<video id='d' controls width='169' height='187'>
diff --git a/accessible/tests/crashtests/1402999.html b/accessible/tests/crashtests/1402999.html
new file mode 100644
index 0000000000..567f20b2df
--- /dev/null
+++ b/accessible/tests/crashtests/1402999.html
@@ -0,0 +1,11 @@
+<script>
+document.addEventListener("DOMContentLoaded", function(){
+ document.getElementById('a').click();
+ window.frames[0].document.body.appendChild(document.getElementById('b'));
+});
+</script>
+<cite id='b'>
+<label>
+<input/>
+<strong id='a'></strong>
+<iframe>
diff --git a/accessible/tests/crashtests/1415667.html b/accessible/tests/crashtests/1415667.html
new file mode 100644
index 0000000000..174099837d
--- /dev/null
+++ b/accessible/tests/crashtests/1415667.html
@@ -0,0 +1 @@
+<iframe role="row">
diff --git a/accessible/tests/crashtests/1463962.html b/accessible/tests/crashtests/1463962.html
new file mode 100644
index 0000000000..c3d233af5c
--- /dev/null
+++ b/accessible/tests/crashtests/1463962.html
@@ -0,0 +1,4 @@
+<style>
+#a { display: contents }
+</style>
+<li id="a">
diff --git a/accessible/tests/crashtests/1472024-1.html b/accessible/tests/crashtests/1472024-1.html
new file mode 100644
index 0000000000..2b745a635e
--- /dev/null
+++ b/accessible/tests/crashtests/1472024-1.html
@@ -0,0 +1,7 @@
+<style>
+.a { position: absolute }
+</style>
+<table>
+<tbody class="a">
+<tr>b</tr>
+<th>
diff --git a/accessible/tests/crashtests/1472024-2.html b/accessible/tests/crashtests/1472024-2.html
new file mode 100644
index 0000000000..74719cb91a
--- /dev/null
+++ b/accessible/tests/crashtests/1472024-2.html
@@ -0,0 +1,20 @@
+<html class="reftest-wait">
+<script>
+function tweak() {
+ document.getElementById('a').appendChild(document.createTextNode('hZ'));
+ document.documentElement.removeAttribute("class");
+}
+
+window.onload = () => {
+ // double-rAF (to ensure layout/paints have been flushed) before we make
+ // the dynamic modification. This seems to make the bug more likely to
+ // reproduce, in unpatched builds.
+ requestAnimationFrame(() => { requestAnimationFrame(() => {
+ tweak();
+ }); });
+};
+</script>
+<table>
+<tfoot id='a'>
+<th>
+<textarea>Text
diff --git a/accessible/tests/crashtests/1484778.html b/accessible/tests/crashtests/1484778.html
new file mode 100644
index 0000000000..9d3fc33f59
--- /dev/null
+++ b/accessible/tests/crashtests/1484778.html
@@ -0,0 +1,26 @@
+<style>
+#a { border-left: solid -moz-hyperlinktext 93em }
+</style>
+<script>
+/*
+ I dont't know why but this seems to be required to trigger the crash...
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+*/
+function go() {
+ var b = document.elementFromPoint(0,0);
+ window.scroll({left: 97, top: -1});
+ document.adoptNode(b);
+}
+</script>
+<body onload=go()>
+<ins id="a">
diff --git a/accessible/tests/crashtests/1494707.html b/accessible/tests/crashtests/1494707.html
new file mode 100644
index 0000000000..f2feb1572d
--- /dev/null
+++ b/accessible/tests/crashtests/1494707.html
@@ -0,0 +1,15 @@
+<html>
+ <head>
+ <style>
+ *::after {
+ content: counters(bar, '', none);
+ }
+ </style>
+ </head>
+ <body>
+ <table>
+ <thead></thead>
+ <th></th>
+ </table>
+ </body>
+</html>
diff --git a/accessible/tests/crashtests/1503964.html b/accessible/tests/crashtests/1503964.html
new file mode 100644
index 0000000000..bc4c359b52
--- /dev/null
+++ b/accessible/tests/crashtests/1503964.html
@@ -0,0 +1,4 @@
+<table>
+<th>X</th>
+<tfoot style="display: table-row">
+<tr role="columnheader">X</tr>
diff --git a/accessible/tests/crashtests/1572811.html b/accessible/tests/crashtests/1572811.html
new file mode 100644
index 0000000000..61dc78ca7b
--- /dev/null
+++ b/accessible/tests/crashtests/1572811.html
@@ -0,0 +1,9 @@
+<script>
+function go() {
+ a.style.overflow = "auto";
+}
+</script>
+<body onload=go()>
+<table id="a" background="3">
+<th>
+<textarea style="position:absolute">A</textarea>
diff --git a/accessible/tests/crashtests/1578282.html b/accessible/tests/crashtests/1578282.html
new file mode 100644
index 0000000000..31cc1ce02b
--- /dev/null
+++ b/accessible/tests/crashtests/1578282.html
@@ -0,0 +1,21 @@
+<html class="reftest-wait">
+<head>
+<script>
+function onload() {
+ document.documentElement.style.display = "none"
+ document.documentElement.getBoundingClientRect()
+ document.documentElement.style.display = ""
+ window.setTimeout(() => {
+ document.documentElement.removeAttribute("class");
+ }, 1000);
+}
+</script>
+</head>
+<body onload="onload()">
+<marquee style="display: inline-grid">
+<video></video>
+<bgsound></bgsound>
+&lt;
+</marquee>
+</body>
+</html> \ No newline at end of file
diff --git a/accessible/tests/crashtests/1585851.html b/accessible/tests/crashtests/1585851.html
new file mode 100644
index 0000000000..3acac2de7d
--- /dev/null
+++ b/accessible/tests/crashtests/1585851.html
@@ -0,0 +1,21 @@
+<style>
+* {
+ border-collapse: collapse;
+}
+</style>
+<script>
+window.onload = () => {
+ c.style.setProperty("border-collapse", "separate")
+}
+function eh() {
+ a.options.add(f)
+ a.add(b, 9)
+ a.add(f, d)
+}
+</script>
+<select id="a"></select>
+<option id="b">
+<dd id="c">
+<audio id="d" controls>
+<details open="true" ontoggle="eh()">
+<optgroup id="f">
diff --git a/accessible/tests/crashtests/1655983.html b/accessible/tests/crashtests/1655983.html
new file mode 100644
index 0000000000..88fee3ec5d
--- /dev/null
+++ b/accessible/tests/crashtests/1655983.html
@@ -0,0 +1,6 @@
+<script>
+ document.addEventListener('DOMContentLoaded', () => {
+ document.getElementById('id_0').type = ''
+ })
+</script>
+<input id="id_0" type="time">
diff --git a/accessible/tests/crashtests/1838250.html b/accessible/tests/crashtests/1838250.html
new file mode 100644
index 0000000000..3ae4957733
--- /dev/null
+++ b/accessible/tests/crashtests/1838250.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<div style="writing-mode:vertical-rl">
+ foo
+ <span style="text-combine-upright: all">abc</span>
+ bar
+</div>
diff --git a/accessible/tests/crashtests/448064.xhtml b/accessible/tests/crashtests/448064.xhtml
new file mode 100644
index 0000000000..c1a25ca550
--- /dev/null
+++ b/accessible/tests/crashtests/448064.xhtml
@@ -0,0 +1,70 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+</head>
+<body>
+<div id="mw_b">
+<div id="mw_f">
+<div id="mw_g" style="display: none;"/>
+</div>
+</div>
+
+<div id="mw_c" style="display: none;">
+<div id="mw_d">
+<div id="mw_e"></div>
+</div>
+</div>
+
+<input id="mw_a"/>
+
+
+<script>
+function dumpAccessibleNode(aNode, level) {
+ var msg = "";
+
+ try {
+ msg += "name=\"" + aNode.name + "\" ";
+ } catch (e) {
+ msg += " noName ";
+ }
+
+ dump(msg + "\n");
+}
+
+
+function dumpAccessibleTree(aNode, level) {
+ level = level || 0;
+
+ dumpAccessibleNode(aNode, level);
+ try {
+ var child = aNode.firstChild;
+ while (child) {
+ dumpAccessibleTree(child, level + 1);
+ child = child.nextSibling;
+ }
+ } catch (e) {
+ dump("Error visiting child nodes: " + e + "\n");
+ }
+}
+
+function A(o) {
+ var acc = SpecialPowers.Cc["@mozilla.org/accessibilityService;1"]
+ .getService(SpecialPowers.Ci.nsIAccessibilityService);
+ return acc.getAccessibleFor(o);
+}
+
+function beginAccessible() {
+ dumpAccessibleTree(A(document), 0);
+}
+setTimeout(beginAccessible, 100);
+
+
+setTimeout(doe, 200);
+function doe() {
+ document.getElementById("mw_a").appendChild(document.getElementById("mw_b"));
+ document.getElementById("mw_c").appendChild(document.getElementById("mw_d"));
+ document.getElementById("mw_e").appendChild(document.getElementById("mw_f"));
+ document.getElementById("mw_g").appendChild(document.getElementById("mw_b"));
+}
+</script>
+</body>
+</html>
diff --git a/accessible/tests/crashtests/884202.html b/accessible/tests/crashtests/884202.html
new file mode 100644
index 0000000000..f29ef55f4a
--- /dev/null
+++ b/accessible/tests/crashtests/884202.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function f()
+{
+ window.removeEventListener("pagehide", f, false);
+ var root = document.documentElement;
+ document.removeChild(root);
+ document.appendChild(root);
+}
+
+function boom()
+{
+ window.addEventListener("pagehide", f, false);
+ window.location = "data:text/html,4";
+}
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/accessible/tests/crashtests/890760.html b/accessible/tests/crashtests/890760.html
new file mode 100644
index 0000000000..ecc76160b9
--- /dev/null
+++ b/accessible/tests/crashtests/890760.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ window.getSelection().collapse(document.createTextNode("."), 0);
+}
+
+</script>
+</head>
+<body onload="setTimeout(boom, 0);"></body>
+</html>
diff --git a/accessible/tests/crashtests/893515.html b/accessible/tests/crashtests/893515.html
new file mode 100644
index 0000000000..a9cc12c928
--- /dev/null
+++ b/accessible/tests/crashtests/893515.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+function boom()
+{
+ document.designMode = 'on';
+ var otherDoc = document.implementation.createDocument("", "", null);
+ otherDoc.adoptNode(document.getElementById("t"));
+}
+</script>
+</head>
+<body onload="setTimeout(boom, 0);"><textarea id="t"></textarea></body>
+</html>
diff --git a/accessible/tests/crashtests/crashtests.list b/accessible/tests/crashtests/crashtests.list
new file mode 100644
index 0000000000..2f628ddfe2
--- /dev/null
+++ b/accessible/tests/crashtests/crashtests.list
@@ -0,0 +1,23 @@
+load 448064.xhtml # This test instantiates a11y, so be careful about adding tests before it
+asserts-if(!browserIsRemote,2) load 884202.html
+load 1572811.html
+load 1578282.html
+load 890760.html
+load 893515.html
+load 1072792.xhtml
+load 1380199.html
+load 1402999.html
+load 1463962.html
+load 1472024-1.html
+load 1472024-2.html
+load 1484778.html
+load 1494707.html
+load 1503964.html
+load 1415667.html
+load 1585851.html
+load 1655983.html
+load 1838250.html
+
+# last_test_to_unload_testsuite.xhtml MUST be the last test in the list because it
+# is responsible for shutting down accessibility service affecting later tests.
+load chrome://reftest/content/crashtests/accessible/tests/crashtests/last_test_to_unload_testsuite.xhtml
diff --git a/accessible/tests/crashtests/last_test_to_unload_testsuite.xhtml b/accessible/tests/crashtests/last_test_to_unload_testsuite.xhtml
new file mode 100644
index 0000000000..9a70c41dcf
--- /dev/null
+++ b/accessible/tests/crashtests/last_test_to_unload_testsuite.xhtml
@@ -0,0 +1,36 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="bug 471493 'crash [@ nsPropertyTable::GetPropertyInternal]'"
+ onload="shutdown();" class="reftest-wait">
+
+ <script type="application/javascript">
+ <![CDATA[
+ function shutdown() {
+ !SpecialPowers.Services.appinfo.accessibilityEnabled &&
+ dump("### Error : Accessibility is expected to be enabled.\n");
+
+ // Force garbage collection. We try really hard to garbage collect
+ // everythin here to ensure that all a11y xpcom bits are shut down and
+ // avoid intermittent timeouts.
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+
+ if (SpecialPowers.Services.appinfo.accessibilityEnabled) {
+ let observe = (subject, topic, data) => {
+ SpecialPowers.Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ data === "0" && document.documentElement.removeAttribute("class");
+ };
+ SpecialPowers.Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+ } else {
+ document.documentElement.removeAttribute("class");
+ }
+ }
+ ]]>
+ </script>
+</window>
diff --git a/accessible/tests/mochitest/.eslintrc.js b/accessible/tests/mochitest/.eslintrc.js
new file mode 100644
index 0000000000..2ce1c5017a
--- /dev/null
+++ b/accessible/tests/mochitest/.eslintrc.js
@@ -0,0 +1,24 @@
+"use strict";
+
+module.exports = {
+ rules: {
+ // XXX These are rules that are enabled in the recommended configuration, but
+ // disabled here due to failures when initially implemented. They should be
+ // removed (and hence enabled) at some stage.
+ "no-nested-ternary": "off",
+ },
+
+ overrides: [
+ {
+ files: [
+ // Bug 1602061 TODO: These tests access DOM elements via
+ // id-as-variable-name, which eslint doesn't have support for yet.
+ "attributes/test_listbox.html",
+ "treeupdate/test_ariaowns.html",
+ ],
+ rules: {
+ "no-undef": "off",
+ },
+ },
+ ],
+};
diff --git a/accessible/tests/mochitest/a11y.toml b/accessible/tests/mochitest/a11y.toml
new file mode 100644
index 0000000000..9125e244d2
--- /dev/null
+++ b/accessible/tests/mochitest/a11y.toml
@@ -0,0 +1,27 @@
+[DEFAULT]
+support-files = [
+ "../../../dom/media/test/bug461281.ogg",
+ "../../../dom/security/test/csp/dummy.pdf",
+ "../../../image/test/mochitest/animated-gif-finalframe.gif",
+ "../../../image/test/mochitest/animated-gif.gif",
+ "dumbfile.zip",
+ "formimage.png",
+ "letters.gif",
+ "moz.png",
+ "longdesc_src.html",
+ "*.js",
+ "treeview.css"]
+
+["test_OuterDocAccessible.html"]
+
+["test_aria_token_attrs.html"]
+
+["test_bug420863.html"]
+
+["test_custom_element_accessibility_defaults.html"]
+
+["test_descr.html"]
+
+["test_nsIAccessibleDocument.html"]
+
+["test_nsIAccessibleImage.html"]
diff --git a/accessible/tests/mochitest/actions.js b/accessible/tests/mochitest/actions.js
new file mode 100644
index 0000000000..dc2f7d929d
--- /dev/null
+++ b/accessible/tests/mochitest/actions.js
@@ -0,0 +1,231 @@
+/* import-globals-from common.js */
+/* import-globals-from events.js */
+
+// //////////////////////////////////////////////////////////////////////////////
+// Event constants
+
+const MOUSEDOWN_EVENT = 1;
+const MOUSEUP_EVENT = 2;
+const CLICK_EVENT = 4;
+const COMMAND_EVENT = 8;
+const FOCUS_EVENT = 16;
+
+const CLICK_EVENTS = MOUSEDOWN_EVENT | MOUSEUP_EVENT | CLICK_EVENT;
+const XUL_EVENTS = CLICK_EVENTS | COMMAND_EVENT;
+
+// //////////////////////////////////////////////////////////////////////////////
+// Public functions
+
+/**
+ * Test default accessible actions.
+ *
+ * Action tester interface is:
+ *
+ * var actionObj = {
+ * // identifier of accessible to perform an action on
+ * get ID() {},
+ *
+ * // index of the action
+ * get actionIndex() {},
+ *
+ * // name of the action
+ * get actionName() {},
+ *
+ * // DOM events (see constants defined above)
+ * get events() {},
+ *
+ * // [optional] identifier of target DOM events listeners are registered on,
+ * // used with 'events', if missing then 'ID' is used instead.
+ * get targetID() {},
+ *
+ * // [optional] true to match DOM events bubbled up to the target,
+ * // false (default) to only match events fired directly on the target.
+ * get allowBubbling() {},
+ *
+ * // [optional] perform checks when 'click' event is handled if 'events'
+ * // is used.
+ * checkOnClickEvent: function() {},
+ *
+ * // [optional] an array of invoker's checker objects (see eventQueue
+ * // constructor events.js)
+ * get eventSeq() {}
+ * };
+ *
+ *
+ * @param aArray [in] an array of action cheker objects
+ */
+function testActions(aArray) {
+ gActionsQueue = new eventQueue();
+
+ for (var idx = 0; idx < aArray.length; idx++) {
+ var actionObj = aArray[idx];
+ var accOrElmOrID = actionObj.ID;
+ var actionIndex = actionObj.actionIndex;
+ var actionName = actionObj.actionName;
+ var events = actionObj.events;
+ var accOrElmOrIDOfTarget = actionObj.targetID
+ ? actionObj.targetID
+ : accOrElmOrID;
+
+ var eventSeq = [];
+ if (events) {
+ var elm = getNode(accOrElmOrIDOfTarget);
+ if (events & MOUSEDOWN_EVENT) {
+ eventSeq.push(new checkerOfActionInvoker("mousedown", elm, actionObj));
+ }
+
+ if (events & MOUSEUP_EVENT) {
+ eventSeq.push(new checkerOfActionInvoker("mouseup", elm, actionObj));
+ }
+
+ if (events & CLICK_EVENT) {
+ eventSeq.push(new checkerOfActionInvoker("click", elm, actionObj));
+ }
+
+ if (events & COMMAND_EVENT) {
+ eventSeq.push(new checkerOfActionInvoker("command", elm, actionObj));
+ }
+
+ if (events & FOCUS_EVENT) {
+ eventSeq.push(new focusChecker(elm));
+ }
+ }
+
+ if (actionObj.eventSeq) {
+ eventSeq = eventSeq.concat(actionObj.eventSeq);
+ }
+
+ var invoker = new actionInvoker(
+ accOrElmOrID,
+ actionIndex,
+ actionName,
+ eventSeq
+ );
+ gActionsQueue.push(invoker);
+ }
+
+ gActionsQueue.invoke();
+}
+
+/**
+ * Test action names and descriptions.
+ */
+function testActionNames(aID, aActions) {
+ var actions = typeof aActions == "string" ? [aActions] : aActions || [];
+
+ var acc = getAccessible(aID);
+ is(acc.actionCount, actions.length, "Wong number of actions.");
+ for (var i = 0; i < actions.length; i++) {
+ is(
+ acc.getActionName(i),
+ actions[i],
+ "Wrong action name at " + i + " index."
+ );
+ is(
+ acc.getActionDescription(0),
+ gActionDescrMap[actions[i]],
+ "Wrong action description at " + i + "index."
+ );
+ }
+}
+
+// //////////////////////////////////////////////////////////////////////////////
+// Private
+
+var gActionsQueue = null;
+
+function actionInvoker(aAccOrElmOrId, aActionIndex, aActionName, aEventSeq) {
+ this.invoke = function actionInvoker_invoke() {
+ var acc = getAccessible(aAccOrElmOrId);
+ if (!acc) {
+ return INVOKER_ACTION_FAILED;
+ }
+
+ var isThereActions = acc.actionCount > 0;
+ ok(
+ isThereActions,
+ "No actions on the accessible for " + prettyName(aAccOrElmOrId)
+ );
+
+ if (!isThereActions) {
+ return INVOKER_ACTION_FAILED;
+ }
+
+ is(
+ acc.getActionName(aActionIndex),
+ aActionName,
+ "Wrong action name of the accessible for " + prettyName(aAccOrElmOrId)
+ );
+
+ try {
+ acc.doAction(aActionIndex);
+ } catch (e) {
+ ok(false, "doAction(" + aActionIndex + ") failed with: " + e.name);
+ return INVOKER_ACTION_FAILED;
+ }
+ return null;
+ };
+
+ this.eventSeq = aEventSeq;
+
+ this.getID = function actionInvoker_getID() {
+ return (
+ "invoke an action " +
+ aActionName +
+ " at index " +
+ aActionIndex +
+ " on " +
+ prettyName(aAccOrElmOrId)
+ );
+ };
+}
+
+function checkerOfActionInvoker(aType, aTarget, aActionObj) {
+ this.type = aType;
+
+ this.target = aTarget;
+
+ if (aActionObj && "eventTarget" in aActionObj) {
+ this.eventTarget = aActionObj.eventTarget;
+ }
+
+ if (aActionObj && aActionObj.allowBubbling) {
+ // Normally, we add event listeners on the document. To catch bubbled
+ // events, we need to add the listener on the target itself.
+ this.eventTarget = "element";
+ // Normally, we only match an event fired directly on the target. Override
+ // this to match a bubbled event.
+ this.match = function (aEvent) {
+ return aEvent.currentTarget == aTarget;
+ };
+ }
+
+ this.phase = false;
+
+ this.getID = function getID() {
+ return aType + " event handling";
+ };
+
+ this.check = function check(aEvent) {
+ if (aType == "click" && aActionObj && "checkOnClickEvent" in aActionObj) {
+ aActionObj.checkOnClickEvent(aEvent);
+ }
+ };
+}
+
+var 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",
+};
diff --git a/accessible/tests/mochitest/actions/a11y.toml b/accessible/tests/mochitest/actions/a11y.toml
new file mode 100644
index 0000000000..30a98d40ac
--- /dev/null
+++ b/accessible/tests/mochitest/actions/a11y.toml
@@ -0,0 +1,26 @@
+[DEFAULT]
+support-files = [
+ "!/accessible/tests/mochitest/*.js",
+ "!/dom/media/test/bug461281.ogg"]
+
+["test_anchors.html"]
+
+["test_aria.html"]
+
+["test_controls.html"]
+
+["test_general.html"]
+
+["test_general.xhtml"]
+
+["test_keys.html"]
+
+["test_keys.xhtml"]
+
+["test_media.html"]
+
+["test_select.html"]
+
+["test_tree.xhtml"]
+
+["test_treegrid.xhtml"]
diff --git a/accessible/tests/mochitest/actions/test_anchors.html b/accessible/tests/mochitest/actions/test_anchors.html
new file mode 100644
index 0000000000..6ee1e0c450
--- /dev/null
+++ b/accessible/tests/mochitest/actions/test_anchors.html
@@ -0,0 +1,146 @@
+<html>
+
+<head>
+ <title>nsIAccessible actions testing for HTML links that
+ scroll the page to named anchors</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../actions.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Event checkers
+
+ function scrollingChecker(aAcc) {
+ this.type = EVENT_SCROLLING_START;
+ this.target = aAcc;
+ this.getID = function scrollingChecker_getID() {
+ return "scrolling start handling for " + prettyName(aAcc);
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "debug"; // debug stuff
+ // gA11yEventDumpToConsole = true; // debug stuff
+
+ function doTest() {
+ var actionsArray = [
+ {
+ ID: "anchor1",
+ actionName: "jump",
+ actionIndex: 0,
+ events: CLICK_EVENTS,
+ eventSeq: [
+ new scrollingChecker(getAccessible("bottom1")),
+ ],
+ },
+ { // jump again (test for bug 437607)
+ ID: "anchor1",
+ actionName: "jump",
+ actionIndex: 0,
+ events: CLICK_EVENTS,
+ eventSeq: [
+ new scrollingChecker(getAccessible("bottom1")),
+ ],
+ },
+ {
+ ID: "anchor2",
+ actionName: "jump",
+ actionIndex: 0,
+ events: CLICK_EVENTS,
+ eventSeq: [
+ new scrollingChecker(getAccessible("bottom2")),
+ ],
+ },
+ ];
+
+ testActions(actionsArray);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=506389"
+ title="Some same page links do not fire EVENT_SYSTEM_SCROLLINGSTART">
+ Mozilla Bug 506389
+ </a><br>
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=437607"
+ title="Clicking the 'Skip to main content' link once works, second time fails to initiate a V cursor jump">
+ Mozilla Bug 437607
+ </a><br>
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=519303"
+ title="Same page links to targets with content fires scrolling start accessible event on leaf text node">
+ Mozilla Bug 519303
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="debug"></div>
+
+ <h1>This is a test page for anchors</h1>
+ This is a top anchor<a name="Top">
+ </a><a id="anchor1" href="#bottom1">Link to anchor</a>
+ <a id="anchor2" href="#bottom2">Link to div</a>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br>This is some text in the middle<br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ This is some text.
+ This is a bottom anchor<a id="bottom1"></a>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <div id="bottom2">This is a div</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/actions/test_aria.html b/accessible/tests/mochitest/actions/test_aria.html
new file mode 100644
index 0000000000..7ec0f8ed35
--- /dev/null
+++ b/accessible/tests/mochitest/actions/test_aria.html
@@ -0,0 +1,200 @@
+<html>
+
+<head>
+ <title>nsIAccessible actions testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../actions.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var actionsArray = [
+ {
+ ID: "clickable",
+ actionName: "click",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "button",
+ actionName: "press",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "checkbox_unchecked",
+ actionName: "check",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "checkbox_checked",
+ actionName: "uncheck",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "checkbox_mixed",
+ actionName: "cycle",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "combobox_collapsed",
+ actionName: "open",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "combobox_expanded",
+ actionName: "close",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "link",
+ actionName: "jump",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "menuitem",
+ actionName: "click",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "menuitemcheckbox",
+ actionName: "click",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "menuitemradio",
+ actionName: "click",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "option",
+ actionName: "select",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "radio",
+ actionName: "select",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "switch_unchecked",
+ actionName: "check",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "switch_checked",
+ actionName: "uncheck",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "tab",
+ actionName: "switch",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "textbox",
+ actionName: "activate",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "treeitem",
+ actionName: "activate",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "sortable",
+ actionName: "sort",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "expandable",
+ actionName: "expand",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "collapseable",
+ actionName: "collapse",
+ events: CLICK_EVENTS,
+ },
+ ];
+ testActions(actionsArray);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=410765"
+ title="nsIAccessible actions testing">
+ Mozilla Bug 410765
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="clickable" onclick="">Clickable text</div>
+
+ <div id="button" role="button">Button</div>
+
+ <div id="checkbox_unchecked" role="checkbox">Checkbox</div>
+
+ <div id="checkbox_checked" role="checkbox" aria-checked="true">Checkbox</div>
+
+ <div id="checkbox_mixed" role="checkbox" aria-checked="mixed">Checkbox</div>
+
+ <div id="combobox_collapsed" role="combobox">
+ <div id="option" role="option">Option of collapsed combobox</div>
+ </div>
+
+ <div id="combobox_expanded" role="combobox" aria-expanded="true">
+ <div role="option">Option of expanded combobox</div>
+ </div>
+
+ <div id="link" role="link">Link</div>
+
+ <div role="menu">
+ <div id="menuitem" role="menuitem">Menuitem</div>
+ <div id="menuitemcheckbox" role="menuitemcheckbox">Menuitem checkbox</div>
+ <div id="menuitemradio" role="menuitemradio">Menuitem radio</div>
+ </div>
+
+ <div role="radiogroup">
+ <div id="radio" role="radio">Radio</div>
+ </div>
+
+ <div id="switch_unchecked" role="switch">Switch</div>
+
+ <div id="switch_checked" role="switch" aria-checked="true">Switch</div>
+
+ <div role="tablist">
+ <div id="tab" role="tab">Tab</div>
+ </div>
+
+ <div id="textbox" role="textbox">Textbox</div>
+
+ <div role="tree">
+ <div id="treeitem" role="treeitem">Treeitem</div>
+ </div>
+
+ <div role="grid">
+ <div id="sortable" role="columnheader" aria-sort="ascending">
+ Columnheader
+ </div>
+ </div>
+
+ <div id="expandable" aria-expanded="false">collapsed</div>
+ <div id="collapseable" aria-expanded="true">expanded</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/actions/test_controls.html b/accessible/tests/mochitest/actions/test_controls.html
new file mode 100644
index 0000000000..8b6f413619
--- /dev/null
+++ b/accessible/tests/mochitest/actions/test_controls.html
@@ -0,0 +1,107 @@
+<html>
+
+<head>
+ <title>nsIAccessible actions testing for inputs</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../actions.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var actionsArray = [
+ {
+ ID: "button",
+ actionName: "press",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "input_button",
+ actionName: "press",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "checkbox_unchecked",
+ actionName: "check",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "checkbox_checked",
+ actionName: "uncheck",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "checkbox_mixed",
+ actionName: "cycle",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "radio",
+ actionName: "select",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "textarea",
+ actionName: "activate",
+ events: FOCUS_EVENT,
+ },
+ {
+ ID: "textinput",
+ actionName: "activate",
+ events: FOCUS_EVENT,
+ },
+
+ ];
+ document.getElementById("checkbox_mixed").indeterminate = true;
+
+ testActions(actionsArray);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=477975"
+ title="nsIAccessible actions testing">
+ Mozilla Bug 477975
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <button id="button">Button</button>
+
+ <input id="input_button" type="button" value="normal">
+
+ <input id="checkbox_unchecked" type="checkbox">Checkbox</input>
+
+ <input id="checkbox_checked" type="checkbox" checked="true">Checkbox</input>
+
+ <input id="checkbox_mixed" type="checkbox">Checkbox</input>
+
+ <fieldset>
+ <input id="radio" type="radio">Radio</input>
+ </fieldset>
+
+ <textarea id="textarea" placeholder="What's happening?"></textarea>
+
+ <input id="textinput" type="text">
+</body>
+</html>
diff --git a/accessible/tests/mochitest/actions/test_general.html b/accessible/tests/mochitest/actions/test_general.html
new file mode 100644
index 0000000000..025b18f175
--- /dev/null
+++ b/accessible/tests/mochitest/actions/test_general.html
@@ -0,0 +1,105 @@
+<html>
+
+<head>
+ <title>nsIAccessible actions testing on HTML elements</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../actions.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var actionsArray = [
+ {
+ ID: "li_clickable1",
+ actionName: "click",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "li_clickable2",
+ actionName: "click",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "li_clickable3",
+ actionName: "click",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "onclick_img",
+ actionName: "click",
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: "label1",
+ actionName: "click",
+ events: CLICK_EVENTS,
+ },
+
+ ];
+
+ testActions(actionsArray);
+
+ is(getAccessible("label1").firstChild.actionCount, 1, "label text should have 1 action");
+
+ getAccessible("onclick_img").takeFocus();
+ is(getAccessible("link1").actionCount, 1, "links should have one action");
+ is(getAccessible("link2").actionCount, 1, "link with onclick handler should have 1 action");
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=523789"
+ title="nsHTMLLiAccessible shouldn't be inherited from linkable accessible">
+ Mozilla Bug 523789
+ </a><br>
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=423409"
+ title="Expose click action if mouseup and mousedown are registered">
+ Mozilla Bug 423409
+ </a>
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=659620"
+ title="hang when trying to edit a page on wikimo with NVDA running">
+ Mozilla Bug 659620
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <ul>
+ <li id="li_clickable1" onclick="">Clickable list item</li>
+ <li id="li_clickable2" onmousedown="">Clickable list item</li>
+ <li id="li_clickable3" onmouseup="">Clickable list item</li>
+ </ul>
+
+ <!-- linkable accessibles -->
+ <img id="onclick_img" onclick="" src="../moz.png">
+
+ <a id="link1" href="www">linkable textleaf accessible</a>
+ <div id="link2" onclick="">linkable textleaf accessible</div>
+
+ <div>
+ <label for="TextBox_t2" id="label1">
+ <span>Explicit</span>
+ </label>
+ <input name="in2" id="TextBox_t2" type="text" maxlength="17">
+ </div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/actions/test_general.xhtml b/accessible/tests/mochitest/actions/test_general.xhtml
new file mode 100644
index 0000000000..5b376b9624
--- /dev/null
+++ b/accessible/tests/mochitest/actions/test_general.xhtml
@@ -0,0 +1,167 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<?xml-stylesheet href="../nsIAccessible_name.css"
+ type="text/css"?>
+
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="nsIAccessible actions testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../actions.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ //gA11yEventDumpToConsole = true;
+ //enableLogging("tree,verbose"); // debug
+
+ SimpleTest.expectAssertions(0, 1);
+
+ function doTest()
+ {
+ var actionsArray = [
+ {
+ ID: "menu",
+ actionName: "click",
+ events: CLICK_EVENTS,
+ // Wait for the submenu to show up.
+ eventSeq: [
+ new invokerChecker(EVENT_SHOW, getNode("submenu"))
+ ]
+ },
+ {
+ ID: "submenu",
+ actionName: "click",
+ events: CLICK_EVENTS
+ },
+ {
+ ID: "menuitem",
+ actionName: "click",
+ events: XUL_EVENTS
+ },
+ {
+ ID: "button",
+ actionName: "press",
+ events: XUL_EVENTS
+ },
+ {
+ ID: "buttonmenu",
+ actionName: "press",
+ events: CLICK_EVENTS
+ },
+ {
+ ID: "name_entry_label",
+ actionName: "click",
+ events: CLICK_EVENTS
+ },
+ {
+ ID: "labelWithPopup",
+ actionName: "click",
+ events: CLICK_EVENTS
+ },
+ {
+ ID: "toolbarbutton_label",
+ actionName: "click",
+ targetID: "toolbarbutton",
+ events: XUL_EVENTS,
+ allowBubbling: true
+ },
+ {
+ ID: "menulist_label",
+ actionName: "click",
+ // focusChecker expects a unique focus event. However, there might
+ // still be pending focus events not caught by previous tests.
+ eventSeq: [
+ new invokerChecker(EVENT_FOCUS, getNode("menulist"))
+ ]
+ }/*, // XXX: bug 490288
+ {
+ ID: "buttonmenu_item",
+ actionName: "click",
+ events: CLICK_EVENTS
+ }*/
+ ];
+
+ is(getAccessible("name_entry_label").firstChild.actionCount, 1, "label text should have 1 action");
+
+ testActions(actionsArray);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=410765"
+ title="nsIAccessible actions testing">
+ Mozilla Bug 410765
+ </a>
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=504252"
+ title="Expose STATE_HASPOPUP on XUL elements that have an @popup attribute">
+ Mozilla Bug 504252
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <menubar>
+ <menu label="menu" id="menu">
+ <menupopup>
+ <menuitem label="menu item" id="menuitem"/>
+ <menu label="submenu" id="submenu">
+ <menupopup>
+ <menuitem label="menu item"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ </menubar>
+
+ <button label="button" id="button"/>
+
+ <button type="menu" id="buttonmenu" label="button">
+ <menupopup>
+ <menuitem label="item1" id="buttonmenu_item"/>
+ <menuitem label="item1"/>
+ </menupopup>
+ </button>
+
+ <label id="labelWithPopup" value="file name"
+ popup="fileContext"
+ tabindex="0"/>
+ <hbox>
+ <label id="name_entry_label" value="Name" control="name_entry"/>
+ <html:input id="name_entry"/>
+ </hbox>
+ <toolbarbutton id="toolbarbutton">
+ <label id="toolbarbutton_label">toolbarbutton</label>
+ </toolbarbutton>
+ <hbox>
+ <label id="menulist_label" control="menulist">menulist</label>
+ <menulist id="menulist"/>
+ </hbox>
+ </vbox>
+ </hbox>
+</window>
+
diff --git a/accessible/tests/mochitest/actions/test_keys.html b/accessible/tests/mochitest/actions/test_keys.html
new file mode 100644
index 0000000000..acacb34c09
--- /dev/null
+++ b/accessible/tests/mochitest/actions/test_keys.html
@@ -0,0 +1,57 @@
+<html>
+
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+ <title>Keyboard shortcuts tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+
+ <script type="application/javascript">
+ function testAcessKey(aAccOrElmOrID, aKey) {
+ var acc = getAccessible(aAccOrElmOrID);
+ if (!acc)
+ return;
+
+ is(acc.accessKey, aKey,
+ "Wrong keyboard shortcut on " + prettyName(aAccOrElmOrID));
+ }
+
+ function doTest() {
+ testAcessKey("input1", "");
+ testAcessKey("input2", MAC ? "⌃⌥b" : "Alt+Shift+b");
+ testAcessKey("link", MAC ? "⌃⌥l" : "Alt+Shift+l");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=381599"
+ title="Inverse relations cache">
+ Mozilla Bug 381599
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <label accesskey="a">
+ <input id="input1"/>
+ </label>
+ <label accesskey="b" for="input2">
+ <input id="input2"/>
+ <a id="link" accesskey="l">link</a>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/actions/test_keys.xhtml b/accessible/tests/mochitest/actions/test_keys.xhtml
new file mode 100644
index 0000000000..88bfd5c15d
--- /dev/null
+++ b/accessible/tests/mochitest/actions/test_keys.xhtml
@@ -0,0 +1,126 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Accessible XUL access keys and shortcut keys tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ SimpleTest.requestCompleteLog(); // To help diagnose bug 1845221
+
+ function openMenu(aMenuID, aMenuitemID)
+ {
+ this.menuNode = getNode(aMenuID);
+ this.menuitemNode = getNode(aMenuitemID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, this.menuNode)
+ ];
+
+ this.invoke = function openMenu_invoke()
+ {
+ // Show menu.
+ this.menuNode.open = true;
+ }
+
+ this.finalCheck = function openMenu_finalCheck()
+ {
+ var menu = getAccessible(aMenuID);
+ is(menu.accessKey, (MAC ? "u" : "Alt+u"),
+ "Wrong accesskey on " + prettyName(this.menuitemNode));
+
+ var menuitem = getAccessible(aMenuitemID);
+ is(menuitem.accessKey, "p",
+ "Wrong accesskey on " + prettyName(this.menuitemNode));
+ is(menuitem.keyboardShortcut, (MAC ? "⌃l" : "Ctrl+l"),
+ "Wrong keyboard shortcut on " + prettyName(this.menuitemNode));
+ }
+
+ this.getID = function openMenu_getID()
+ {
+ return "menuitem accesskey and shortcut test " +
+ prettyName(this.menuItemNode);
+ }
+ }
+
+ var gQueue = null;
+ function doTest()
+ {
+ // HTML element should get accessKey from associated XUL label.
+ let input = getAccessible("input");
+ is(input.accessKey, (MAC ? "⌃⌥i" : "Alt+Shift+i"),
+ "Wrong accessKey on input");
+
+ // Test accessKey on HTML element inside shadow DOM.
+ let shadowButton = getAccessible(
+ document.getElementById("buttonShadow").shadowRoot.firstElementChild);
+ is(shadowButton.accessKey, (MAC ? "⌃⌥t" : "Alt+Shift+t"),
+ "Wrong accessKey on shadow button");
+
+ gQueue = new eventQueue();
+ gQueue.push(new openMenu("menu", "menuitem"));
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=672092"
+ title="Reorganize access key and keyboard shortcut handling code">
+ Mozilla Bug 672092
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <label control="input" accesskey="i">input</label>
+ <html:input id="input"/>
+
+ <html:div id="buttonShadow"/>
+ <script>
+ <![CDATA[
+ let host = document.getElementById("buttonShadow");
+ let shadow = host.attachShadow({mode: "open"});
+ let button = document.createElement("button");
+ button.setAttribute("accesskey", "t");
+ shadow.append(button);
+ ]]>
+ </script>
+
+ <keyset>
+ <key key="l" modifiers="control" id="key1"/>
+ </keyset>
+
+ <menubar>
+ <menu label="menu" id="menu" accesskey="u">
+ <menupopup>
+ <menuitem accesskey="p" key="key1" label="item1" id="menuitem"/>
+ </menupopup>
+ </menu>
+ </menubar>
+
+ <vbox id="debug"/>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/actions/test_media.html b/accessible/tests/mochitest/actions/test_media.html
new file mode 100644
index 0000000000..5327901e56
--- /dev/null
+++ b/accessible/tests/mochitest/actions/test_media.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=483573
+-->
+<head>
+ <title>HTML5 audio/video tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../actions.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ SimpleTest.requestCompleteLog(); // To help diagnose bug 1845221
+
+ // gA11yEventDumpID = "eventDump";
+ // gA11yEventDumpToConsole = true; // debug stuff
+
+ function focusChecker(aAcc) {
+ this.type = EVENT_FOCUS;
+ this.target = aAcc;
+ this.getID = function focusChecker_getID() {
+ return "focus handling";
+ };
+ this.check = function focusChecker_check(aEvent) {
+ testStates(this.target, STATE_FOCUSED);
+ };
+ }
+
+ function nameChecker(aAcc, aName) {
+ this.type = EVENT_NAME_CHANGE;
+ this.target = aAcc;
+ this.getID = function nameChecker_getID() {
+ return "name change handling";
+ };
+ this.check = function nameChecker_check(aEvent) {
+ is(aEvent.accessible.name, aName,
+ "Wrong name of " + prettyName(aEvent.accessible) + " on focus");
+ };
+ }
+
+ async function loadAudioSource() {
+ /**
+ * Setting the source dynamically and wait for it to load,
+ * so we can test the accessibility tree of the control in its ready and
+ * stable state.
+ *
+ * See bug 1484048 comment 25 for discussion on how it switches UI when
+ * loading a statically declared source.
+ */
+ await new Promise(resolve => {
+ let el = document.getElementById("audio");
+ el.addEventListener("canplaythrough", resolve, {once: true});
+ el.src = "../bug461281.ogg";
+ });
+
+ doTest();
+ }
+
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // test actions of audio controls
+
+ todo(false, "Focus test are disabled until bug 494175 is fixed.");
+
+ var audioElm = getAccessible("audio");
+ var playBtn = audioElm.firstChild;
+ // var scrubber = playBtn.nextSibling.nextSibling.nextSibling;
+ var muteBtn = audioElm.lastChild.previousSibling;
+
+ var actions = [
+ {
+ ID: muteBtn,
+ actionName: "press",
+ eventTarget: "element",
+ eventSeq: [
+ // new focusChecker(muteBtn),
+ new nameChecker(muteBtn, "Unmute"),
+ ],
+ },
+ // {
+ // ID: scrubber,
+ // actionName: "activate",
+ // events: null,
+ // eventSeq: [
+ // new focusChecker(scrubber)
+ // ]
+ // },
+ {
+ ID: playBtn,
+ actionName: "press",
+ eventTarget: "element",
+ eventSeq: [
+ // new focusChecker(playBtn),
+ new nameChecker(playBtn, "Pause"),
+ ],
+ },
+ ];
+
+ testActions(actions); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(loadAudioSource);
+ </script>
+</head>
+<body>
+
+ <a target="_blank" rel="opener"
+ title="Expose HTML5 video and audio elements' embedded controls through accessibility APIs"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=483573">Mozilla Bug 483573</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <audio id="audio" controls="true"></audio>
+
+ <div id="eventDump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/actions/test_select.html b/accessible/tests/mochitest/actions/test_select.html
new file mode 100644
index 0000000000..7f55dfc0ef
--- /dev/null
+++ b/accessible/tests/mochitest/actions/test_select.html
@@ -0,0 +1,67 @@
+<html>
+
+<head>
+ <title>nsIAccessible actions testing for HTML select</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../actions.js"></script>
+
+ <script type="application/javascript">
+ // gA11yEventDumpToConsole = true; // debugging
+ function doTest() {
+ var actionsArray = [
+ {
+ ID: "lb_apple",
+ actionIndex: 0,
+ actionName: "select",
+ events: CLICK_EVENTS,
+ eventSeq: [
+ new focusChecker("lb_apple"),
+ ],
+ },
+ ];
+
+ testActions(actionsArray);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958"
+ title="Rework accessible focus handling">
+ Mozilla Bug 673958
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <select id="listbox" size="2">
+ <option id="lb_orange">orange</option>
+ <option id="lb_apple">apple</option>
+ </select>
+
+ <select id="combobox">
+ <option id="cb_orange">orange</option>
+ <option id="cb_apple">apple</option>
+ </select>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/actions/test_tree.xhtml b/accessible/tests/mochitest/actions/test_tree.xhtml
new file mode 100644
index 0000000000..17710cbdce
--- /dev/null
+++ b/accessible/tests/mochitest/actions/test_tree.xhtml
@@ -0,0 +1,127 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL tree actions tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../actions.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Accessible tree testers
+
+ function stateFocusChecker(aAcc, aStates)
+ {
+ this.__proto__ = new focusChecker(aAcc);
+
+ this.check = function focusChecker_check(aEvent)
+ {
+ var states = aStates ? aStates : 0;
+ testStates(this.target, STATE_FOCUSED | STATE_SELECTED | states);
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "debug";
+ //gA11yEventDumpToConsole = true; // debug
+
+ function doTest()
+ {
+ var treeNode = getNode("tree");
+
+ var treeBodyNode = treeNode.treeBody;
+
+ var tree = getAccessible(treeNode);
+ var expandedTreeItem = tree.getChildAt(2);
+ var collapsedTreeItem = tree.getChildAt(5);
+
+ var actions = [
+ {
+ ID: expandedTreeItem,
+ actionName: "activate",
+ actionIndex: 0,
+ events: CLICK_EVENTS,
+ targetID: treeBodyNode,
+ eventSeq: [
+ new stateFocusChecker(expandedTreeItem, STATE_EXPANDED)
+ ]
+ },
+ {
+ ID: collapsedTreeItem,
+ actionName: "expand",
+ actionIndex: 1,
+ events: CLICK_EVENTS,
+ targetID: treeBodyNode,
+ checkOnClickEvent: function check(aEvent)
+ {
+ testStates(this.ID, STATE_EXPANDED);
+ }
+ },
+ {
+ ID: collapsedTreeItem,
+ actionName: "collapse",
+ actionIndex: 1,
+ events: CLICK_EVENTS,
+ targetID: treeBodyNode,
+ checkOnClickEvent: function check(aEvent)
+ {
+ testStates(this.ID, STATE_COLLAPSED);
+ }
+ }
+ ];
+
+ testActions(actions); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yXULTreeLoadEvent(doTest, "tree", new nsTreeTreeView());
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727"
+ title="Reorganize implementation of XUL tree accessibility">
+ Mozilla Bug 503727
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <tree id="tree" flex="1" minheight="100px">
+ <treecols>
+ <treecol id="col" flex="1" primary="true" label="column"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <vbox id="debug"/>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/actions/test_treegrid.xhtml b/accessible/tests/mochitest/actions/test_treegrid.xhtml
new file mode 100644
index 0000000000..1c6e1bb8aa
--- /dev/null
+++ b/accessible/tests/mochitest/actions/test_treegrid.xhtml
@@ -0,0 +1,190 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<?xml-stylesheet href="../treeview.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL tree actions tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../actions.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Accessible tree testers
+
+ function focusChecker(aAcc, aStates)
+ {
+ this.type = EVENT_FOCUS;
+ this.target = aAcc;
+ this.getID = function focusChecker_getID()
+ {
+ return "focus handling";
+ }
+ this.check = function focusChecker_check(aEvent)
+ {
+ var states = aStates ? aStates : 0;
+ testStates(this.target, STATE_FOCUSED | STATE_SELECTED | states);
+ }
+ }
+
+ function stateChangeChecker(aAcc, aIsEnabled)
+ {
+ this.type = EVENT_STATE_CHANGE;
+ this.target = aAcc;
+ this.getID = function stateChangeChecker_getID()
+ {
+ return "state change handling";
+ }
+ this.check = function stateChangeChecker_check(aEvent)
+ {
+ if (aIsEnabled)
+ testStates(this.target, STATE_CHECKED);
+ else
+ testStates(this.target, STATE_CHECKABLE, 0, STATE_CHECKED);
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ function doTestActions()
+ {
+ var treeNode = getNode("tabletree");
+
+ var treeBodyNode = treeNode.treeBody;
+ treeNode.focus();
+
+ var tree = getAccessible(treeNode);
+
+ var expandedTreeItem = tree.getChildAt(2);
+ var collapsedTreeItem = tree.getChildAt(5);
+ var cycleCell = expandedTreeItem.getChildAt(0);
+ var checkableCell = expandedTreeItem.getChildAt(3);
+
+ var actions = [
+ {
+ ID: expandedTreeItem,
+ actionName: "activate",
+ actionIndex: 0,
+ events: CLICK_EVENTS,
+ targetID: treeBodyNode,
+ eventSeq: [
+ new focusChecker(expandedTreeItem, STATE_EXPANDED)
+ ]
+ },
+ {
+ ID: collapsedTreeItem,
+ actionName: "expand",
+ actionIndex: 1,
+ events: CLICK_EVENTS,
+ targetID: treeBodyNode,
+ check: function check(aEvent)
+ {
+ testStates(this.ID, STATE_EXPANDED);
+ }
+ },
+ {
+ ID: collapsedTreeItem,
+ actionName: "collapse",
+ actionIndex: 1,
+ events: CLICK_EVENTS,
+ targetID: treeBodyNode,
+ check: function check(aEvent)
+ {
+ testStates(this.ID, STATE_COLLAPSED);
+ }
+ },
+ {
+ ID: cycleCell,
+ actionName: "cycle",
+ actionIndex: 0,
+ events: CLICK_EVENTS,
+ targetID: treeBodyNode
+ },
+ {
+ ID: checkableCell,
+ actionName: "uncheck",
+ actionIndex: 0,
+ events: CLICK_EVENTS,
+ targetID: treeBodyNode,
+ eventSeq: [
+ new stateChangeChecker(checkableCell, false)
+ ]
+ },
+ {
+ ID: checkableCell,
+ actionName: "check",
+ actionIndex: 0,
+ events: CLICK_EVENTS,
+ targetID: treeBodyNode,
+ eventSeq: [
+ new stateChangeChecker(checkableCell, true)
+ ]
+ }
+ ];
+
+ testActions(actions); // Will call SimpleTest.finish();
+ }
+
+ // gA11yEventDumpID = "debug";
+
+ function doTest()
+ {
+ var treeNode = getNode("tabletree");
+ waitForEvent(EVENT_REORDER, treeNode, doTestActions);
+ treeNode.view = new nsTreeTreeView();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank" rel="opener"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727"
+ title="Reorganize implementation of XUL tree accessibility">
+ Mozilla Bug 503727
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <tree id="tabletree" flex="1" editable="true">
+ <treecols>
+ <treecol id="tabletree_col1" cycler="true" label="cycler"/>
+ <treecol id="tabletree_col2" flex="1" primary="true" label="column1"/>
+ <treecol id="tabletree_col3" flex="1" label="column2"/>
+ <treecol id="tabletree_col4" flex="1" label="checker"
+ type="checkbox" editable="true"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <vbox id="debug"/>
+ </vbox>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/aom/a11y.toml b/accessible/tests/mochitest/aom/a11y.toml
new file mode 100644
index 0000000000..08ee9e476e
--- /dev/null
+++ b/accessible/tests/mochitest/aom/a11y.toml
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+["test_general.html"]
diff --git a/accessible/tests/mochitest/aom/test_general.html b/accessible/tests/mochitest/aom/test_general.html
new file mode 100644
index 0000000000..dc63fb659b
--- /dev/null
+++ b/accessible/tests/mochitest/aom/test_general.html
@@ -0,0 +1,208 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Accessibility API: generic</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script>
+ "use strict";
+
+ SimpleTest.waitForExplicitFinish();
+ const finish = SimpleTest.finish.bind(SimpleTest);
+ enablePref()
+ .then(createIframe)
+ .then(checkImplementation)
+ .catch(err => {
+ dump(`${err}: ${err.stack}`);
+ finish();
+ });
+
+ function enablePref() {
+ const ops = {
+ "set": [
+ [ "accessibility.AOM.enabled", true ],
+ ],
+ };
+ return SpecialPowers.pushPrefEnv(ops);
+ }
+
+ // WebIDL conditional annotations for an interface are evaluated once per
+ // global, so we need to create an iframe to see the effects of calling
+ // enablePref().
+ function createIframe() {
+ return new Promise((resolve) => {
+ let iframe = document.createElement("iframe");
+ iframe.src = `data:text/html,<html><body>hey</body></html>`;
+ iframe.onload = () => resolve(iframe.contentDocument);
+ document.body.appendChild(iframe);
+ document.body.offsetTop; // We rely on the a11y tree being created
+ // already, and it's created off layout.
+ });
+ }
+
+ function testStringProp(anode, prop) {
+ is(anode[prop], null, `anode.${prop} should be null`);
+ let text = "This is a string test";
+ anode[prop] = text;
+ is(anode[prop], text, `anode.${prop} was assigned "${text}"`);
+ anode[prop] = null;
+ is(anode[prop], null, `anode.${prop} was assigned null`);
+ }
+
+ function testBoolProp(anode, prop) {
+ is(anode[prop], null, `anode.${prop} should be null`);
+ anode[prop] = true;
+ is(anode[prop], true, `anode.${prop} was assigned true`);
+ anode[prop] = false;
+ is(anode[prop], false, `anode.${prop} was assigned false`);
+ anode[prop] = null;
+ is(anode[prop], null, `anode.${prop} was assigned null`);
+ }
+
+ function testDoubleProp(anode, prop) {
+ is(anode[prop], null, `anode.${prop} should be null`);
+ anode[prop] = Number.MAX_VALUE;
+ is(anode[prop], Number.MAX_VALUE, `anode.${prop} was assigned ${Number.MAX_VALUE}`);
+ anode[prop] = null;
+ is(anode[prop], null, `anode.${prop} was assigned null`);
+ }
+
+ function testIntProp(anode, prop) {
+ is(anode[prop], null, `anode.${prop} should be null`);
+ anode[prop] = -1;
+ is(anode[prop], -1, `anode.${prop} was assigned -1`);
+ anode[prop] = null;
+ is(anode[prop], null, `anode.${prop} was assigned null`);
+ }
+
+ function testUIntProp(anode, prop) {
+ is(anode[prop], null, `anode.${prop} should be null`);
+ anode[prop] = 4294967295;
+ is(anode[prop], 4294967295, `anode.${prop} was assigned 4294967295`);
+ anode[prop] = null;
+ is(anode[prop], null, `anode.${prop} was assigned null`);
+ }
+
+ function testRelationProp(anode, node, prop) {
+ is(anode[prop], null, `anode.${prop} should be null`);
+ anode[prop] = node.accessibleNode;
+ is(anode[prop], node.accessibleNode, `anode.${prop} was assigned AccessibleNode`);
+ anode[prop] = null;
+ is(anode[prop], null, `anode.${prop} was assigned null`);
+ }
+ // Check that the WebIDL is as expected.
+ function checkImplementation(ifrDoc) {
+ let anode = ifrDoc.accessibleNode;
+ ok(anode, "DOM document has accessible node");
+
+ is(anode.computedRole, "document", "correct role of a document accessible node");
+ is(anode.DOMNode, ifrDoc, "correct DOM Node of a document accessible node");
+
+ // States may differ depending on the document state, for example, if it is
+ // loaded or is loading still.
+ var states = null;
+ switch (anode.states.length) {
+ case 5:
+ states = [
+ "readonly", "focusable", "opaque", "enabled", "sensitive",
+ ];
+ break;
+ case 6:
+ states = [
+ "readonly", "busy", "focusable", "opaque", "enabled", "sensitive",
+ ];
+ break;
+ case 7:
+ states = [
+ "readonly", "busy", "focusable", "opaque", "stale", "enabled", "sensitive",
+ ];
+ break;
+ default:
+ ok(false, "Unexpected amount of states: " + JSON.stringify(anode.states));
+ }
+ if (states) {
+ for (let i = 0; i < states.length; i++) {
+ is(anode.states[i], states[i], `${states[i]} state is expected at ${i}th index`);
+ }
+ }
+
+ ok(anode.is("document", "focusable"),
+ "correct role and state on an accessible node");
+
+ is(anode.get("explicit-name"), "true",
+ "correct object attribute value on an accessible node");
+
+ ok(anode.has("explicit-name"),
+ "object attributes are present");
+
+ var attrs = [ "explicit-name" ];
+ if (anode.attributes.length > 1) {
+ attrs = [
+ "margin-left", "text-align", "text-indent", "margin-right",
+ "tag", "margin-top", "margin-bottom", "display",
+ "explicit-name",
+ ];
+ }
+
+ is(anode.attributes.length, attrs.length, "correct number of attributes");
+ for (let i = 0; i < attrs.length; i++) {
+ ok(attrs.includes(anode.attributes[i]),
+ `${anode.attributes[i]} attribute is expected and found`);
+ }
+
+ const strProps = ["autocomplete", "checked", "current", "hasPopUp", "invalid",
+ "keyShortcuts", "label", "live", "orientation", "placeholder",
+ "pressed", "relevant", "role", "roleDescription", "sort",
+ "valueText"];
+
+ for (const strProp of strProps) {
+ testStringProp(anode, strProp);
+ }
+
+ const boolProps = ["atomic", "busy", "disabled", "expanded", "hidden", "modal",
+ "multiline", "multiselectable", "readOnly", "required", "selected"];
+
+ for (const boolProp of boolProps) {
+ testBoolProp(anode, boolProp);
+ }
+
+ const doubleProps = ["valueMax", "valueMin", "valueNow"];
+
+ for (const doubleProp of doubleProps) {
+ testDoubleProp(anode, doubleProp);
+ }
+
+ const intProps = ["colCount", "rowCount", "setSize"];
+
+ for (const intProp of intProps) {
+ testIntProp(anode, intProp);
+ }
+
+ const uintProps = ["colIndex", "colSpan", "level", "posInSet", "rowIndex", "rowSpan"];
+
+ for (const uintProp of uintProps) {
+ testUIntProp(anode, uintProp);
+ }
+
+ // Check if an AccessibleNode is properly cached.
+ let node = ifrDoc.createElement("div");
+ anode = node.accessibleNode;
+ is(anode, node.accessibleNode, "an AccessibleNode is properly cached");
+
+ // Adopting node to another document doesn't change .accessibleNode
+ let anotherDoc = ifrDoc.implementation.createDocument("", "", null);
+ let adopted_node = anotherDoc.adoptNode(node);
+ is(anode, adopted_node.accessibleNode, "adopting node to another document doesn't change node.accessibleNode");
+
+ const relationProps = ["activeDescendant", "details", "errorMessage"];
+
+ for (const relationProp of relationProps) {
+ testRelationProp(anode, node, relationProp);
+ }
+
+ finish();
+ }
+ </script>
+</head>
diff --git a/accessible/tests/mochitest/attributes.js b/accessible/tests/mochitest/attributes.js
new file mode 100644
index 0000000000..ebb5a54b85
--- /dev/null
+++ b/accessible/tests/mochitest/attributes.js
@@ -0,0 +1,516 @@
+/* import-globals-from common.js */
+
+// //////////////////////////////////////////////////////////////////////////////
+// Object attributes.
+
+/**
+ * Test object attributes.
+ *
+ * @param aAccOrElmOrID [in] the accessible identifier
+ * @param aAttrs [in] the map of expected object attributes
+ * (name/value pairs)
+ * @param aSkipUnexpectedAttrs [in] points this function doesn't fail if
+ * unexpected attribute is encountered
+ * @param aTodo [in] true if this is a 'todo'
+ */
+function testAttrs(aAccOrElmOrID, aAttrs, aSkipUnexpectedAttrs, aTodo) {
+ testAttrsInternal(aAccOrElmOrID, aAttrs, aSkipUnexpectedAttrs, null, aTodo);
+}
+
+/**
+ * Test object attributes that must not be present.
+ *
+ * @param aAccOrElmOrID [in] the accessible identifier
+ * @param aAbsentAttrs [in] map of attributes that should not be
+ * present (name/value pairs)
+ * @param aTodo [in] true if this is a 'todo'
+ */
+function testAbsentAttrs(aAccOrElmOrID, aAbsentAttrs, aTodo) {
+ testAttrsInternal(aAccOrElmOrID, {}, true, aAbsentAttrs, aTodo);
+}
+
+/**
+ * Test object attributes that aren't right, but should be (todo)
+ *
+ * @param aAccOrElmOrID [in] the accessible identifier
+ * @param aKey [in] attribute name
+ * @param aExpectedValue [in] expected attribute value
+ */
+function todoAttr(aAccOrElmOrID, aKey, aExpectedValue) {
+ testAttrs(
+ aAccOrElmOrID,
+ Object.fromEntries([[aKey, aExpectedValue]]),
+ true,
+ true
+ );
+}
+
+/**
+ * Test CSS based object attributes.
+ */
+function testCSSAttrs(aID) {
+ var node = document.getElementById(aID);
+ var computedStyle = document.defaultView.getComputedStyle(node);
+
+ var attrs = {
+ display: computedStyle.display,
+ "text-align": computedStyle.textAlign,
+ "text-indent": computedStyle.textIndent,
+ "margin-left": computedStyle.marginLeft,
+ "margin-right": computedStyle.marginRight,
+ "margin-top": computedStyle.marginTop,
+ "margin-bottom": computedStyle.marginBottom,
+ };
+ testAttrs(aID, attrs, true);
+}
+
+/**
+ * Test the accessible that it doesn't have CSS-based object attributes.
+ */
+function testAbsentCSSAttrs(aID) {
+ var attrs = {
+ display: "",
+ "text-align": "",
+ "text-indent": "",
+ "margin-left": "",
+ "margin-right": "",
+ "margin-top": "",
+ "margin-bottom": "",
+ };
+ testAbsentAttrs(aID, attrs);
+}
+
+/**
+ * Test group object attributes (posinset, setsize and level) and
+ * nsIAccessible::groupPosition() method.
+ *
+ * @param aAccOrElmOrID [in] the ID, DOM node or accessible
+ * @param aPosInSet [in] the value of 'posinset' attribute
+ * @param aSetSize [in] the value of 'setsize' attribute
+ * @param aLevel [in, optional] the value of 'level' attribute
+ */
+function testGroupAttrs(aAccOrElmOrID, aPosInSet, aSetSize, aLevel, aTodo) {
+ var acc = getAccessible(aAccOrElmOrID);
+ var levelObj = {},
+ posInSetObj = {},
+ setSizeObj = {};
+ acc.groupPosition(levelObj, setSizeObj, posInSetObj);
+
+ let groupPos = {},
+ expectedGroupPos = {};
+
+ if (aPosInSet && aSetSize) {
+ groupPos.setsize = String(setSizeObj.value);
+ groupPos.posinset = String(posInSetObj.value);
+
+ expectedGroupPos.setsize = String(aSetSize);
+ expectedGroupPos.posinset = String(aPosInSet);
+ }
+
+ if (aLevel) {
+ groupPos.level = String(levelObj.value);
+
+ expectedGroupPos.level = String(aLevel);
+ }
+
+ compareSimpleObjects(
+ groupPos,
+ expectedGroupPos,
+ false,
+ "wrong groupPos",
+ aTodo
+ );
+
+ testAttrs(aAccOrElmOrID, expectedGroupPos, true, aTodo);
+}
+
+function testGroupParentAttrs(
+ aAccOrElmOrID,
+ aChildItemCount,
+ aIsHierarchical,
+ aTodo
+) {
+ testAttrs(
+ aAccOrElmOrID,
+ { "child-item-count": String(aChildItemCount) },
+ true,
+ aTodo
+ );
+
+ if (aIsHierarchical) {
+ testAttrs(aAccOrElmOrID, { tree: "true" }, true, aTodo);
+ } else {
+ testAbsentAttrs(aAccOrElmOrID, { tree: "true" });
+ }
+}
+
+// //////////////////////////////////////////////////////////////////////////////
+// Text attributes.
+
+/**
+ * Test text attributes.
+ *
+ * @param aID [in] the ID of DOM element having text
+ * accessible
+ * @param aOffset [in] the offset inside text accessible to fetch
+ * text attributes
+ * @param aAttrs [in] the map of expected text attributes
+ * (name/value pairs) exposed at the offset
+ * @param aDefAttrs [in] the map of expected text attributes
+ * (name/value pairs) exposed on hyper text
+ * accessible
+ * @param aStartOffset [in] expected start offset where text attributes
+ * are applied
+ * @param aEndOffset [in] expected end offset where text attribute
+ * are applied
+ * @param aSkipUnexpectedAttrs [in] points the function doesn't fail if
+ * unexpected attribute is encountered
+ */
+function testTextAttrs(
+ aID,
+ aOffset,
+ aAttrs,
+ aDefAttrs,
+ aStartOffset,
+ aEndOffset,
+ aSkipUnexpectedAttrs
+) {
+ var accessible = getAccessible(aID, [nsIAccessibleText]);
+ if (!accessible) {
+ return;
+ }
+
+ var startOffset = { value: -1 };
+ var endOffset = { value: -1 };
+
+ // do not include attributes exposed on hyper text accessible
+ var attrs = getTextAttributes(
+ aID,
+ accessible,
+ false,
+ aOffset,
+ startOffset,
+ endOffset
+ );
+
+ if (!attrs) {
+ return;
+ }
+
+ var errorMsg = " for " + aID + " at offset " + aOffset;
+
+ is(startOffset.value, aStartOffset, "Wrong start offset" + errorMsg);
+ is(endOffset.value, aEndOffset, "Wrong end offset" + errorMsg);
+
+ compareAttrs(errorMsg, attrs, aAttrs, aSkipUnexpectedAttrs);
+
+ // include attributes exposed on hyper text accessible
+ var expectedAttrs = {};
+ for (let name in aAttrs) {
+ expectedAttrs[name] = aAttrs[name];
+ }
+
+ for (let name in aDefAttrs) {
+ if (!(name in expectedAttrs)) {
+ expectedAttrs[name] = aDefAttrs[name];
+ }
+ }
+
+ attrs = getTextAttributes(
+ aID,
+ accessible,
+ true,
+ aOffset,
+ startOffset,
+ endOffset
+ );
+
+ if (!attrs) {
+ return;
+ }
+
+ compareAttrs(errorMsg, attrs, expectedAttrs, aSkipUnexpectedAttrs);
+}
+
+/**
+ * Test default text attributes.
+ *
+ * @param aID [in] the ID of DOM element having text
+ * accessible
+ * @param aDefAttrs [in] the map of expected text attributes
+ * (name/value pairs)
+ * @param aSkipUnexpectedAttrs [in] points the function doesn't fail if
+ * unexpected attribute is encountered
+ */
+function testDefaultTextAttrs(aID, aDefAttrs, aSkipUnexpectedAttrs) {
+ var accessible = getAccessible(aID, [nsIAccessibleText]);
+ if (!accessible) {
+ return;
+ }
+
+ var defAttrs = null;
+ try {
+ defAttrs = accessible.defaultTextAttributes;
+ } catch (e) {}
+
+ if (!defAttrs) {
+ ok(false, "Can't get default text attributes for " + aID);
+ return;
+ }
+
+ var errorMsg = ". Getting default text attributes for " + aID;
+ compareAttrs(errorMsg, defAttrs, aDefAttrs, aSkipUnexpectedAttrs);
+}
+
+/**
+ * Test text attributes for wrong offset.
+ */
+function testTextAttrsWrongOffset(aID, aOffset) {
+ var res = false;
+ try {
+ var s = {},
+ e = {};
+ // Bug 1602031
+ // eslint-disable-next-line no-undef
+ var acc = getAccessible(ID, [nsIAccessibleText]);
+ acc.getTextAttributes(false, 157, s, e);
+ } catch (ex) {
+ res = true;
+ }
+
+ ok(
+ res,
+ "text attributes are calculated successfully at wrong offset " +
+ aOffset +
+ " for " +
+ prettyName(aID)
+ );
+}
+
+const kNormalFontWeight = function equalsToNormal(aWeight) {
+ return aWeight <= 400;
+};
+
+const kBoldFontWeight = function equalsToBold(aWeight) {
+ return aWeight > 400;
+};
+
+let isNNT = SpecialPowers.getBoolPref("widget.non-native-theme.enabled");
+// The pt font size of the input element can vary by Linux distro.
+const kInputFontSize =
+ WIN || (MAC && isNNT)
+ ? "10pt"
+ : MAC
+ ? "8pt"
+ : function () {
+ return true;
+ };
+
+const kAbsentFontFamily = function (aFontFamily) {
+ return aFontFamily != "sans-serif";
+};
+const kInputFontFamily = function (aFontFamily) {
+ return aFontFamily != "sans-serif";
+};
+
+const kMonospaceFontFamily = function (aFontFamily) {
+ return aFontFamily != "monospace";
+};
+const kSansSerifFontFamily = function (aFontFamily) {
+ return aFontFamily != "sans-serif";
+};
+const kSerifFontFamily = function (aFontFamily) {
+ return aFontFamily != "serif";
+};
+
+const kCursiveFontFamily = LINUX ? "DejaVu Serif" : "Comic Sans MS";
+
+/**
+ * Return used font from the given computed style.
+ */
+function fontFamily(aComputedStyle) {
+ var name = aComputedStyle.fontFamily;
+ switch (name) {
+ case "monospace":
+ return kMonospaceFontFamily;
+ case "sans-serif":
+ return kSansSerifFontFamily;
+ case "serif":
+ return kSerifFontFamily;
+ default:
+ return name;
+ }
+}
+
+/**
+ * Returns a computed system color for this document.
+ */
+function getSystemColor(aColor) {
+ let { r, g, b, a } = InspectorUtils.colorToRGBA(aColor, document);
+ return a == 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a})`;
+}
+
+/**
+ * Build an object of default text attributes expected for the given accessible.
+ *
+ * @param aID [in] identifier of accessible
+ * @param aFontSize [in] font size
+ * @param aFontWeight [in, optional] kBoldFontWeight or kNormalFontWeight,
+ * default value is kNormalFontWeight
+ */
+function buildDefaultTextAttrs(aID, aFontSize, aFontWeight, aFontFamily) {
+ var elm = getNode(aID);
+ var computedStyle = document.defaultView.getComputedStyle(elm);
+ var bgColor =
+ computedStyle.backgroundColor == "rgba(0, 0, 0, 0)"
+ ? getSystemColor("Canvas")
+ : computedStyle.backgroundColor;
+
+ var defAttrs = {
+ "font-style": computedStyle.fontStyle,
+ "font-size": aFontSize,
+ "background-color": bgColor,
+ "font-weight": aFontWeight ? aFontWeight : kNormalFontWeight,
+ color: computedStyle.color,
+ "font-family": aFontFamily ? aFontFamily : fontFamily(computedStyle),
+ "text-position": computedStyle.verticalAlign,
+ };
+
+ return defAttrs;
+}
+
+// //////////////////////////////////////////////////////////////////////////////
+// Private.
+
+function getTextAttributes(
+ aID,
+ aAccessible,
+ aIncludeDefAttrs,
+ aOffset,
+ aStartOffset,
+ aEndOffset
+) {
+ // This function expects the passed in accessible to already be queried for
+ // nsIAccessibleText.
+ var attrs = null;
+ try {
+ attrs = aAccessible.getTextAttributes(
+ aIncludeDefAttrs,
+ aOffset,
+ aStartOffset,
+ aEndOffset
+ );
+ } catch (e) {}
+
+ if (attrs) {
+ return attrs;
+ }
+
+ ok(false, "Can't get text attributes for " + aID);
+ return null;
+}
+
+function testAttrsInternal(
+ aAccOrElmOrID,
+ aAttrs,
+ aSkipUnexpectedAttrs,
+ aAbsentAttrs,
+ aTodo
+) {
+ var accessible = getAccessible(aAccOrElmOrID);
+ if (!accessible) {
+ return;
+ }
+
+ var attrs = null;
+ try {
+ attrs = accessible.attributes;
+ } catch (e) {}
+
+ if (!attrs) {
+ ok(false, "Can't get object attributes for " + prettyName(aAccOrElmOrID));
+ return;
+ }
+
+ var errorMsg = " for " + prettyName(aAccOrElmOrID);
+ compareAttrs(
+ errorMsg,
+ attrs,
+ aAttrs,
+ aSkipUnexpectedAttrs,
+ aAbsentAttrs,
+ aTodo
+ );
+}
+
+function compareAttrs(
+ aErrorMsg,
+ aAttrs,
+ aExpectedAttrs,
+ aSkipUnexpectedAttrs,
+ aAbsentAttrs,
+ aTodo
+) {
+ // Check if all obtained attributes are expected and have expected value.
+ let attrObject = {};
+ for (let prop of aAttrs.enumerate()) {
+ attrObject[prop.key] = prop.value;
+ }
+
+ // Create expected attributes set by using the return values from
+ // embedded functions to determine the entry's value.
+ let expectedObj = Object.fromEntries(
+ Object.entries(aExpectedAttrs).map(([k, v]) => {
+ if (v instanceof Function) {
+ // If value is a function that returns true given the received
+ // attribute value, assign the attribute value to the entry.
+ // If it is false, stringify the function for good error reporting.
+ let value = v(attrObject[k]) ? attrObject[k] : v.toString();
+ return [k, value];
+ }
+
+ return [k, v];
+ })
+ );
+
+ compareSimpleObjects(
+ attrObject,
+ expectedObj,
+ aSkipUnexpectedAttrs,
+ aErrorMsg,
+ aTodo
+ );
+
+ // Check if all unexpected attributes are absent.
+ if (aAbsentAttrs) {
+ let presentAttrs = Object.keys(attrObject).filter(
+ k => aAbsentAttrs[k] !== undefined
+ );
+ if (presentAttrs.length) {
+ (aTodo ? todo : ok)(
+ false,
+ `There were unexpected attributes: ${presentAttrs}`
+ );
+ }
+ }
+}
+
+function compareSimpleObjects(
+ aObj,
+ aExpectedObj,
+ aSkipUnexpectedAttrs,
+ aMessage,
+ aTodo
+) {
+ let keys = aSkipUnexpectedAttrs
+ ? Object.keys(aExpectedObj).sort()
+ : Object.keys(aObj).sort();
+ let o1 = JSON.stringify(aObj, keys);
+ let o2 = JSON.stringify(aExpectedObj, keys);
+
+ if (aTodo) {
+ todo_is(o1, o2, `${aMessage} - Got ${o1}, expected ${o2}`);
+ } else {
+ is(o1, o2, aMessage);
+ }
+}
diff --git a/accessible/tests/mochitest/attributes/a11y.toml b/accessible/tests/mochitest/attributes/a11y.toml
new file mode 100644
index 0000000000..7ea2d1824f
--- /dev/null
+++ b/accessible/tests/mochitest/attributes/a11y.toml
@@ -0,0 +1,22 @@
+[DEFAULT]
+support-files = "!/accessible/tests/mochitest/*.js"
+
+["test_dpub_aria_xml-roles.html"]
+
+["test_graphics_aria_xml-roles.html"]
+
+["test_listbox.html"]
+
+["test_obj.html"]
+
+["test_obj_css.html"]
+
+["test_obj_group.html"]
+
+["test_obj_group.xhtml"]
+
+["test_obj_group_tree.xhtml"]
+
+["test_tag.html"]
+
+["test_xml-roles.html"]
diff --git a/accessible/tests/mochitest/attributes/test_dpub_aria_xml-roles.html b/accessible/tests/mochitest/attributes/test_dpub_aria_xml-roles.html
new file mode 100644
index 0000000000..a6b4dd4840
--- /dev/null
+++ b/accessible/tests/mochitest/attributes/test_dpub_aria_xml-roles.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>XML roles tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ // DPub ARIA roles should be exposed via the xml-roles object attribute.
+ let dpub_attrs = [
+ "doc-abstract",
+ "doc-acknowledgments",
+ "doc-afterword",
+ "doc-appendix",
+ "doc-backlink",
+ "doc-biblioentry",
+ "doc-bibliography",
+ "doc-biblioref",
+ "doc-chapter",
+ "doc-colophon",
+ "doc-conclusion",
+ "doc-cover",
+ "doc-credit",
+ "doc-credits",
+ "doc-dedication",
+ "doc-endnote",
+ "doc-endnotes",
+ "doc-epigraph",
+ "doc-epilogue",
+ "doc-errata",
+ "doc-example",
+ "doc-footnote",
+ "doc-foreword",
+ "doc-glossary",
+ "doc-glossref",
+ "doc-index",
+ "doc-introduction",
+ "doc-noteref",
+ "doc-notice",
+ "doc-pagebreak",
+ "doc-pagelist",
+ "doc-part",
+ "doc-preface",
+ "doc-prologue",
+ "doc-pullquote",
+ "doc-qna",
+ "doc-subtitle",
+ "doc-tip",
+ "doc-toc",
+ ];
+ for (let attr of dpub_attrs) {
+ testAttrs(attr, {"xml-roles": attr}, true);
+ }
+ SimpleTest.finish();
+ }
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1343537"
+ title="implement ARIA DPUB extension">
+ Bug 1343537
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ <div id="doc-abstract" role="doc-abstract">abstract</div>
+ <div id="doc-acknowledgments" role="doc-acknowledgments">acknowledgments</div>
+ <div id="doc-afterword" role="doc-afterword">afterword</div>
+ <div id="doc-appendix" role="doc-appendix">appendix</div>
+ <div id="doc-backlink" role="doc-backlink">backlink</div>
+ <div id="doc-biblioentry" role="doc-biblioentry">biblioentry</div>
+ <div id="doc-bibliography" role="doc-bibliography">bibliography</div>
+ <div id="doc-biblioref" role="doc-biblioref">biblioref</div>
+ <div id="doc-chapter" role="doc-chapter">chapter</div>
+ <div id="doc-colophon" role="doc-colophon">colophon</div>
+ <div id="doc-conclusion" role="doc-conclusion">conclusion</div>
+ <div id="doc-cover" role="doc-cover">cover</div>
+ <div id="doc-credit" role="doc-credit">credit</div>
+ <div id="doc-credits" role="doc-credits">credits</div>
+ <div id="doc-dedication" role="doc-dedication">dedication</div>
+ <div id="doc-endnote" role="doc-endnote">endnote</div>
+ <div id="doc-endnotes" role="doc-endnotes">endnotes</div>
+ <div id="doc-epigraph" role="doc-epigraph">epigraph</div>
+ <div id="doc-epilogue" role="doc-epilogue">epilogue</div>
+ <div id="doc-errata" role="doc-errata">errata</div>
+ <div id="doc-example" role="doc-example">example</div>
+ <div id="doc-footnote" role="doc-footnote">footnote</div>
+ <div id="doc-foreword" role="doc-foreword">foreword</div>
+ <div id="doc-glossary" role="doc-glossary">glossary</div>
+ <div id="doc-glossref" role="doc-glossref">glossref</div>
+ <div id="doc-index" role="doc-index">index</div>
+ <div id="doc-introduction" role="doc-introduction">introduction</div>
+ <div id="doc-noteref" role="doc-noteref">noteref</div>
+ <div id="doc-notice" role="doc-notice">notice</div>
+ <div id="doc-pagebreak" role="doc-pagebreak">pagebreak</div>
+ <div id="doc-pagelist" role="doc-pagelist">pagelist</div>
+ <div id="doc-part" role="doc-part">part</div>
+ <div id="doc-preface" role="doc-preface">preface</div>
+ <div id="doc-prologue" role="doc-prologue">prologue</div>
+ <div id="doc-pullquote" role="doc-pullquote">pullquote</div>
+ <div id="doc-qna" role="doc-qna">qna</div>
+ <div id="doc-subtitle" role="doc-subtitle">subtitle</div>
+ <div id="doc-tip" role="doc-tip">tip</div>
+ <div id="doc-toc" role="doc-toc">toc</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/attributes/test_graphics_aria_xml-roles.html b/accessible/tests/mochitest/attributes/test_graphics_aria_xml-roles.html
new file mode 100644
index 0000000000..45d5e2fa0b
--- /dev/null
+++ b/accessible/tests/mochitest/attributes/test_graphics_aria_xml-roles.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>XML roles tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ // Graphics ARIA roles should be exposed via the xml-roles object attribute.
+ let graphics_attrs = [
+ "graphics-document",
+ "graphics-object",
+ "graphics-symbol",
+ ];
+ for (let attr of graphics_attrs) {
+ testAttrs(attr, {"xml-roles": attr}, true);
+ }
+ SimpleTest.finish();
+ }
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432513"
+ title="implement ARIA Graphics roles">
+ Bug 1432513
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ <div id="graphics-document" role="graphics-document">document</div>
+ <div id="graphics-object" role="graphics-object">object</div>
+ <div id="graphics-symbol" role="graphics-symbol">symbol</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/attributes/test_listbox.html b/accessible/tests/mochitest/attributes/test_listbox.html
new file mode 100644
index 0000000000..5489e74b74
--- /dev/null
+++ b/accessible/tests/mochitest/attributes/test_listbox.html
@@ -0,0 +1,82 @@
+<html>
+
+<head>
+ <title>Listbox group attribute tests</title>
+ <meta charset="utf-8" />
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ async function doTest() {
+ // First test the whole lot.
+ testGroupAttrs("a", 1, 6);
+ testGroupAttrs("b", 2, 6);
+ testGroupAttrs("c", 3, 6);
+ testGroupAttrs("d", 4, 6);
+ testGroupAttrs("e", 5, 6);
+ testGroupAttrs("f", 6, 6);
+ // Remove c, reducing the set to 5.
+ let listbox = getAccessible("listbox");
+ let updated = waitForEvent(EVENT_REORDER, listbox);
+ c.remove();
+ await updated;
+ testGroupAttrs("a", 1, 5);
+ testGroupAttrs("b", 2, 5);
+ testGroupAttrs("d", 3, 5);
+ testGroupAttrs("e", 4, 5);
+ testGroupAttrs("f", 5, 5);
+ // Now, remove the first element.
+ updated = waitForEvent(EVENT_REORDER, listbox);
+ a.remove();
+ await updated;
+ testGroupAttrs("b", 1, 4);
+ testGroupAttrs("d", 2, 4);
+ testGroupAttrs("e", 3, 4);
+ testGroupAttrs("f", 4, 4);
+ // Remove the last item.
+ updated = waitForEvent(EVENT_REORDER, listbox);
+ f.remove();
+ await updated;
+ testGroupAttrs("b", 1, 3);
+ testGroupAttrs("d", 2, 3);
+ testGroupAttrs("e", 3, 3);
+ // Finally, remove the middle item.
+ updated = waitForEvent(EVENT_REORDER, listbox);
+ d.remove();
+ await updated;
+ testGroupAttrs("b", 1, 2);
+ testGroupAttrs("e", 2, 2);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- Group information updated after removal of list items, bug 1515186 -->
+ <div id="listbox" role="listbox">
+ <div id="a" role="option">Option a</div>
+ <div id="b" role="option">Option b</div>
+ <div id="c" role="option">Option c</div>
+ <div id="d" role="option">Option d</div>
+ <div id="e" role="option">Option e</div>
+ <div id="f" role="option">Option f</div>
+ </div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/attributes/test_obj.html b/accessible/tests/mochitest/attributes/test_obj.html
new file mode 100644
index 0000000000..fedf6a1b7b
--- /dev/null
+++ b/accessible/tests/mochitest/attributes/test_obj.html
@@ -0,0 +1,292 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=475006
+https://bugzilla.mozilla.org/show_bug.cgi?id=391829
+https://bugzilla.mozilla.org/show_bug.cgi?id=581952
+https://bugzilla.mozilla.org/show_bug.cgi?id=558036
+-->
+<head>
+ <title>Group attributes tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // aria
+ testAttrs("atomic", {"atomic": "true", "container-atomic": "true"}, true);
+ testAttrs(getNode("atomic").firstChild, {"container-atomic": "true"}, true);
+ testAbsentAttrs("atomic_false", {"atomic": "false", "container-atomic": "false"});
+ testAbsentAttrs(getNode("atomic_false").firstChild, {"container-atomic": "false"});
+
+ testAttrs("autocomplete", {"autocomplete": "true"}, true);
+ testAttrs("checkbox", {"checkable": "true"}, true);
+ testAttrs("checkedCheckbox", {"checkable": "true"}, true);
+ testAbsentAttrs("checkedMenuitem", {"checkable": "true"});
+ testAttrs("checkedMenuitemCheckbox", {"checkable": "true"}, true);
+ testAttrs("checkedMenuitemRadio", {"checkable": "true"}, true);
+ testAttrs("checkedOption", {"checkable": "true"}, true);
+ testAttrs("checkedRadio", {"checkable": "true"}, true);
+ testAttrs("checkedTreeitem", {"checkable": "true"}, true);
+ testAttrs("dropeffect", {"dropeffect": "copy"}, true);
+ testAttrs("grabbed", {"grabbed": "true"}, true);
+ testAttrs("haspopupTrue", { "haspopup": "true" }, true);
+ testAbsentAttrs("haspopupFalse", { "haspopup": "false" });
+ testAbsentAttrs("haspopupEmpty", { "haspopup": "" });
+ testAttrs("haspopupDialog", { "haspopup": "dialog" }, true);
+ testAttrs("haspopupListbox", { "haspopup": "listbox" }, true);
+ testAttrs("haspopupMenu", { "haspopup": "menu" }, true);
+ testAttrs("haspopupTree", { "haspopup": "tree" }, true);
+ testAbsentAttrs("modal", {"modal": "true"});
+ testAttrs("sortAscending", {"sort": "ascending"}, true);
+ testAttrs("sortDescending", {"sort": "descending"}, true);
+ testAttrs("sortNone", {"sort": "none"}, true);
+ testAttrs("sortOther", {"sort": "other"}, true);
+ testAttrs("roledescr", {"roledescription": "spreadshit"}, true);
+ testAttrs("currentPage", {"current": "page"}, true);
+ testAttrs("currentStep", {"current": "step"}, true);
+ testAttrs("currentLocation", {"current": "location"}, true);
+ testAttrs("currentDate", {"current": "date"}, true);
+ testAttrs("currentTime", {"current": "time"}, true);
+ testAttrs("currentTrue", {"current": "true"}, true);
+ testAttrs("currentOther", {"current": "true"}, true);
+ testAbsentAttrs("currentFalse", {"current": "true"});
+ testAttrs("currentSpan", {"current": "page"}, true);
+
+ // live object attribute
+
+ // HTML
+ testAttrs("output", {"live": "polite"}, true);
+
+ // ARIA
+ testAttrs("live", {"live": "polite"}, true);
+ testAttrs("live2", {"live": "polite"}, true);
+ testAbsentAttrs("live3", {"live": ""});
+ if (MAC) {
+ testAttrs("alert", {"live": "assertive"}, true);
+ } else {
+ testAbsentAttrs("alert", {"live": "assertive"});
+ }
+ testAttrs("log", {"live": "polite"}, true);
+ testAttrs("logAssertive", {"live": "assertive"}, true);
+ testAttrs("marquee", {"live": "off"}, true);
+ testAttrs("status", {"live": "polite"}, true);
+ testAttrs("timer", {"live": "off"}, true);
+ testAbsentAttrs("tablist", {"live": "polite"});
+
+ // container-live object attribute
+ testAttrs("liveChild", {"container-live": "polite"}, true);
+ testAttrs("live2Child", {"container-live": "polite"}, true);
+ if (MAC) {
+ testAttrs("alertChild", {"container-live": "assertive"}, true);
+ } else {
+ testAbsentAttrs("alertChild", {"container-live": "assertive"});
+ }
+ testAttrs("logChild", {"container-live": "polite"}, true);
+ testAttrs("logAssertiveChild", {"container-live": "assertive"}, true);
+ testAttrs("marqueeChild", {"container-live": "off"}, true);
+ testAttrs("statusChild", {"container-live": "polite"}, true);
+ testAttrs("timerChild", {"container-live": "off"}, true);
+ testAbsentAttrs("tablistChild", {"container-live": "polite"});
+ testAttrs("containerLiveOutput", {"container-live": "polite"}, true);
+ testAttrs("containerLiveOutput1", {"container-live": "polite"}, true);
+ testAttrs("containerLiveOutput2", {"container-live": "polite"}, true);
+
+ // container-live-role object attribute
+ testAttrs("log", {"container-live-role": "log"}, true);
+ testAttrs("logAssertive", {"container-live-role": "log"}, true);
+ testAttrs("marquee", {"container-live-role": "marquee"}, true);
+ testAttrs("status", {"container-live-role": "status"}, true);
+ testAttrs("timer", {"container-live-role": "timer"}, true);
+ testAttrs("logChild", {"container-live-role": "log"}, true);
+ testAttrs("logAssertive", {"container-live-role": "log"}, true);
+ testAttrs("logAssertiveChild", {"container-live-role": "log"}, true);
+ testAttrs("marqueeChild", {"container-live-role": "marquee"}, true);
+ testAttrs("statusChild", {"container-live-role": "status"}, true);
+ testAttrs("timerChild", {"container-live-role": "timer"}, true);
+ testAbsentAttrs("tablistChild", {"container-live-role": "tablist"});
+
+ // absent aria-label and aria-labelledby object attribute
+ testAbsentAttrs("label", {"label": "foo"});
+ testAbsentAttrs("labelledby", {"labelledby": "label"});
+
+ // container that has no default live attribute
+ testAttrs("liveGroup", {"live": "polite"}, true);
+ testAttrs("liveGroupChild", {"container-live": "polite"}, true);
+ testAttrs("liveGroup", {"container-live-role": "group"}, true);
+ testAttrs("liveGroupChild", {"container-live-role": "group"}, true);
+
+ // text input type
+ testAbsentAttrs("button", { "text-input-type": "button"});
+ testAbsentAttrs("checkbox", { "text-input-type": "checkbox"});
+ testAbsentAttrs("radio", { "text-input-type": "radio"});
+ testAttrs("email", {"text-input-type": "email"}, true);
+ testAttrs("search", {"text-input-type": "search"}, true);
+ testAttrs("tel", {"text-input-type": "tel"}, true);
+ testAttrs("url", {"text-input-type": "url"}, true);
+ testAttrs("number", {"text-input-type": "number"}, true);
+
+ // ARIA
+ testAttrs("searchbox", {"text-input-type": "search"}, true);
+
+ // html
+ testAttrs("radio", {"checkable": "true"}, true);
+ testAttrs("checkbox", {"checkable": "true"}, true);
+ testAttrs("draggable", {"draggable": "true"}, true);
+ testAttrs("th1", { "abbr": "SS#" }, true);
+ testAttrs("th2", { "abbr": "SS#" }, true);
+ testAttrs("th2", { "axis": "social" }, true);
+
+ // don't barf on an empty abbr element.
+ testAbsentAttrs("th3", { "abbr": "" });
+
+ // application accessible
+ if (WIN) {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].
+ getService(Ci.nsIGfxInfo);
+ var attrs = {
+ "D2D": (gfxInfo.D2DEnabled ? "true" : "false"),
+ };
+ testAttrs(getApplicationAccessible(), attrs, false);
+ }
+
+ // no object attributes
+ testAbsentAttrs(getAccessible("listitem").firstChild, { "tag": "" });
+
+ // experimental aria
+ testAttrs("experimental", {"blah": "true"}, true);
+
+ // HTML5 aside element xml-roles
+ testAttrs("aside0", {"xml-roles": "note"}, true);
+ testAttrs("aside1", {"xml-roles": "group"}, true);
+ testAttrs("aside2", {"xml-roles": "complementary"}, true);
+
+ // non-standard data-at-shortcutkeys attribute:
+ testAttrs("shortcuts", {'data-at-shortcutkeys': '{"n":"New message","r":"Reply to message"}'}, true);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- container live -->
+ <output id="containerLiveOutput"><div id="containerLiveOutput1"><div id="containerLiveOutput2">Test</div></div></output>
+
+ <!-- aria -->
+ <div id="atomic" aria-atomic="true">live region</div>
+ <div id="atomic_false" aria-atomic="false">live region</div>
+ <div id="autocomplete" role="textbox" aria-autocomplete="true"></div>
+ <div id="checkbox" role="checkbox"></div>
+ <div id="checkedCheckbox" role="checkbox" aria-checked="true"></div>
+ <div id="checkedMenuitem" role="menuitem" aria-checked="true"></div>
+ <div id="checkedMenuitemCheckbox" role="menuitemcheckbox" aria-checked="true"></div>
+ <div id="checkedMenuitemRadio" role="menuitemradio" aria-checked="true"></div>
+ <div id="checkedOption" role="option" aria-checked="true"></div>
+ <div id="checkedRadio" role="radio" aria-checked="true"></div>
+ <div id="checkedTreeitem" role="treeitem" aria-checked="true"></div>
+ <div id="dropeffect" aria-dropeffect="copy"></div>
+ <div id="grabbed" aria-grabbed="true"></div>
+ <div id="haspopupTrue" aria-haspopup="true"></div>
+ <div id="haspopupFalse" aria-haspopup="false"></div>
+ <div id="haspopupEmpty" aria-haspopup=""></div>
+ <div id="haspopupDialog" aria-haspopup="dialog"></div>
+ <div id="haspopupListbox" aria-haspopup="listbox"></div>
+ <div id="haspopupMenu" aria-haspopup="menu"></div>
+ <div id="haspopupTree" aria-haspopup="tree"></div>
+ <div id="modal" aria-modal="true"></div>
+ <div id="sortAscending" role="columnheader" aria-sort="ascending"></div>
+ <div id="sortDescending" role="columnheader" aria-sort="descending"></div>
+ <div id="sortNone" role="columnheader" aria-sort="none"></div>
+ <div id="sortOther" role="columnheader" aria-sort="other"></div>
+ <div id="roledescr" aria-roledescription="spreadshit"></div>
+ <div id="currentPage" aria-current="page"></div>
+ <div id="currentStep" aria-current="step"></div>
+ <div id="currentLocation" aria-current="location"></div>
+ <div id="currentDate" aria-current="date"></div>
+ <div id="currentTime" aria-current="time"></div>
+ <div id="currentTrue" aria-current="true"></div>
+ <div id="currentOther" aria-current="other"></div>
+ <div id="currentFalse" aria-current="false"></div>
+
+ <!-- aria-current on a span which must create an accessible -->
+ <ol>
+ <li><a href="...">Page 1</a></li>
+ <li><a href="...">Page 2</a></li>
+ <li><span id="currentSpan" aria-current="page">This page</span></li>
+ </ol>
+
+ <!-- html -->
+ <output id="output"></output>
+
+ <!-- back to aria -->
+ <div id="live" aria-live="polite">excuse <div id="liveChild">me</div></div>
+ <div id="live2" role="marquee" aria-live="polite">excuse <div id="live2Child">me</div></div>
+ <div id="live3" role="region">excuse</div>
+ <div id="alert" role="alert">excuse <div id="alertChild">me</div></div>
+ <div id="log" role="log">excuse <div id="logChild">me</div></div>
+ <div id="logAssertive" role="log" aria-live="assertive">excuse <div id="logAssertiveChild">me</div></div>
+ <div id="marquee" role="marquee">excuse <div id="marqueeChild">me</div></div>
+ <div id="status" role="status">excuse <div id="statusChild">me</div></div>
+ <div id="tablist" role="tablist">tablist <div id="tablistChild">tab</div></div>
+ <div id="timer" role="timer">excuse <div id="timerChild">me</div></div>
+
+ <!-- aria-label[ledby] should not be an object attribute -->
+ <div id="label" role="checkbox" aria-label="foo"></div>
+ <div id="labelledby" role="checkbox" aria-labelledby="label"></div>
+
+ <!-- unusual live case -->
+ <div id="liveGroup" role="group" aria-live="polite">
+ excuse <div id="liveGroupChild">me</div>
+ </div>
+
+ <!-- text input type -->
+ <input id="button" type="button"/>
+ <input id="email" type="email"/>
+ <input id="search" type="search"/>
+ <input id="tel" type="tel"/>
+ <input id="url" type="url"/>
+ <input id="number" type="number"/>
+ <div id="searchbox" role="searchbox"></div>
+
+ <!-- html -->
+ <input id="radio" type="radio"/>
+ <input id="checkbox" type="checkbox"/>
+ <div id="draggable" draggable="true">Draggable div</div>
+ <table>
+ <tr>
+ <th id="th1"><abbr title="Social Security Number">SS#</abbr></th>
+ <th id="th2" abbr="SS#" axis="social">Social Security Number</th>
+ <th id="th3"><abbr></abbr></th>
+ </tr>
+ </table>
+
+ <ul>
+ <li id="listitem">item
+ </ul>
+
+ <!-- experimental aria -->
+ <div id="experimental" aria-blah="true">Fake beer</div>
+
+ <!-- HTML5 aside elements -->
+ <aside id="aside0" role="note">aside 0</aside>
+ <aside id="aside1" role="group">aside 1</aside>
+ <aside id="aside2">aside 2</aside>
+
+ <!-- Shortcuts for web applications -->
+ <div id="shortcuts" data-at-shortcutkeys='{"n":"New message","r":"Reply to message"}'></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/attributes/test_obj_css.html b/accessible/tests/mochitest/attributes/test_obj_css.html
new file mode 100644
index 0000000000..6c702ba5a6
--- /dev/null
+++ b/accessible/tests/mochitest/attributes/test_obj_css.html
@@ -0,0 +1,225 @@
+<html>
+<head>
+ <title>CSS-like attributes tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ var gQueue = null;
+
+ function removeElm(aID) {
+ this.node = getNode(aID);
+ this.accessible = getAccessible(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.accessible),
+ ];
+
+ this.invoke = function removeElm_invoke() {
+ this.node.remove();
+ };
+
+ this.check = function removeElm_check() {
+ testAbsentCSSAttrs(this.accessible);
+ };
+
+ this.getID = function removeElm_getID() {
+ return "test CSS-based attributes on removed accessible";
+ };
+ }
+
+ function doTest() {
+ // CSS display
+ testCSSAttrs("display_block");
+ testCSSAttrs("display_inline");
+ testCSSAttrs("display_inline-block");
+ testCSSAttrs("display_list-item");
+ testCSSAttrs("display_table");
+ testCSSAttrs("display_inline-table");
+ testCSSAttrs("display_table-row-group");
+ testCSSAttrs("display_table-column");
+ testCSSAttrs("display_table-column-group");
+ testCSSAttrs("display_table-header-group");
+ testCSSAttrs("display_table-footer-group");
+ testCSSAttrs("display_table-row");
+ testCSSAttrs("display_table-cell");
+ testCSSAttrs("display_table-caption");
+
+ // CSS text-align
+ testCSSAttrs("text-align_left");
+ testCSSAttrs("text-align_right");
+ testCSSAttrs("text-align_center");
+ testCSSAttrs("text-align_justify");
+ testCSSAttrs("text-align_inherit");
+
+ // CSS text-indent
+ testCSSAttrs("text-indent_em");
+ testCSSAttrs("text-indent_ex");
+ testCSSAttrs("text-indent_in");
+ testCSSAttrs("text-indent_cm");
+ testCSSAttrs("text-indent_mm");
+ testCSSAttrs("text-indent_pt");
+ testCSSAttrs("text-indent_pc");
+ testCSSAttrs("text-indent_px");
+ testCSSAttrs("text-indent_percent");
+ testCSSAttrs("text-indent_inherit");
+
+ // CSS margin
+ testCSSAttrs("margin_em");
+ testCSSAttrs("margin_ex");
+ testCSSAttrs("margin_in");
+ testCSSAttrs("margin_cm");
+ testCSSAttrs("margin_mm");
+ testCSSAttrs("margin_pt");
+ testCSSAttrs("margin_pc");
+ testCSSAttrs("margin_px");
+ testCSSAttrs("margin_percent");
+ testCSSAttrs("margin_auto");
+ testCSSAttrs("margin_inherit");
+
+ testCSSAttrs("margin-left");
+ testCSSAttrs("margin-right");
+ testCSSAttrs("margin-top");
+ testCSSAttrs("margin-bottom");
+
+ // Elements
+ testCSSAttrs("span");
+ testCSSAttrs("div");
+ testCSSAttrs("p");
+ testCSSAttrs("input");
+ testCSSAttrs("table");
+ testCSSAttrs("tr");
+ testCSSAttrs("td");
+
+ // no CSS-based object attributes
+ testAbsentCSSAttrs(getAccessible("listitem").firstChild);
+
+ gQueue = new eventQueue();
+ gQueue.push(new removeElm("div"));
+ gQueue.invoke(); // SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=439566"
+ title="Include the css display property as an IAccessible2 object attribute">
+ Mozilla Bug 439566
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=460932"
+ title="text-indent and text-align should really be object attribute">
+ Mozilla Bug 460932
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=689540"
+ title="Expose IA2 margin- object attributes">
+ Mozilla Bug 689540
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=714579"
+ title="Don't use GetComputedStyle for object attribute calculation">
+ Mozilla Bug 714579
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=729831"
+ title="Don't expose CSS-based object attributes on not in tree accessible and accessible having no DOM element">
+ Mozilla Bug 729831
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="display_block" role="img"
+ style="display: block;">display: block</div>
+ <div id="display_inline" role="img"
+ style="display: inline;">display: inline</div>
+ <div id="display_inline-block" role="img"
+ style="display: inline-block;">display: inline-block</div>
+ <div id="display_list-item" role="img"
+ style="display: list-item;">display: list-item</div>
+ <div id="display_table" role="img"
+ style="display: table;">display: table</div>
+ <div id="display_inline-table" role="img"
+ style="display: inline-table;">display: inline-table</div>
+ <div id="display_table-row-group" role="img"
+ style="display: table-row-group;">display: table-row-group</div>
+ <div id="display_table-column" role="img"
+ style="display: table-column;">display: table-column</div>
+ <div id="display_table-column-group" role="img"
+ style="display: table-column-group;">display: table-column-group</div>
+ <div id="display_table-header-group" role="img"
+ style="display: table-header-group;">display: table-header-group</div>
+ <div id="display_table-footer-group" role="img"
+ style="display: table-footer-group;">display: table-footer-group</div>
+ <div id="display_table-row" role="img"
+ style="display: table-row;">display: table-row</div>
+ <div id="display_table-cell" role="img"
+ style="display: table-cell;">display: table-cell</div>
+ <div id="display_table-caption" role="img"
+ style="display: table-caption;">display: table-caption</div>
+
+ <p id="text-align_left" style="text-align: left;">text-align: left</p>
+ <p id="text-align_right" style="text-align: right;">text-align: right</p>
+ <p id="text-align_center" style="text-align: center;">text-align: center</p>
+ <p id="text-align_justify" style="text-align: justify;">text-align: justify</p>
+ <p id="text-align_inherit" style="text-align: inherit;">text-align: inherit</p>
+
+ <p id="text-indent_em" style="text-indent: 0.5em;">text-indent: 0.5em</p>
+ <p id="text-indent_ex" style="text-indent: 1ex;">text-indent: 1ex</p>
+ <p id="text-indent_in" style="text-indent: 0.5in;">text-indent: 0.5in</p>
+ <p id="text-indent_cm" style="text-indent: 2cm;">text-indent: 2cm</p>
+ <p id="text-indent_mm" style="text-indent: 10mm;">text-indent: 10mm</p>
+ <p id="text-indent_pt" style="text-indent: 30pt;">text-indent: 30pt</p>
+ <p id="text-indent_pc" style="text-indent: 2pc;">text-indent: 2pc</p>
+ <p id="text-indent_px" style="text-indent: 5px;">text-indent: 5px</p>
+ <p id="text-indent_percent" style="text-indent: 10%;">text-indent: 10%</p>
+ <p id="text-indent_inherit" style="text-indent: inherit;">text-indent: inherit</p>
+
+ <p id="margin_em" style="margin: 0.5em;">margin: 0.5em</p>
+ <p id="margin_ex" style="margin: 1ex;">margin: 1ex</p>
+ <p id="margin_in" style="margin: 0.5in;">margin: 0.5in</p>
+ <p id="margin_cm" style="margin: 2cm;">margin: 2cm</p>
+ <p id="margin_mm" style="margin: 10mm;">margin: 10mm</p>
+ <p id="margin_pt" style="margin: 30pt;">margin: 30pt</p>
+ <p id="margin_pc" style="margin: 2pc;">margin: 2pc</p>
+ <p id="margin_px" style="margin: 5px;">margin: 5px</p>
+ <p id="margin_percent" style="margin: 10%;">margin: 10%</p>
+ <p id="margin_auto" style="margin: auto;">margin: auto</p>
+ <p id="margin_inherit" style="margin: inherit;">margin: inherit</p>
+
+ <p id="margin-left" style="margin-left: 11px;">margin-left: 11px</p>
+ <p id="margin-right" style="margin-right: 21px;">margin-right</p>
+ <p id="margin-top" style="margin-top: 31px;">margin-top: 31px</p>
+ <p id="margin-bottom" style="margin-bottom: 41px;">margin-bottom: 41px</p>
+
+ <span id="span" role="group">It's span</span>
+ <div id="div">It's div</div>
+ <p id="p">It's paragraph"</p>
+ <input id="input"/>
+ <table id="table" style="margin: 2px; text-align: center; text-indent: 10%;">
+ <tr id="tr" role="group">
+ <td id="td">td</td>
+ </tr>
+ </table>
+
+ <ul>
+ <li id="listitem">item
+ </ul>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/attributes/test_obj_group.html b/accessible/tests/mochitest/attributes/test_obj_group.html
new file mode 100644
index 0000000000..f245d485c7
--- /dev/null
+++ b/accessible/tests/mochitest/attributes/test_obj_group.html
@@ -0,0 +1,564 @@
+<html>
+
+<head>
+ <title>Group attributes tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML select with no size attribute.
+ testGroupAttrs("opt1-nosize", 1, 4);
+ testGroupAttrs("opt2-nosize", 2, 4);
+ testGroupAttrs("opt3-nosize", 3, 4);
+ testGroupAttrs("opt4-nosize", 4, 4);
+
+ // Container should have item count and not hierarchical
+ testGroupParentAttrs(getAccessible("opt1-nosize").parent, 4, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML select
+ testGroupAttrs("opt1", 1, 2);
+ testGroupAttrs("opt2", 2, 2);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML select with optgroup
+ testGroupAttrs("select2_opt3", 1, 2, 1);
+ testGroupAttrs("select2_opt4", 2, 2, 1);
+ testGroupAttrs("select2_opt1", 1, 2, 2);
+ testGroupAttrs("select2_opt2", 2, 2, 2);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML input@type="radio" within form
+ testGroupAttrs("radio1", 1, 2);
+ testGroupAttrs("radio2", 2, 2);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML input@type="radio" within document
+ testGroupAttrs("radio3", 1, 2);
+ testGroupAttrs("radio4", 2, 2);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Hidden HTML input@type="radio"
+ testGroupAttrs("radio5", 1, 1);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML ul/ol
+ testGroupAttrs("li1", 1, 3);
+ testGroupAttrs("li2", 2, 3);
+ testGroupAttrs("li3", 3, 3);
+
+ // ul should have item count and not hierarchical
+ testGroupParentAttrs("ul", 3, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML ul/ol (nested lists)
+
+ testGroupAttrs("li4", 1, 3, 1);
+ testGroupAttrs("li5", 2, 3, 1);
+ testGroupAttrs("li6", 3, 3, 1);
+ // ol with nested list should have 1st level item count and be hierarchical
+ testGroupParentAttrs("ol", 3, true);
+
+ testGroupAttrs("n_li4", 1, 3, 2);
+ testGroupAttrs("n_li5", 2, 3, 2);
+ testGroupAttrs("n_li6", 3, 3, 2);
+ // nested ol should have item count and be hierarchical
+ testGroupParentAttrs("ol_nested", 3, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA list
+ testGroupAttrs("li7", 1, 3);
+ testGroupAttrs("li8", 2, 3);
+ testGroupAttrs("li9", 3, 3);
+ // simple flat aria list
+ testGroupParentAttrs("aria-list_1", 3, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA list (nested lists: list -> listitem -> list -> listitem)
+ testGroupAttrs("li10", 1, 3, 1);
+ testGroupAttrs("li11", 2, 3, 1);
+ testGroupAttrs("li12", 3, 3, 1);
+ // aria list with nested list
+ testGroupParentAttrs("aria-list_2", 3, true);
+
+ testGroupAttrs("n_li10", 1, 3, 2);
+ testGroupAttrs("n_li11", 2, 3, 2);
+ testGroupAttrs("n_li12", 3, 3, 2);
+ // nested aria list.
+ testGroupParentAttrs("aria-list_2_1", 3, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA list (nested lists: list -> listitem -> group -> listitem)
+ testGroupAttrs("lgt_li1", 1, 2, 1);
+ testGroupAttrs("lgt_li1_nli1", 1, 2, 2);
+ testGroupAttrs("lgt_li1_nli2", 2, 2, 2);
+ testGroupAttrs("lgt_li2", 2, 2, 1);
+ testGroupAttrs("lgt_li2_nli1", 1, 2, 2);
+ testGroupAttrs("lgt_li2_nli2", 2, 2, 2);
+ // aria list with nested list
+ testGroupParentAttrs("aria-list_3", 2, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA menu (menuitem, separator, menuitemradio and menuitemcheckbox)
+ testGroupAttrs("menu_item1", 1, 2);
+ testGroupAttrs("menu_item2", 2, 2);
+ testGroupAttrs("menu_item1.1", 1, 2);
+ testGroupAttrs("menu_item1.2", 2, 2);
+ testGroupAttrs("menu_item1.3", 1, 3);
+ testGroupAttrs("menu_item1.4", 2, 3);
+ testGroupAttrs("menu_item1.5", 3, 3);
+ // menu bar item count
+ testGroupParentAttrs("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("menu", "child-item-count", "5");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA tab
+ testGroupAttrs("tab_1", 1, 3);
+ testGroupAttrs("tab_2", 2, 3);
+ testGroupAttrs("tab_3", 3, 3);
+ // tab list tab count
+ testGroupParentAttrs("tablist_1", 3, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA radio
+ testGroupAttrs("r1", 1, 3);
+ testGroupAttrs("r2", 2, 3);
+ testGroupAttrs("r3", 3, 3);
+ // explicit aria radio group
+ testGroupParentAttrs("rg1", 3, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA tree
+ testGroupAttrs("ti1", 1, 3, 1);
+ testGroupAttrs("ti2", 1, 2, 2);
+ testGroupAttrs("ti3", 2, 2, 2);
+ testGroupAttrs("ti4", 2, 3, 1);
+ testGroupAttrs("ti5", 1, 3, 2);
+ testGroupAttrs("ti6", 2, 3, 2);
+ testGroupAttrs("ti7", 3, 3, 2);
+ testGroupAttrs("ti8", 3, 3, 1);
+ testGroupParentAttrs("tree_1", 3, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA tree (tree -> treeitem -> group -> treeitem)
+ testGroupAttrs("tree2_ti1", 1, 2, 1);
+ testGroupAttrs("tree2_ti1a", 1, 2, 2);
+ testGroupAttrs("tree2_ti1b", 2, 2, 2);
+ testGroupAttrs("tree2_ti2", 2, 2, 1);
+ testGroupAttrs("tree2_ti2a", 1, 2, 2);
+ testGroupAttrs("tree2_ti2b", 2, 2, 2);
+ testGroupParentAttrs("tree_2", 2, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA tree (tree -> treeitem, group -> treeitem)
+ testGroupAttrs("tree3_ti1", 1, 2, 1);
+ testGroupAttrs("tree3_ti1a", 1, 2, 2);
+ testGroupAttrs("tree3_ti1b", 2, 2, 2);
+ testGroupAttrs("tree3_ti2", 2, 2, 1);
+ testGroupAttrs("tree3_ti2a", 1, 2, 2);
+ testGroupAttrs("tree3_ti2b", 2, 2, 2);
+ testGroupParentAttrs("tree_3", 2, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA grid
+ testGroupAttrs("grid_row1", 1, 2);
+ testAbsentAttrs("grid_cell1", {"posinset": "", "setsize": ""});
+ testAbsentAttrs("grid_cell2", {"posinset": "", "setsize": ""});
+
+ testGroupAttrs("grid_row2", 2, 2);
+ testAbsentAttrs("grid_cell3", {"posinset": "", "setsize": ""});
+ testAbsentAttrs("grid_cell4", {"posinset": "", "setsize": ""});
+ testGroupParentAttrs("grid", 2, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA treegrid
+ testGroupAttrs("treegrid_row1", 1, 2, 1);
+ testAbsentAttrs("treegrid_cell1", {"posinset": "", "setsize": ""});
+ testAbsentAttrs("treegrid_cell2", {"posinset": "", "setsize": ""});
+
+ testGroupAttrs("treegrid_row2", 1, 1, 2);
+ testAbsentAttrs("treegrid_cell3", {"posinset": "", "setsize": ""});
+ testAbsentAttrs("treegrid_cell4", {"posinset": "", "setsize": ""});
+
+ testGroupAttrs("treegrid_row3", 2, 2, 1);
+ testAbsentAttrs("treegrid_cell5", {"posinset": "", "setsize": ""});
+ testAbsentAttrs("treegrid_cell6", {"posinset": "", "setsize": ""});
+
+ testGroupParentAttrs("treegrid", 2, true);
+ // row child item count provided by parent grid's aria-colcount
+ testGroupParentAttrs("treegrid_row1", 4, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML headings
+ testGroupAttrs("h1", 0, 0, 1);
+ testGroupAttrs("h2", 0, 0, 2);
+ testGroupAttrs("h3", 0, 0, 3);
+ testGroupAttrs("h4", 0, 0, 4);
+ testGroupAttrs("h5", 0, 0, 5);
+ testGroupAttrs("h6", 0, 0, 6);
+ testGroupAttrs("ariaHeadingNoLevel", 0, 0, 2);
+ // No child item counts or "tree" flag for parent of headings
+ testAbsentAttrs("headings", {"child-item-count": "", "tree": ""});
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA combobox
+ testGroupAttrs("combo1_opt1", 1, 4);
+ testGroupAttrs("combo1_opt2", 2, 4);
+ testGroupAttrs("combo1_opt3", 3, 4);
+ testGroupAttrs("combo1_opt4", 4, 4);
+ testGroupParentAttrs("combo1", 4, false);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA table
+ testGroupAttrs("table_cell", 3, 4);
+ testGroupAttrs("table_row", 2, 2);
+
+ // grid child item count provided by aria-rowcount
+ testGroupParentAttrs("table", 2, false);
+ // row child item count provided by parent grid's aria-colcount
+ testGroupParentAttrs("table_row", 4, false);
+
+ // Attributes calculated even when row is wrapped in a div.
+ testGroupAttrs("wrapped_row_1", 1, 2);
+ testGroupAttrs("wrapped_row_2", 2, 2);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA list constructed by ARIA owns
+ testGroupAttrs("t1_li1", 1, 3);
+ testGroupAttrs("t1_li2", 2, 3);
+ testGroupAttrs("t1_li3", 3, 3);
+ testGroupParentAttrs("aria-list_4", 3, false);
+
+ // Test group attributes of ARIA comments
+ testGroupAttrs("comm_single_1", 1, 2, 1);
+ testGroupAttrs("comm_single_2", 2, 2, 1);
+ testGroupAttrs("comm_nested_1", 1, 3, 1);
+ testGroupAttrs("comm_nested_1_1", 1, 2, 2);
+ testGroupAttrs("comm_nested_1_2", 2, 2, 2);
+ testGroupAttrs("comm_nested_2", 2, 3, 1);
+ testGroupAttrs("comm_nested_2_1", 1, 1, 2);
+ testGroupAttrs("comm_nested_2_1_1", 1, 1, 3);
+ testGroupAttrs("comm_nested_3", 3, 3, 1);
+
+ // Test that group position information updates after deleting node.
+ testGroupAttrs("tree4_ti1", 1, 2, 1);
+ testGroupAttrs("tree4_ti2", 2, 2, 1);
+ testGroupParentAttrs("tree4", 2, true);
+
+ var tree4element = document.getElementById("tree4_ti1");
+ var tree4acc = getAccessible("tree4");
+ tree4element.remove();
+ waitForEvent(EVENT_REORDER, tree4acc, function() {
+ testGroupAttrs("tree4_ti2", 1, 1, 1);
+ testGroupParentAttrs("tree4", 1, true);
+ SimpleTest.finish();
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=468418"
+ title="Expose level for nested lists in HTML">
+ Mozilla Bug 468418
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=844023"
+ title="group info might not be properly updated when flat trees mutate">
+ Bug 844023
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=864224"
+ title="Support nested ARIA listitems structured by role='group'">
+ Bug 864224
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=907682"
+ title=" HTML:option group position is not correct when select is collapsed">
+ Mozilla Bug 907682
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <select>
+ <option id="opt1-nosize">option1</option>
+ <option id="opt2-nosize">option2</option>
+ <option id="opt3-nosize">option3</option>
+ <option id="opt4-nosize">option4</option>
+ </select>
+
+ <select size="4">
+ <option id="opt1">option1</option>
+ <option id="opt2">option2</option>
+ </select>
+
+ <select size="4">
+ <optgroup id="select2_optgroup" label="group">
+ <option id="select2_opt1">option1</option>
+ <option id="select2_opt2">option2</option>
+ </optgroup>
+ <option id="select2_opt3">option3</option>
+ <option id="select2_opt4">option4</option>
+ </select>
+
+ <form>
+ <input type="radio" id="radio1" name="group1"/>
+ <input type="radio" id="radio2" name="group1"/>
+ </form>
+
+ <input type="radio" id="radio3" name="group2"/>
+ <input type="radio" id="radio4" name="group2"/>
+
+ <ul id="ul">
+ <li id="li1">Oranges</li>
+ <li id="li2">Apples</li>
+ <li id="li3">Bananas</li>
+ </ul>
+
+ <ol id="ol">
+ <li id="li4">Oranges</li>
+ <li id="li5">Apples</li>
+ <li id="li6">Bananas
+ <ul id="ol_nested">
+ <li id="n_li4">Oranges</li>
+ <li id="n_li5">Apples</li>
+ <li id="n_li6">Bananas</li>
+ </ul>
+ </li>
+ </ol>
+
+ <span role="list" id="aria-list_1">
+ <span role="listitem" id="li7">Oranges</span>
+ <span role="listitem" id="li8">Apples</span>
+ <span role="listitem" id="li9">Bananas</span>
+ </span>
+
+ <span role="list" id="aria-list_2">
+ <span role="listitem" id="li10">Oranges</span>
+ <span role="listitem" id="li11">Apples</span>
+ <span role="listitem" id="li12">Bananas
+ <span role="list" id="aria-list_2_1">
+ <span role="listitem" id="n_li10">Oranges</span>
+ <span role="listitem" id="n_li11">Apples</span>
+ <span role="listitem" id="n_li12">Bananas</span>
+ </span>
+ </span>
+ </span>
+
+ <div role="list" id="aria-list_3">
+ <div role="listitem" id="lgt_li1">Item 1
+ <div role="group">
+ <div role="listitem" id="lgt_li1_nli1">Item 1A</div>
+ <div role="listitem" id="lgt_li1_nli2">Item 1B</div>
+ </div>
+ </div>
+ <div role="listitem" id="lgt_li2">Item 2
+ <div role="group">
+ <div role="listitem" id="lgt_li2_nli1">Item 2A</div>
+ <div role="listitem" id="lgt_li2_nli2">Item 2B</div>
+ </div>
+ </div>
+ </div>
+
+ <ul role="menubar" id="menubar">
+ <li role="menuitem" aria-haspopup="true" id="menu_item1">File
+ <ul role="menu" id="menu">
+ <li role="menuitem" id="menu_item1.1">New</li>
+ <li role="menuitem" id="menu_item1.2">Open…</li>
+ <li role="separator">-----</li>
+ <li role="menuitem" id="menu_item1.3">Item</li>
+ <li role="menuitemradio" id="menu_item1.4">Radio</li>
+ <li role="menuitemcheckbox" id="menu_item1.5">Checkbox</li>
+ </ul>
+ </li>
+ <li role="menuitem" aria-haspopup="false" id="menu_item2">Help</li>
+ </ul>
+
+ <ul id="tablist_1" role="tablist">
+ <li id="tab_1" role="tab">Crust</li>
+ <li id="tab_2" role="tab">Veges</li>
+ <li id="tab_3" role="tab">Carnivore</li>
+ </ul>
+
+ <ul id="rg1" role="radiogroup">
+ <li id="r1" role="radio" aria-checked="false">Thai</li>
+ <li id="r2" role="radio" aria-checked="false">Subway</li>
+ <li id="r3" role="radio" aria-checked="false">Jimmy Johns</li>
+ </ul>
+
+ <table role="tree" id="tree_1">
+ <tr role="presentation">
+ <td role="treeitem" aria-expanded="true" aria-level="1"
+ id="ti1">vegetables</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti2">cucumber</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti3">carrot</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-expanded="false" aria-level="1"
+ id="ti4">cars</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti5">mercedes</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti6">BMW</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="2" id="ti7">Audi</td>
+ </tr>
+ <tr role="presentation">
+ <td role="treeitem" aria-level="1" id="ti8">people</td>
+ </tr>
+ </table>
+
+ <ul role="tree" id="tree_2">
+ <li role="treeitem" id="tree2_ti1">Item 1
+ <ul role="group">
+ <li role="treeitem" id="tree2_ti1a">Item 1A</li>
+ <li role="treeitem" id="tree2_ti1b">Item 1B</li>
+ </ul>
+ </li>
+ <li role="treeitem" id="tree2_ti2">Item 2
+ <ul role="group">
+ <li role="treeitem" id="tree2_ti2a">Item 2A</li>
+ <li role="treeitem" id="tree2_ti2b">Item 2B</li>
+ </ul>
+ </li>
+ </div>
+
+ <div role="tree" id="tree_3">
+ <div role="treeitem" id="tree3_ti1">Item 1</div>
+ <div role="group">
+ <li role="treeitem" id="tree3_ti1a">Item 1A</li>
+ <li role="treeitem" id="tree3_ti1b">Item 1B</li>
+ </div>
+ <div role="treeitem" id="tree3_ti2">Item 2</div>
+ <div role="group">
+ <div role="treeitem" id="tree3_ti2a">Item 2A</div>
+ <div role="treeitem" id="tree3_ti2b">Item 2B</div>
+ </div>
+ </div>
+
+ <!-- IMPORTANT: Need to have no whitespace between elements in this tree. -->
+ <div role="tree" id="tree4"><div role="treeitem"
+ id="tree4_ti1">Item 1</div><div role="treeitem"
+ id="tree4_ti2">Item 2</div></div>
+
+ <table role="grid" id="grid">
+ <tr role="row" id="grid_row1">
+ <td role="gridcell" id="grid_cell1">cell1</td>
+ <td role="gridcell" id="grid_cell2">cell2</td>
+ </tr>
+ <tr role="row" id="grid_row2">
+ <td role="gridcell" id="grid_cell3">cell3</td>
+ <td role="gridcell" id="grid_cell4">cell4</td>
+ </tr>
+ </table>
+
+ <div role="treegrid" id="treegrid" aria-colcount="4">
+ <div role="row" aria-level="1" id="treegrid_row1">
+ <div role="gridcell" id="treegrid_cell1">cell1</div>
+ <div role="gridcell" id="treegrid_cell2">cell2</div>
+ </div>
+ <div role="row" aria-level="2" id="treegrid_row2">
+ <div role="gridcell" id="treegrid_cell3">cell1</div>
+ <div role="gridcell" id="treegrid_cell4">cell2</div>
+ </div>
+ <div role="row" id="treegrid_row3">
+ <div role="gridcell" id="treegrid_cell5">cell1</div>
+ <div role="gridcell" id="treegrid_cell6">cell2</div>
+ </div>
+ </div>
+
+ <div id="headings">
+ <h1 id="h1">heading1</h1>
+ <h2 id="h2">heading2</h2>
+ <h3 id="h3">heading3</h3>
+ <h4 id="h4">heading4</h4>
+ <h5 id="h5">heading5</h5>
+ <h6 id="h6">heading6</h6>
+ <div id="ariaHeadingNoLevel" role="heading">ariaHeadingNoLevel</div>
+ </div>
+
+ <ul id="combo1" role="combobox">Password
+ <li id="combo1_opt1" role="option">Xyzzy</li>
+ <li id="combo1_opt2" role="option">Plughs</li>
+ <li id="combo1_opt3" role="option">Shazaam</li>
+ <li id="combo1_opt4" role="option">JoeSentMe</li>
+ </ul>
+
+ <form>
+ <input type="radio" style="display: none;" name="group3">
+ <input type="radio" id="radio5" name="group3">
+ </form>
+
+ <div role="table" aria-colcount="4" aria-rowcount="2" id="table">
+ <div role="row" id="table_row" aria-rowindex="2">
+ <div role="cell" id="table_cell" aria-colindex="3">cell</div>
+ </div>
+ </div>
+
+ <div role="grid" aria-readonly="true">
+ <div tabindex="-1">
+ <div role="row" id="wrapped_row_1">
+ <div role="gridcell">cell content</div>
+ </div>
+ </div>
+ <div tabindex="-1">
+ <div role="row" id="wrapped_row_2">
+ <div role="gridcell">cell content</div>
+ </div>
+ </div>
+ </div>
+
+ <div role="list" aria-owns="t1_li1 t1_li2 t1_li3" id="aria-list_4">
+ <div role="listitem" id="t1_li2">Apples</div>
+ <div role="listitem" id="t1_li1">Oranges</div>
+ </div>
+ <div role="listitem" id="t1_li3">Bananas</div>
+
+ <!-- ARIA comments, 1 level, group pos and size calculation -->
+ <article>
+ <p id="comm_single_1" role="comment">Comment 1</p>
+ <p id="comm_single_2" role="comment">Comment 2</p>
+ </article>
+
+ <!-- Nested comments -->
+ <article>
+ <div id="comm_nested_1" role="comment"><p>Comment 1 level 1</p>
+ <div id="comm_nested_1_1" role="comment"><p>Comment 1 level 2</p></div>
+ <div id="comm_nested_1_2" role="comment"><p>Comment 2 level 2</p></div>
+ </div>
+ <div id="comm_nested_2" role="comment"><p>Comment 2 level 1</p>
+ <div id="comm_nested_2_1" role="comment"><p>Comment 3 level 2</p>
+ <div id="comm_nested_2_1_1" role="comment"><p>Comment 1 level 3</p></div>
+ </div>
+ </div>
+ <div id="comm_nested_3" role="comment"><p>Comment 3 level 1</p></div>
+ </article>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/attributes/test_obj_group.xhtml b/accessible/tests/mochitest/attributes/test_obj_group.xhtml
new file mode 100644
index 0000000000..0eda4b6f2d
--- /dev/null
+++ b/accessible/tests/mochitest/attributes/test_obj_group.xhtml
@@ -0,0 +1,215 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessibility Group Attributes ('level', 'setsize', 'posinset') Test.">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../attributes.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function openMenu(aID)
+ {
+ this.menuNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, this.menuNode)
+ ];
+
+ this.invoke = function openMenu_invoke()
+ {
+ this.menuNode.open = true;
+ }
+
+ this.finalCheck = function openMenu_finalCheck()
+ {
+ testGroupAttrs("menu_item1.1", 1, 1);
+ testGroupAttrs("menu_item1.2", 1, 3);
+ testGroupAttrs("menu_item1.4", 2, 3);
+ testGroupAttrs("menu_item2", 3, 3);
+ }
+
+ this.getID = function openMenu_getID()
+ {
+ return "open menu " + prettyName(aID);
+ }
+ }
+
+ function openSubMenu(aID)
+ {
+ this.menuNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, this.menuNode)
+ ];
+
+ this.invoke = function openSubMenu_invoke()
+ {
+ this.menuNode.open = true;
+ }
+
+ this.finalCheck = function openSubMenu_finalCheck()
+ {
+ testGroupAttrs("menu_item2.1", 1, 2, 1);
+ testGroupAttrs("menu_item2.2", 2, 2, 1);
+ }
+
+ this.getID = function openSubMenu_getID()
+ {
+ return "open submenu " + prettyName(aID);
+ }
+ }
+
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTest()
+ {
+ //////////////////////////////////////////////////////////////////////////
+ // xul:listbox (bug 417317)
+ testGroupAttrs("listitem1", 1, 4);
+ testGroupAttrs("listitem2", 2, 4);
+ testGroupAttrs("listitem3", 3, 4);
+ testGroupAttrs("listitem4", 4, 4);
+
+ //////////////////////////////////////////////////////////////////////////
+ // xul:tab
+ testGroupAttrs("tab1", 1, 2);
+ testGroupAttrs("tab2", 2, 2);
+
+ //////////////////////////////////////////////////////////////////////////
+ // xul:radio
+ testGroupAttrs("radio1", 1, 2);
+ testGroupAttrs("radio2", 2, 2);
+
+ //////////////////////////////////////////////////////////////////////////
+ // xul:menulist
+ testGroupAttrs("menulist1.1", 1);
+ testGroupAttrs("menulist1.2", 2);
+ testGroupAttrs("menulist1.3", 3);
+ testGroupAttrs("menulist1.4", 4);
+
+ //////////////////////////////////////////////////////////////////////////
+ // ARIA menu (bug 441888)
+ testGroupAttrs("aria-menuitem", 1, 3);
+ testGroupAttrs("aria-menuitemcheckbox", 2, 3);
+ testGroupAttrs("aria-menuitemradio", 3, 3);
+ testGroupAttrs("aria-menuitem2", 1, 1);
+
+ //////////////////////////////////////////////////////////////////////////
+ // xul:menu (bug 443881)
+ gQueue = new eventQueue();
+ gQueue.push(new openMenu("menu_item1"));
+ gQueue.push(new openSubMenu("menu_item2"));
+ gQueue.invoke(); // SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=417317"
+ title="Certain types of LISTITEM accessibles no longer get attributes set like 'x of y', regression from fix for bug 389926">
+ Mozilla Bug 417317
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=443881"
+ title="take into account separators in xul menus when group attributes are calculating">
+ Mozilla Bug 443881
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=441888"
+ title="ARIA checked menu items are not included in the total list of menu items">
+ Mozilla Bug 441888
+ </a><br/>
+
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+
+ <richlistbox>
+ <richlistitem id="listitem1"/>
+ <richlistitem id="listitem2"><label value="listitem2"/></richlistitem>
+ <richlistitem id="listitem3"/>
+ <richlistitem id="listitem4"><label value="listitem4"/></richlistitem>
+ </richlistbox>
+
+ <menubar>
+ <menu label="item1" id="menu_item1">
+ <menupopup>
+ <menuitem label="item1.1" id="menu_item1.1"/>
+ <menuseparator/>
+ <menuitem label="item1.2" id="menu_item1.2"/>
+ <menuitem label="item1.3" hidden="true"/>
+ <menuitem label="item1.4" id="menu_item1.4"/>
+ <menu label="item2" id="menu_item2">
+ <menupopup>
+ <menuitem label="item2.1" id="menu_item2.1"/>
+ <menuitem label="item2.2" id="menu_item2.2"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ </menubar>
+
+ <tabbox>
+ <tabs>
+ <tab id="tab1" label="tab1"/>
+ <tab id="tab2" label="tab3"/>
+ </tabs>
+ <tabpanels>
+ <tabpanel/>
+ <tabpanel/>
+ </tabpanels>
+ </tabbox>
+
+ <radiogroup>
+ <radio id="radio1" label="radio1"/>
+ <radio id="radio2" label="radio2"/>
+ </radiogroup>
+
+ <menulist id="menulist1" label="Vehicle">
+ <menupopup>
+ <menuitem id="menulist1.1" label="Car"/>
+ <menuitem id="menulist1.2" label="Taxi"/>
+ <menuitem id="menulist1.3" label="Bus" selected="true"/>
+ <menuitem id="menulist1.4" label="Train"/>
+ </menupopup>
+ </menulist>
+
+ <vbox>
+ <description role="menuitem" id="aria-menuitem"
+ value="conventional menuitem"/>
+ <description role="menuitemcheckbox" id="aria-menuitemcheckbox"
+ value="conventional checkbox menuitem"/>
+ <description role="menuitem" hidden="true"/>
+ <description role="menuitemradio" id="aria-menuitemradio"
+ value="conventional radio menuitem"/>
+ <description role="separator"
+ value="conventional separator"/>
+ <description role="menuitem" id="aria-menuitem2"
+ value="conventional menuitem"/>
+ </vbox>
+
+ </vbox>
+ </hbox>
+</window>
+
diff --git a/accessible/tests/mochitest/attributes/test_obj_group_tree.xhtml b/accessible/tests/mochitest/attributes/test_obj_group_tree.xhtml
new file mode 100644
index 0000000000..287ee7989e
--- /dev/null
+++ b/accessible/tests/mochitest/attributes/test_obj_group_tree.xhtml
@@ -0,0 +1,84 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL tree attributes tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../attributes.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ function doTest()
+ {
+ var treeNode = getNode("tree");
+
+ var tree = getAccessible(treeNode);
+ var treeitem1 = tree.firstChild.nextSibling;
+ testGroupAttrs(treeitem1, 1, 4, 1);
+
+ var treeitem2 = treeitem1.nextSibling;
+ testGroupAttrs(treeitem2, 2, 4, 1);
+
+ var treeitem3 = treeitem2.nextSibling;
+ testGroupAttrs(treeitem3, 1, 2, 2);
+
+ var treeitem4 = treeitem3.nextSibling;
+ testGroupAttrs(treeitem4, 2, 2, 2);
+
+ var treeitem5 = treeitem4.nextSibling;
+ testGroupAttrs(treeitem5, 3, 4, 1);
+
+ var treeitem6 = treeitem5.nextSibling;
+ testGroupAttrs(treeitem6, 4, 4, 1);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yXULTreeLoadEvent(doTest, "tree", new nsTreeTreeView());
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727"
+ title="Reorganize implementation of XUL tree accessibility">
+ Mozilla Bug 503727
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col" flex="1" primary="true" label="column"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <vbox id="debug"/>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/attributes/test_tag.html b/accessible/tests/mochitest/attributes/test_tag.html
new file mode 100644
index 0000000000..b57cc2dca8
--- /dev/null
+++ b/accessible/tests/mochitest/attributes/test_tag.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML landmark tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ // And some AT may look for this
+ testAttrs("nav", {"tag": "nav"}, true);
+ testAttrs("header", {"tag": "header"}, true);
+ testAttrs("footer", {"tag": "footer"}, true);
+ testAttrs("article", {"tag": "article"}, true);
+ testAttrs("aside", {"tag": "aside"}, true);
+ testAttrs("section", {"tag": "section"}, true);
+ testAttrs("main", {"tag": "article"}, true);
+ testAttrs("form", {"tag": "article"}, true);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Provide mappings for html5 <nav> <header> <footer> <article>"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=593368">
+ Bug 593368
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=613502"
+ title="Map <article> like we do aria role article">
+ Bug 613502
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=610650"
+ title="Change implementation of HTML5 landmark elements to conform">
+ Bug 610650
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=614310"
+ title="Map section to pane (like role=region)">
+ Mozilla Bug 614310
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=734982"
+ title="Map ARIA role FORM">
+ Bug 734982
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <nav id="nav">a nav</nav>
+ <header id="header">a header</header>
+ <footer id="footer">a footer</footer>
+ <aside id="aside">by the way I am an aside</aside>
+ <section id="section">a section</section>
+
+ <article id="article">an article</article>
+ <article id="main" role="main">a main area</article>
+ <article id="form" role="form">a form area</article>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/attributes/test_xml-roles.html b/accessible/tests/mochitest/attributes/test_xml-roles.html
new file mode 100644
index 0000000000..ff71f0da3a
--- /dev/null
+++ b/accessible/tests/mochitest/attributes/test_xml-roles.html
@@ -0,0 +1,267 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>XML roles tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ // Some AT may look for this
+ testAttrs("nav", {"xml-roles": "navigation"}, true);
+ testAttrs("header", {"xml-roles": "banner"}, true);
+ testAbsentAttrs("article_header", {"xml-roles": "banner"});
+ testAbsentAttrs("main_header", {"xml-roles": "banner"});
+ testAbsentAttrs("section_header", {"xml-roles": "banner"});
+ testAttrs("footer", {"xml-roles": "contentinfo"}, true);
+ testAbsentAttrs("article_footer", {"xml-roles": "contentinfo"});
+ testAbsentAttrs("main_footer", {"xml-roles": "contentinfo"});
+ testAbsentAttrs("section_footer", {"xml-roles": "contentinfo"});
+ testAttrs("aside", {"xml-roles": "complementary"}, true);
+ testAbsentAttrs("section", {"xml-roles": "region"});
+ testAttrs("main", {"xml-roles": "main"}, true); // // ARIA override
+ testAttrs("form", {"xml-roles": "form"}, true);
+ testAttrs("feed", {"xml-roles": "feed"}, true);
+ testAttrs("article", {"xml-roles": "article"}, true);
+ testAttrs("main_element", {"xml-roles": "main"}, true);
+ testAttrs("figure", {"xml-roles": "figure"}, true);
+
+ testAttrs("search", {"xml-roles": "searchbox"}, true);
+
+ testAttrs("code", {"xml-roles": "code"}, true);
+
+ testAttrs("open-1", {"xml-roles": "open-fence"}, true);
+ testAttrs("open-2", {"xml-roles": "open-fence"}, true);
+ testAttrs("open-3", {"xml-roles": "open-fence"}, true);
+ testAttrs("open-4", {"xml-roles": "open-fence"}, true);
+ testAttrs("open-5", {"xml-roles": "open-fence"}, true);
+ testAttrs("open-6", {"xml-roles": "open-fence"}, true);
+ testAttrs("open-7", {"xml-roles": "open-fence"}, true);
+
+ testAttrs("sep-1", {"xml-roles": "separator"}, true);
+ testAttrs("sep-2", {"xml-roles": "separator"}, true);
+ testAttrs("sep-3", {"xml-roles": "separator"}, true);
+ testAttrs("sep-4", {"xml-roles": "separator"}, true);
+ testAttrs("sep-5", {"xml-roles": "separator"}, true);
+ testAttrs("sep-6", {"xml-roles": "separator"}, true);
+ testAttrs("sep-7", {"xml-roles": "separator"}, true);
+
+ testAttrs("close-1", {"xml-roles": "close-fence"}, true);
+ testAttrs("close-2", {"xml-roles": "close-fence"}, true);
+ testAttrs("close-3", {"xml-roles": "close-fence"}, true);
+ testAttrs("close-4", {"xml-roles": "close-fence"}, true);
+ testAttrs("close-5", {"xml-roles": "close-fence"}, true);
+ testAttrs("close-6", {"xml-roles": "close-fence"}, true);
+ testAttrs("close-7", {"xml-roles": "close-fence"}, true);
+
+ testAttrs("num", {"xml-roles": "numerator"}, true);
+ testAttrs("den", {"xml-roles": "denominator"}, true);
+
+ testAttrs("sub-1", {"xml-roles": "subscript"}, true);
+ testAttrs("sub-2", {"xml-roles": "subscript"}, true);
+ testAttrs("sub-3", {"xml-roles": "subscript"}, true);
+ testAttrs("sup-1", {"xml-roles": "superscript"}, true);
+ testAttrs("sup-2", {"xml-roles": "superscript"}, true);
+ testAttrs("sup-3", {"xml-roles": "superscript"}, true);
+ testAttrs("sup-4", {"xml-roles": "superscript"}, true);
+ testAttrs("presub-1", {"xml-roles": "presubscript"}, true);
+ testAttrs("presub-2", {"xml-roles": "presubscript"}, true);
+ testAttrs("presup-1", {"xml-roles": "presuperscript"}, true);
+
+ testAttrs("under-1", {"xml-roles": "underscript"}, true);
+ testAttrs("under-2", {"xml-roles": "underscript"}, true);
+ testAttrs("over-1", {"xml-roles": "overscript"}, true);
+ testAttrs("over-2", {"xml-roles": "overscript"}, true);
+
+ testAttrs("root-index-1", {"xml-roles": "root-index"}, true);
+
+ testAttrs("base-1", {"xml-roles": "base"}, true);
+ testAttrs("base-2", {"xml-roles": "base"}, true);
+ testAttrs("base-3", {"xml-roles": "base"}, true);
+ testAttrs("base-4", {"xml-roles": "base"}, true);
+ testAttrs("base-5", {"xml-roles": "base"}, true);
+ testAttrs("base-6", {"xml-roles": "base"}, true);
+ testAttrs("base-7", {"xml-roles": "base"}, true);
+ testAttrs("base-8", {"xml-roles": "base"}, true);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Provide mappings for html5 <nav> <header> <footer> <article>"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=593368">
+ Bug 593368
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=613502"
+ title="Map <article> like we do aria role article">
+ Bug 613502
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=610650"
+ title="Change implementation of HTML5 landmark elements to conform">
+ Bug 610650
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=614310"
+ title="Map section to pane (like role=region)">
+ Mozilla Bug 614310
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=734982"
+ title="Map ARIA role FORM">
+ Bug 734982
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=761891"
+ title="HTML5 article element should expose xml-roles:article object attribute">
+ Bug 761891
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=849624"
+ title="modify HTML5 header and footer accessibility API mapping">
+ Bug 849624
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1121518"
+ title="ARIA 1.1: Support role 'searchbox'">
+ Bug 1121518
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1356049"
+ title="Map ARIA figure role">
+ Bug 1356049
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <nav id="nav">a nav</nav>
+ <header id="header">a header</header>
+ <footer id="footer">a footer</footer>
+ <article id="article_with_header_and_footer">
+ <header id="article_header">a header within an article</header>
+ <footer id="article_footer">a footer within an article</footer>
+ </article>
+ <main id="main_with_header_and_footer">
+ <header id="main_header">a header within a main</header>
+ <footer id="main_footer">a footer within a main</footer>
+ </main>
+ <section id="section_with_header_and_footer">
+ <header id="section_header">a header within an section</header>
+ <footer id="section_footer">a footer within an section</footer>
+ </section>
+ <aside id="aside">by the way I am an aside</aside>
+ <section id="section">a section</section>
+ <article id="main" role="main">a main area</article>
+ <article id="form" role="form">a form area</article>
+ <div id="feed" role="feed">a feed</div>
+ <article id="article">article</article>
+ <main id="main_element">another main area</main>
+ <div id="figure" role="figure">a figure</div>
+
+ <input id="search" type="search"/>
+
+ <div id="code" role="code"></div>
+
+ <!-- open-fence, separator, close-fence -->
+ <math><mo id="open-1">(</mo><mi>x</mi><mo id="sep-1">,</mo><mi>y</mi><mo id="close-1">)</mo></math>
+ <math><mrow><mo id="open-2">(</mo><mi>x</mi><mo id="sep-2">,</mo><mi>y</mi><mo id="close-2">)</mo></mrow></math>
+ <math><mstyle><mo id="open-3">(</mo><mi>x</mi><mo id="sep-3">,</mo><mi>y</mi><mo id="close-3">)</mo></mstyle></math>
+ <math><msqrt><mo id="open-4">(</mo><mi>x</mi><mo id="sep-4">,</mo><mi>y</mi><mo id="close-4">)</mo></msqrt></math>
+ <math><menclose><mo id="open-5">(</mo><mi>x</mi><mo id="sep-5">,</mo><mi>y</mi><mo id="close-5">)</mo></menclose></math>
+ <math><merror><mo id="open-6">(</mo><mi>x</mi><mo id="sep-6">,</mo><mi>y</mi><mo id="close-6">)</mo></merror></math>
+ <math><mtable><mtr><mtd><mo id="open-7">(</mo><mi>x</mi><mo id="sep-7">,</mo><mi>y</mi><mo id="close-7">)</mo></mtd></mtr></mtable></math>
+
+ <!-- numerator, denominator -->
+ <math>
+ <mfrac>
+ <mi id="num">a</mi>
+ <mi id="den">b</mi>
+ </mfrac>
+ </math>
+
+ <!-- subscript, superscript, presubscript, presuperscript -->
+ <math>
+ <msub>
+ <mi id="base-1">a</mi>
+ <mi id="sub-1">b</mi>
+ </msub>
+ </math>
+ <math>
+ <msup>
+ <mi id="base-2">a</mi>
+ <mi id="sup-1">b</mi>
+ </msup>
+ </math>
+ <math>
+ <msubsup>
+ <mi id="base-3">a</mi>
+ <mi id="sub-2">b</mi>
+ <mi id="sup-2">c</mi>
+ </msubsup>
+ </math>
+ <math>
+ <mmultiscripts>
+ <mi id="base-4">a</mi>
+ <mi id="sub-3">b</mi>
+ <mi id="sup-3">c</mi>
+ <none/>
+ <mi id="sup-4">d</mi>
+ <mprescripts/>
+ <mi id="presub-1">e</mi>
+ <none/>
+ <mi id="presub-2">f</mi>
+ <mi id="presup-1">g</mi>
+ </mmultiscripts>
+ </math>
+
+ <!-- underscript, overscript -->
+ <math>
+ <munder>
+ <mi id="base-5">a</mi>
+ <mi id="under-1">b</mi>
+ </munder>
+ </math>
+ <math>
+ <mover>
+ <mi id="base-6">a</mi>
+ <mi id="over-1">b</mi>
+ </mover>
+ </math>
+ <math>
+ <munderover>
+ <mi id="base-7">a</mi>
+ <mi id="under-2">b</mi>
+ <mi id="over-2">c</mi>
+ </munderover>
+ </math>
+
+ <!-- root-index -->
+ <math>
+ <mroot>
+ <mi id="base-8">a</mi>
+ <mi id="root-index-1">b</mi>
+ </mroot>
+ </math>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/autocomplete.js b/accessible/tests/mochitest/autocomplete.js
new file mode 100644
index 0000000000..bd5458c51d
--- /dev/null
+++ b/accessible/tests/mochitest/autocomplete.js
@@ -0,0 +1,198 @@
+const nsISupports = Ci.nsISupports;
+const nsIAutoCompleteResult = Ci.nsIAutoCompleteResult;
+const nsIAutoCompleteSearch = Ci.nsIAutoCompleteSearch;
+const nsIFactory = Ci.nsIFactory;
+const nsIUUIDGenerator = Ci.nsIUUIDGenerator;
+const nsIComponentRegistrar = Ci.nsIComponentRegistrar;
+
+var gDefaultAutoCompleteSearch = null;
+
+/**
+ * Register 'test-a11y-search' AutoCompleteSearch.
+ *
+ * @param aValues [in] set of possible results values
+ * @param aComments [in] set of possible results descriptions
+ */
+function initAutoComplete(aValues, aComments) {
+ var allResults = new ResultsHeap(aValues, aComments);
+ gDefaultAutoCompleteSearch = new AutoCompleteSearch(
+ "test-a11y-search",
+ allResults
+ );
+ registerAutoCompleteSearch(
+ gDefaultAutoCompleteSearch,
+ "Accessibility Test AutoCompleteSearch"
+ );
+}
+
+/**
+ * Unregister 'test-a11y-search' AutoCompleteSearch.
+ */
+function shutdownAutoComplete() {
+ unregisterAutoCompleteSearch(gDefaultAutoCompleteSearch);
+ gDefaultAutoCompleteSearch.cid = null;
+ gDefaultAutoCompleteSearch = null;
+}
+
+/**
+ * Register the given AutoCompleteSearch.
+ *
+ * @param aSearch [in] AutoCompleteSearch object
+ * @param aDescription [in] description of the search object
+ */
+function registerAutoCompleteSearch(aSearch, aDescription) {
+ var name = "@mozilla.org/autocomplete/search;1?name=" + aSearch.name;
+
+ var uuidGenerator =
+ Cc["@mozilla.org/uuid-generator;1"].getService(nsIUUIDGenerator);
+ var cid = uuidGenerator.generateUUID();
+
+ var componentManager = Components.manager.QueryInterface(
+ nsIComponentRegistrar
+ );
+ componentManager.registerFactory(cid, aDescription, name, aSearch);
+
+ // Keep the id on the object so we can unregister later.
+ aSearch.cid = cid;
+}
+
+/**
+ * Unregister the given AutoCompleteSearch.
+ */
+function unregisterAutoCompleteSearch(aSearch) {
+ var componentManager = Components.manager.QueryInterface(
+ nsIComponentRegistrar
+ );
+ componentManager.unregisterFactory(aSearch.cid, aSearch);
+}
+
+/**
+ * A container to keep all possible results of autocomplete search.
+ */
+function ResultsHeap(aValues, aComments) {
+ this.values = aValues;
+ this.comments = aComments;
+}
+
+ResultsHeap.prototype = {
+ constructor: ResultsHeap,
+
+ /**
+ * Return AutoCompleteResult for the given search string.
+ */
+ getAutoCompleteResultFor(aSearchString) {
+ var values = [],
+ comments = [];
+ for (var idx = 0; idx < this.values.length; idx++) {
+ if (this.values[idx].includes(aSearchString)) {
+ values.push(this.values[idx]);
+ comments.push(this.comments[idx]);
+ }
+ }
+ return new AutoCompleteResult(values, comments);
+ },
+};
+
+/**
+ * nsIAutoCompleteSearch implementation.
+ *
+ * @param aName [in] the name of autocomplete search
+ * @param aAllResults [in] ResultsHeap object
+ */
+function AutoCompleteSearch(aName, aAllResults) {
+ this.name = aName;
+ this.allResults = aAllResults;
+}
+
+AutoCompleteSearch.prototype = {
+ constructor: AutoCompleteSearch,
+
+ // nsIAutoCompleteSearch implementation
+ startSearch(aSearchString, aSearchParam, aPreviousResult, aListener) {
+ var result = this.allResults.getAutoCompleteResultFor(aSearchString);
+ aListener.onSearchResult(this, result);
+ },
+
+ stopSearch() {},
+
+ // nsISupports implementation
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIFactory",
+ "nsIAutoCompleteSearch",
+ ]),
+
+ // nsIFactory implementation
+ createInstance(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ // Search name. Used by AutoCompleteController.
+ name: null,
+
+ // Results heap.
+ allResults: null,
+};
+
+/**
+ * nsIAutoCompleteResult implementation.
+ */
+function AutoCompleteResult(aValues, aComments) {
+ this.values = aValues;
+ this.comments = aComments;
+
+ if (this.values.length) {
+ this.searchResult = nsIAutoCompleteResult.RESULT_SUCCESS;
+ } else {
+ this.searchResult = nsIAutoCompleteResult.NOMATCH;
+ }
+}
+
+AutoCompleteResult.prototype = {
+ constructor: AutoCompleteResult,
+
+ searchString: "",
+ searchResult: null,
+
+ defaultIndex: 0,
+
+ get matchCount() {
+ return this.values.length;
+ },
+
+ getValueAt(aIndex) {
+ return this.values[aIndex];
+ },
+
+ getLabelAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ getCommentAt(aIndex) {
+ return this.comments[aIndex];
+ },
+
+ getStyleAt(aIndex) {
+ return null;
+ },
+
+ getImageAt(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ isRemovableAt(aRowIndex) {
+ return true;
+ },
+
+ removeValueAt(aRowIndex) {},
+
+ // nsISupports implementation
+ QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteResult"]),
+
+ // Data
+ values: null,
+ comments: null,
+};
diff --git a/accessible/tests/mochitest/bounds/a11y.toml b/accessible/tests/mochitest/bounds/a11y.toml
new file mode 100644
index 0000000000..9e8fc86045
--- /dev/null
+++ b/accessible/tests/mochitest/bounds/a11y.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+support-files = "!/accessible/tests/mochitest/*.js"
+
+["test_list.html"]
diff --git a/accessible/tests/mochitest/bounds/test_list.html b/accessible/tests/mochitest/bounds/test_list.html
new file mode 100644
index 0000000000..7e5b75868d
--- /dev/null
+++ b/accessible/tests/mochitest/bounds/test_list.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Accessible boundaries when page is zoomed</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../layout.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // Inside list
+ var li = getAccessible("insidelist_item");
+ testBounds(li);
+
+ var [xLI, yLI, widthLI, heightLI] = getBounds(li);
+ var bullet = li.firstChild;
+ var [x, y, width, height] = getBounds(bullet);
+ is(x, xLI,
+ "Bullet x should match to list item x");
+ ok(y >= yLI,
+ "Bullet y= " + y + " should be not less than list item y=" + yLI);
+ ok(width < widthLI,
+ "Bullet width should be lesser list item width");
+ ok(height <= heightLI,
+ "Bullet height= " + height + " should be not greater than list item height=" + heightLI);
+
+ // Outside list
+ li = getAccessible("outsidelist_item");
+ var [xLIElm, yLIElm, widthLIElm, heightLIElm] = getBoundsForDOMElm(li);
+ [xLI, yLI, widthLI, heightLI] = getBounds(li);
+
+ ok(xLI < xLIElm,
+ "Outside list item x=" + xLI + " should be lesser than list item element x=" + xLIElm);
+ is(yLI, yLIElm,
+ "Outside list item y should match to list item element y");
+ ok(widthLI > widthLIElm,
+ "Outside list item width=" + widthLI + " should be greater than list item element width=" + widthLIElm);
+ ok(heightLI >= Math.trunc(heightLIElm),
+ "Outside list item height=" + heightLI + " should not be less than list item element height=" + heightLIElm);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=754627"
+ title="GetBounds on bullet return wrong values">
+ Mozilla Bug 754627
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <ul style="list-style-position: inside;">
+ <li id="insidelist_item">item</li>
+ </ul>
+
+ <ul style="list-style-position: outside;">
+ <li id="outsidelist_item">item</li>
+ </ul>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/browser.js b/accessible/tests/mochitest/browser.js
new file mode 100644
index 0000000000..b08214cfa8
--- /dev/null
+++ b/accessible/tests/mochitest/browser.js
@@ -0,0 +1,156 @@
+/* import-globals-from common.js */
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+/**
+ * Load the browser with the given url and then invokes the given function.
+ */
+function openBrowserWindow(aFunc, aURL, aRect) {
+ gBrowserContext.testFunc = aFunc;
+ gBrowserContext.startURL = aURL;
+ gBrowserContext.browserRect = aRect;
+
+ addLoadEvent(openBrowserWindowIntl);
+}
+
+/**
+ * Close the browser window.
+ */
+function closeBrowserWindow() {
+ gBrowserContext.browserWnd.close();
+}
+
+/**
+ * Return the browser window object.
+ */
+function browserWindow() {
+ return gBrowserContext.browserWnd;
+}
+
+/**
+ * Return the document of the browser window.
+ */
+function browserDocument() {
+ return browserWindow().document;
+}
+
+/**
+ * Return tab browser object.
+ */
+function tabBrowser() {
+ return browserWindow().gBrowser;
+}
+
+/**
+ * Return browser element of the current tab.
+ */
+function currentBrowser() {
+ return tabBrowser().selectedBrowser;
+}
+
+/**
+ * Return DOM document of the current tab.
+ */
+function currentTabDocument() {
+ return currentBrowser().contentDocument;
+}
+
+/**
+ * Return window of the current tab.
+ */
+function currentTabWindow() {
+ return currentTabDocument().defaultView;
+}
+
+/**
+ * Return browser element of the tab at the given index.
+ */
+function browserAt(aIndex) {
+ return tabBrowser().getBrowserAtIndex(aIndex);
+}
+
+/**
+ * Return DOM document of the tab at the given index.
+ */
+function tabDocumentAt(aIndex) {
+ return browserAt(aIndex).contentDocument;
+}
+
+/**
+ * Return input element of address bar.
+ */
+function urlbarInput() {
+ return browserWindow().document.getElementById("urlbar").inputField;
+}
+
+/**
+ * Return reload button.
+ */
+function reloadButton() {
+ return browserWindow().document.getElementById("urlbar-reload-button");
+}
+
+// //////////////////////////////////////////////////////////////////////////////
+// private section
+
+var gBrowserContext = {
+ browserWnd: null,
+ testFunc: null,
+ startURL: "",
+};
+
+function openBrowserWindowIntl() {
+ var params = "chrome,all,dialog=no,non-remote";
+ var rect = gBrowserContext.browserRect;
+ if (rect) {
+ if ("left" in rect) {
+ params += ",left=" + rect.left;
+ }
+ if ("top" in rect) {
+ params += ",top=" + rect.top;
+ }
+ if ("width" in rect) {
+ params += ",width=" + rect.width;
+ }
+ if ("height" in rect) {
+ params += ",height=" + rect.height;
+ }
+ }
+
+ gBrowserContext.browserWnd =
+ window.browsingContext.topChromeWindow.openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ params,
+ gBrowserContext.startURL || "data:text/html,<html></html>"
+ );
+
+ whenDelayedStartupFinished(browserWindow(), function () {
+ addA11yLoadEvent(startBrowserTests, browserWindow());
+ });
+}
+
+function startBrowserTests() {
+ if (gBrowserContext.startURL) {
+ // Make sure the window is the one loading our URL.
+ if (currentBrowser().contentWindow.location != gBrowserContext.startURL) {
+ setTimeout(startBrowserTests, 0);
+ return;
+ }
+ // wait for load
+ addA11yLoadEvent(gBrowserContext.testFunc, currentBrowser().contentWindow);
+ } else {
+ gBrowserContext.testFunc();
+ }
+}
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ setTimeout(aCallback, 0);
+ }
+ }, "browser-delayed-startup-finished");
+}
diff --git a/accessible/tests/mochitest/common.js b/accessible/tests/mochitest/common.js
new file mode 100644
index 0000000000..f0ee117452
--- /dev/null
+++ b/accessible/tests/mochitest/common.js
@@ -0,0 +1,1048 @@
+// This file has circular dependencies that may require other files. Rather
+// than use import-globals-from, we list the globals individually here to save
+// confusing ESLint.
+// actions.js
+/* globals testActionNames */
+// attributes.js
+/* globals testAttrs, testAbsentAttrs, testTextAttrs */
+// relations.js
+/* globals testRelation */
+// role.js
+/* globals isRole */
+// state.js
+/* globals testStates */
+
+// //////////////////////////////////////////////////////////////////////////////
+// Interfaces
+
+const nsIAccessibilityService = Ci.nsIAccessibilityService;
+
+const nsIAccessibleEvent = Ci.nsIAccessibleEvent;
+const nsIAccessibleStateChangeEvent = Ci.nsIAccessibleStateChangeEvent;
+const nsIAccessibleCaretMoveEvent = Ci.nsIAccessibleCaretMoveEvent;
+const nsIAccessibleScrollingEvent = Ci.nsIAccessibleScrollingEvent;
+const nsIAccessibleTextChangeEvent = Ci.nsIAccessibleTextChangeEvent;
+const nsIAccessibleTextSelectionChangeEvent =
+ Ci.nsIAccessibleTextSelectionChangeEvent;
+const nsIAccessibleObjectAttributeChangedEvent =
+ Ci.nsIAccessibleObjectAttributeChangedEvent;
+const nsIAccessibleAnnouncementEvent = Ci.nsIAccessibleAnnouncementEvent;
+
+const nsIAccessibleStates = Ci.nsIAccessibleStates;
+const nsIAccessibleRole = Ci.nsIAccessibleRole;
+const nsIAccessibleScrollType = Ci.nsIAccessibleScrollType;
+const nsIAccessibleCoordinateType = Ci.nsIAccessibleCoordinateType;
+
+const nsIAccessibleRelation = Ci.nsIAccessibleRelation;
+const nsIAccessibleTextRange = Ci.nsIAccessibleTextRange;
+
+const nsIAccessible = Ci.nsIAccessible;
+
+const nsIAccessibleDocument = Ci.nsIAccessibleDocument;
+const nsIAccessibleApplication = Ci.nsIAccessibleApplication;
+
+const nsIAccessibleText = Ci.nsIAccessibleText;
+const nsIAccessibleEditableText = Ci.nsIAccessibleEditableText;
+
+const nsIAccessibleHyperLink = Ci.nsIAccessibleHyperLink;
+const nsIAccessibleHyperText = Ci.nsIAccessibleHyperText;
+
+const nsIAccessibleImage = Ci.nsIAccessibleImage;
+const nsIAccessiblePivot = Ci.nsIAccessiblePivot;
+const nsIAccessibleSelectable = Ci.nsIAccessibleSelectable;
+const nsIAccessibleTable = Ci.nsIAccessibleTable;
+const nsIAccessibleTableCell = Ci.nsIAccessibleTableCell;
+const nsIAccessibleTraversalRule = Ci.nsIAccessibleTraversalRule;
+const nsIAccessibleValue = Ci.nsIAccessibleValue;
+
+const nsIObserverService = Ci.nsIObserverService;
+
+const nsIDOMWindow = Ci.nsIDOMWindow;
+
+const nsIPropertyElement = Ci.nsIPropertyElement;
+
+// //////////////////////////////////////////////////////////////////////////////
+// OS detect
+
+const MAC = navigator.platform.includes("Mac");
+const LINUX = navigator.platform.includes("Linux");
+const SOLARIS = navigator.platform.includes("SunOS");
+const WIN = navigator.platform.includes("Win");
+
+// //////////////////////////////////////////////////////////////////////////////
+// Application detect
+// Firefox is assumed by default.
+
+const SEAMONKEY = navigator.userAgent.match(/ SeaMonkey\//);
+
+// //////////////////////////////////////////////////////////////////////////////
+// Accessible general
+
+const STATE_BUSY = nsIAccessibleStates.STATE_BUSY;
+
+const SCROLL_TYPE_TOP_EDGE = nsIAccessibleScrollType.SCROLL_TYPE_TOP_EDGE;
+const SCROLL_TYPE_ANYWHERE = nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE;
+
+const COORDTYPE_SCREEN_RELATIVE =
+ nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE;
+const COORDTYPE_WINDOW_RELATIVE =
+ nsIAccessibleCoordinateType.COORDTYPE_WINDOW_RELATIVE;
+const COORDTYPE_PARENT_RELATIVE =
+ nsIAccessibleCoordinateType.COORDTYPE_PARENT_RELATIVE;
+
+const kEmbedChar = String.fromCharCode(0xfffc);
+
+const kDiscBulletChar = String.fromCharCode(0x2022);
+const kDiscBulletText = kDiscBulletChar + " ";
+const kCircleBulletText = String.fromCharCode(0x25e6) + " ";
+const kSquareBulletText = String.fromCharCode(0x25aa) + " ";
+
+const MAX_TRIM_LENGTH = 100;
+
+/**
+ * Services to determine if e10s is enabled.
+ */
+
+/**
+ * nsIAccessibilityService service.
+ */
+var gAccService = Cc["@mozilla.org/accessibilityService;1"].getService(
+ nsIAccessibilityService
+);
+
+/**
+ * Enable/disable logging.
+ */
+function enableLogging(aModules) {
+ gAccService.setLogging(aModules);
+}
+function disableLogging() {
+ gAccService.setLogging("");
+}
+function isLogged(aModule) {
+ return gAccService.isLogged(aModule);
+}
+
+/**
+ * Dumps the accessible tree into console.
+ */
+function dumpTree(aId, aMsg) {
+ function dumpTreeIntl(acc, indent) {
+ dump(indent + prettyName(acc) + "\n");
+
+ var children = acc.children;
+ for (var i = 0; i < children.length; i++) {
+ var child = children.queryElementAt(i, nsIAccessible);
+ dumpTreeIntl(child, indent + " ");
+ }
+ }
+
+ function dumpDOMTreeIntl(node, indent) {
+ dump(indent + prettyName(node) + "\n");
+
+ var children = node.childNodes;
+ for (var i = 0; i < children.length; i++) {
+ var child = children.item(i);
+ dumpDOMTreeIntl(child, indent + " ");
+ }
+ }
+
+ dump(aMsg + "\n");
+ var root = getAccessible(aId);
+ dumpTreeIntl(root, " ");
+
+ dump("DOM tree:\n");
+ dumpDOMTreeIntl(getNode(aId), " ");
+}
+
+/**
+ * Invokes the given function when document is loaded and focused. Preferable
+ * to mochitests 'addLoadEvent' function -- additionally ensures state of the
+ * document accessible is not busy.
+ *
+ * @param aFunc the function to invoke
+ */
+function addA11yLoadEvent(aFunc, aWindow) {
+ function waitForDocLoad() {
+ window.setTimeout(function () {
+ var targetDocument = aWindow ? aWindow.document : document;
+ var accDoc = getAccessible(targetDocument);
+ var state = {};
+ accDoc.getState(state, {});
+ if (state.value & STATE_BUSY) {
+ waitForDocLoad();
+ return;
+ }
+
+ window.setTimeout(aFunc, 0);
+ }, 0);
+ }
+
+ if (
+ aWindow &&
+ aWindow.document.activeElement &&
+ aWindow.document.activeElement.localName == "browser"
+ ) {
+ waitForDocLoad();
+ } else {
+ SimpleTest.waitForFocus(waitForDocLoad, aWindow);
+ }
+}
+
+/**
+ * Analogy of SimpleTest.is function used to compare objects.
+ */
+function isObject(aObj, aExpectedObj, aMsg) {
+ if (aObj == aExpectedObj) {
+ ok(true, aMsg);
+ return;
+ }
+
+ ok(
+ false,
+ aMsg +
+ " - got '" +
+ prettyName(aObj) +
+ "', expected '" +
+ prettyName(aExpectedObj) +
+ "'"
+ );
+}
+
+/**
+ * is() function checking the expected value is within the range.
+ */
+function isWithin(aExpected, aGot, aWithin, aMsg) {
+ if (Math.abs(aGot - aExpected) <= aWithin) {
+ ok(true, `${aMsg} - Got ${aGot}`);
+ } else {
+ ok(
+ false,
+ `${aMsg} - Got ${aGot}, expected ${aExpected} with error of ${aWithin}`
+ );
+ }
+}
+
+// //////////////////////////////////////////////////////////////////////////////
+// Helpers for getting DOM node/accessible
+
+/**
+ * Return the DOM node by identifier (may be accessible, DOM node or ID).
+ */
+function getNode(aAccOrNodeOrID, aDocument) {
+ if (!aAccOrNodeOrID) {
+ return null;
+ }
+
+ if (Node.isInstance(aAccOrNodeOrID)) {
+ return aAccOrNodeOrID;
+ }
+
+ if (aAccOrNodeOrID instanceof nsIAccessible) {
+ return aAccOrNodeOrID.DOMNode;
+ }
+
+ var node = (aDocument || document).getElementById(aAccOrNodeOrID);
+ if (!node) {
+ ok(false, "Can't get DOM element for " + aAccOrNodeOrID);
+ return null;
+ }
+
+ return node;
+}
+
+/**
+ * Constants indicates getAccessible doesn't fail if there is no accessible.
+ */
+const DONOTFAIL_IF_NO_ACC = 1;
+
+/**
+ * Constants indicates getAccessible won't fail if accessible doesn't implement
+ * the requested interfaces.
+ */
+const DONOTFAIL_IF_NO_INTERFACE = 2;
+
+/**
+ * Return accessible for the given identifier (may be ID attribute or DOM
+ * element or accessible object) or null.
+ *
+ * @param aAccOrElmOrID [in] identifier to get an accessible implementing
+ * the given interfaces
+ * @param aInterfaces [in, optional] the interface or an array interfaces
+ * to query it/them from obtained accessible
+ * @param aElmObj [out, optional] object to store DOM element which
+ * accessible is obtained for
+ * @param aDoNotFailIf [in, optional] no error for special cases (see
+ * constants above)
+ */
+function getAccessible(aAccOrElmOrID, aInterfaces, aElmObj, aDoNotFailIf) {
+ if (!aAccOrElmOrID) {
+ return null;
+ }
+
+ var elm = null;
+
+ if (aAccOrElmOrID instanceof nsIAccessible) {
+ try {
+ elm = aAccOrElmOrID.DOMNode;
+ } catch (e) {}
+ } else if (Node.isInstance(aAccOrElmOrID)) {
+ elm = aAccOrElmOrID;
+ } else {
+ elm = document.getElementById(aAccOrElmOrID);
+ if (!elm) {
+ ok(false, "Can't get DOM element for " + aAccOrElmOrID);
+ return null;
+ }
+ }
+
+ if (aElmObj && typeof aElmObj == "object") {
+ aElmObj.value = elm;
+ }
+
+ var acc = aAccOrElmOrID instanceof nsIAccessible ? aAccOrElmOrID : null;
+ if (!acc) {
+ try {
+ acc = gAccService.getAccessibleFor(elm);
+ } catch (e) {}
+
+ if (!acc) {
+ if (!(aDoNotFailIf & DONOTFAIL_IF_NO_ACC)) {
+ ok(false, "Can't get accessible for " + prettyName(aAccOrElmOrID));
+ }
+
+ return null;
+ }
+ }
+
+ if (!aInterfaces) {
+ return acc;
+ }
+
+ if (!(aInterfaces instanceof Array)) {
+ aInterfaces = [aInterfaces];
+ }
+
+ for (var index = 0; index < aInterfaces.length; index++) {
+ if (acc instanceof aInterfaces[index]) {
+ continue;
+ }
+ try {
+ acc.QueryInterface(aInterfaces[index]);
+ } catch (e) {
+ if (!(aDoNotFailIf & DONOTFAIL_IF_NO_INTERFACE)) {
+ ok(
+ false,
+ "Can't query " + aInterfaces[index] + " for " + aAccOrElmOrID
+ );
+ }
+
+ return null;
+ }
+ }
+
+ return acc;
+}
+
+/**
+ * Return true if the given identifier has an accessible, or exposes the wanted
+ * interfaces.
+ */
+function isAccessible(aAccOrElmOrID, aInterfaces) {
+ return !!getAccessible(
+ aAccOrElmOrID,
+ aInterfaces,
+ null,
+ DONOTFAIL_IF_NO_ACC | DONOTFAIL_IF_NO_INTERFACE
+ );
+}
+
+/**
+ * Return an accessible that contains the DOM node for the given identifier.
+ */
+function getContainerAccessible(aAccOrElmOrID) {
+ var node = getNode(aAccOrElmOrID);
+ if (!node) {
+ return null;
+ }
+
+ // eslint-disable-next-line no-empty
+ while ((node = node.parentNode) && !isAccessible(node)) {}
+ return node ? getAccessible(node) : null;
+}
+
+/**
+ * Return root accessible for the given identifier.
+ */
+function getRootAccessible(aAccOrElmOrID) {
+ var acc = getAccessible(aAccOrElmOrID ? aAccOrElmOrID : document);
+ return acc ? acc.rootDocument.QueryInterface(nsIAccessible) : null;
+}
+
+/**
+ * Return tab document accessible the given accessible is contained by.
+ */
+function getTabDocAccessible(aAccOrElmOrID) {
+ var acc = getAccessible(aAccOrElmOrID ? aAccOrElmOrID : document);
+
+ var docAcc = acc.document.QueryInterface(nsIAccessible);
+ var containerDocAcc = docAcc.parent.document;
+
+ // Test is running is stand-alone mode.
+ if (acc.rootDocument == containerDocAcc) {
+ return docAcc;
+ }
+
+ // In the case of running all tests together.
+ return containerDocAcc.QueryInterface(nsIAccessible);
+}
+
+/**
+ * Return application accessible.
+ */
+function getApplicationAccessible() {
+ return gAccService
+ .getApplicationAccessible()
+ .QueryInterface(nsIAccessibleApplication);
+}
+
+/**
+ * A version of accessible tree testing, doesn't fail if tree is not complete.
+ */
+function testElm(aID, aTreeObj) {
+ testAccessibleTree(aID, aTreeObj, kSkipTreeFullCheck);
+}
+
+/**
+ * Flags used for testAccessibleTree
+ */
+const kSkipTreeFullCheck = 1;
+
+/**
+ * Compare expected and actual accessibles trees.
+ *
+ * @param aAccOrElmOrID [in] accessible identifier
+ * @param aAccTree [in] JS object, each field corresponds to property of
+ * accessible object. Additionally special properties
+ * are presented:
+ * children - an array of JS objects representing
+ * children of accessible
+ * states - an object having states and extraStates
+ * fields
+ * @param aFlags [in, optional] flags, see constants above
+ */
+// eslint-disable-next-line complexity
+function testAccessibleTree(aAccOrElmOrID, aAccTree, aFlags) {
+ var acc = getAccessible(aAccOrElmOrID);
+ if (!acc) {
+ return;
+ }
+
+ var accTree = aAccTree;
+
+ // Support of simplified accessible tree object.
+ accTree = normalizeAccTreeObj(accTree);
+
+ // Test accessible properties.
+ for (var prop in accTree) {
+ var msg =
+ "Wrong value of property '" + prop + "' for " + prettyName(acc) + ".";
+
+ switch (prop) {
+ case "actions": {
+ testActionNames(acc, accTree.actions);
+ break;
+ }
+
+ case "attributes":
+ testAttrs(acc, accTree[prop], true);
+ break;
+
+ case "absentAttributes":
+ testAbsentAttrs(acc, accTree[prop]);
+ break;
+
+ case "interfaces": {
+ var ifaces =
+ accTree[prop] instanceof Array ? accTree[prop] : [accTree[prop]];
+ for (let i = 0; i < ifaces.length; i++) {
+ ok(
+ acc instanceof ifaces[i],
+ "No " + ifaces[i] + " interface on " + prettyName(acc)
+ );
+ }
+ break;
+ }
+
+ case "relations": {
+ for (var rel in accTree[prop]) {
+ testRelation(acc, window[rel], accTree[prop][rel]);
+ }
+ break;
+ }
+
+ case "role":
+ isRole(acc, accTree[prop], msg);
+ break;
+
+ case "states":
+ case "extraStates":
+ case "absentStates":
+ case "absentExtraStates": {
+ testStates(
+ acc,
+ accTree.states,
+ accTree.extraStates,
+ accTree.absentStates,
+ accTree.absentExtraStates
+ );
+ break;
+ }
+
+ case "tagName":
+ is(accTree[prop], acc.DOMNode.tagName, msg);
+ break;
+
+ case "textAttrs": {
+ var prevOffset = -1;
+ for (var offset in accTree[prop]) {
+ if (prevOffset != -1) {
+ let attrs = accTree[prop][prevOffset];
+ testTextAttrs(
+ acc,
+ prevOffset,
+ attrs,
+ {},
+ prevOffset,
+ +offset,
+ true
+ );
+ }
+ prevOffset = +offset;
+ }
+
+ if (prevOffset != -1) {
+ var charCount = getAccessible(acc, [
+ nsIAccessibleText,
+ ]).characterCount;
+ let attrs = accTree[prop][prevOffset];
+ testTextAttrs(
+ acc,
+ prevOffset,
+ attrs,
+ {},
+ prevOffset,
+ charCount,
+ true
+ );
+ }
+
+ break;
+ }
+
+ default:
+ if (prop.indexOf("todo_") == 0) {
+ todo(false, msg);
+ } else if (prop != "children") {
+ is(acc[prop], accTree[prop], msg);
+ }
+ }
+ }
+
+ // Test children.
+ if ("children" in accTree && accTree.children instanceof Array) {
+ var children = acc.children;
+ var childCount = children.length;
+
+ if (accTree.children.length != childCount) {
+ for (let i = 0; i < Math.max(accTree.children.length, childCount); i++) {
+ var accChild = null,
+ testChild = null;
+ try {
+ testChild = accTree.children[i];
+ accChild = children.queryElementAt(i, nsIAccessible);
+
+ if (!testChild) {
+ ok(
+ false,
+ prettyName(acc) +
+ " has an extra child at index " +
+ i +
+ " : " +
+ prettyName(accChild)
+ );
+ continue;
+ }
+
+ testChild = normalizeAccTreeObj(testChild);
+ if (accChild.role !== testChild.role) {
+ ok(
+ false,
+ prettyName(accTree) +
+ " and " +
+ prettyName(acc) +
+ " have different children at index " +
+ i +
+ " : " +
+ prettyName(testChild) +
+ ", " +
+ prettyName(accChild)
+ );
+ }
+ info(
+ "Matching " +
+ prettyName(accTree) +
+ " and " +
+ prettyName(acc) +
+ " child at index " +
+ i +
+ " : " +
+ prettyName(accChild)
+ );
+ } catch (e) {
+ ok(
+ false,
+ prettyName(accTree) +
+ " is expected to have a child at index " +
+ i +
+ " : " +
+ prettyName(testChild) +
+ ", original tested: " +
+ prettyName(aAccOrElmOrID) +
+ ", " +
+ e
+ );
+ }
+ }
+ } else {
+ if (aFlags & kSkipTreeFullCheck) {
+ for (let i = 0; i < childCount; i++) {
+ let child = children.queryElementAt(i, nsIAccessible);
+ testAccessibleTree(child, accTree.children[i], aFlags);
+ }
+ return;
+ }
+
+ // nsIAccessible::firstChild
+ var expectedFirstChild =
+ childCount > 0 ? children.queryElementAt(0, nsIAccessible) : null;
+ var firstChild = null;
+ try {
+ firstChild = acc.firstChild;
+ } catch (e) {}
+ is(
+ firstChild,
+ expectedFirstChild,
+ "Wrong first child of " + prettyName(acc)
+ );
+
+ // nsIAccessible::lastChild
+ var expectedLastChild =
+ childCount > 0
+ ? children.queryElementAt(childCount - 1, nsIAccessible)
+ : null;
+ var lastChild = null;
+ try {
+ lastChild = acc.lastChild;
+ } catch (e) {}
+ is(
+ lastChild,
+ expectedLastChild,
+ "Wrong last child of " + prettyName(acc)
+ );
+
+ for (var i = 0; i < childCount; i++) {
+ let child = children.queryElementAt(i, nsIAccessible);
+
+ // nsIAccessible::parent
+ var parent = null;
+ try {
+ parent = child.parent;
+ } catch (e) {}
+ is(parent, acc, "Wrong parent of " + prettyName(child));
+
+ // nsIAccessible::indexInParent
+ var indexInParent = -1;
+ try {
+ indexInParent = child.indexInParent;
+ } catch (e) {}
+ is(indexInParent, i, "Wrong index in parent of " + prettyName(child));
+
+ // nsIAccessible::nextSibling
+ var expectedNextSibling =
+ i < childCount - 1
+ ? children.queryElementAt(i + 1, nsIAccessible)
+ : null;
+ var nextSibling = null;
+ try {
+ nextSibling = child.nextSibling;
+ } catch (e) {}
+ is(
+ nextSibling,
+ expectedNextSibling,
+ "Wrong next sibling of " + prettyName(child)
+ );
+
+ // nsIAccessible::previousSibling
+ var expectedPrevSibling =
+ i > 0 ? children.queryElementAt(i - 1, nsIAccessible) : null;
+ var prevSibling = null;
+ try {
+ prevSibling = child.previousSibling;
+ } catch (e) {}
+ is(
+ prevSibling,
+ expectedPrevSibling,
+ "Wrong previous sibling of " + prettyName(child)
+ );
+
+ // Go down through subtree
+ testAccessibleTree(child, accTree.children[i], aFlags);
+ }
+ }
+ }
+}
+
+/**
+ * Return true if accessible for the given node is in cache.
+ */
+function isAccessibleInCache(aNodeOrId) {
+ var node = getNode(aNodeOrId);
+ return !!gAccService.getAccessibleFromCache(node);
+}
+
+/**
+ * Test accessible tree for defunct accessible.
+ *
+ * @param aAcc [in] the defunct accessible
+ * @param aNodeOrId [in] the DOM node identifier for the defunct accessible
+ */
+function testDefunctAccessible(aAcc, aNodeOrId) {
+ if (aNodeOrId) {
+ ok(
+ !isAccessible(aNodeOrId),
+ "Accessible for " + aNodeOrId + " wasn't properly shut down!"
+ );
+ }
+
+ var msg =
+ " doesn't fail for shut down accessible " + prettyName(aNodeOrId) + "!";
+
+ // firstChild
+ var success = false;
+ try {
+ aAcc.firstChild;
+ } catch (e) {
+ success = e.result == Cr.NS_ERROR_FAILURE;
+ }
+ ok(success, "firstChild" + msg);
+
+ // lastChild
+ success = false;
+ try {
+ aAcc.lastChild;
+ } catch (e) {
+ success = e.result == Cr.NS_ERROR_FAILURE;
+ }
+ ok(success, "lastChild" + msg);
+
+ // childCount
+ success = false;
+ try {
+ aAcc.childCount;
+ } catch (e) {
+ success = e.result == Cr.NS_ERROR_FAILURE;
+ }
+ ok(success, "childCount" + msg);
+
+ // children
+ success = false;
+ try {
+ aAcc.children;
+ } catch (e) {
+ success = e.result == Cr.NS_ERROR_FAILURE;
+ }
+ ok(success, "children" + msg);
+
+ // nextSibling
+ success = false;
+ try {
+ aAcc.nextSibling;
+ } catch (e) {
+ success = e.result == Cr.NS_ERROR_FAILURE;
+ }
+ ok(success, "nextSibling" + msg);
+
+ // previousSibling
+ success = false;
+ try {
+ aAcc.previousSibling;
+ } catch (e) {
+ success = e.result == Cr.NS_ERROR_FAILURE;
+ }
+ ok(success, "previousSibling" + msg);
+
+ // parent
+ success = false;
+ try {
+ aAcc.parent;
+ } catch (e) {
+ success = e.result == Cr.NS_ERROR_FAILURE;
+ }
+ ok(success, "parent" + msg);
+}
+
+/**
+ * Convert role to human readable string.
+ */
+function roleToString(aRole) {
+ return gAccService.getStringRole(aRole);
+}
+
+/**
+ * Convert states to human readable string.
+ */
+function statesToString(aStates, aExtraStates) {
+ var list = gAccService.getStringStates(aStates, aExtraStates);
+
+ var str = "";
+ for (var index = 0; index < list.length - 1; index++) {
+ str += list.item(index) + ", ";
+ }
+
+ if (list.length) {
+ str += list.item(index);
+ }
+
+ return str;
+}
+
+/**
+ * Convert event type to human readable string.
+ */
+function eventTypeToString(aEventType) {
+ return gAccService.getStringEventType(aEventType);
+}
+
+/**
+ * Convert relation type to human readable string.
+ */
+function relationTypeToString(aRelationType) {
+ return gAccService.getStringRelationType(aRelationType);
+}
+
+function getLoadContext() {
+ return window.docShell.QueryInterface(Ci.nsILoadContext);
+}
+
+/**
+ * Return text from clipboard.
+ */
+function getTextFromClipboard() {
+ var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(getLoadContext());
+ if (!trans) {
+ return "";
+ }
+
+ trans.addDataFlavor("text/plain");
+ Services.clipboard.getData(
+ trans,
+ Services.clipboard.kGlobalClipboard,
+ SpecialPowers.wrap(window).browsingContext.currentWindowContext
+ );
+
+ var str = {};
+ trans.getTransferData("text/plain", str);
+
+ if (str) {
+ str = str.value.QueryInterface(Ci.nsISupportsString);
+ }
+ if (str) {
+ return str.data;
+ }
+
+ return "";
+}
+
+/**
+ * Extract DOMNode id from an accessible. If e10s is enabled, DOMNode is not
+ * present in parent process but, if available, DOMNode id is attached to an
+ * accessible object.
+ * @param {nsIAccessible} accessible accessible
+ * @return {String?} DOMNode id if available
+ */
+function getAccessibleDOMNodeID(accessible) {
+ if (accessible instanceof nsIAccessibleDocument) {
+ // If accessible is a document, trying to find its document body id.
+ try {
+ return accessible.DOMNode.body.id;
+ } catch (e) {
+ /* This only works if accessible is not a proxy. */
+ }
+ }
+ try {
+ return accessible.DOMNode.id;
+ } catch (e) {
+ /* This will fail if DOMNode is in different process. */
+ }
+ try {
+ // When e10s is enabled, accessible will have an "id" property if its
+ // corresponding DOMNode has an id. If accessible is a document, its "id"
+ // property corresponds to the "id" of its body element.
+ return accessible.id;
+ } catch (e) {
+ /* This will fail if accessible is not a proxy. */
+ }
+ return null;
+}
+
+/**
+ * Return pretty name for identifier, it may be ID, DOM node or accessible.
+ */
+function prettyName(aIdentifier) {
+ if (aIdentifier instanceof Array) {
+ let msg = "";
+ for (var idx = 0; idx < aIdentifier.length; idx++) {
+ if (msg != "") {
+ msg += ", ";
+ }
+
+ msg += prettyName(aIdentifier[idx]);
+ }
+ return msg;
+ }
+
+ if (aIdentifier instanceof nsIAccessible) {
+ var acc = getAccessible(aIdentifier);
+ var domID = getAccessibleDOMNodeID(acc);
+ let msg = "[";
+ try {
+ if (Services.appinfo.browserTabsRemoteAutostart) {
+ if (domID) {
+ msg += `DOM node id: ${domID}, `;
+ }
+ } else {
+ msg += `${getNodePrettyName(acc.DOMNode)}, `;
+ }
+ msg += "role: " + roleToString(acc.role);
+ if (acc.name) {
+ msg += ", name: '" + shortenString(acc.name) + "'";
+ }
+ } catch (e) {
+ msg += "defunct";
+ }
+
+ if (acc) {
+ msg += ", address: " + getObjAddress(acc);
+ }
+ msg += "]";
+
+ return msg;
+ }
+
+ if (Node.isInstance(aIdentifier)) {
+ return "[ " + getNodePrettyName(aIdentifier) + " ]";
+ }
+
+ if (aIdentifier && typeof aIdentifier === "object") {
+ var treeObj = normalizeAccTreeObj(aIdentifier);
+ if ("role" in treeObj) {
+ function stringifyTree(aObj) {
+ var text = roleToString(aObj.role) + ": [ ";
+ if ("children" in aObj) {
+ for (var i = 0; i < aObj.children.length; i++) {
+ var c = normalizeAccTreeObj(aObj.children[i]);
+ text += stringifyTree(c);
+ if (i < aObj.children.length - 1) {
+ text += ", ";
+ }
+ }
+ }
+ return text + "] ";
+ }
+ return `{ ${stringifyTree(treeObj)} }`;
+ }
+ return JSON.stringify(aIdentifier);
+ }
+
+ return " '" + aIdentifier + "' ";
+}
+
+/**
+ * Shorten a long string if it exceeds MAX_TRIM_LENGTH.
+ * @param aString the string to shorten.
+ * @returns the shortened string.
+ */
+function shortenString(aString, aMaxLength) {
+ if (aString.length <= MAX_TRIM_LENGTH) {
+ return aString;
+ }
+
+ // Trim the string if its length is > MAX_TRIM_LENGTH characters.
+ var trimOffset = MAX_TRIM_LENGTH / 2;
+ return (
+ aString.substring(0, trimOffset - 1) +
+ "..." +
+ aString.substring(aString.length - trimOffset, aString.length)
+ );
+}
+
+// //////////////////////////////////////////////////////////////////////////////
+// General Utils
+// //////////////////////////////////////////////////////////////////////////////
+/**
+ * Return main chrome window (crosses chrome boundary)
+ */
+function getMainChromeWindow(aWindow) {
+ return aWindow.browsingContext.topChromeWindow;
+}
+
+// //////////////////////////////////////////////////////////////////////////////
+// Private
+// //////////////////////////////////////////////////////////////////////////////
+
+// //////////////////////////////////////////////////////////////////////////////
+// Accessible general
+
+function getNodePrettyName(aNode) {
+ try {
+ var tag = "";
+ if (aNode.nodeType == Node.DOCUMENT_NODE) {
+ tag = "document";
+ } else {
+ tag = aNode.localName;
+ if (aNode.nodeType == Node.ELEMENT_NODE && aNode.hasAttribute("id")) {
+ tag += '@id="' + aNode.getAttribute("id") + '"';
+ }
+ }
+
+ return "'" + tag + " node', address: " + getObjAddress(aNode);
+ } catch (e) {
+ return "' no node info '";
+ }
+}
+
+function getObjAddress(aObj) {
+ var exp = /native\s*@\s*(0x[a-f0-9]+)/g;
+ var match = exp.exec(aObj.toString());
+ if (match) {
+ return match[1];
+ }
+
+ return aObj.toString();
+}
+
+function normalizeAccTreeObj(aObj) {
+ var key = Object.keys(aObj)[0];
+ var roleName = "ROLE_" + key;
+ if (roleName in nsIAccessibleRole) {
+ return {
+ role: nsIAccessibleRole[roleName],
+ children: aObj[key],
+ };
+ }
+ return aObj;
+}
diff --git a/accessible/tests/mochitest/dumbfile.zip b/accessible/tests/mochitest/dumbfile.zip
new file mode 100644
index 0000000000..15cb0ecb3e
--- /dev/null
+++ b/accessible/tests/mochitest/dumbfile.zip
Binary files differ
diff --git a/accessible/tests/mochitest/elm/a11y.toml b/accessible/tests/mochitest/elm/a11y.toml
new file mode 100644
index 0000000000..0559fbf55b
--- /dev/null
+++ b/accessible/tests/mochitest/elm/a11y.toml
@@ -0,0 +1,19 @@
+[DEFAULT]
+support-files = [
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/moz.png",
+ "!/dom/media/test/bug461281.ogg",
+ "!/dom/security/test/csp/dummy.pdf"]
+
+["test_HTMLSpec.html"]
+
+["test_MathMLSpec.html"]
+
+["test_figure.html"]
+
+["test_listbox.xhtml"]
+
+["test_nsApplicationAcc.html"]
+
+["test_shadowroot.html"]
+support-files = "test_shadowroot_subframe.html"
diff --git a/accessible/tests/mochitest/elm/test_HTMLSpec.html b/accessible/tests/mochitest/elm/test_HTMLSpec.html
new file mode 100644
index 0000000000..082be79ba7
--- /dev/null
+++ b/accessible/tests/mochitest/elm/test_HTMLSpec.html
@@ -0,0 +1,2029 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML a11y spec tests</title>
+ <link id="link" rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../actions.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+ <script type="application/javascript"
+ src="../relations.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+
+ <script type="application/javascript">
+ async function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:a@href
+
+ var obj = {
+ role: ROLE_LINK,
+ states: STATE_LINKED,
+ actions: "jump",
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText, nsIAccessibleHyperLink ],
+ children: [ // all kids inherits linked state and action
+ {
+ role: ROLE_TEXT_LEAF,
+ states: STATE_LINKED,
+ actions: "click ancestor",
+ },
+ ],
+ };
+ testElm("a_href", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:a no @href
+
+ obj = {
+ todo_role: ROLE_TEXT_CONTAINER,
+ absentStates: STATE_LINKED,
+ actions: null,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ absentStates: STATE_LINKED,
+ actions: null,
+ },
+ ],
+ };
+ testElm("a_nohref", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:abbr contained by HTML:td
+
+ obj = {
+ role: ROLE_CELL,
+ attributes: { abbr: "WWW" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ children: [
+ {
+ role: ROLE_TEXT,
+ children: [ { role: ROLE_TEXT_LEAF } ],
+ },
+ ],
+ };
+ testElm("td_abbr", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:address
+
+ obj = {
+ role: ROLE_GROUPING,
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("address", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:area@href
+
+ obj = {
+ role: ROLE_LINK,
+ states: STATE_LINKED,
+ actions: "jump",
+ interfaces: [ nsIAccessibleHyperLink ],
+ children: [],
+ };
+ testElm(getAccessible("imgmap").firstChild, obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:area no @href
+
+ obj = {
+ todo_role: "ROLE_SHAPE",
+ absentStates: STATE_LINKED,
+ children: [],
+ };
+ testElm(getAccessible("imgmap").lastChild, obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:article
+ obj = {
+ role: ROLE_ARTICLE,
+ states: STATE_READONLY,
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("article", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:aside
+ obj = {
+ role: ROLE_LANDMARK,
+ attributes: { "xml-roles": "complementary" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("aside", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ obj = { // HTML:audio
+ role: ROLE_GROUPING,
+ };
+ testElm("audio", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ obj = { // HTML:b contained by paragraph
+ role: ROLE_PARAGRAPH,
+ textAttrs: {
+ 0: { },
+ 6: { "font-weight": kBoldFontWeight },
+ },
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_TEXT_LEAF }, // HTML:b text
+ ],
+ };
+ testElm("b_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ obj = { // HTML:bdi contained by paragraph
+ role: ROLE_PARAGRAPH,
+ todo_textAttrs: {
+ 0: { },
+ 5: { "writing-mode": "rl" },
+ 8: { },
+ },
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_TEXT_LEAF }, // HTML:bdi text
+ { role: ROLE_TEXT_LEAF }, // plain text
+ ],
+ };
+ testElm("bdi_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:bdo contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ todo_textAttrs: {
+ 0: { },
+ 6: { "writing-mode": "rl" },
+ },
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ ],
+ };
+ testElm("bdo_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:blockquote
+
+ obj = {
+ role: ROLE_BLOCKQUOTE,
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ children: [ { role: ROLE_PARAGRAPH } ],
+ };
+ testElm("blockquote", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:br contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ children: [
+ { role: ROLE_WHITESPACE },
+ { role: ROLE_WHITESPACE }
+ ]
+ };
+ testElm("br_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ obj = { // HTML:button
+ role: ROLE_PUSHBUTTON,
+ absentStates: STATE_DEFAULT,
+ actions: "press",
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("button", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:button@type="submit" (default button)
+
+ obj = {
+ role: ROLE_PUSHBUTTON,
+ states: STATE_DEFAULT,
+ actions: "press",
+ };
+ testElm("button_default", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:canvas
+
+ obj = {
+ role: ROLE_CANVAS,
+ };
+ testElm("canvas", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:caption under table
+
+ obj = {
+ role: ROLE_TABLE,
+ relations: {
+ RELATION_LABELLED_BY: "caption",
+ },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText, nsIAccessibleTable ],
+ children: [
+ {
+ role: ROLE_CAPTION,
+ relations: {
+ RELATION_LABEL_FOR: "table",
+ },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ },
+ { // td inside thead
+ role: ROLE_ROW,
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ children: [
+ {
+ role: ROLE_COLUMNHEADER,
+ interfaces: [ nsIAccessibleTableCell, nsIAccessibleText, nsIAccessibleHyperText ],
+ },
+ { role: ROLE_COLUMNHEADER },
+ ],
+ },
+ { // td inside tbody
+ role: ROLE_ROW,
+ children: [
+ {
+ role: ROLE_ROWHEADER,
+ interfaces: [ nsIAccessibleTableCell, nsIAccessibleText, nsIAccessibleHyperText ],
+ },
+ {
+ role: ROLE_CELL,
+ interfaces: [ nsIAccessibleTableCell, nsIAccessibleText, nsIAccessibleHyperText ],
+ },
+ ],
+ },
+ { // td inside tfoot
+ role: ROLE_ROW,
+ },
+ ],
+ };
+ testElm("table", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:cite contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ textAttrs: {
+ 0: { },
+ 6: { "font-style": "italic" },
+ },
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_TEXT_LEAF }, // HTML:cite text
+ ],
+ };
+ testElm("cite_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:code contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ textAttrs: {
+ 0: { },
+ 6: { "font-family": kMonospaceFontFamily },
+ },
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_CODE },
+ ],
+ };
+ testElm("code_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:col and HTML:colgroup under table
+
+ obj =
+ { TABLE: [
+ { ROW: [
+ { role: ROLE_CELL },
+ { role: ROLE_CELL },
+ { role: ROLE_CELL },
+ ] },
+ ] };
+ testElm("colNcolgroup_table", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:data contained by paragraph
+
+ obj =
+ { PARAGRAPH: [
+ { TEXT_LEAF: [] }, // HTML:data text
+ ] };
+ testElm("data_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:datalist associated with input
+
+ todo(false, "datalist and summary tree hierarchy test missed");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:dd, HTML:dl, HTML:dd
+
+ obj = {
+ role: ROLE_DEFINITION_LIST,
+ states: STATE_READONLY,
+ children: [ // dl
+ {
+ role: ROLE_TERM,
+ states: STATE_READONLY,
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ children: [ // dt
+ { role: ROLE_TEXT_LEAF },
+ ],
+ },
+ {
+ role: ROLE_DEFINITION,
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ children: [ // dd
+ { role: ROLE_TEXT_LEAF },
+ ],
+ },
+ ],
+ };
+ testElm("dl", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:del contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_CONTENT_DELETION },
+ ],
+ };
+ testElm("del_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:details with open state
+
+ obj = {
+ role: ROLE_DETAILS,
+ children: [
+ {
+ role: ROLE_SUMMARY,
+ states: STATE_EXPANDED,
+ actions: "collapse",
+ },
+ { role: ROLE_PARAGRAPH },
+ ],
+ };
+ testElm("details", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:details with closed (default) state
+
+ obj = {
+ role: ROLE_DETAILS,
+ children: [
+ {
+ role: ROLE_SUMMARY,
+ states: STATE_COLLAPSED,
+ actions: "expand",
+ },
+ ],
+ };
+ testElm("details_closed", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:dfn contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ textAttrs: {
+ 0: { },
+ 1: { },
+ },
+ children: [
+ {
+ role: ROLE_TERM, // HTML:dfn
+ textAttrs: { 0: { }, },
+ children: [
+ { role: ROLE_TEXT_LEAF },
+ ],
+ },
+ { role: ROLE_TEXT_LEAF }, // plain text
+ ],
+ };
+ testElm("dfn_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:dialog
+
+ obj = {
+ role: ROLE_DIALOG,
+ children: [
+ { role: ROLE_TEXT_LEAF },
+ ],
+ };
+ testElm("dialog", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:div
+
+ obj = {
+ role: ROLE_SECTION,
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ ],
+ };
+ testElm("div", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:em in a paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ textAttrs: {
+ 0: { },
+ 6: { },
+ },
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_EMPHASIS, // HTML:em text
+ children: [
+ { role: ROLE_TEXT_LEAF, },
+ ],
+ textAttrs: {
+ 0: { },
+ },
+ },
+ ],
+ };
+ testElm("em_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:embed
+
+ obj = {
+ INTERNAL_FRAME: [ {
+ DOCUMENT: [ {
+ role: ROLE_GRAPHIC,
+ interfaces: [ nsIAccessibleImage ],
+ } ],
+ } ],
+ };
+ testElm("embed_png", obj);
+
+ obj = {
+ INTERNAL_FRAME: [ {
+ DOCUMENT: [ {
+ role: ROLE_PARAGRAPH,
+ } ],
+ } ],
+ };
+ testElm("embed_html", obj);
+
+ obj = {
+ INTERNAL_FRAME: [ {
+ DOCUMENT: [ {
+ } ],
+ } ],
+ };
+ testElm("embed_pdf", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:fieldset and HTML:legend
+
+ obj = {
+ role: ROLE_GROUPING,
+ name: "legend",
+ relations: {
+ RELATION_LABELLED_BY: "legend",
+ },
+ children: [
+ {
+ role: ROLE_LABEL,
+ name: "legend",
+ relations: {
+ RELATION_LABEL_FOR: "fieldset",
+ },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ },
+ {
+ role: ROLE_ENTRY,
+ },
+ ],
+ };
+ testElm("fieldset", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:figure and HTML:figcaption
+
+ obj = {
+ role: ROLE_FIGURE,
+ attributes: { "xml-roles": "figure" },
+ relations: {
+ RELATION_LABELLED_BY: "figcaption",
+ },
+ children: [
+ { role: ROLE_GRAPHIC },
+ {
+ role: ROLE_CAPTION,
+ relations: {
+ RELATION_LABEL_FOR: "figure",
+ },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ },
+ ],
+ };
+ testElm("figure", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:footer
+
+ obj = {
+ role: ROLE_LANDMARK,
+ attributes: { "xml-roles": "contentinfo" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("footer", obj);
+
+ obj = {
+ role: ROLE_SECTION,
+ absentAttributes: { "xml-roles": "contentinfo" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("footer_in_article", obj);
+ testElm("footer_in_aside", obj);
+ testElm("footer_in_main", obj);
+ testElm("footer_in_nav", obj);
+ testElm("footer_in_section", obj);
+ testElm("footer_in_blockquote", obj);
+ testElm("footer_in_details", obj);
+ testElm("footer_in_dialog", obj);
+ testElm("footer_in_fieldset", obj);
+ testElm("footer_in_figure", obj);
+ testElm("footer_in_td", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:form
+
+ obj = {
+ role: ROLE_FORM,
+ absentAttributes: { "xml-roles": "form" },
+ };
+ testElm("form", obj);
+
+ // HTML:form with an accessible name
+
+ obj = {
+ role: ROLE_FORM_LANDMARK,
+ attributes: { "xml-roles": "form" },
+ };
+ testElm("named_form", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // // HTML:frameset, HTML:frame and HTML:iframe
+
+ obj = {
+ INTERNAL_FRAME: [ { // HTML:iframe
+ DOCUMENT: [ {
+ INTERNAL_FRAME: [ { // HTML:frame
+ DOCUMENT: [ { role: ROLE_TEXT_LEAF} ],
+ } ],
+ } ],
+ } ],
+ };
+ testElm("frameset_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:h1, HTML:h2, HTML:h3, HTML:h4, HTML:h5, HTML:h6
+
+ function headingWithLevel(i) {
+ return {
+ role: ROLE_HEADING,
+ attributes: { "level": i.toString() },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ }
+
+ for (let level = 1; level <= 6; ++level) {
+ testElm("h" + level, headingWithLevel(level));
+ for (const ancestor of ["section", "article", "aside", "nav"]) {
+ testElm("h" + level + "_in_" + ancestor, headingWithLevel(level));
+ testElm("h" + level + "_in_" + ancestor + "_in_hgroup", headingWithLevel(level));
+ }
+ }
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:header
+
+ obj = {
+ role: ROLE_LANDMARK,
+ attributes: { "xml-roles": "banner" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("header", obj);
+
+ obj = {
+ role: ROLE_SECTION,
+ absentAttributes: { "xml-roles": "banner" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("header_in_article", obj);
+ testElm("header_in_aside", obj);
+ testElm("header_in_main", obj);
+ testElm("header_in_nav", obj);
+ testElm("header_in_section", obj);
+ testElm("header_in_blockquote", obj);
+ testElm("header_in_details", obj);
+ testElm("header_in_dialog", obj);
+ testElm("header_in_fieldset", obj);
+ testElm("header_in_figure", obj);
+ testElm("header_in_td", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:hr
+
+ obj = {
+ role: ROLE_SEPARATOR,
+ };
+ testElm("hr", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ obj = { // HTML:i contained by paragraph
+ role: ROLE_PARAGRAPH,
+ textAttrs: {
+ 0: { },
+ 6: { "font-style": "italic" },
+ },
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_TEXT_LEAF }, // HTML:i text
+ ],
+ };
+ testElm("i_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:img
+
+ obj = {
+ role: ROLE_GRAPHIC,
+ interfaces: [ nsIAccessibleImage ],
+ };
+ testElm("img", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:input@type="button"
+
+ obj = {
+ role: ROLE_PUSHBUTTON,
+ absentStates: STATE_DEFAULT,
+ };
+ testElm("input_button", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:input@type="checkbox"
+
+ obj = {
+ role: ROLE_CHECKBUTTON,
+ states: STATE_CHECKABLE,
+ absentStates: STATE_CHECKED,
+ actions: "check",
+ };
+ testElm("input_checkbox", obj);
+
+ obj = {
+ role: ROLE_CHECKBUTTON,
+ states: STATE_CHECKABLE | STATE_CHECKED,
+ actions: "uncheck",
+ };
+ testElm("input_checkbox_true", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:input@type="file"
+
+ obj = {
+ role: ROLE_PUSHBUTTON,
+ actions: "press",
+ };
+ testElm("input_file", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:input@type="image"
+
+ obj = {
+ role: ROLE_PUSHBUTTON,
+ absentStates: STATE_DEFAULT,
+ actions: "press",
+ };
+ testElm("input_image", obj);
+ testElm("input_image_display", obj);
+ testElm("input_submit", obj);
+
+ obj = {
+ role: ROLE_PUSHBUTTON,
+ actions: "press",
+ states: STATE_DEFAULT,
+ };
+ testElm("input_image_default", obj);
+ testElm("input_submit_default", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:input@type="number" and etc
+
+ obj = {
+ role: ROLE_SPINBUTTON,
+ interfaces: [ nsIAccessibleValue, nsIAccessibleText, nsIAccessibleEditableText ],
+ children: [
+ { role: ROLE_TEXT_LEAF },
+ ],
+ };
+ testElm("input_number", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:input@type="text" and etc
+
+ obj = {
+ role: ROLE_ENTRY,
+ extraStates: EXT_STATE_EDITABLE | EXT_STATE_SINGLE_LINE,
+ actions: "activate",
+ interfaces: [ nsIAccessibleText, nsIAccessibleEditableText ],
+ children: [
+ { role: ROLE_TEXT_LEAF },
+ ],
+ };
+ testElm("input_email", obj);
+ testElm("input_search", obj);
+ testElm("input_tel", obj);
+ testElm("input_text", obj);
+ testElm("input_url", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // input @type="text" with placeholder attribute
+
+ // First: Label and placeholder, text is the same, no attribute.
+ obj = {
+ role: ROLE_ENTRY,
+ name: "Your name",
+ extraStates: EXT_STATE_EDITABLE | EXT_STATE_SINGLE_LINE,
+ actions: "activate",
+ absentAttributes: { placeholder: "Your name" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleEditableText ],
+ children: [],
+ };
+ testElm("input_placeholder_same", obj);
+
+ // Second: Label and placeholder, text is different, attribute.
+ obj = {
+ role: ROLE_ENTRY,
+ name: "First name:",
+ extraStates: EXT_STATE_EDITABLE | EXT_STATE_SINGLE_LINE,
+ actions: "activate",
+ attributes: { placeholder: "Enter your first name" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleEditableText ],
+ children: [],
+ };
+ testElm("input_placeholder_different", obj);
+
+ // Third: placeholder only, text is name, no attribute.
+ obj = {
+ role: ROLE_ENTRY,
+ name: "Date of birth",
+ extraStates: EXT_STATE_EDITABLE | EXT_STATE_SINGLE_LINE,
+ actions: "activate",
+ absentAttributes: { placeholder: "Date of birth" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleEditableText ],
+ children: [],
+ };
+ testElm("input_placeholder_only", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:input@type="password"
+
+ obj = {
+ role: ROLE_PASSWORD_TEXT,
+ states: STATE_PROTECTED,
+ extraStates: EXT_STATE_EDITABLE,
+ actions: "activate",
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ },
+ ],
+ };
+ testElm("input_password", obj);
+ ok(getAccessible("input_password").firstChild.name != "44",
+ "text leaf for password shouldn't have its real value as its name!");
+
+ obj = {
+ role: ROLE_PASSWORD_TEXT,
+ states: STATE_PROTECTED | STATE_READONLY,
+ actions: "activate",
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ },
+ ],
+ };
+ testElm("input_password_readonly", obj);
+ ok(getAccessible("input_password_readonly").firstChild.name != "44",
+ "text leaf for password shouldn't have its real value as its name!");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:input@type="radio"
+
+ obj = {
+ role: ROLE_RADIOBUTTON,
+ states: STATE_CHECKABLE,
+ absentStates: STATE_CHECKED,
+ actions: "select",
+ };
+ testElm("input_radio", obj);
+
+ obj = {
+ role: ROLE_RADIOBUTTON,
+ states: STATE_CHECKABLE | STATE_CHECKED,
+ actions: "select",
+ };
+ testElm("input_radio_true", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:input@type="range"
+
+ obj = {
+ role: ROLE_SLIDER,
+ };
+ testElm("input_range", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:input@type="reset"
+
+ obj = {
+ role: ROLE_PUSHBUTTON,
+ actions: "press",
+ absentStates: STATE_DEFAULT,
+ };
+ testElm("input_reset", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:input@type="time"
+
+ obj = {
+ role: ROLE_TIME_EDITOR,
+ name: "time label",
+ attributes: { "text-input-type": "time" },
+ children: [
+ { role: ROLE_SPINBUTTON },
+ { role: ROLE_TEXT_LEAF },
+ { role: ROLE_SPINBUTTON },
+ { role: ROLE_TEXT_LEAF },
+ { role: ROLE_ENTRY },
+ ],
+ };
+ testElm("input_time", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:input@type="date"
+
+ obj = {
+ role: ROLE_DATE_EDITOR,
+ name: "date label",
+ attributes: { "text-input-type": "date" },
+ children: [
+ { role: ROLE_SPINBUTTON },
+ { role: ROLE_TEXT_LEAF },
+ { role: ROLE_SPINBUTTON },
+ { role: ROLE_TEXT_LEAF },
+ { role: ROLE_SPINBUTTON },
+ { role: ROLE_PUSHBUTTON },
+ ],
+ };
+ testElm("input_date", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:input@type="datetime-local"
+
+ obj = {
+ role: ROLE_DATE_EDITOR,
+ name: "datetime-local label",
+ attributes: { "text-input-type": "datetime-local" },
+ children: [
+ { role: ROLE_SPINBUTTON }, // Month
+ { role: ROLE_TEXT_LEAF }, // "/""
+ { role: ROLE_SPINBUTTON }, // Day
+ { role: ROLE_TEXT_LEAF }, // "/"
+ { role: ROLE_SPINBUTTON }, // Year
+ { role: ROLE_TEXT_LEAF }, // " "
+ { role: ROLE_SPINBUTTON }, // Hours
+ { role: ROLE_TEXT_LEAF }, // ":"
+ { role: ROLE_SPINBUTTON }, // Minutes
+ { role: ROLE_TEXT_LEAF }, // " "
+ { role: ROLE_ENTRY }, // "AM" or "PM"
+ { role: ROLE_PUSHBUTTON }, // Calendar
+ ],
+ };
+ testElm("input_datetime_local", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:ins contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_CONTENT_INSERTION },
+ ],
+ };
+ testElm("ins_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:kbd contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ textAttrs: {
+ 0: { },
+ 6: { "font-family": kMonospaceFontFamily },
+ },
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_TEXT_LEAF }, // HTML:kbd text
+ ],
+ };
+ testElm("kbd_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:label
+
+ obj = {
+ role: ROLE_LABEL,
+ todo_relations: {
+ RELATION_LABEL_FOR: "label_input",
+ },
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ {
+ role: ROLE_ENTRY,
+ relations: {
+ RELATION_LABELLED_BY: "label",
+ },
+ },
+ ],
+ };
+ testElm("label", obj);
+
+ obj = {
+ role: ROLE_LABEL,
+ relations: {
+ RELATION_LABEL_FOR: "label_for_input",
+ },
+ };
+ testElm("label_for", obj);
+
+ obj = {
+ role: ROLE_ENTRY,
+ relations: {
+ RELATION_LABELLED_BY: "label_for",
+ },
+ };
+ testElm("label_for_input", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:ul, HTML:ol, HTML:li
+
+ obj = { // ul or ol
+ role: ROLE_LIST,
+ states: STATE_READONLY,
+ children: [
+ { // li
+ role: ROLE_LISTITEM,
+ states: STATE_READONLY,
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ },
+ ],
+ };
+ testElm("ul", obj);
+ testElm("ol", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:link
+
+ ok(!isAccessible("link"), "link element is not accessible");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:main
+
+ obj = {
+ role: ROLE_LANDMARK,
+ attributes: { "xml-roles": "main" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("main", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:map
+
+ ok(!isAccessible("map_imagemap"),
+ "map element is not accessible if used as an image map");
+
+ obj = {
+ role: ROLE_TEXT_CONTAINER,
+ };
+ testElm("map", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:mark contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_MARK, // HTML:mark text
+ attributes: { "xml-roles": "mark" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ textAttrs: {
+ 0: { },
+ }
+ }
+ ],
+ };
+ testElm("mark_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:math
+
+ obj = {
+ role: ROLE_MATHML_MATH,
+ };
+ testElm("math", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:menu
+
+ obj = {
+ role: ROLE_LIST, // menu
+ children: [
+ { role: ROLE_LISTITEM,
+ children: [ // home
+ { role: ROLE_LISTITEM_MARKER },
+ { role: ROLE_TEXT_LEAF }
+ ]
+ },
+ {
+ role: ROLE_LISTITEM,
+ children: [
+ { role: ROLE_LISTITEM_MARKER },
+ { role: ROLE_TEXT_LEAF }, // about
+ {
+ role: ROLE_LIST, // menu
+ children: [
+ { role: ROLE_LISTITEM,
+ children: [
+ { role: ROLE_LISTITEM_MARKER },
+ { role: ROLE_TEXT_LEAF } // our story
+ ]
+ },
+ ]
+ },
+ ]
+ },
+ ]
+ };
+
+ testElm("menu", obj);
+ obj = {
+ role: ROLE_LIST,
+ children: [
+ {
+ role: ROLE_LISTITEM,
+ children: [
+ { role: ROLE_LISTITEM_MARKER },
+ {
+ role: ROLE_PUSHBUTTON,
+ children: [
+ { role: ROLE_TEXT_LEAF }
+ ]
+ },
+ {
+ role: ROLE_LIST,
+ children: [
+ {
+ role: ROLE_LISTITEM,
+ children: [
+ { role: ROLE_LISTITEM_MARKER },
+ {
+ role: ROLE_PUSHBUTTON,
+ children: [
+ { role: ROLE_TEXT_LEAF }
+ ]
+ }
+ ]
+ },
+ ]
+ }
+ ]
+ }
+ ]
+ };
+ testElm("menu1", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:meter
+ obj = {
+ role: ROLE_METER
+ };
+ testElm("meter", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:nav
+
+ obj = {
+ role: ROLE_LANDMARK,
+ attributes: { "xml-roles": "navigation" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("nav", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:object and HTML:param
+
+ obj = {
+ INTERNAL_FRAME: [ {
+ DOCUMENT: [ {
+ role: ROLE_GRAPHIC,
+ interfaces: [ nsIAccessibleImage ],
+ } ],
+ } ],
+ };
+ testElm("object_png", obj);
+
+ obj = {
+ INTERNAL_FRAME: [ {
+ DOCUMENT: [ {
+ role: ROLE_PARAGRAPH,
+ } ],
+ } ],
+ };
+ testElm("object_html", obj);
+
+ obj = {
+ INTERNAL_FRAME: [ {
+ DOCUMENT: [ {
+ } ],
+ } ],
+ };
+ testElm("object_pdf", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:select, HTML:optgroup and HTML:option
+
+ obj = { // HMTL:select@size > 1
+ role: ROLE_LISTBOX,
+ states: STATE_FOCUSABLE,
+ absentStates: STATE_MULTISELECTABLE,
+ interfaces: [ nsIAccessibleSelectable ],
+ children: [
+ { GROUPING: [ // HTML:optgroup
+ { role: ROLE_STATICTEXT },
+ { role: ROLE_STATICTEXT },
+ { role: ROLE_OPTION }, // HTML:option
+ { role: ROLE_OPTION },
+ ] },
+ {
+ role: ROLE_OPTION,
+ states: STATE_FOCUSABLE,
+ actions: "select",
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ },
+ ],
+ };
+ testElm("select_listbox", obj);
+
+ obj = { // HTML:select@multiple
+ role: ROLE_LISTBOX,
+ states: STATE_FOCUSABLE | STATE_MULTISELECTABLE,
+ children: [
+ { role: ROLE_OPTION },
+ { role: ROLE_OPTION },
+ { role: ROLE_OPTION },
+ ],
+ };
+ testElm("select_listbox_multiselectable", obj);
+
+ obj = { // HTML:select
+ role: ROLE_COMBOBOX,
+ states: STATE_FOCUSABLE,
+ children: [
+ {
+ role: ROLE_COMBOBOX_LIST,
+ children: [
+ { role: ROLE_COMBOBOX_OPTION },
+ { role: ROLE_COMBOBOX_OPTION },
+ { role: ROLE_COMBOBOX_OPTION },
+ ],
+ },
+ ],
+ };
+ testElm("select_combobox", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:output
+
+ obj = {
+ role: ROLE_STATUSBAR,
+ attributes: { "live": "polite" },
+ todo_relations: {
+ RELATION_CONTROLLED_BY: "output_input",
+ },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("output", obj);
+
+ obj = {
+ role: ROLE_ENTRY,
+ relations: {
+ RELATION_CONTROLLER_FOR: "output",
+ },
+ };
+ testElm("output_input", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:pre
+
+ obj = {
+ role: ROLE_TEXT_CONTAINER,
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("pre", obj);
+
+ // /////////////////////////////////////////////////////////////////////////
+ // HTML:progress
+
+ obj = {
+ role: ROLE_PROGRESSBAR,
+ absentStates: STATE_MIXED,
+ interfaces: [ nsIAccessibleValue ],
+ };
+ testElm("progress", obj);
+
+ obj = {
+ role: ROLE_PROGRESSBAR,
+ states: STATE_MIXED,
+ };
+ testElm("progress_indeterminate", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:q
+
+ obj = {
+ role: ROLE_TEXT,
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ children: [
+ { role: ROLE_STATICTEXT }, // left quote
+ { role: ROLE_TEXT_LEAF }, // quoted text
+ { role: ROLE_STATICTEXT }, // right quote
+ ],
+ };
+ testElm("q", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:ruby
+
+ todo(isAccessible("ruby"), "ruby element is not accessible");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:s contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_CONTENT_DELETION },
+ ],
+ };
+ testElm("s_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:samp contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_TEXT_LEAF }, // HTML:samp text
+ ],
+ };
+ testElm("samp_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:search
+
+ obj = {
+ role: ROLE_LANDMARK,
+ attributes: { "xml-roles": "search" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("search", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:section without an accessible name
+
+ obj = {
+ role: ROLE_SECTION,
+ absentAttributes: { "xml-roles": "region" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("section", obj);
+
+ // HTML:section with an accessible name
+
+ obj = {
+ role: ROLE_REGION,
+ attributes: { "xml-roles": "region" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("named_section", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:small contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ textAttrs: {
+ 0: { },
+ 6: { "font-size": "10pt" },
+ },
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_TEXT_LEAF }, // HTML:small text
+ ],
+ };
+ testElm("small_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:source
+
+ ok(!isAccessible("source"), "source element is not accessible");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:span
+
+ ok(!isAccessible("span"), "span element is not accessible");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // html:span with a title attribute, which should make it accessible.
+ obj = {
+ role: ROLE_TEXT,
+ };
+ testElm("span_explicit", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:strong contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ {
+ role: ROLE_STRONG, // HTML:strong text
+ children: [
+ { role: ROLE_TEXT_LEAF, },
+ ],
+ },
+ ],
+ };
+ testElm("strong_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:sub
+ obj = {
+ role: ROLE_SUBSCRIPT
+ };
+ testElm("sub", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:sup
+ obj = {
+ role: ROLE_SUPERSCRIPT
+ };
+ testElm("sup", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:sub contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ {
+ role: ROLE_SUBSCRIPT, // HTML:sub
+ children: [
+ { role: ROLE_TEXT_LEAF } // HTML:sub text
+ ]
+ }
+ ],
+ };
+ testElm("sub_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:sup contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ {
+ role: ROLE_SUPERSCRIPT, // HTML:sup
+ children: [
+ { role: ROLE_TEXT_LEAF } // HTML:sup text
+ ]
+ }
+ ],
+ };
+ testElm("sup_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:svg
+
+ obj = {
+ todo_role: ROLE_GRAPHIC,
+ };
+ testElm("svg", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:textarea
+
+ obj = {
+ role: ROLE_ENTRY,
+ extraStates: EXT_STATE_MULTI_LINE | EXT_STATE_EDITABLE,
+ actions: "activate",
+ interfaces: [ nsIAccessibleText, nsIAccessibleEditableText ],
+ };
+ testElm("textarea", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:time
+
+ obj = {
+ role: ROLE_TIME,
+ attributes: { "xml-roles": "time", "datetime": "2001-05-15 19:00" },
+ interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ],
+ };
+ testElm("time", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:u contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ textAttrs: {
+ 0: { },
+ 6: { "text-underline-style": "solid" },
+ },
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_TEXT_LEAF }, // HTML:u text
+ ],
+ };
+ testElm("u_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML:var contained by paragraph
+
+ obj = {
+ role: ROLE_PARAGRAPH,
+ children: [
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_TEXT_LEAF }, // HTML:var text
+ { role: ROLE_TEXT_LEAF }, // plain text
+ { role: ROLE_TEXT_LEAF }, // HTML:var text
+ ],
+ };
+ testElm("var_container", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ obj = { // HTML:video
+ role: ROLE_GROUPING,
+ };
+ testElm("video", obj);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Implement figure and figcaption accessibility"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=658272">
+ Mozilla Bug 658272
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <a id="a_href" href="www.mozilla.com">mozilla site</a>
+ <a id="a_nohref">anchor</a>
+ <table>
+ <tr>
+ <td id="td_abbr"><abbr title="World Wide Web">WWW</abbr></td>
+ </tr>
+ </table>
+ <address id="address">
+ Mozilla Foundation<br>
+ 1981 Landings Drive<br>
+ Building K<br>
+ Mountain View, CA 94043-0801<br>
+ USA
+ </address>
+
+ <map name="atoz_map">
+ <area id="area_href"
+ href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b"
+ coords="17,0,30,14" alt="b" shape="rect">
+ <area id="area_nohref"
+ coords="0,0,13,14" alt="a" shape="rect">
+ </map>
+ <img id="imgmap" width="447" height="15"
+ usemap="#atoz_map"
+ src="../letters.gif">
+
+ <article id="article">A document</article>
+ <audio id="audio" controls="true">
+ <source id="source" src="../bug461281.ogg" type="video/ogg">
+ </audio>
+
+ <aside id="aside">
+ <p>Some content related to an &lt;article&gt;</p>
+ </aside>
+
+ <p id="b_container">normal<b>bold</b></p>
+ <p id="bdi_container">User <bdi>إيان</bdi>: 90 points</p>
+ <p id="bdo_container"><bdo dir="rtl">This text will go right to left.</bdo></p>
+
+ <blockquote id="blockquote" cite="http://developer.mozilla.org">
+ <p>This is a quotation taken from the Mozilla Developer Center.</p>
+ </blockquote>
+
+ <!-- two BRs, both will be present -->
+ <p id="br_container"><br><br></p>
+
+ <button id="button">button</button>
+ <form>
+ <button id="button_default" type="submit">button</button>
+ </form>
+
+ <canvas id="canvas"></canvas>
+
+ <table id="table">
+ <caption id="caption">caption</caption>
+ <thead>
+ <tr>
+ <th>col1</th><th>col2</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th>col1</th><td>cell2</td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr>
+ <td>cell5</td><td>cell6</td>
+ </tr>
+ </tfoot>
+ </table>
+
+ <p id="cite_container">normal<cite>cite</cite></p>
+ <p id="code_container">normal<code>code</code></p>
+
+ <table id="colNcolgroup_table">
+ <colgroup>
+ <col>
+ <col span="2">
+ </colgroup>
+ <tr>
+ <td>Lime</td>
+ <td>Lemon</td>
+ <td>Orange</td>
+ </tr>
+ </table>
+
+ <p id="data_container"><data value="8">Eight</data></p>
+
+ <datalist id="datalist">
+ <summary id="summary">details</summary>
+ <option>Paris</option>
+ <option>San Francisco</option>
+ </datalist>
+ <input id="autocomplete_datalist" list="datalist">
+
+ <dl id="dl">
+ <dt>item1</dt><dd>description</dd>
+ </dl>
+
+ <p id="del_container">normal<del>Removed</del></p>
+
+ <details id="details" open="open">
+ <summary>Information</summary>
+ <p>If your browser supports this element, it should allow you to expand and collapse these details.</p>
+ </details>
+
+ <details id="details_closed">
+ <summary>Information</summary>
+ <p>If your browser supports this element, it should allow you to expand and collapse these details.</p>
+ </details>
+
+ <p id="dfn_container"><dfn id="def-internet">The Internet</dfn> is a global
+ system of interconnected networks that use the Internet Protocol Suite (TCP/IP)
+ to serve billions of users worldwide.</p>
+
+ <dialog id="dialog" open="true">This is a dialog</dialog>
+
+ <div id="div">div</div>
+
+ <p id="em_container">normal<em>emphasis</em></p>
+
+ <embed id="embed_png" type="image/png" src="../moz.png"
+ width="300" height="300">
+ </embed>
+ <embed id="embed_html" type="text/html" src="../longdesc_src.html"
+ width="300" height="300">
+ </embed>
+ <embed id="embed_pdf" type="application/pdf" src="../dummy.pdf"
+ width="300" height="300">
+ </embed>
+
+ <fieldset id="fieldset">
+ <legend id="legend">legend</legend>
+ <input />
+ </fieldset>
+
+ <!-- Depending on whether or not the image is cached, layout may be able to
+ optimize away spaces between the figure, img and figcaption tags. As
+ such, we should keep everything on one line to get consistent results.
+ -->
+ <figure id="figure"><img src="../moz.png" alt="An awesome picture"><figcaption id="figcaption">Caption for the awesome picture</figcaption></figure>
+
+ <footer id="footer">Some copyright info</footer>
+ <article>
+ <footer id="footer_in_article">Some copyright info</footer>
+ </article>
+ <aside>
+ <footer id="footer_in_aside">Some copyright info</footer>
+ </aside>
+ <main>
+ <footer id="footer_in_main">Some copyright info</footer>
+ </main>
+ <nav>
+ <footer id="footer_in_nav">Some copyright info</footer>
+ </nav>
+ <section>
+ <footer id="footer_in_section">Some copyright info</footer>
+ </section>
+ <blockquote>
+ <footer id="footer_in_blockquote">Some copyright info</footer>
+ </blockquote>
+ <details open="true">
+ <footer id="footer_in_details">Some copyright info</footer>
+ </details>
+ <dialog open="true">
+ <footer id="footer_in_dialog">Some copyright info</footer>
+ </dialog>
+ <fieldset>
+ <footer id="footer_in_fieldset">Some copyright info</footer>
+ </fieldset>
+ <figure>
+ <footer id="footer_in_figure">Some copyright info</footer>
+ </figure>
+ <table><tr><td>
+ <footer id="footer_in_td">Some copyright info</footer>
+ </td></tr></table>
+
+ <form id="form"></form>
+ <form id="named_form" aria-label="New form"></form>
+
+ <iframe id="frameset_container"
+ src="data:text/html,<html><frameset><frame src='data:text/html,hi'></frame></frameset></html>">
+ </iframe>
+
+ <h1 id="h1">heading1</h1>
+ <h2 id="h2">heading2</h2>
+ <h3 id="h3">heading3</h3>
+ <h4 id="h4">heading4</h4>
+ <h5 id="h5">heading5</h5>
+ <h6 id="h6">heading6</h6>
+
+ <header id="header">A logo</header>
+ <article>
+ <header id="header_in_article">Not logo</header>
+ <h1 id="h1_in_article">heading1</h1>
+ <h2 id="h2_in_article">heading2</h2>
+ <h3 id="h3_in_article">heading3</h3>
+ <h4 id="h4_in_article">heading4</h4>
+ <h5 id="h5_in_article">heading5</h5>
+ <h6 id="h6_in_article">heading6</h6>
+ <hgroup>
+ <h1 id="h1_in_article_in_hgroup">heading1</h1>
+ <h2 id="h2_in_article_in_hgroup">heading2</h2>
+ <h3 id="h3_in_article_in_hgroup">heading3</h3>
+ <h4 id="h4_in_article_in_hgroup">heading4</h4>
+ <h5 id="h5_in_article_in_hgroup">heading5</h5>
+ <h6 id="h6_in_article_in_hgroup">heading6</h6>
+ </hgroup>
+ </article>
+ <aside>
+ <header id="header_in_aside">Not logo</header>
+ <h1 id="h1_in_aside">heading1</h1>
+ <h2 id="h2_in_aside">heading2</h2>
+ <h3 id="h3_in_aside">heading3</h3>
+ <h4 id="h4_in_aside">heading4</h4>
+ <h5 id="h5_in_aside">heading5</h5>
+ <h6 id="h6_in_aside">heading6</h6>
+ <hgroup>
+ <h1 id="h1_in_aside_in_hgroup">heading1</h1>
+ <h2 id="h2_in_aside_in_hgroup">heading2</h2>
+ <h3 id="h3_in_aside_in_hgroup">heading3</h3>
+ <h4 id="h4_in_aside_in_hgroup">heading4</h4>
+ <h5 id="h5_in_aside_in_hgroup">heading5</h5>
+ <h6 id="h6_in_aside_in_hgroup">heading6</h6>
+ </hgroup>
+ </aside>
+ <main>
+ <header id="header_in_main">Not logo</header>
+ </main>
+ <nav>
+ <header id="header_in_nav">Not logo</header>
+ <h1 id="h1_in_nav">heading1</h1>
+ <h2 id="h2_in_nav">heading2</h2>
+ <h3 id="h3_in_nav">heading3</h3>
+ <h4 id="h4_in_nav">heading4</h4>
+ <h5 id="h5_in_nav">heading5</h5>
+ <h6 id="h6_in_nav">heading6</h6>
+ <hgroup>
+ <h1 id="h1_in_nav_in_hgroup">heading1</h1>
+ <h2 id="h2_in_nav_in_hgroup">heading2</h2>
+ <h3 id="h3_in_nav_in_hgroup">heading3</h3>
+ <h4 id="h4_in_nav_in_hgroup">heading4</h4>
+ <h5 id="h5_in_nav_in_hgroup">heading5</h5>
+ <h6 id="h6_in_nav_in_hgroup">heading6</h6>
+ </hgroup>
+ </nav>
+ <section>
+ <header id="header_in_section">Not logo</header>
+ <h1 id="h1_in_section">heading1</h1>
+ <h2 id="h2_in_section">heading2</h2>
+ <h3 id="h3_in_section">heading3</h3>
+ <h4 id="h4_in_section">heading4</h4>
+ <h5 id="h5_in_section">heading5</h5>
+ <h6 id="h6_in_section">heading6</h6>
+ <hgroup>
+ <h1 id="h1_in_section_in_hgroup">heading1</h1>
+ <h2 id="h2_in_section_in_hgroup">heading2</h2>
+ <h3 id="h3_in_section_in_hgroup">heading3</h3>
+ <h4 id="h4_in_section_in_hgroup">heading4</h4>
+ <h5 id="h5_in_section_in_hgroup">heading5</h5>
+ <h6 id="h6_in_section_in_hgroup">heading6</h6>
+ </hgroup>
+ </section>
+ <blockquote>
+ <header id="header_in_blockquote">Not logo</header>
+ </blockquote>
+ <details open="true">
+ <header id="header_in_details">Not logo</header>
+ </details>
+ <dialog open="true">
+ <header id="header_in_dialog">Not logo</header>
+ </dialog>
+ <fieldset>
+ <header id="header_in_fieldset">Not logo</header>
+ </fieldset>
+ <figure>
+ <header id="header_in_figure">Not logo</header>
+ </figure>
+ <table><tr><td>
+ <header id="header_in_td">Not logo</header>
+ </td></tr></table>
+
+ <hr id="hr">
+ <p id="i_container">normal<i>italic</i></p>
+ <img id="img" src="../moz.png">
+
+ <input id="input_button" type="button" value="Button">
+ <input id="input_checkbox" type="checkbox">
+ <input id="input_checkbox_true" type="checkbox" checked>
+ <input id="input_file" type="file">
+ <input id="input_image" type="image">
+ <input id="input_image_display" type="image" style="display: block">
+ <form>
+ <input id="input_image_default" type="image">
+ </form>
+ <input id="input_submit" type="submit">
+ <form>
+ <input id="input_submit_default" type="submit">
+ </form>
+ <input id="input_number" type="number" value="44">
+ <input id="input_text" type="text" value="hi">
+ <form>
+ <label for="input_placeholder_same">Your name</label>
+ <input id="input_placeholder_same" placeholder="Your name"/>
+ <label for="input_placeholder_different">First name:</label>
+ <input id="input_placeholder_different" placeholder="Enter your first name"/>
+ <input id="input_placeholder_only" placeholder="Date of birth"/>
+ </form>
+ <input id="input_search" type="search" value="cats">
+ <input id="input_email" type="email" value="me@mozilla.com">
+ <input id="input_tel" type="tel" value="111.111.1111">
+ <input id="input_url" type="url" value="www.mozilla.com">
+ <input id="input_password" type="password" value="44">
+ <input id="input_password_readonly" type="password" value="44" readonly>
+ <input id="input_radio" type="radio">
+ <input id="input_radio_true" type="radio" checked>
+ <input id="input_range" type="range">
+ <form>
+ <input id="input_reset" type="reset">
+ </form>
+ <label>time label
+ <input id="input_time" type="time" value="23:23">
+ </label>
+ <label>date label
+ <input id="input_date" type="date" value="2017-11-10">
+ </label>
+ <label>datetime-local label
+ <input id="input_datetime_local" type="datetime-local" value="2017-11-10T23:23">
+ </label>
+
+ <p id="ins_container">normal<ins>Inserted</ins></p>
+ <p id="kbd_container">normal<kbd>cmd</kbd></p>
+
+ <label id="label">label<input id="label_input"></label>
+ <label id="label_for" for="label_for_input">label</label>
+ <input id="label_for_input">
+
+ <ul id="ul">
+ <li>item1</li>
+ </ul>
+ <ol id="ol">
+ <li>item1</li>
+ </ol>
+
+ <main id="main">main</main>
+
+ <map id="map_imagemap" name="atoz_map">
+ <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b"
+ coords="17,0,30,14" alt="b" shape="rect">
+ <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a"
+ coords="0,0,13,14" alt="a" shape="rect">
+ </map>
+ <img id="imgmap" width="447" height="15"
+ usemap="#atoz_map"
+ src="../letters.gif">
+
+ <map id="map" title="Navigation Bar" name="mapgroup">
+ <p>
+ [<a href="#how">Bypass navigation bar</a>]
+ [<a href="home.html">Home</a>]
+ </p>
+ </map>
+
+ <p id="mark_container">normal<mark>highlighted</mark></p>
+
+ <math id="math">
+ <mrow>
+ <mrow>
+ <msup>
+ <mi>a</mi>
+ <mn>2</mn>
+ </msup>
+ <mo>+</mo>
+ <msup>
+ <mi>b</mi>
+ <mn>2</mn>
+ </msup>
+ </mrow>
+ <mo>=</mo>
+ <msup>
+ <mi>c</mi>
+ <mn>2</mn>
+ </msup>
+ </mrow>
+ </math>
+
+ <menu id="menu">
+ <li>Home</li>
+ <li>About
+ <menu>
+ <li>Our Story</li>
+ </menu>
+ </li>
+ </menu>
+
+ <menu id="menu1">
+ <li>
+ <button>File</button>
+ <menu>
+ <li>
+ <button type="button" onclick="new()">New...</button>
+ </li>
+ </menu>
+ </li>
+ </menu>
+
+ <meter id="meter" min="0" max="1000" low="300" high="700" value="200">200 Euro</meter>
+
+ <nav id="nav">
+ <ul>
+ <li><a href="#">Home</a></li>
+ <li><a href="#">About</a></li>
+ <li><a href="#">Contact</a></li>
+ </ul>
+ </nav>
+
+ <object id="object_png" type="image/png" data="../moz.png"
+ width="300" height="300">
+ </object>
+ <object id="object_html" type="text/html" data="../longdesc_src.html"
+ width="300" height="300">
+ </object>
+ <object id="object_pdf" type="application/pdf" data="../dummy.pdf"
+ width="300" height="300">
+ </object>
+
+ <select id="select_listbox" size="4">
+ <optgroup label="Colors">
+ <option>Red</option>
+ <option>Blue</option>
+ </optgroup>
+ <option>Animal</option>
+ </select>
+
+ <select id="select_listbox_multiselectable" multiple>
+ <option>Red</option>
+ <option>Blue</option>
+ <option>Green</option>
+ </select>
+
+ <select id="select_combobox">
+ <option>Red</option>
+ <option>Blue</option>
+ <option>Green</option>
+ </select>
+
+ <input id="output_input">
+ <output id="output" for="output_input"></output>
+
+ <pre id="pre">pre</pre>
+
+ <progress id="progress" min="0" value="21" max="42"></progress>
+ <progress id="progress_indeterminate"></progress>
+
+ <q id="q" cite="http://en.wikipedia.org/wiki/Kenny_McCormick#Cultural_impact">
+ Oh my God, they killed Kenny!
+ </q>
+
+ <ruby id="ruby">
+ 漢 <rp>(</rp><rt>Kan</rt><rp>)</rp>
+ 字 <rp>(</rp><rt>ji</rt><rp>)</rp>
+ </ruby>
+
+ <p id="s_container">normal<s>Removed</s></p>
+ <p id="samp_container">normal<samp>sample</samp></p>
+ <search id="search">search</search>
+ <section id="section">section</section>
+ <section id="named_section" aria-label="foo">named section</section>
+ <p id="small_container">normal<small>small</small></p>
+ <span id="span"></span>
+ <span id="span_explicit" title="explicit"></span>
+ <p id="strong_container">normal<strong>strong</strong></p>
+ <sub id="sub"></sub>
+ <sup id="sup"></sup>
+ <p id="sub_container">normal<sub>sub</sub></p>
+ <p id="sup_container">normal<sup>sup</sup></p>
+
+ <svg id="svg"></svg>
+ <textarea id="textarea"></textarea>
+
+ <p>The concert took place on <time id="time" datetime="2001-05-15 19:00">May 15</time></p>
+ <p id="u_container">normal<u>underline</u></p>
+ <p id="var_container">An equation: <var>x</var> = <var>y</var></p>
+
+ <video id="video" controls="true">
+ <source id="source" src="../bug461281.ogg" type="video/ogg">
+ </video>
+
+</video>
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML a11y spec tests</title>
+ <link id="link" rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../actions.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+ <script type="application/javascript"
+ src="../relations.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // math
+
+ let obj = {
+ role: ROLE_MATHML_MATH,
+ };
+ testElm("math", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mi
+
+ obj = {
+ role: ROLE_MATHML_IDENTIFIER,
+ };
+ testElm("mi", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mn
+
+ obj = {
+ role: ROLE_MATHML_NUMBER,
+ };
+ testElm("mn", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mo
+
+ obj = {
+ role: ROLE_MATHML_OPERATOR,
+ attributes: { accent: "true", largeop: "true" },
+ };
+ testElm("mo", obj);
+
+ obj = {
+ role: ROLE_MATHML_OPERATOR,
+ attributes: { fence: "true" },
+ };
+ testElm("mo_fence", obj);
+
+ obj = {
+ role: ROLE_MATHML_OPERATOR,
+ attributes: { separator: "true" },
+ };
+ testElm("mo_separator", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mtext
+
+ obj = {
+ role: ROLE_MATHML_TEXT,
+ };
+ testElm("mtext", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ms
+
+ obj = {
+ role: ROLE_MATHML_STRING_LITERAL,
+ };
+ testElm("ms", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mglyph
+
+ obj = {
+ role: ROLE_MATHML_GLYPH,
+ };
+ testElm("mglyph", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mrow
+
+ obj = {
+ role: ROLE_MATHML_ROW,
+ };
+ testElm("mrow", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mfrac
+
+ obj = {
+ role: ROLE_MATHML_FRACTION,
+ attributes: { bevelled: "true", linethickness: "thick" },
+ };
+ testElm("mfrac", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // msqrt
+
+ obj = {
+ role: ROLE_MATHML_SQUARE_ROOT,
+ };
+ testElm("msqrt", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mroot
+
+ obj = {
+ role: ROLE_MATHML_ROOT,
+ relations: {
+ RELATION_NODE_PARENT_OF: ["mroot_index", "mroot_base"],
+ },
+ children: [
+ {
+ role: ROLE_MATHML_IDENTIFIER,
+ relations: { RELATION_NODE_CHILD_OF: "mroot" },
+ },
+ {
+ role: ROLE_MATHML_NUMBER,
+ relations: { RELATION_NODE_CHILD_OF: "mroot" },
+ },
+ ],
+ };
+ testElm("mroot", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Deprecated mfenced element (treated as an mrow).
+
+ obj = {
+ role: ROLE_MATHML_ROW,
+ };
+ testElm("mfenced", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // menclose
+
+ obj = {
+ role: ROLE_MATHML_ENCLOSED,
+ attributes: { notation: "circle" },
+ };
+ testElm("menclose", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mstyle, mpadded, mphantom
+
+ obj = {
+ role: ROLE_MATHML_STYLE,
+ };
+ testElm("mstyle", obj);
+
+ ok(!isAccessible("mpadded"), "mpadded should not have accessible");
+ ok(!isAccessible("mphantom"), "mphantom should not have accessible");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // msub
+
+ obj = {
+ role: ROLE_MATHML_SUB,
+ };
+ testElm("msub", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // msup
+
+ obj = {
+ role: ROLE_MATHML_SUP,
+ };
+ testElm("msup", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // msubsup
+
+ obj = {
+ role: ROLE_MATHML_SUB_SUP,
+ };
+ testElm("msubsup", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // munder
+
+ obj = {
+ role: ROLE_MATHML_UNDER,
+ attributes: { accentunder: "true", align: "center" },
+ };
+ testElm("munder", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mover
+
+ obj = {
+ role: ROLE_MATHML_OVER,
+ attributes: { accent: "true", align: "center" },
+ };
+ testElm("mover", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // munderover
+
+ obj = {
+ role: ROLE_MATHML_UNDER_OVER,
+ attributes: { accent: "true", accentunder: "true", align: "center" },
+ };
+ testElm("munderover", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mmultiscripts
+
+ obj = {
+ role: ROLE_MATHML_MULTISCRIPTS,
+ };
+ testElm("mmultiscripts", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mtable
+
+ obj = {
+ role: ROLE_MATHML_TABLE,
+ attributes: { align: "center", columnlines: "solid", rowlines: "solid" },
+ };
+ testElm("mtable", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mlabeledtr
+
+ obj = {
+ role: ROLE_MATHML_LABELED_ROW,
+ };
+ testElm("mlabeledtr", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mtr
+
+ obj = {
+ role: ROLE_MATHML_TABLE_ROW,
+ };
+ testElm("mtr", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mtd
+
+ obj = {
+ role: ROLE_MATHML_CELL,
+ };
+ testElm("mtd", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // maction
+
+ obj = {
+ role: ROLE_MATHML_ACTION,
+ attributes: { actiontype: "toggle", selection: "1" },
+ };
+ testElm("maction", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // merror
+
+ obj = {
+ role: ROLE_MATHML_ERROR,
+ };
+ testElm("merror", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // semantics, annotation, annotation-xml
+ ok(!isAccessible("semantics"), "semantics should not have accessible");
+ ok(!isAccessible("annotation"), "annotation should not have accessible");
+ ok(!isAccessible("annotation-xml"), "annotation-xml should not have accessible");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mstack
+
+ obj = {
+ role: ROLE_MATHML_STACK,
+ attributes: { align: "center" },
+ };
+ testElm("mstack", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mlongdiv
+
+ obj = {
+ role: ROLE_MATHML_LONG_DIVISION,
+ attributes: { longdivstyle: "stackedrightright" },
+ };
+ testElm("mlongdiv", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // msgroup
+
+ obj = {
+ role: ROLE_MATHML_STACK_GROUP,
+ attributes: { position: "2", shift: "-1" },
+ };
+ testElm("msgroup", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // msrow
+
+ obj = {
+ role: ROLE_MATHML_STACK_ROW,
+ attributes: { position: "1" },
+ };
+ testElm("msrow", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mscarries
+
+ obj = {
+ role: ROLE_MATHML_STACK_CARRIES,
+ attributes: { location: "nw", position: "1" },
+ };
+ testElm("mscarries", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // mscarry
+
+ obj = {
+ role: ROLE_MATHML_STACK_CARRY,
+ attributes: { crossout: "updiagonalstrike" },
+ };
+ testElm("mscarry", obj);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // msline
+
+ obj = {
+ role: ROLE_MATHML_STACK_LINE,
+ attributes: { position: "1" },
+ };
+ testElm("msline", obj);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Implement figure and figcaption accessibility"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=658272">
+ Mozilla Bug 658272
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <math id="math">
+ <mrow id="mrow">
+ <mrow>
+ <msup id="msup">
+ <mi id="mi">a</mi>
+ <mn id="mn">2</mn>
+ </msup>
+ <mo id="mo" accent="true" largeop="true">+</mo>
+ <msqrt id="msqrt">
+ <mn>2</mn>
+ </msqrt>
+ </mrow>
+ <mo>=</mo>
+ <msub id="msub">
+ <mi>c</mi>
+ <mn>2</mn>
+ </msub>
+ </mrow>
+ <mspace id="mspace" width="1em"/>
+ <mtext id="mtext">Arbitrary text</mtext>
+ <mspace width="1em"/>
+ <ms id="ms">InterpretedStringLiteral</ms>
+ <mi>
+ <mglyph id="mglyph" src="../letters.gif" alt="letters"/>
+ </mi>
+ <mfrac id="mfrac" bevelled="true" linethickness="thick">
+ <mi>x</mi>
+ <mn>2</mn>
+ </mfrac>
+ <mroot id="mroot">
+ <mi id="mroot_base">x</mi>
+ <mn id="mroot_index">5</mn>
+ </mroot>
+ <mspace width="1em"/>
+ <mfenced id="mfenced" close="[" open="]" separators=".">
+ <mrow>
+ <mi>x</mi>
+ <mi>y</mi>
+ </mrow>
+ </mfenced>
+ <mrow>
+ <mo id="mo_fence" fence="true">[</mo>
+ <mrow>
+ X
+ <mo id="mo_separator" separator="true">,</mo>
+ Y
+ </mrow>
+ <mo fence="true"> closing-fence </mo>
+ </mrow>
+ <mspace width="1em"/>
+ <menclose id="menclose" notation="circle">
+ <mi>a</mi>
+ <mo>+</mo>
+ <mi>b</mi>
+ </menclose>
+ <mstyle id="mstyle" dir="rtl" mathcolor="blue">
+ <mpadded id="mpadded" height="100px" width="200px">
+ <mi>x</mi>
+ <mphantom id="mphantom">
+ <mo>+</mo>
+ <mi>y</mi>
+ </mphantom>
+ </mpadded>
+ </mstyle>
+
+ <msubsup id="msubsup">
+ <mi>b</mi>
+ <mn>1</mn>
+ <mn>2</mn>
+ </msubsup>
+ <munder id="munder" accentunder="true" align="center">
+ <mrow>
+ <mi> x </mi>
+ <mo> + </mo>
+ <mi> y </mi>
+ <mo> + </mo>
+ <mi> z </mi>
+ </mrow>
+ <mo> &#x23DF;<!--BOTTOM CURLY BRACKET--> </mo>
+ </munder>
+ <mspace width="1em"/>
+ <mover id="mover" accent="true" align="center">
+ <mi> x </mi>
+ <mo> &#x5E;<!--CIRCUMFLEX ACCENT--> </mo>
+ </mover>
+ <munderover id="munderover" accentunder="true" accent="true" align="center">
+ <mo> &#x222B;<!--INTEGRAL--> </mo>
+ <mn> 0 </mn>
+ <mi> &#x221E;<!--INFINITY--> </mi>
+ </munderover>
+ <mmultiscripts id="mmultiscripts">
+ <mi> R </mi>
+ <mi> i </mi>
+ <none/>
+ <none/>
+ <mi> j </mi>
+ <mi> k </mi>
+ <none/>
+ <mi> l </mi>
+ <none/>
+ </mmultiscripts>
+
+ <mtable id="mtable" align="center" columnlines="solid" rowlines="solid">
+ <mlabeledtr id="mlabeledtr">
+ <mtd>
+ <mtext> (2.1) </mtext>
+ </mtd>
+ <mtd>
+ <mrow>
+ <mi>E</mi>
+ <mo>=</mo>
+ <mrow>
+ <mi>m</mi>
+ <mo>&#x2062;<!--INVISIBLE TIMES--></mo>
+ <msup>
+ <mi>c</mi>
+ <mn>2</mn>
+ </msup>
+ </mrow>
+ </mrow>
+ </mtd>
+ </mlabeledtr>
+ </mtable>
+ <mrow>
+ <mo> ( </mo>
+ <mtable>
+ <mtr id="mtr">
+ <mtd id="mtd"> <mn>1</mn> </mtd>
+ <mtd> <mn>0</mn> </mtd>
+ <mtd> <mn>0</mn> </mtd>
+ </mtr>
+ <mtr>
+ <mtd> <mn>0</mn> </mtd>
+ <mtd> <mn>1</mn> </mtd>
+ <mtd> <mn>0</mn> </mtd>
+ </mtr>
+ <mtr>
+ <mtd> <mn>0</mn> </mtd>
+ <mtd> <mn>0</mn> </mtd>
+ <mtd> <mn>1</mn> </mtd>
+ </mtr>
+ </mtable>
+ <mo> ) </mo>
+ </mrow>
+
+ <maction id="maction" actiontype="toggle" selection="1">
+ <mfrac>
+ <mn>6</mn>
+ <mn>8</mn>
+ </mfrac>
+ <mfrac>
+ <mrow>
+ <mn>3</mn>
+ <mo>⋅</mo>
+ <mn>2</mn>
+ </mrow>
+ <mrow>
+ <mn>4</mn>
+ <mo>⋅</mo>
+ <mn>2</mn>
+ </mrow>
+ </mfrac>
+ <mfrac>
+ <mn>3</mn>
+ <mn>4</mn>
+ </mfrac>
+ </maction>
+
+ <merror id="merror">
+ <mrow>
+ <mtext>Division by zero: </mtext>
+ <mfrac>
+ <mn>1</mn>
+ <mn>0</mn>
+ </mfrac>
+ </mrow>
+ </merror>
+
+ <semantics id="semantics">
+ <!-- Presentation MathML -->
+ <mrow>
+ <msup>
+ <mi>x</mi>
+ <mn>2</mn>
+ </msup>
+ <mo>+</mo>
+ <mi>y</mi>
+ </mrow>
+ <!-- Content MathML -->
+ <annotation-xml id="annotation-xml" encoding="MathML-Content">
+ <apply>
+ <plus/>
+ <apply>
+ <power/>
+ <ci>x</ci>
+ <cn type="integer">2</cn>
+ </apply>
+ <ci>y</ci>
+ </apply>
+ </annotation-xml>
+ <!-- annotate TeX -->
+ <annotation id="annotation" encoding="application/x-tex">
+ x^{2} + y
+ </annotation>
+ </semantics>
+
+ <mstack id="mstack" align="center">
+ <mscarries id="mscarries" location="nw" position="1">
+ <none/>
+ <mscarry id="mscarry" crossout="updiagonalstrike">
+ <mn>1</mn>
+ </mscarry>
+ <mscarry location="w">
+ <mn>1</mn>
+ </mscarry>
+ </mscarries>
+ <mn>523</mn>
+ <msrow id="msrow" position="1">
+ <mo>-</mo>
+ <none/>
+ <mn>15</mn>
+ </msrow>
+ <msline id="msline" position="1"/>
+ <mn>508</mn>
+ </mstack>
+ <mspace width="1em"/>
+ <mlongdiv id="mlongdiv" longdivstyle="stackedrightright">
+ <mn>5</mn>
+ <mn>1</mn>
+ <mn>5</mn>
+ </mlongdiv>
+
+ <mstack>
+ <msgroup id="msgroup" position="2" shift="-1">
+ <mn>123</mn>
+ <msrow><mo>&#xD7;<!--MULTIPLICATION SIGN--></mo><mn>321</mn></msrow>
+ </msgroup>
+ <msline/>
+ <msgroup shift="1">
+ <mn>123</mn>
+ <mn>246</mn>
+ <mn>369</mn>
+ </msgroup>
+ <msline/>
+ </mstack>
+ </math>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML5 figure/figcaption tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+ <script type="application/javascript"
+ src="../relations.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ testRole("figure", ROLE_FIGURE);
+ testRole("figcaption", ROLE_CAPTION);
+
+ todo(false, "figure name gets extra whitespace in the end!");
+ testName("figure", "figure caption");
+ testName("figcaption", null);
+
+ testRelation("figure", RELATION_LABELLED_BY, "figcaption");
+ testRelation("figcaption", RELATION_LABEL_FOR, "figure");
+
+ testAttrs("figure", {"xml-roles": "figure"}, true);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Implement figure and figcaption accessibility"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=658272">
+ Mozilla Bug 658272
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <figure id="figure">
+ <figcaption id="figcaption">figure caption</figcaption>
+ </figure>
+
+</body>
+</html>
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 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="XUL listbox element test.">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function doTest()
+ {
+ var id = "";
+ var acc = null;
+
+ //////////////////////////////////////////////////////////////////////////
+ // Simple listbox. There is no nsIAccessibleTable interface.
+
+ id = "listbox1";
+ acc = getAccessible(id);
+
+ // query nsIAccessibleTable
+ try {
+ acc.QueryInterface(nsIAccessibleTable);
+ ok(false,
+ id + " shouldn't implement nsIAccessibleTable interface.");
+ } catch(e) {
+ ok(true, id + " doesn't implement nsIAccessibleTable interface.");
+ }
+
+ // role
+ testRole(id, ROLE_LISTBOX);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=418371"
+ title="implement the rest of methods of nsIAccessibleTable on xul:listbox">
+ Mozilla Bug 418371
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <label control="listbox1" value="listbox: "/>
+ <richlistbox id="listbox1">
+ <richlistitem id="item1"><label value="item1"/></richlistitem>
+ <richlistitem id="item1"><label value="item2"/></richlistitem>
+ </richlistbox>
+ </vbox>
+ </hbox>
+
+</window>
+
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 @@
+<html>
+
+<head>
+ <title>application accessible name</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var accessible = getApplicationAccessible();
+ if (!accessible) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var brandBundle =
+ Services.strings.createBundle("chrome://branding/locale/brand.properties");
+
+ // nsIAccessible::name
+ var applicationName = "";
+ if (LINUX || SOLARIS) {
+ applicationName = Services.appinfo.name;
+ } else {
+ try {
+ applicationName = brandBundle.GetStringFromName("brandShortName");
+ } catch (e) {
+ }
+
+ if (applicationName == "")
+ applicationName = "Gecko based application";
+ }
+ is(accessible.name, applicationName, "wrong application accessible name");
+
+ // nsIAccessibleApplication
+ is(accessible.appName, Services.appinfo.name, "Wrong application name");
+ is(accessible.appVersion, Services.appinfo.version, "Wrong application version");
+ is(accessible.platformName, "Gecko", "Wrong platform name");
+ is(accessible.platformVersion, Services.appinfo.platformVersion,
+ "Wrong platform version");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+ </head>
+ <body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=456121"
+ title="nsApplicationAccessible::GetName does not return a default value when brand.properties does not exist">
+ Mozilla Bug 454211
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ </body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>ShadowRoot tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+</head>
+<body>
+
+ <a target="_blank"
+ title="Ensure accessible objects are created for shadow root"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1026125">
+ Mozilla Bug 1026125
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ window.onload = () => {
+ var iframe = document.createElement("iframe");
+ iframe.src = "test_shadowroot_subframe.html";
+ document.body.appendChild(iframe);
+ };
+
+ </script>
+
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>ShadowRoot tests</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript" src="../common.js"></script>
+ <script type="application/javascript" src="../role.js"></script>
+
+ <script type="application/javascript">
+ let SimpleTest = window.parent.SimpleTest;
+ let ok = window.parent.ok;
+ let is = window.parent.is;
+
+ function doTest() {
+ testElm("component", {
+ role: ROLE_GROUPING,
+ children: [
+ {
+ role: ROLE_PUSHBUTTON,
+ },
+ {
+ role: ROLE_LINK,
+ },
+ ],
+ });
+
+ // Shadow root boundary between table and row
+ testElm("table", {
+ role: ROLE_TABLE,
+ children: [
+ {
+ role: ROLE_ROW,
+ },
+ ],
+ });
+
+ SimpleTest.finish();
+ }
+
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+<body>
+ <div role="group" id="component"></div>
+ <div id="table" role="table" style="display: table;"></div>
+
+ <script>
+ var component = document.getElementById("component");
+ var shadow = component.attachShadow({mode: "open"});
+
+ var button = document.createElement("button");
+ button.append("Hello");
+
+ var a = document.createElement("a");
+ a.setAttribute("href", "#");
+ a.append(" World");
+
+ shadow.appendChild(button);
+ shadow.appendChild(a);
+
+ var table = document.getElementById("table");
+ shadow = table.attachShadow({mode: "open"});
+ shadow.innerHTML = "<div style='display: table-row' role='row'>" +
+ "<div style='display: table-cell' role='cell'>hi</div>" +
+ "</div>";
+ </script>
+</body>
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.toml b/accessible/tests/mochitest/events/a11y.toml
new file mode 100644
index 0000000000..501b59f7b7
--- /dev/null
+++ b/accessible/tests/mochitest/events/a11y.toml
@@ -0,0 +1,128 @@
+[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_attrchange.html"]
+
+["test_attrs.html"]
+
+["test_bug1322593-2.html"]
+
+["test_bug1322593.html"]
+
+["test_caretmove.html"]
+
+["test_coalescence.html"]
+
+["test_contextmenu.html"]
+
+["test_descrchange.html"]
+
+["test_dragndrop.html"]
+
+["test_flush.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_focusable_statechange.html"]
+
+["test_fromUserInput.html"]
+
+["test_label.xhtml"]
+
+["test_menu.xhtml"]
+
+["test_mutation.html"]
+
+["test_namechange.html"]
+
+["test_namechange.xhtml"]
+
+["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.toml b/accessible/tests/mochitest/events/docload/a11y.toml
new file mode 100644
index 0000000000..dc71817725
--- /dev/null
+++ b/accessible/tests/mochitest/events/docload/a11y.toml
@@ -0,0 +1,21 @@
+[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
+ "display == 'wayland'", # bug 1850412
+]
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 @@
+<html>
+<head>
+ <title>Accessible events testing for document</title>
+ <script>
+ const STATE_BUSY = Ci.nsIAccessibleStates.STATE_BUSY;
+
+ var gService = null;
+ function waitForDocLoad() {
+ if (!gService) {
+ gService = Cc["@mozilla.org/accessibilityService;1"].
+ getService(Ci.nsIAccessibilityService);
+ }
+
+ var accDoc = gService.getAccessibleFor(document);
+
+ var state = {};
+ accDoc.getState(state, {});
+ if (state.value & STATE_BUSY) {
+ window.setTimeout(waitForDocLoad, 0);
+ return;
+ }
+
+ hideIFrame();
+ }
+
+ function hideIFrame() {
+ var iframe = document.getElementById("iframe");
+ gService.getAccessibleFor(iframe.contentDocument);
+ iframe.style.display = "none";
+ }
+ </script>
+</head>
+
+<body onload="waitForDocLoad();">
+ <iframe id="iframe"></iframe>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible events testing for ARIA document</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../../common.js"></script>
+ <script type="application/javascript"
+ src="../../role.js"></script>
+ <script type="application/javascript"
+ src="../../states.js"></script>
+ <script type="application/javascript"
+ src="../../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function showARIADialog(aID) {
+ this.dialogNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, this.dialogNode),
+ ];
+
+ this.invoke = function showARIADialog_invoke() {
+ this.dialogNode.style.display = "block";
+ };
+
+ this.getID = function showARIADialog_getID() {
+ return "show ARIA dialog";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ var gQueue = null;
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new showARIADialog("dialog"));
+ gQueue.push(new showARIADialog("document"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=759833"
+ title="ARIA documents should fire document loading events">
+ Mozilla Bug 759833
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div role="dialog" id="dialog" style="display: none;">It's a dialog</div>
+ <div role="document" id="document" style="display: none;">It's a document</div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible events testing for document</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../../common.js"></script>
+ <script type="application/javascript"
+ src="../../role.js"></script>
+ <script type="application/javascript"
+ src="../../states.js"></script>
+ <script type="application/javascript"
+ src="../../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function makeIFrameVisible(aID) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.DOMNode.parentNode),
+ {
+ type: EVENT_STATE_CHANGE,
+ get target() {
+ return getAccessible("iframe").firstChild;
+ },
+ match(aEvent) {
+ // The document shouldn't have busy state (the DOM document was
+ // loaded before its accessible was created). Do this test lately to
+ // make sure the content of document accessible was created
+ // initially, prior to this the document accessible keeps busy
+ // state. The initial creation happens asynchronously after document
+ // creation, there are no events we could use to catch it.
+ let { state, isEnabled } = aEvent.QueryInterface(nsIAccessibleStateChangeEvent);
+ return state & STATE_BUSY && !isEnabled;
+ },
+ },
+ ];
+
+ this.invoke = () => (this.DOMNode.style.visibility = "visible");
+
+ this.getID = () =>
+ "The accessible for DOM document loaded before it's shown shouldn't have busy state.";
+ }
+
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ function doTests() {
+ const gQueue = new eventQueue();
+ gQueue.push(new makeIFrameVisible("iframe"));
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=658185"
+ title="The DOM document loaded before it's shown shouldn't have busy state">
+ Mozilla Bug 658185
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="testContainer"><iframe id="iframe" src="about:mozilla" style="visibility: hidden;"></iframe></div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible events testing for document</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../../common.js"></script>
+ <script type="application/javascript"
+ src="../../role.js"></script>
+ <script type="application/javascript"
+ src="../../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function changeIframeSrc(aIdentifier, aURL, aTitle) {
+ this.DOMNode = getNode(aIdentifier);
+
+ function getIframeDoc() {
+ return getAccessible(getNode(aIdentifier).contentDocument);
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, getAccessible(this.DOMNode)),
+ new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, getIframeDoc),
+ ];
+
+ this.invoke = () => (this.DOMNode.src = aURL);
+
+ this.finalCheck = () =>
+ testAccessibleTree(this.DOMNode, {
+ role: ROLE_INTERNAL_FRAME,
+ children: [
+ {
+ role: ROLE_DOCUMENT,
+ name: aTitle,
+ },
+ ],
+ });
+
+ this.getID = () => `change iframe src on ${aURL}`;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ function doTests() {
+ const gQueue = new eventQueue();
+ gQueue.push(new changeIframeSrc("iframe", "about:license", "Licenses"));
+ gQueue.push(new changeIframeSrc("iframe", "about:buildconfig", "Build Configuration"));
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=420845"
+ title="Fire event_reorder on any embedded frames/iframes whos document has just loaded">
+ Mozilla Bug 420845
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=754165"
+ title="Fire document load events on iframes too">
+ Mozilla Bug 754165
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="testContainer"><iframe id="iframe"></iframe></div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible events testing for document</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../../common.js"></script>
+ <script type="application/javascript"
+ src="../../role.js"></script>
+ <script type="application/javascript"
+ src="../../states.js"></script>
+ <script type="application/javascript"
+ src="../../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ const kHide = 1;
+ const kShow = 2;
+ const kRemove = 3;
+
+ function morphIFrame(aIdentifier, aAction) {
+ this.DOMNode = getNode(aIdentifier);
+ this.IFrameContainerDOMNode = this.DOMNode.parentNode;
+
+ this.eventSeq = [
+ new invokerChecker(aAction === kShow ? EVENT_SHOW : EVENT_HIDE, this.DOMNode),
+ new invokerChecker(EVENT_REORDER, this.IFrameContainerDOMNode),
+ ];
+
+ this.invoke = () => {
+ if (aAction === kRemove) {
+ this.IFrameContainerDOMNode.removeChild(this.DOMNode);
+ } else {
+ this.DOMNode.style.display = aAction === kHide ? "none" : "block";
+ }
+ };
+
+ this.finalCheck = () =>
+ testAccessibleTree(this.IFrameContainerDOMNode, {
+ role: ROLE_SECTION,
+ children: (aAction == kHide || aAction == kRemove) ? [ ] :
+ [
+ {
+ role: ROLE_INTERNAL_FRAME,
+ children: [
+ { role: ROLE_DOCUMENT },
+ ],
+ },
+ ],
+ });
+
+ this.getID = () => {
+ if (aAction === kRemove) {
+ return "remove iframe";
+ }
+
+ return `change display style of iframe to ${aAction === kHide ? "none" : "block"}`;
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ function doTests() {
+ const gQueue = new eventQueue(EVENT_REORDER);
+ gQueue.push(new morphIFrame("iframe", kHide));
+ gQueue.push(new morphIFrame("iframe", kShow));
+ gQueue.push(new morphIFrame("iframe", kRemove));
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=566103"
+ title="Reorganize accessible document handling">
+ Mozilla Bug 566103
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="testContainer"><iframe id="iframe"></iframe></div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible events testing for document</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../../common.js"></script>
+ <script type="application/javascript"
+ src="../../role.js"></script>
+ <script type="application/javascript"
+ src="../../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ let gDialog;
+ let gDialogDoc;
+ let gRootAcc;
+
+ function openDialogWnd(aURL) {
+ // Get application root accessible.
+ let docAcc = getAccessible(document);
+ while (docAcc) {
+ gRootAcc = docAcc;
+ try {
+ docAcc = docAcc.parent;
+ } catch (e) {
+ ok(false, `Can't get parent for ${prettyName(docAcc)}`);
+ throw e;
+ }
+ }
+
+ this.eventSeq = [
+ new asyncInvokerChecker(EVENT_REORDER, gRootAcc),
+ // We use a function here to get the target because gDialog isn't set
+ // yet, but it will be when the function is called.
+ new invokerChecker(EVENT_FOCUS, () => gDialog.document)
+ ];
+
+ this.invoke = () => (gDialog = window.browsingContext.topChromeWindow.openDialog(aURL));
+
+ this.finalCheck = () => {
+ const accTree = {
+ role: ROLE_APP_ROOT,
+ children: [
+ {
+ role: ROLE_CHROME_WINDOW,
+ },
+ {
+ role: ROLE_CHROME_WINDOW,
+ },
+ ],
+ };
+
+ testAccessibleTree(gRootAcc, accTree);
+
+ gDialogDoc = gDialog.document;
+ ok(isAccessibleInCache(gDialogDoc),
+ `The document accessible for '${aURL}' is not in cache!`);
+ };
+
+ this.getID = () => `open dialog '${aURL}'`;
+ }
+
+ function closeDialogWnd() {
+ this.eventSeq = [ new invokerChecker(EVENT_FOCUS, getAccessible(document)) ];
+
+ this.invoke = () => {
+ gDialog.close();
+ window.focus();
+ };
+
+ this.finalCheck = () => {
+ ok(!isAccessibleInCache(gDialogDoc),
+ `The document accessible for dialog is in cache still!`);
+
+ gDialog = gDialogDoc = gRootAcc = null;
+ };
+
+ this.getID = () => "close dialog";
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ function doTests() {
+ // Front end stuff sometimes likes to stuff things in the hidden window(s)
+ // in which case we should repress all accessibles for those.
+
+ // Try to create an accessible for the hidden window's document.
+ let doc = Services.appShell.hiddenDOMWindow.document;
+ let hiddenDocAcc = gAccService.getAccessibleFor(doc);
+ ok(!hiddenDocAcc, "hiddenDOMWindow should not have an accessible.");
+
+ const gQueue = new eventQueue();
+ gQueue.push(new openDialogWnd("about:about"));
+ gQueue.push(new closeDialogWnd());
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=506206"
+ title="Fire event_reorder application root accessible">
+ Mozilla Bug 506206
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible events testing for document</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../../common.js"></script>
+ <script type="application/javascript"
+ src="../../role.js"></script>
+ <script type="application/javascript"
+ src="../../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ let gDialog;
+ let gDialogDoc;
+ let gRootAcc;
+ let gIframeDoc;
+
+ function openWndShutdownDoc(aURL) {
+ // Get application root accessible.
+ let docAcc = getAccessible(document);
+ while (docAcc) {
+ gRootAcc = docAcc;
+ try {
+ docAcc = docAcc.parent;
+ } catch (e) {
+ ok(false, `Can't get parent for ${prettyName(docAcc)}`);
+ throw e;
+ }
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, gRootAcc),
+ {
+ type: EVENT_HIDE,
+ get target() {
+ gDialogDoc = gDialog.document;
+ const iframe = gDialogDoc.getElementById("iframe");
+ gIframeDoc = iframe.contentDocument;
+ return iframe;
+ },
+ get targetDescr() {
+ return "inner iframe of docload_wnd.html document";
+ },
+ },
+ ];
+
+
+ this.invoke = () => gDialog = window.browsingContext.topChromeWindow.openDialog(aURL);
+
+ this.finalCheck = () => {
+ const accTree = {
+ role: ROLE_APP_ROOT,
+ children: [
+ {
+ role: ROLE_CHROME_WINDOW,
+ },
+ {
+ role: ROLE_CHROME_WINDOW,
+ },
+ ],
+ };
+
+ testAccessibleTree(gRootAcc, accTree);
+ // After timeout after event hide for iframe was handled the document
+ // accessible for iframe's document should no longer be in cache.
+ ok(!isAccessibleInCache(gIframeDoc),
+ "The document accessible for iframe is in cache still after iframe hide!");
+ ok(isAccessibleInCache(gDialogDoc),
+ `The document accessible for '${aURL}' is not in cache!`);
+ };
+
+ this.getID = () => `open dialog '${aURL}'`;
+ }
+
+ function closeWndShutdownDoc() {
+ this.eventSeq = [ new invokerChecker(EVENT_FOCUS, getAccessible(document)) ];
+
+ this.invoke = () => {
+ gDialog.close();
+ window.focus();
+ };
+
+ this.finalCheck = () => {
+ ok(!isAccessibleInCache(gDialogDoc),
+ "The document accessible for dialog is in cache still!");
+ // After the window is closed all alive subdocument accessibles should
+ // be shut down.
+ ok(!isAccessibleInCache(gIframeDoc),
+ "The document accessible for iframe is in cache still!");
+
+ gDialog = gDialogDoc = gRootAcc = gIframeDoc = null;
+ };
+
+ this.getID = () => "close dialog";
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ function doTests() {
+ // Front end stuff sometimes likes to stuff things in the hidden window(s)
+ // in which case we should repress all accessibles for those.
+
+ // Try to create an accessible for the hidden window's document.
+ let doc = Services.appShell.hiddenDOMWindow.document;
+ let hiddenDocAcc = gAccService.getAccessibleFor(doc);
+ ok(!hiddenDocAcc, "hiddenDOMWindow should not have an accessible.");
+
+ const gQueue = new eventQueue();
+ gQueue.push(new openWndShutdownDoc("../../events/docload/docload_wnd.html"));
+ gQueue.push(new closeWndShutdownDoc());
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=571459"
+ title="Shutdown document accessible when presshell goes away">
+ Mozilla Bug 571459
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>editable document</title>
+</head>
+
+<body contentEditable="true">
+ editable document
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>nsIAccessible actions testing for anchors</title>
+</head>
+
+<body>
+ <p>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ </p>
+ <a name="link1">link1</a>
+
+ <p style="color: blue">
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ </p>
+
+ <h1 id="heading_1">heading 1</h1>
+ <p style="color: blue">
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ text text text text text text text text text text text text text text <br>
+ </p>
+</body>
+<html>
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 @@
+<html>
+
+<head>
+ <title>Announcement event and method testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ async function doTests() {
+ let acc = getAccessible("display");
+
+ 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");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1525980"
+ title="Introduce announcement event and method">
+ Mozilla Bug 1525980
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>ARIA alert event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function showAlert(aID) {
+ this.DOMNode = document.createElement("div");
+
+ this.invoke = function showAlert_invoke() {
+ this.DOMNode.setAttribute("role", "alert");
+ this.DOMNode.setAttribute("id", aID);
+ var text = document.createTextNode("alert");
+ this.DOMNode.appendChild(text);
+ document.body.appendChild(this.DOMNode);
+ };
+
+ this.getID = function showAlert_getID() {
+ return "Show ARIA alert " + aID;
+ };
+ }
+
+ function changeAlert(aID) {
+ this.__defineGetter__("DOMNode", function() { return getNode(aID); });
+
+ this.invoke = function changeAlert_invoke() {
+ this.DOMNode.textContent = "new alert";
+ };
+
+ this.getID = function showAlert_getID() {
+ return "Change ARIA alert " + aID;
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ // gA11yEventDumpToConsole = true; // debuging
+ // enableLogging("tree,events,verbose");
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue(nsIAccessibleEvent.EVENT_ALERT);
+
+ gQueue.push(new showAlert("alert"));
+ gQueue.push(new changeAlert("alert"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=591199"
+ title="mochitest for bug 334386: fire alert event when ARIA alert is shown or new its children are inserted">
+ Mozilla Bug 591199
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>ARIA menu events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ const kViaDisplayStyle = 0;
+ const kViaVisibilityStyle = 1;
+
+ function focusMenu(aMenuBarID, aMenuID, aActiveMenuBarID) {
+ this.eventSeq = [];
+
+ if (aActiveMenuBarID) {
+ this.eventSeq.push(new invokerChecker(EVENT_MENU_END,
+ getNode(aActiveMenuBarID)));
+ }
+
+ this.eventSeq.push(new invokerChecker(EVENT_MENU_START, getNode(aMenuBarID)));
+ this.eventSeq.push(new invokerChecker(EVENT_FOCUS, getNode(aMenuID)));
+
+ this.invoke = function focusMenu_invoke() {
+ getNode(aMenuID).focus();
+ };
+
+ this.getID = function focusMenu_getID() {
+ return "focus menu '" + aMenuID + "'";
+ };
+ }
+
+ function showMenu(aMenuID, aParentMenuID, aHow) {
+ this.menuNode = getNode(aMenuID);
+
+ // Because of aria-owns processing we may have menupopup start fired before
+ // related show event.
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.menuNode),
+ new invokerChecker(EVENT_REORDER, getNode(aParentMenuID)),
+ new invokerChecker(EVENT_MENUPOPUP_START, this.menuNode),
+ ];
+
+ this.invoke = function showMenu_invoke() {
+ if (aHow == kViaDisplayStyle)
+ this.menuNode.style.display = "block";
+ else
+ this.menuNode.style.visibility = "visible";
+ };
+
+ this.getID = function showMenu_getID() {
+ return "Show ARIA menu '" + aMenuID + "' by " +
+ (aHow == kViaDisplayStyle ? "display" : "visibility") +
+ " style tricks";
+ };
+ }
+
+ function closeMenu(aMenuID, aParentMenuID, aHow) {
+ this.menuNode = getNode(aMenuID);
+ this.menu = null;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getMenu, this),
+ new invokerChecker(EVENT_MENUPOPUP_END, getMenu, this),
+ new invokerChecker(EVENT_REORDER, getNode(aParentMenuID)),
+ ];
+
+ this.invoke = function closeMenu_invoke() {
+ // Store menu accessible reference while menu is still open.
+ this.menu = getAccessible(this.menuNode);
+
+ // Hide menu.
+ if (aHow == kViaDisplayStyle)
+ this.menuNode.style.display = "none";
+ else
+ this.menuNode.style.visibility = "hidden";
+ };
+
+ this.getID = function closeMenu_getID() {
+ return "Close ARIA menu " + aMenuID + " by " +
+ (aHow == kViaDisplayStyle ? "display" : "visibility") +
+ " style tricks";
+ };
+
+ function getMenu(aThisObj) {
+ return aThisObj.menu;
+ }
+ }
+
+ function focusInsideMenu(aMenuID, aMenuBarID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, getNode(aMenuID)),
+ ];
+
+ this.unexpectedEventSeq = [
+ new invokerChecker(EVENT_MENU_END, getNode(aMenuBarID)),
+ ];
+
+ this.invoke = function focusInsideMenu_invoke() {
+ getNode(aMenuID).focus();
+ };
+
+ this.getID = function focusInsideMenu_getID() {
+ return "focus menu '" + aMenuID + "'";
+ };
+ }
+
+ function blurMenu(aMenuBarID) {
+ var eventSeq = [
+ new invokerChecker(EVENT_MENU_END, getNode(aMenuBarID)),
+ new invokerChecker(EVENT_FOCUS, getNode("outsidemenu")),
+ ];
+
+ this.__proto__ = new synthClick("outsidemenu", eventSeq);
+
+ this.getID = function blurMenu_getID() {
+ return "blur menu";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ // gA11yEventDumpToConsole = true; // debuging
+ // enableLogging("tree,events,verbose");
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new focusMenu("menubar2", "menu-help"));
+ gQueue.push(new focusMenu("menubar", "menu-file", "menubar2"));
+ gQueue.push(new showMenu("menupopup-file", "menu-file", kViaDisplayStyle));
+ gQueue.push(new closeMenu("menupopup-file", "menu-file", kViaDisplayStyle));
+ gQueue.push(new showMenu("menupopup-edit", "menu-edit", kViaVisibilityStyle));
+ gQueue.push(new closeMenu("menupopup-edit", "menu-edit", kViaVisibilityStyle));
+ gQueue.push(new focusInsideMenu("menu-edit", "menubar"));
+ gQueue.push(new blurMenu("menubar"));
+
+ gQueue.push(new focusMenu("menubar3", "mb3-mi-outside"));
+ gQueue.push(new showMenu("mb4-menu", document, kViaDisplayStyle));
+ gQueue.push(new focusMenu("menubar4", "mb4-item1"));
+ gQueue.push(new focusMenu("menubar5", "mb5-mi"));
+
+ gQueue.push(new synthFocus("mi6"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=606207"
+ title="Dojo dropdown buttons are broken">
+ Bug 606207
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=614829"
+ title="Menupopup end event isn't fired for ARIA menus">
+ Bug 614829
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=615189"
+ title="Clean up FireAccessibleFocusEvent">
+ Bug 615189
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958"
+ title="Rework accessible focus handling">
+ Bug 673958
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=933322"
+ title="menustart/end events are missing when aria-owns makes a menu hierarchy">
+ Bug 933322
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=934460"
+ title="menustart/end events may be missed when top level menuitem is focused">
+ Bug 934460
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=970005"
+ title="infinite long loop in a11y:FocusManager::ProcessFocusEvent">
+ Bug 970005
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="menubar" role="menubar">
+ <div id="menu-file" role="menuitem" tabindex="0">
+ File
+ <div id="menupopup-file" role="menu" style="display: none;">
+ <div id="menuitem-newtab" role="menuitem" tabindex="0">New Tab</div>
+ <div id="menuitem-newwindow" role="menuitem" tabindex="0">New Window</div>
+ </div>
+ </div>
+ <div id="menu-edit" role="menuitem" tabindex="0">
+ Edit
+ <div id="menupopup-edit" role="menu" style="visibility: hidden;">
+ <div id="menuitem-undo" role="menuitem" tabindex="0">Undo</div>
+ <div id="menuitem-redo" role="menuitem" tabindex="0">Redo</div>
+ </div>
+ </div>
+ </div>
+ <div id="menubar2" role="menubar">
+ <div id="menu-help" role="menuitem" tabindex="0">
+ Help
+ <div id="menupopup-help" role="menu" style="display: none;">
+ <div id="menuitem-about" role="menuitem" tabindex="0">About</div>
+ </div>
+ </div>
+ </div>
+ <div tabindex="0" id="outsidemenu">outsidemenu</div>
+
+ <!-- aria-owns relations -->
+ <div id="menubar3" role="menubar" aria-owns="mb3-mi-outside"></div>
+ <div id="mb3-mi-outside" role="menuitem" tabindex="0">Outside</div>
+
+ <div id="menubar4" role="menubar">
+ <div id="mb4_topitem" role="menuitem" aria-haspopup="true"
+ aria-owns="mb4-menu">Item</div>
+ </div>
+ <div id="mb4-menu" role="menu" style="display:none;">
+ <div role="menuitem" id="mb4-item1" tabindex="0">Item 1.1</div>
+ <div role="menuitem" tabindex="0">Item 1.2</div>
+ </div>
+
+ <!-- focus top-level menu item having haspopup -->
+ <div id="menubar5" role="menubar">
+ <div role="menuitem" aria-haspopup="true" id="mb5-mi" tabindex="0">
+ Item
+ <div role="menu" style="display:none;">
+ <div role="menuitem" tabindex="0">Item 1.1</div>
+ <div role="menuitem" tabindex="0">Item 1.2</div>
+ </div>
+ </div>
+ </div>
+
+ <!-- other aria-owns relations -->
+ <div id="mi6" tabindex="0" role="menuitem">aria-owned item</div>
+ <div aria-owns="mi6">Obla</div>
+
+ <div id="eventdump"></div>
+
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible ARIA object attribute changes</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+ function updateAttribute(aID, aAttr, aValue) {
+ this.node = getNode(aID);
+ this.accessible = getAccessible(this.node);
+
+ this.eventSeq = [
+ new objAttrChangedChecker(aID, aAttr),
+ ];
+
+ this.invoke = function updateAttribute_invoke() {
+ this.node.setAttribute(aAttr, aValue);
+ };
+
+ this.getID = function updateAttribute_getID() {
+ return aAttr + " for " + aID + " " + aValue;
+ };
+ }
+
+ // gA11yEventDumpToConsole = true;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new updateAttribute("sortable", "aria-sort", "ascending"));
+
+ // For experimental ARIA extensions
+ gQueue.push(new updateAttribute("custom", "aria-blah", "true"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="sortable" role="columnheader" aria-sort="none">aria-sort</div>
+
+ <div id="custom" role="custom" aria-blah="false">Fat free cheese</div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Aria-owns targets shouldn't be on invalidation list so shouldn't have
+ show/hide events</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests.
+
+ // gA11yEventDumpToConsole = true; // debug stuff
+ // enableLogging("tree,eventTree,verbose");
+
+ /**
+ * Aria-owns target shouldn't have a show event.
+ * Markup:
+ * <div id="t1_fc" aria-owns="t1_owns"></div>
+ * <span id="t1_owns"></div>
+ */
+ function testAriaOwns() {
+ this.parent = getNode("t1");
+ this.fc = document.createElement("div");
+ this.fc.setAttribute("id", "t1_fc");
+ this.owns = document.createElement("span");
+ this.owns.setAttribute("id", "t1_owns");
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.fc),
+ new unexpectedInvokerChecker(EVENT_SHOW, this.owns),
+ ];
+
+ this.invoke = function testAriaOwns_invoke() {
+ getNode("t1").appendChild(this.fc);
+ getNode("t1").appendChild(this.owns);
+ getNode("t1_fc").setAttribute("aria-owns", "t1_owns");
+ };
+
+ this.getID = function testAriaOwns_getID() {
+ return "Aria-owns target shouldn't have show event";
+ };
+ }
+
+ /**
+ * Target of both aria-owns and other aria attribute like aria-labelledby
+ * shouldn't have a show event.
+ * Markup:
+ * <div id="t2_fc" aria-owns="t1_owns"></div>
+ * <div id="t2_sc" aria-labelledby="t2_owns"></div>
+ * <span id="t2_owns"></div>
+ */
+ function testAriaOwnsAndLabelledBy() {
+ this.parent = getNode("t2");
+ this.fc = document.createElement("div");
+ this.fc.setAttribute("id", "t2_fc");
+ this.sc = document.createElement("div");
+ this.sc.setAttribute("id", "t2_sc");
+ this.owns = document.createElement("span");
+ this.owns.setAttribute("id", "t2_owns");
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.fc),
+ new invokerChecker(EVENT_SHOW, this.sc),
+ new unexpectedInvokerChecker(EVENT_SHOW, this.owns),
+ ];
+
+ this.invoke = function testAriaOwns_invoke() {
+ getNode("t2").appendChild(this.fc);
+ getNode("t2").appendChild(this.sc);
+ getNode("t2").appendChild(this.owns);
+ getNode("t2_fc").setAttribute("aria-owns", "t2_owns");
+ getNode("t2_sc").setAttribute("aria-labelledby", "t2_owns");
+ };
+
+ this.getID = function testAriaOwns_getID() {
+ return "Aria-owns and aria-labelledby target shouldn't have show event";
+ };
+ }
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+ gQueue.push(new testAriaOwns());
+ gQueue.push(new testAriaOwnsAndLabelledBy());
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1296420"
+ title="Aria-owns targets shouldn't be on invalidation list so shouldn't
+ have show/hide events">
+ Mozilla Bug 1296420
+ </a><br>
+
+ <div id="testContainer">
+ <div id="t1"></div>
+
+ <div id="t2"></div>
+ </div>
+
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>ARIA state change event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ let PromEvents = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js",
+ PromEvents);
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ // gA11yEventDumpID = "eventdump"; // debugging
+ // gA11yEventDumpToConsole = true; // debugging
+
+ function expandNode(aID, aIsExpanded) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new expandedStateChecker(aIsExpanded, this.DOMNode),
+ ];
+
+ this.invoke = function expandNode_invoke() {
+ this.DOMNode.setAttribute("aria-expanded",
+ (aIsExpanded ? "true" : "false"));
+ };
+
+ this.getID = function expandNode_getID() {
+ return prettyName(aID) + " aria-expanded changed to '" + aIsExpanded + "'";
+ };
+ }
+
+ function busyify(aID, aIsBusy) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new stateChangeChecker(STATE_BUSY, kOrdinalState, aIsBusy, this.DOMNode),
+ ];
+
+ this.invoke = function busyify_invoke() {
+ this.DOMNode.setAttribute("aria-busy", (aIsBusy ? "true" : "false"));
+ };
+
+ this.getID = function busyify_getID() {
+ return prettyName(aID) + " aria-busy changed to '" + aIsBusy + "'";
+ };
+ }
+
+ function makeCurrent(aID, aIsCurrent, aValue) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new stateChangeChecker(EXT_STATE_CURRENT, true, aIsCurrent, this.DOMNode),
+ ];
+
+ this.invoke = function makeCurrent_invoke() {
+ this.DOMNode.setAttribute("aria-current", aValue);
+ };
+
+ this.getID = function makeCurrent_getID() {
+ return prettyName(aID) + " aria-current changed to " + aValue;
+ };
+ }
+
+ async function testToggleAttribute(aID, aAttribute, aIncludeMixed) {
+ let toggleState = aAttribute == "aria-pressed" ? STATE_PRESSED : STATE_CHECKED;
+
+ // bug 472142. Role changes here if aria-pressed is added,
+ // accessible should be recreated?
+ let stateChange = PromEvents.waitForStateChange(aID, toggleState, true);
+ getNode(aID).setAttribute(aAttribute, "true");
+ await stateChange;
+
+ stateChange = PromEvents.waitForStateChange(aID, toggleState, false);
+ getNode(aID).setAttribute(aAttribute, "false");
+ await stateChange;
+
+ if (aIncludeMixed) {
+ stateChange = PromEvents.waitForStateChange(aID, STATE_MIXED, true);
+ getNode(aID).setAttribute(aAttribute, "mixed");
+ await stateChange;
+
+ stateChange = PromEvents.waitForStateChange(aID, STATE_MIXED, false);
+ getNode(aID).setAttribute(aAttribute, "");
+ await stateChange;
+ }
+
+ stateChange = PromEvents.waitForStateChange(aID, toggleState, true);
+ getNode(aID).setAttribute(aAttribute, "true");
+ await stateChange;
+
+ if (aIncludeMixed) {
+ stateChange = Promise.all([
+ PromEvents.waitForStateChange(aID, STATE_MIXED, true),
+ PromEvents.waitForStateChange(aID, toggleState, false)]);
+ getNode(aID).setAttribute(aAttribute, "mixed");
+ await stateChange;
+
+ stateChange = Promise.all([
+ PromEvents.waitForStateChange(aID, STATE_MIXED, false),
+ PromEvents.waitForStateChange(aID, toggleState, true)]);
+ getNode(aID).setAttribute(aAttribute, "true");
+ await stateChange;
+ }
+
+ // bug 472142. Role changes here too if aria-pressed is removed,
+ // accessible should be recreated?
+ stateChange = PromEvents.waitForStateChange(aID, toggleState, false);
+ getNode(aID).removeAttribute(aAttribute);
+ await stateChange;
+ }
+
+ async function doTests() {
+ gQueue = new eventQueue();
+
+ let queueFinished = new Promise(resolve => {
+ gQueue.onFinish = function() {
+ resolve();
+ return DO_NOT_FINISH_TEST;
+ };
+ });
+
+ gQueue.push(new expandNode("section", true));
+ gQueue.push(new expandNode("section", false));
+ gQueue.push(new expandNode("div", true));
+ gQueue.push(new expandNode("div", false));
+
+ gQueue.push(new busyify("aria_doc", true));
+ gQueue.push(new busyify("aria_doc", false));
+
+ gQueue.push(new makeCurrent("current_page_1", false, "false"));
+ gQueue.push(new makeCurrent("current_page_2", true, "page"));
+ gQueue.push(new makeCurrent("current_page_2", false, "false"));
+ gQueue.push(new makeCurrent("current_page_3", true, "true"));
+ gQueue.push(new makeCurrent("current_page_3", false, ""));
+
+ gQueue.invoke();
+ await queueFinished;
+ // Tests beyond this point use await rather than eventQueue.
+
+ await testToggleAttribute("pressable", "aria-pressed", true);
+ await testToggleAttribute("pressable_native", "aria-pressed", true);
+ await testToggleAttribute("checkable", "aria-checked", true);
+ await testToggleAttribute("checkableBool", "aria-checked", false);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=551684"
+ title="No statechange event for aria-expanded on native HTML elements, is fired on ARIA widgets">
+ Mozilla Bug 551684
+ </a><br>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=648133"
+ title="fire state change event for aria-busy">
+ Mozilla Bug 648133
+ </a><br>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=467143"
+ title="mixed state change event is fired for focused accessible only">
+ Mozilla Bug 467143
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=989958"
+ title="Pressed state is not exposed on a button element with aria-pressed attribute">
+ Mozilla Bug 989958
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1136563"
+ title="Support ARIA 1.1 switch role">
+ Mozilla Bug 1136563
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1355921"
+ title="Elements with a defined, non-false value for aria-current should expose ATK_STATE_ACTIVE">
+ Mozilla Bug 1355921
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="eventdump"></div>
+
+ <!-- aria-expanded -->
+ <div id="section" role="section" aria-expanded="false">expandable section</div>
+ <div id="div" aria-expanded="false">expandable native div</div>
+
+ <!-- aria-busy -->
+ <div id="aria_doc" role="document" tabindex="0">A document</div>
+
+ <!-- aria-pressed -->
+ <div id="pressable" role="button"></div>
+ <button id="pressable_native"></button>
+
+ <!-- aria-checked -->
+ <div id="checkable" role="checkbox"></div>
+ <div id="checkableBool" role="switch"></div>
+
+ <!-- aria-current -->
+ <div id="current_page_1" role="link" aria-current="page">1</div>
+ <div id="current_page_2" role="link" aria-current="false">2</div>
+ <div id="current_page_3" role="link">3</div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible attr change event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ async function testGotAttrChange(elem, name, value) {
+ const waitFor = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, elem);
+ if (value) {
+ document.getElementById(elem).setAttribute(name, value);
+ } else {
+ document.getElementById(elem).removeAttribute(name);
+ }
+ await waitFor;
+ }
+
+ async function doTests() {
+ info("Removing summary attr");
+ // after summary is removed, we should have a layout table
+ await testGotAttrChange(
+ "sampleTable",
+ "summary",
+ null
+ );
+
+ info("Setting abbr attr");
+ // after abbr is set we should have a data table again
+ await testGotAttrChange(
+ "cellOne",
+ "abbr",
+ "hello world"
+ );
+
+ info("Removing abbr attr");
+ // after abbr is removed we should have a layout table again
+ await testGotAttrChange(
+ "cellOne",
+ "abbr",
+ null
+ );
+
+ info("Setting scope attr");
+ // after scope is set we should have a data table again
+ await testGotAttrChange(
+ "cellOne",
+ "scope",
+ "col"
+ );
+
+ info("Removing scope attr");
+ // remove scope should give layout
+ await testGotAttrChange(
+ "cellOne",
+ "scope",
+ null
+ );
+
+ info("Setting headers attr");
+ // add headers attr should give data
+ await testGotAttrChange(
+ "cellThree",
+ "headers",
+ "cellOne"
+ );
+
+ info("Removing headers attr");
+ // remove headers attr should give layout
+ await testGotAttrChange(
+ "cellThree",
+ "headers",
+ null
+ );
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+<body>
+ <table id="sampleTable" summary="example summary">
+ <tr>
+ <td id="cellOne">cell1</td>
+ <td>cell2</td>
+ </tr>
+ <tr>
+ <td id="cellThree">cell3</td>
+ <td>cell4</td>
+ </tr>
+ </table>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Event object attributes tests</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+
+ /**
+ * Test "event-from-input" object attribute.
+ */
+ function eventFromInputChecker(aEventType, aID, aValue, aNoTargetID) {
+ this.type = aEventType;
+ this.target = getAccessible(aID);
+
+ this.noTarget = getNode(aNoTargetID);
+
+ this.check = function checker_check(aEvent) {
+ testAttrs(aEvent.accessible, { "event-from-input": aValue }, true);
+
+ var accessible = getAccessible(this.noTarget);
+ testAbsentAttrs(accessible, { "event-from-input": "" });
+ };
+ }
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true; // debug stuff
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ var id = "textbox", noTargetId = "textarea";
+ let checker =
+ new eventFromInputChecker(EVENT_FOCUS, id, "false", noTargetId);
+ gQueue.push(new synthFocus(id, checker));
+
+ if (!MAC) { // Mac failure is bug 541093
+ checker =
+ new eventFromInputChecker(EVENT_TEXT_CARET_MOVED, id, "true", noTargetId);
+ gQueue.push(new synthHomeKey(id, checker));
+ }
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=540285"
+ title="Event object attributes testing">
+ Mozilla Bug 540285
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="textbox" value="hello">
+ <textarea id="textarea"></textarea>
+
+ <div id="eventdump"></div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible mutation events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function changeMultipleElements() {
+ this.node1 = getNode("span1");
+ this.node2 = getNode("span2");
+
+ this.eventSeq = [
+ new textChangeChecker("container", 0, 5, "hello", false, undefined, true),
+ new textChangeChecker("container", 6, 11, "world", false, undefined, true),
+ new orderChecker(),
+ new textChangeChecker("container", 0, 1, "a", true, undefined, true),
+ new textChangeChecker("container", 7, 8, "b", true, undefined, true),
+ ];
+
+ this.invoke = function changeMultipleElements_invoke() {
+ this.node1.textContent = "a";
+ this.node2.textContent = "b";
+ };
+
+ this.getID = function changeMultipleElements_invoke_getID() {
+ return "Change the text content of multiple sibling divs";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+// gA11yEventDumpToConsole = true; // debugging
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new changeMultipleElements());
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1322593"
+ title="missing text change events when multiple elements updated at once">
+ Mozilla Bug 1322593
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="container">
+ <span id="span1">hello</span>
+ <span>your</span>
+ <span id="span2">world</span>
+ </div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible mutation events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function changeMultipleElements() {
+ this.node1 = getNode("div1");
+ this.node2 = getNode("div2");
+
+ this.eventSeq = [
+ new textChangeChecker("div1", 0, 5, "hello", false, undefined, true),
+ new textChangeChecker("div2", 0, 5, "world", false, undefined, true),
+ new orderChecker(),
+ new textChangeChecker("div1", 0, 1, "a", true, undefined, true),
+ new textChangeChecker("div2", 0, 1, "b", true, undefined, true),
+ ];
+
+ this.invoke = function changeMultipleElements_invoke() {
+ this.node1.textContent = "a";
+ this.node2.textContent = "b";
+ };
+
+ this.getID = function changeMultipleElements_invoke_getID() {
+ return "Change the text content of multiple sibling divs";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+// gA11yEventDumpToConsole = true; // debugging
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new changeMultipleElements());
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1322593"
+ title="missing text change events when multiple elements updated at once">
+ Mozilla Bug 1322593
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="div1">hello</div>
+ <div id="div2">world</div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible caret move events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ /**
+ * Click checker.
+ */
+ function clickChecker(aCaretOffset, aIsSelectionCollapsed, aID, aExtraNodeOrID, aExtraCaretOffset) {
+ this.__proto__ = new caretMoveChecker(aCaretOffset, aIsSelectionCollapsed, aID);
+
+ this.extraNode = getNode(aExtraNodeOrID);
+
+ this.check = function clickChecker_check(aEvent) {
+ this.__proto__.check(aEvent);
+
+ if (this.extraNode) {
+ var acc = getAccessible(this.extraNode, [nsIAccessibleText]);
+ is(acc.caretOffset, aExtraCaretOffset,
+ "Wrong caret offset for " + aExtraNodeOrID);
+ }
+ };
+ }
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ // gA11yEventDumpToConsole = true;
+
+ function doTests() {
+ // test caret move events and caret offsets
+ gQueue = new eventQueue();
+
+ var id = "textbox";
+ gQueue.push(new synthFocus(id, new caretMoveChecker(5, true, id)));
+ gQueue.push(new synthSelectAll(id, new caretMoveChecker(5, false, id)));
+ gQueue.push(new synthClick(id, new caretMoveChecker(0, true, id)));
+ gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id)));
+
+ if (!MAC) {
+ gQueue.push(new synthSelectAll(id, new caretMoveChecker(5, false, id)));
+ gQueue.push(new synthHomeKey(id, new caretMoveChecker(0, true, id)));
+ gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id)));
+ }
+ else {
+ todo(false, "Make these tests pass on OSX (bug 650294)");
+ }
+
+ id = "textarea";
+ gQueue.push(new synthClick(id, new caretMoveChecker(0, true, id)));
+ gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id)));
+ gQueue.push(new synthDownKey(id, new caretMoveChecker(12, true, id)));
+
+ id = "textarea_wrapped";
+ gQueue.push(new setCaretOffset(id, 4, id));
+ gQueue.push(new synthLeftKey(id, new caretMoveChecker(4, true, id)));
+
+ id = "p";
+ gQueue.push(new synthClick(id, new caretMoveChecker(0, true, id)));
+ gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id)));
+ gQueue.push(new synthDownKey(id, new caretMoveChecker(6, true, id)));
+
+ id = "p1_in_div";
+ gQueue.push(new synthClick(id, new clickChecker(0, true, id, "p2_in_div", -1)));
+
+ id = "p";
+ gQueue.push(new synthShiftTab(id, new caretMoveChecker(0, true, id)));
+ id = "textarea";
+ gQueue.push(new synthShiftTab(id, new caretMoveChecker(12, true, id)));
+ id = "p";
+ gQueue.push(new synthTab(id, new caretMoveChecker(0, true, id)));
+
+ // Set caret after a child of span element, i.e. after 'text' text.
+ gQueue.push(new moveCaretToDOMPoint("test1", getNode("test1_span"), 1,
+ 4, "test1"));
+ gQueue.push(new moveCaretToDOMPoint("test2", getNode("test2_span"), 1,
+ 4, "test2"));
+
+ // empty text element
+ gQueue.push(new moveCaretToDOMPoint("test3", getNode("test3"), 0,
+ 0, "test3"));
+ gQueue.push(new moveCaretToDOMPoint("test4", getNode("test4_span"), 0,
+ 0, "test4"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=454377"
+ title="Accessible caret move events testing">
+ Bug 454377
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=567571"
+ title="caret-moved events missing at the end of a wrapped line of text">
+ Bug 567571
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=824901"
+ title="HyperTextAccessible::DOMPointToHypertextOffset fails for node and offset equal to node child count">
+ Bug 824901
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="textbox" value="hello"/>
+
+ <textarea id="textarea">text<br>text</textarea>
+ <p id="p" contentEditable="true"><span>text</span><br/>text</p>
+ <div id="div" contentEditable="true"><p id="p1_in_div">text</p><p id="p2_in_div">text</p></div>
+
+ <p contentEditable="true" id="test1"><span id="test1_span">text</span>ohoho</p>
+ <p contentEditable="true" id="test2"><span><span id="test2_span">text</span></span>ohoho</p>
+ <p contentEditable="true" id="test3"></p>
+ <p contentEditable="true" id="test4"><span id="test4_span"></span></p>
+
+ <textarea id="textarea_wrapped" cols="5">hey friend</textarea>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible mutation events coalescence testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invoker base classes
+
+ const kRemoveElm = 1;
+ const kHideElm = 2;
+ const kAddElm = 3;
+ const kShowElm = 4;
+
+ /**
+ * Base class to test of mutation events coalescence.
+ */
+ function coalescenceBase(aChildAction, aParentAction,
+ aPerformActionOnChildInTheFirstPlace) {
+ // Invoker interface
+
+ this.invoke = function coalescenceBase_invoke() {
+ if (aPerformActionOnChildInTheFirstPlace) {
+ this.invokeAction(this.childNode, aChildAction);
+ this.invokeAction(this.parentNode, aParentAction);
+ } else {
+ this.invokeAction(this.parentNode, aParentAction);
+ this.invokeAction(this.childNode, aChildAction);
+ }
+ };
+
+ this.getID = function coalescenceBase_getID() {
+ var childAction = this.getActionName(aChildAction) + " child";
+ var parentAction = this.getActionName(aParentAction) + " parent";
+
+ if (aPerformActionOnChildInTheFirstPlace)
+ return childAction + " and then " + parentAction;
+
+ return parentAction + " and then " + childAction;
+ };
+
+ this.finalCheck = function coalescenceBase_check() {
+ if (this.getEventType(aChildAction) == EVENT_HIDE) {
+ testIsDefunct(this.child);
+ }
+ if (this.getEventType(aParentAction) == EVENT_HIDE) {
+ testIsDefunct(this.parent);
+ }
+ };
+
+ // Implementation details
+
+ this.invokeAction = function coalescenceBase_invokeAction(aNode, aAction) {
+ switch (aAction) {
+ case kRemoveElm:
+ aNode.remove();
+ break;
+
+ case kHideElm:
+ aNode.style.display = "none";
+ break;
+
+ case kAddElm:
+ if (aNode == this.parentNode)
+ this.hostNode.appendChild(this.parentNode);
+ else
+ this.parentNode.appendChild(this.childNode);
+ break;
+
+ case kShowElm:
+ aNode.style.display = "block";
+ break;
+
+ default:
+ return INVOKER_ACTION_FAILED;
+ }
+ // 0 means the action succeeded.
+ return 0;
+ };
+
+ this.getEventType = function coalescenceBase_getEventType(aAction) {
+ switch (aAction) {
+ case kRemoveElm: case kHideElm:
+ return EVENT_HIDE;
+ case kAddElm: case kShowElm:
+ return EVENT_SHOW;
+ }
+ return 0;
+ };
+
+ this.getActionName = function coalescenceBase_getActionName(aAction) {
+ switch (aAction) {
+ case kRemoveElm:
+ return "remove";
+ case kHideElm:
+ return "hide";
+ case kAddElm:
+ return "add";
+ case kShowElm:
+ return "show";
+ default:
+ return "??";
+ }
+ };
+
+ this.initSequence = function coalescenceBase_initSequence() {
+ // expected events
+ var eventType = this.getEventType(aParentAction);
+ this.eventSeq = [
+ new invokerChecker(eventType, this.parentNode),
+ new invokerChecker(EVENT_REORDER, this.hostNode),
+ ];
+
+ // unexpected events
+ this.unexpectedEventSeq = [
+ new invokerChecker(this.getEventType(aChildAction), this.childNode),
+ new invokerChecker(EVENT_REORDER, this.parentNode),
+ ];
+ };
+ }
+
+ /**
+ * Remove or hide mutation events coalescence testing.
+ */
+ function removeOrHideCoalescenceBase(aChildID, aParentID,
+ aChildAction, aParentAction,
+ aPerformActionOnChildInTheFirstPlace) {
+ this.__proto__ = new coalescenceBase(aChildAction, aParentAction,
+ aPerformActionOnChildInTheFirstPlace);
+
+ this.init = function removeOrHideCoalescenceBase_init() {
+ this.childNode = getNode(aChildID);
+ this.parentNode = getNode(aParentID);
+ this.child = getAccessible(this.childNode);
+ this.parent = getAccessible(this.parentNode);
+ this.hostNode = this.parentNode.parentNode;
+ };
+
+ // Initalization
+
+ this.init();
+ this.initSequence();
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ /**
+ * Remove child node and then its parent node from DOM tree.
+ */
+ function removeChildNParent(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kRemoveElm, kRemoveElm,
+ true);
+ }
+
+ /**
+ * Remove parent node and then its child node from DOM tree.
+ */
+ function removeParentNChild(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kRemoveElm, kRemoveElm,
+ false);
+ }
+
+ /**
+ * Hide child node and then its parent node.
+ */
+ function hideChildNParent(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kHideElm, kHideElm,
+ true);
+ }
+
+ /**
+ * Hide parent node and then its child node.
+ */
+ function hideParentNChild(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kHideElm, kHideElm,
+ false);
+ }
+
+ /**
+ * Hide child node and then remove its parent node.
+ */
+ function hideChildNRemoveParent(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kHideElm, kRemoveElm,
+ true);
+ }
+
+ /**
+ * Hide parent node and then remove its child node.
+ */
+ function hideParentNRemoveChild(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kRemoveElm, kHideElm,
+ false);
+ }
+
+ /**
+ * Remove child node and then hide its parent node.
+ */
+ function removeChildNHideParent(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kRemoveElm, kHideElm,
+ true);
+ }
+
+ /**
+ * Remove parent node and then hide its child node.
+ */
+ function removeParentNHideChild(aChildID, aParentID) {
+ this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID,
+ kHideElm, kRemoveElm,
+ false);
+ }
+
+ /**
+ * Create and append parent node and create and append child node to it.
+ */
+ function addParentNChild(aHostID, aPerformActionOnChildInTheFirstPlace) {
+ this.init = function addParentNChild_init() {
+ this.hostNode = getNode(aHostID);
+ this.parentNode = document.createElement("select");
+ this.childNode = document.createElement("option");
+ this.childNode.textContent = "testing";
+ };
+
+ this.__proto__ = new coalescenceBase(kAddElm, kAddElm,
+ aPerformActionOnChildInTheFirstPlace);
+
+ this.init();
+ this.initSequence();
+ }
+
+ /**
+ * Show parent node and show child node to it.
+ */
+ function showParentNChild(aParentID, aChildID,
+ aPerformActionOnChildInTheFirstPlace) {
+ this.init = function showParentNChild_init() {
+ this.parentNode = getNode(aParentID);
+ this.hostNode = this.parentNode.parentNode;
+ this.childNode = getNode(aChildID);
+ };
+
+ this.__proto__ = new coalescenceBase(kShowElm, kShowElm,
+ aPerformActionOnChildInTheFirstPlace);
+
+ this.init();
+ this.initSequence();
+ }
+
+ /**
+ * Create and append child node to the DOM and then show parent node.
+ */
+ function showParentNAddChild(aParentID,
+ aPerformActionOnChildInTheFirstPlace) {
+ this.init = function showParentNAddChild_init() {
+ this.parentNode = getNode(aParentID);
+ this.hostNode = this.parentNode.parentNode;
+ this.childNode = document.createElement("option");
+ this.childNode.textContent = "testing";
+ };
+
+ this.__proto__ = new coalescenceBase(kAddElm, kShowElm,
+ aPerformActionOnChildInTheFirstPlace);
+
+ this.init();
+ this.initSequence();
+ }
+
+ /**
+ * Remove children and parent
+ */
+ function removeGrandChildrenNHideParent(aChild1Id, aChild2Id, aParentId) {
+ this.child1 = getNode(aChild1Id);
+ this.child2 = getNode(aChild2Id);
+ this.parent = getNode(aParentId);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getAccessible(aParentId)),
+ new invokerChecker(EVENT_REORDER, getNode(aParentId).parentNode),
+ new unexpectedInvokerChecker(EVENT_HIDE, getAccessible(aChild1Id)),
+ new unexpectedInvokerChecker(EVENT_HIDE, getAccessible(aChild2Id)),
+ new unexpectedInvokerChecker(EVENT_REORDER, getAccessible(aParentId)),
+ ];
+
+ this.invoke = function removeGrandChildrenNHideParent_invoke() {
+ this.child1.remove();
+ this.child2.remove();
+ this.parent.hidden = true;
+ };
+
+ this.getID = function removeGrandChildrenNHideParent_getID() {
+ return "remove grand children of different parents and then hide their grand parent";
+ };
+ }
+
+ /**
+ * Remove a child, and then its parent.
+ */
+ function test3() {
+ this.o = getAccessible("t3_o");
+ this.ofc = getAccessible("t3_o").firstChild;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.o),
+ new invokerChecker(EVENT_REORDER, "t3_lb"),
+ new unexpectedInvokerChecker(EVENT_HIDE, this.ofc),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.o),
+ ];
+
+ this.invoke = function test3_invoke() {
+ getNode("t3_o").textContent = "";
+ getNode("t3_lb").removeChild(getNode("t3_o"));
+ };
+
+ this.finalCheck = function test3_finalCheck() {
+ testIsDefunct(this.o);
+ testIsDefunct(this.ofc);
+ };
+
+ this.getID = function test3_getID() {
+ return "remove a child, and then its parent";
+ };
+ }
+
+ /**
+ * Remove children, and then a parent of 2nd child.
+ */
+ function test4() {
+ this.o1 = getAccessible("t4_o1");
+ this.o1fc = this.o1.firstChild;
+ this.o2 = getAccessible("t4_o2");
+ this.o2fc = this.o2.firstChild;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.o1fc),
+ new invokerChecker(EVENT_HIDE, this.o2),
+ new invokerChecker(EVENT_REORDER, "t4_lb"),
+ new unexpectedInvokerChecker(EVENT_HIDE, this.o2fc),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.o1),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.o2),
+ ];
+
+ this.invoke = function test4_invoke() {
+ getNode("t4_o1").textContent = "";
+ getNode("t4_o2").textContent = "";
+ getNode("t4_lb").removeChild(getNode("t4_o2"));
+ };
+
+ this.finalCheck = function test4_finalCheck() {
+ testIsDefunct(this.o1fc);
+ testIsDefunct(this.o2);
+ testIsDefunct(this.o2fc);
+ };
+
+ this.getID = function test4_getID() {
+ return "remove children, and then a parent of 2nd child";
+ };
+ }
+
+ /**
+ * Remove a child, remove a parent sibling, remove the parent
+ */
+ function test5() {
+ this.o = getAccessible("t5_o");
+ this.ofc = this.o.firstChild;
+ this.b = getAccessible("t5_b");
+ this.lb = getAccessible("t5_lb");
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.b),
+ new invokerChecker(EVENT_HIDE, this.o),
+ new invokerChecker(EVENT_REORDER, "t5"),
+ new unexpectedInvokerChecker(EVENT_HIDE, this.ofc),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.o),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.lb),
+ ];
+
+ this.invoke = function test5_invoke() {
+ getNode("t5_o").textContent = "";
+ getNode("t5").removeChild(getNode("t5_b"));
+ getNode("t5_lb").removeChild(getNode("t5_o"));
+ };
+
+ this.finalCheck = function test5_finalCheck() {
+ testIsDefunct(this.ofc);
+ testIsDefunct(this.o);
+ testIsDefunct(this.b);
+ };
+
+ this.getID = function test5_getID() {
+ return "remove a child, remove a parent sibling, remove the parent";
+ };
+ }
+
+ /**
+ * Insert accessibles with a child node moved by aria-owns
+ * Markup:
+ * <div id="t6_fc">
+ * <div id="t6_owns"></div>
+ * </div>
+ * <div id="t6_sc" aria-owns="t6_owns"></div>
+ */
+ function test6() {
+ this.parent = getNode("t6");
+ this.fc = document.createElement("div");
+ this.fc.setAttribute("id", "t6_fc");
+ this.owns = document.createElement("div");
+ this.owns.setAttribute("id", "t6_owns");
+ this.sc = document.createElement("div");
+ this.sc.setAttribute("id", "t6_sc");
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.fc),
+ new invokerChecker(EVENT_SHOW, this.sc),
+ new invokerChecker(EVENT_REORDER, this.parent),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.fc),
+ new unexpectedInvokerChecker(EVENT_REORDER, this.sc),
+ new unexpectedInvokerChecker(EVENT_HIDE, this.owns),
+ new unexpectedInvokerChecker(EVENT_SHOW, this.owns),
+ ];
+
+ this.invoke = function test6_invoke() {
+ getNode("t6").appendChild(this.fc);
+ getNode("t6_fc").appendChild(this.owns);
+ getNode("t6").appendChild(this.sc);
+ getNode("t6_sc").setAttribute("aria-owns", "t6_owns");
+ };
+
+ this.getID = function test6_getID() {
+ return "Insert accessibles with a child node moved by aria-owns";
+ };
+ }
+
+ /**
+ * Insert text nodes under direct and grand children, and then hide
+ * their container by means of aria-owns.
+ *
+ * Markup:
+ * <div id="t7_moveplace" aria-owns="t7_c"></div>
+ * <div id="t7_c">
+ * <div id="t7_c_directchild">ha</div>
+ * <div><div id="t7_c_grandchild">ha</div></div>
+ * </div>
+ */
+ function test7() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t7_c")),
+ new invokerChecker(EVENT_SHOW, getNode("t7_c")),
+ new invokerChecker(EVENT_REORDER, getNode("t7")),
+ new unexpectedInvokerChecker(EVENT_REORDER, getNode("t7_c_directchild")),
+ new unexpectedInvokerChecker(EVENT_REORDER, getNode("t7_c_grandchild")),
+ new unexpectedInvokerChecker(EVENT_SHOW, () => getNode("t7_c_directchild").firstChild),
+ new unexpectedInvokerChecker(EVENT_SHOW, () => getNode("t7_c_grandchild").firstChild),
+ ];
+
+ this.invoke = function test7_invoke() {
+ getNode("t7_c_directchild").textContent = "ha";
+ getNode("t7_c_grandchild").textContent = "ha";
+ getNode("t7_moveplace").setAttribute("aria-owns", "t7_c");
+ };
+
+ this.getID = function test7_getID() {
+ return "Show child accessibles and then hide their container";
+ };
+ }
+
+ /**
+ * Move a node by aria-owns from right to left in the tree, so that
+ * the eventing looks this way:
+ * reorder for 't8_c1'
+ * hide for 't8_c1_child'
+ * show for 't8_c2_moved'
+ * reorder for 't8_c2'
+ * hide for 't8_c2_moved'
+ *
+ * The hide event should be delivered before the paired show event.
+ */
+ function test8() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t8_c1_child")),
+ new invokerChecker(EVENT_HIDE, "t8_c2_moved"),
+ new invokerChecker(EVENT_SHOW, "t8_c2_moved"),
+ new invokerChecker(EVENT_REORDER, "t8_c2"),
+ new invokerChecker(EVENT_REORDER, "t8_c1"),
+ ];
+
+ this.invoke = function test8_invoke() {
+ // Remove a node from 't8_c1' container to give the event tree a
+ // desired structure (the 't8_c1' container node goes first in the event
+ // tree)
+ getNode("t8_c1_child").remove();
+ // then move 't8_c2_moved' from 't8_c2' to 't8_c1'.
+ getNode("t8_c1").setAttribute("aria-owns", "t8_c2_moved");
+ };
+
+ this.getID = function test8_getID() {
+ return "Move a node by aria-owns to left within the tree";
+ };
+ }
+
+ /**
+ * Move 't9_c3_moved' node under 't9_c2_moved', and then move 't9_c2_moved'
+ * node by aria-owns (same as test10 but has different aria-owns
+ * ordering), the eventing looks same way as in test10:
+ * reorder for 't9_c1'
+ * hide for 't9_c1_child'
+ * show for 't9_c2_moved'
+ * reorder for 't9_c2'
+ * hide for 't9_c2_child'
+ * hide for 't9_c2_moved'
+ * reorder for 't9_c3'
+ * hide for 't9_c3_moved'
+ *
+ * The hide events for 't9_c2_moved' and 't9_c3_moved' should be delivered
+ * before the show event for 't9_c2_moved'.
+ */
+ function test9() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t9_c1_child")),
+ new invokerChecker(EVENT_HIDE, getNode("t9_c2_child")),
+ new invokerChecker(EVENT_HIDE, "t9_c3_moved"),
+ new invokerChecker(EVENT_HIDE, "t9_c2_moved"),
+ new invokerChecker(EVENT_SHOW, "t9_c2_moved"),
+ new invokerChecker(EVENT_REORDER, "t9_c3"),
+ new invokerChecker(EVENT_REORDER, "t9_c2"),
+ new invokerChecker(EVENT_REORDER, "t9_c1"),
+ new unexpectedInvokerChecker(EVENT_SHOW, "t9_c3_moved"),
+ ];
+
+ this.invoke = function test9_invoke() {
+ // Remove child nodes from 't9_c1' and 't9_c2' containers to give
+ // the event tree a needed structure ('t9_c1' and 't9_c2' nodes go
+ // first in the event tree),
+ getNode("t9_c1_child").remove();
+ getNode("t9_c2_child").remove();
+ // then do aria-owns magic.
+ getNode("t9_c2_moved").setAttribute("aria-owns", "t9_c3_moved");
+ getNode("t9_c1").setAttribute("aria-owns", "t9_c2_moved");
+ };
+
+ this.getID = function test9_getID() {
+ return "Move node #1 by aria-owns and then move node #2 into node #1";
+ };
+ }
+
+ /**
+ * Move a node 't10_c3_moved' by aria-owns under a node 't10_c2_moved',
+ * moved by under 't10_1', so that the eventing looks this way:
+ * reorder for 't10_c1'
+ * hide for 't10_c1_child'
+ * show for 't10_c2_moved'
+ * reorder for 't10_c2'
+ * hide for 't10_c2_child'
+ * hide for 't10_c2_moved'
+ * reorder for 't10_c3'
+ * hide for 't10_c3_moved'
+ *
+ * The hide events for 't10_c2_moved' and 't10_c3_moved' should be delivered
+ * before the show event for 't10_c2_moved'.
+ */
+ function test10() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t10_c1_child")),
+ new invokerChecker(EVENT_HIDE, getNode("t10_c2_child")),
+ new invokerChecker(EVENT_HIDE, getNode("t10_c2_moved")),
+ new invokerChecker(EVENT_HIDE, getNode("t10_c3_moved")),
+ new invokerChecker(EVENT_SHOW, getNode("t10_c2_moved")),
+ new invokerChecker(EVENT_REORDER, "t10_c2"),
+ new invokerChecker(EVENT_REORDER, "t10_c1"),
+ new invokerChecker(EVENT_REORDER, "t10_c3"),
+ ];
+
+ this.invoke = function test10_invoke() {
+ // Remove child nodes from 't10_c1' and 't10_c2' containers to give
+ // the event tree a needed structure ('t10_c1' and 't10_c2' nodes go first
+ // in the event tree),
+ getNode("t10_c1_child").remove();
+ getNode("t10_c2_child").remove();
+ // then do aria-owns stuff.
+ getNode("t10_c1").setAttribute("aria-owns", "t10_c2_moved");
+ getNode("t10_c2_moved").setAttribute("aria-owns", "t10_c3_moved");
+ };
+
+ this.getID = function test10_getID() {
+ return "Move a node by aria-owns into a node moved by aria-owns to left within the tree";
+ };
+ }
+
+ /**
+ * Move a node by aria-owns from right to left in the tree, and then
+ * move its parent too by aria-owns. No hide event should be fired for
+ * original node.
+ */
+ function test11() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t11_c1_child")),
+ new invokerChecker(EVENT_HIDE, getNode("t11_c2")),
+ new orderChecker(),
+ new asyncInvokerChecker(EVENT_SHOW, "t11_c2_child"),
+ new asyncInvokerChecker(EVENT_SHOW, "t11_c2"),
+ new orderChecker(),
+ new invokerChecker(EVENT_REORDER, "t11"),
+ new unexpectedInvokerChecker(EVENT_HIDE, "t11_c2_child"),
+ new unexpectedInvokerChecker(EVENT_REORDER, "t11_c1"),
+ new unexpectedInvokerChecker(EVENT_REORDER, "t11_c2"),
+ new unexpectedInvokerChecker(EVENT_REORDER, "t11_c3"),
+ ];
+
+ this.invoke = function test11_invoke() {
+ // Remove a node from 't11_c1' container to give the event tree a
+ // desired structure (the 't11_c1' container node goes first in
+ // the event tree),
+ getNode("t11_c1_child").remove();
+ // then move 't11_c2_moved' from 't11_c2' to 't11_c1', and then move
+ // 't11_c2' to 't11_c3'.
+ getNode("t11_c1").setAttribute("aria-owns", "t11_c2_child");
+ getNode("t11_c3").setAttribute("aria-owns", "t11_c2");
+ };
+
+ this.getID = function test11_getID() {
+ return "Move a node by aria-owns to left within the tree";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests.
+
+ gA11yEventDumpToConsole = true; // debug stuff
+ // enableLogging("eventTree");
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new removeChildNParent("option1", "select1"));
+ gQueue.push(new removeParentNChild("option2", "select2"));
+ gQueue.push(new hideChildNParent("option3", "select3"));
+ gQueue.push(new hideParentNChild("option4", "select4"));
+ gQueue.push(new hideChildNRemoveParent("option5", "select5"));
+ gQueue.push(new hideParentNRemoveChild("option6", "select6"));
+ gQueue.push(new removeChildNHideParent("option7", "select7"));
+ gQueue.push(new removeParentNHideChild("option8", "select8"));
+
+ gQueue.push(new addParentNChild("testContainer", false));
+ gQueue.push(new addParentNChild("testContainer", true));
+ gQueue.push(new showParentNChild("select9", "option9", false));
+ gQueue.push(new showParentNChild("select10", "option10", true));
+ gQueue.push(new showParentNAddChild("select11", false));
+ gQueue.push(new showParentNAddChild("select12", true));
+
+ gQueue.push(new removeGrandChildrenNHideParent("t1_child1", "t1_child2", "t1_parent"));
+ gQueue.push(new test3());
+ gQueue.push(new test4());
+ gQueue.push(new test5());
+ gQueue.push(new test6());
+ gQueue.push(new test7());
+ gQueue.push(new test8());
+ gQueue.push(new test9());
+ gQueue.push(new test10());
+ gQueue.push(new test11());
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=513213"
+ title="coalesce events when new event is appended to the queue">
+ Mozilla Bug 513213
+ </a><br>
+ <a target="_blank"
+ title="Rework accessible tree update code"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275">
+ Mozilla Bug 570275
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="testContainer">
+ <select id="select1">
+ <option id="option1">option</option>
+ </select>
+ <select id="select2">
+ <option id="option2">option</option>
+ </select>
+ <select id="select3">
+ <option id="option3">option</option>
+ </select>
+ <select id="select4">
+ <option id="option4">option</option>
+ </select>
+ <select id="select5">
+ <option id="option5">option</option>
+ </select>
+ <select id="select6">
+ <option id="option6">option</option>
+ </select>
+ <select id="select7">
+ <option id="option7">option</option>
+ </select>
+ <select id="select8">
+ <option id="option8">option</option>
+ </select>
+
+ <select id="select9" style="display: none">
+ <option id="option9" style="display: none">testing</option>
+ </select>
+ <select id="select10" style="display: none">
+ <option id="option10" style="display: none">testing</option>
+ </select>
+ <select id="select11" style="display: none"></select>
+ <select id="select12" style="display: none"></select>
+ </div>
+
+ <div id="testContainer2">
+ <div id="t1_parent">
+ <div id="t1_mid1"><div id="t1_child1"></div></div>
+ <div id="t1_mid2"><div id="t1_child2"></div></div>
+ </div>
+ </div>
+
+ <div id="t3">
+ <div role="listbox" id="t3_lb">
+ <div role="option" id="t3_o">opt</div>
+ </div>
+ </div>
+
+ <div id="t4">
+ <div role="listbox" id="t4_lb">
+ <div role="option" id="t4_o1">opt1</div>
+ <div role="option" id="t4_o2">opt2</div>
+ </div>
+ </div>
+
+ <div id="t5">
+ <div role="button" id="t5_b">btn</div>
+ <div role="listbox" id="t5_lb">
+ <div role="option" id="t5_o">opt</div>
+ </div>
+ </div>
+
+ <div id="t6">
+ </div>
+
+ <div id="t7">
+ <div id="t7_moveplace"></div>
+ <div id="t7_c">
+ <div><div id="t7_c_grandchild"></div></div>
+ <div id="t7_c_directchild"></div>
+ </div>
+ </div>
+
+ <div id="t8">
+ <div id="t8_c1"><div id="t8_c1_child"></div></div>
+ <div id="t8_c2">
+ <div id="t8_c2_moved"></div>
+ </div>
+ </div>
+
+ <div id="t9">
+ <div id="t9_c1"><div id="t9_c1_child"></div></div>
+ <div id="t9_c2">
+ <div id="t9_c2_child"></div>
+ <div id="t9_c2_moved"></div>
+ </div>
+ <div id="t9_c3">
+ <div id="t9_c3_moved"></div>
+ </div>
+ </div>
+
+ <div id="t10">
+ <div id="t10_c1"><div id="t10_c1_child"></div></div>
+ <div id="t10_c2">
+ <div id="t10_c2_child"></div>
+ <div id="t10_c2_moved"></div>
+ </div>
+ <div id="t10_c3">
+ <div id="t10_c3_moved"></div>
+ </div>
+ </div>
+
+ <div id="t11">
+ <div id="t11_c1"><div id="t11_c1_child"></div></div>
+ <div id="t11_c2"><div id="t11_c2_child"></div></div>
+ <div id="t11_c3"></div>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_contextmenu.html b/accessible/tests/mochitest/events/test_contextmenu.html
new file mode 100644
index 0000000000..6200efc00d
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_contextmenu.html
@@ -0,0 +1,133 @@
+<html>
+
+<head>
+ <title>Context menu tests</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function showContextMenu(aID) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_MENUPOPUP_START, getContextMenuNode()),
+ ];
+
+ this.invoke = function showContextMenu_invoke() {
+ synthesizeMouse(this.DOMNode, 4, 4, { type: "contextmenu", button: 2 });
+ };
+
+ this.getID = function showContextMenu_getID() {
+ return "show context menu";
+ };
+ }
+
+ function selectMenuItem() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, getFocusedMenuItem),
+ ];
+
+ this.invoke = function selectMenuItem_invoke() {
+ synthesizeKey("KEY_ArrowDown");
+ };
+
+ this.getID = function selectMenuItem_getID() {
+ return "select first menuitem";
+ };
+ }
+
+ function closeContextMenu(aID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_MENUPOPUP_END,
+ getAccessible(getContextMenuNode())),
+ ];
+
+ this.invoke = function closeContextMenu_invoke() {
+ synthesizeKey("KEY_Escape");
+ };
+
+ this.getID = function closeContextMenu_getID() {
+ return "close context menu";
+ };
+ }
+
+ function getContextMenuNode() {
+ return getRootAccessible().DOMDocument.
+ getElementById("contentAreaContextMenu");
+ }
+
+ function getFocusedMenuItem() {
+ var menu = getAccessible(getAccessible(getContextMenuNode()));
+ for (var idx = 0; idx < menu.childCount; idx++) {
+ var item = menu.getChildAt(idx);
+
+ if (hasState(item, STATE_FOCUSED))
+ return getAccessible(item);
+ }
+ return null;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ var gQueue = null;
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new showContextMenu("input"));
+ gQueue.push(new selectMenuItem());
+ gQueue.push(new closeContextMenu());
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ const {AppConstants} = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+ if (AppConstants.platform == "macosx" &&
+ Services.prefs.getBoolPref("widget.macos.native-context-menus", false)) {
+ ok(true, "Native context menus handle accessibility notifications natively and cannot be tested with synthesized key events.");
+ } else {
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ }
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=580535"
+ title="Broken accessibility in context menus">
+ Mozilla Bug 580535
+ </a><br>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="input">
+
+ <div id="eventdump"></div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible description change event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ let PromEvents = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js",
+ PromEvents);
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function setAttr(aID, aAttr, aValue, aChecker) {
+ this.eventSeq = [ aChecker ];
+ this.invoke = function setAttr_invoke() {
+ getNode(aID).setAttribute(aAttr, aValue);
+ };
+
+ this.getID = function setAttr_getID() {
+ return "set attr '" + aAttr + "', value '" + aValue + "'";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ // gA11yEventDumpToConsole = true; // debuggin
+
+ var gQueue = null;
+ async function doTests() {
+ gQueue = new eventQueue();
+ // Later tests use await.
+ let queueFinished = new Promise(resolve => {
+ gQueue.onFinish = function() {
+ resolve();
+ return DO_NOT_FINISH_TEST;
+ };
+ });
+
+ gQueue.push(new setAttr("tst1", "aria-describedby", "display",
+ new invokerChecker(EVENT_DESCRIPTION_CHANGE, "tst1")));
+ gQueue.push(new setAttr("tst1", "title", "title",
+ new unexpectedInvokerChecker(EVENT_DESCRIPTION_CHANGE, "tst1")));
+
+ // A title has lower priority over text content. There should be no name change event.
+ gQueue.push(new setAttr("tst2", "title", "title",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2")));
+
+ gQueue.invoke();
+ await queueFinished;
+ // Tests beyond this point use await rather than eventQueue.
+
+ const describedBy = getNode("describedBy");
+ const description = getNode("description");
+ let descChanged = PromEvents.waitForEvent(
+ EVENT_DESCRIPTION_CHANGE,
+ describedBy
+ );
+ info("Changing text of aria-describedby target");
+ description.textContent = "d2";
+ await descChanged;
+ descChanged = PromEvents.waitForEvent(
+ EVENT_DESCRIPTION_CHANGE,
+ describedBy
+ );
+ info("Adding node to aria-describedby target");
+ description.innerHTML = '<p id="descriptionChild">d3</p>';
+ await descChanged;
+ descChanged = PromEvents.waitForEvent(
+ EVENT_DESCRIPTION_CHANGE,
+ describedBy
+ );
+ info("Changing text of aria-describedby target's child");
+ getNode("descriptionChild").textContent = "d4";
+ await descChanged;
+
+ const lateDescribedBy = getNode("lateDescribedBy");
+ descChanged = PromEvents.waitForEvent(
+ EVENT_DESCRIPTION_CHANGE,
+ lateDescribedBy
+ );
+ info("Setting aria-describedby");
+ lateDescribedBy.setAttribute("aria-describedby", "lateDescription");
+ await descChanged;
+ descChanged = PromEvents.waitForEvent(
+ EVENT_DESCRIPTION_CHANGE,
+ lateDescribedBy
+ );
+ info("Changing text of late aria-describedby target's child");
+ getNode("lateDescriptionChild").textContent = "d2";
+ await descChanged;
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=991969"
+ title="Event not fired when description changes">
+ Bug 991969
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <button id="tst1">btn1</button>
+ <button id="tst2">btn2</button>
+
+ <div id="describedBy" aria-describedby="description"></div>
+ <div id="description">d1</div>
+
+ <div id="lateDescribedBy"></div>
+ <div id="lateDescription"><p id="lateDescriptionChild">d1</p></div>
+
+ <div id="eventdump"></div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible drag and drop event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ // aria grabbed invoker
+ function changeGrabbed(aNodeOrID, aGrabValue) {
+ this.DOMNode = getNode(aNodeOrID);
+
+ this.invoke = function changeGrabbed_invoke() {
+ if (aGrabValue != undefined) {
+ this.DOMNode.setAttribute("aria-grabbed", aGrabValue);
+ }
+ };
+
+ this.check = function changeGrabbed_check() {
+ testAttrs(aNodeOrID, {"grabbed": aGrabValue}, true);
+ };
+
+ this.getID = function changeGrabbed_getID() {
+ return prettyName(aNodeOrID) + " aria-grabbed changed";
+ };
+ }
+
+ // aria dropeffect invoker
+ function changeDropeffect(aNodeOrID, aDropeffectValue) {
+ this.DOMNode = getNode(aNodeOrID);
+
+ this.invoke = function changeDropeffect_invoke() {
+ if (aDropeffectValue != undefined) {
+ this.DOMNode.setAttribute("aria-dropeffect", aDropeffectValue);
+ }
+ };
+
+ this.check = function changeDropeffect_check() {
+ testAttrs(aNodeOrID, {"dropeffect": aDropeffectValue}, true);
+ };
+
+ this.getID = function changeDropeffect_getID() {
+ return prettyName(aNodeOrID) + " aria-dropeffect changed";
+ };
+ }
+
+ function doTests() {
+ // Test aria attribute mutation events
+ gQueue = new eventQueue(nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED);
+
+ let id = "grabbable";
+ gQueue.push(new changeGrabbed(id, "true"));
+ gQueue.push(new changeGrabbed(id, "false"));
+ todo(false, "uncomment this test when 472142 is fixed.");
+ // gQueue.push(new changeGrabbed(id, "undefined"));
+
+ id = "dropregion";
+ gQueue.push(new changeDropeffect(id, "copy"));
+ gQueue.push(new changeDropeffect(id, "execute"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=510441"
+ title="Add support for nsIAccessibleEvent::OBJECT_ATTRIBUTE_CHANGED">
+ Mozilla Bug 510441
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="eventdump"></div>
+
+ <!-- ARIA grabbed -->
+ <div id="grabbable" role="button" aria-grabbed="foo">button</div>
+
+ <!-- ARIA dropeffect -->
+ <div id="dropregion" role="region" aria-dropeffect="none">button</div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Flush delayed events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ SimpleTest.expectAssertions(0, 1);
+
+ var gFocusHandler = {
+ handleEvent(aEvent) {
+ switch (this.count) {
+ case 0:
+ is(aEvent.DOMNode, getNode("input1"),
+ "Focus event for input1 was expected!");
+ getAccessible("input2").takeFocus();
+ break;
+
+ case 1:
+ is(aEvent.DOMNode, getNode("input2"),
+ "Focus event for input2 was expected!");
+
+ unregisterA11yEventListener(EVENT_FOCUS, gFocusHandler);
+ SimpleTest.finish();
+ break;
+
+ default:
+ ok(false, "Wrong focus event!");
+ }
+
+ this.count++;
+ },
+
+ count: 0,
+ };
+
+ function doTests() {
+ registerA11yEventListener(EVENT_FOCUS, gFocusHandler);
+
+ getAccessible("input1").takeFocus();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=477551"
+ title="DocAccessible::FlushPendingEvents isn't robust">
+ Mozilla Bug 477551
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="input1">
+ <input id="input2">
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=429547
+-->
+<head>
+ <title>aria-activedescendant focus tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ let PromEvents = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js",
+ PromEvents);
+ // gA11yEventDumpToConsole = true; // debugging
+
+ function changeARIAActiveDescendant(aContainer, aItem, aPrevItemId) {
+ let itemID = Node.isInstance(aItem) ? aItem.id : aItem;
+ this.eventSeq = [new focusChecker(aItem)];
+
+ if (aPrevItemId) {
+ this.eventSeq.push(
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, false, aPrevItemId)
+ );
+ }
+
+ this.eventSeq.push(
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, true, aItem)
+ );
+
+ this.invoke = function changeARIAActiveDescendant_invoke() {
+ getNode(aContainer).setAttribute("aria-activedescendant", itemID);
+ };
+
+ this.getID = function changeARIAActiveDescendant_getID() {
+ return "change aria-activedescendant on " + itemID;
+ };
+ }
+
+ function clearARIAActiveDescendant(aID, aPrevItemId) {
+ this.eventSeq = [
+ new focusChecker(aID),
+ ];
+
+ if (aPrevItemId) {
+ this.eventSeq.push(
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, false, aPrevItemId)
+ );
+ }
+
+ this.invoke = function clearARIAActiveDescendant_invoke() {
+ getNode(aID).removeAttribute("aria-activedescendant");
+ };
+
+ this.getID = function clearARIAActiveDescendant_getID() {
+ return "clear aria-activedescendant on container " + aID;
+ };
+ }
+
+ /**
+ * Change aria-activedescendant to an invalid (non-existent) id.
+ * Ensure that focus is fired on the element itself.
+ */
+ function changeARIAActiveDescendantInvalid(aID, aInvalidID, aPrevItemId) {
+ if (!aInvalidID) {
+ aInvalidID = "invalid";
+ }
+
+ this.eventSeq = [
+ new focusChecker(aID),
+ ];
+
+ if (aPrevItemId) {
+ this.eventSeq.push(
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, false, aPrevItemId)
+ );
+ }
+
+ this.invoke = function changeARIAActiveDescendant_invoke() {
+ getNode(aID).setAttribute("aria-activedescendant", aInvalidID);
+ };
+
+ this.getID = function changeARIAActiveDescendant_getID() {
+ return "change aria-activedescendant to invalid id";
+ };
+ }
+
+ function insertItemNFocus(aID, aNewItemID, aPrevItemId) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, aNewItemID),
+ new focusChecker(aNewItemID),
+ ];
+
+ if (aPrevItemId) {
+ this.eventSeq.push(
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, false, aPrevItemId)
+ );
+ }
+
+ this.eventSeq.push(
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, true, aNewItemID)
+ );
+
+ this.invoke = function insertItemNFocus_invoke() {
+ var container = getNode(aID);
+
+ var itemNode = document.createElement("div");
+ itemNode.setAttribute("id", aNewItemID);
+ itemNode.setAttribute("role", "listitem");
+ itemNode.textContent = aNewItemID;
+ container.appendChild(itemNode);
+
+ container.setAttribute("aria-activedescendant", aNewItemID);
+ };
+
+ this.getID = function insertItemNFocus_getID() {
+ return "insert new node and focus it with ID: " + aNewItemID;
+ };
+ }
+
+ /**
+ * Change the id of an element to another id which is the target of
+ * aria-activedescendant.
+ * If another element already has the desired id, remove it from that
+ * element first.
+ * Ensure that focus is fired on the target element which was given the
+ * desired id.
+ * @param aFromID The existing id of the target element.
+ * @param aToID The desired id to be given to the target element.
+ */
+ function moveARIAActiveDescendantID(aFromID, aToID) {
+ this.eventSeq = [
+ new focusChecker(aToID),
+ new stateChangeChecker(EXT_STATE_ACTIVE, true, true, aToID),
+ ];
+
+ this.invoke = function moveARIAActiveDescendantID_invoke() {
+ let orig = document.getElementById(aToID);
+ if (orig) {
+ orig.id = "";
+ }
+ document.getElementById(aFromID).id = aToID;
+ };
+
+ this.getID = function moveARIAActiveDescendantID_getID() {
+ return "move aria-activedescendant id " + aToID;
+ };
+ }
+
+ var gQueue = null;
+ async function doTest() {
+ gQueue = new eventQueue();
+ // Later tests use await.
+ let queueFinished = new Promise(resolve => {
+ gQueue.onFinish = function() {
+ resolve();
+ return DO_NOT_FINISH_TEST;
+ };
+ });
+
+ gQueue.push(new synthFocus("listbox", new focusChecker("item1")));
+ gQueue.push(new changeARIAActiveDescendant("listbox", "item2", "item1"));
+ gQueue.push(new changeARIAActiveDescendant("listbox", "item3", "item2"));
+
+ gQueue.push(new synthFocus("combobox_entry", new focusChecker("combobox_entry")));
+ gQueue.push(new changeARIAActiveDescendant("combobox", "combobox_option2"));
+
+ gQueue.push(new synthFocus("listbox", new focusChecker("item3")));
+ gQueue.push(new insertItemNFocus("listbox", "item4", "item3"));
+
+ gQueue.push(new clearARIAActiveDescendant("listbox", "item4"));
+ gQueue.push(new changeARIAActiveDescendant("listbox", "item1"));
+ gQueue.push(new changeARIAActiveDescendantInvalid("listbox", "invalid", "item1"));
+
+ gQueue.push(new changeARIAActiveDescendant("listbox", "roaming"));
+ gQueue.push(new moveARIAActiveDescendantID("roaming2", "roaming"));
+ gQueue.push(new changeARIAActiveDescendantInvalid("listbox", "roaming3", "roaming"));
+ gQueue.push(new moveARIAActiveDescendantID("roaming", "roaming3"));
+
+ gQueue.push(new synthFocus("activedesc_nondesc_input",
+ new focusChecker("activedesc_nondesc_option")));
+
+ let shadowRoot = document.getElementById("shadow").shadowRoot;
+ let shadowListbox = shadowRoot.getElementById("shadowListbox");
+ let shadowItem1 = shadowRoot.getElementById("shadowItem1");
+ let shadowItem2 = shadowRoot.getElementById("shadowItem2");
+ gQueue.push(new synthFocus(shadowListbox, new focusChecker(shadowItem1)));
+ gQueue.push(new changeARIAActiveDescendant(shadowListbox, shadowItem2));
+
+ gQueue.invoke();
+ await queueFinished;
+ // Tests beyond this point use await rather than eventQueue.
+
+ info("Testing simultaneous insertion, relocation and aria-activedescendant");
+ let comboboxWithHiddenList = getNode("comboboxWithHiddenList");
+ let evtProm = PromEvents.waitForEvent(EVENT_FOCUS, comboboxWithHiddenList);
+ comboboxWithHiddenList.focus();
+ await evtProm;
+ testStates(comboboxWithHiddenList, STATE_FOCUSED);
+ // hiddenList is owned, so unhiding causes insertion and relocation.
+ getNode("hiddenList").hidden = false;
+ evtProm = Promise.all([
+ PromEvents.waitForEvent(EVENT_FOCUS, "hiddenListOption"),
+ PromEvents.waitForStateChange("hiddenListOption", EXT_STATE_ACTIVE, true, true),
+ ]);
+ comboboxWithHiddenList.setAttribute("aria-activedescendant", "hiddenListOption");
+ await evtProm;
+ testStates("hiddenListOption", STATE_FOCUSED);
+
+ info("Testing active state changes when not focused");
+ testStates("listbox", 0, 0, STATE_FOCUSED);
+ evtProm = Promise.all([
+ PromEvents.waitForStateChange("roaming3", EXT_STATE_ACTIVE, false, true),
+ PromEvents.waitForStateChange("item1", EXT_STATE_ACTIVE, true, true),
+ ]);
+ getNode("listbox").setAttribute("aria-activedescendant", "item1");
+ await evtProm;
+
+ info("Testing that focus is always fired first");
+ const listbox = getNode("listbox");
+ evtProm = PromEvents.waitForEvent(EVENT_FOCUS, "item1");
+ listbox.focus();
+ await evtProm;
+ const item1 = getNode("item1");
+ evtProm = PromEvents.waitForOrderedEvents([
+ [EVENT_FOCUS, "item2"],
+ [EVENT_NAME_CHANGE, item1],
+ ], "Focus then name change");
+ item1.setAttribute("aria-label", "changed");
+ listbox.setAttribute("aria-activedescendant", "item2");
+ await evtProm;
+
+ info("Setting aria-activedescendant to invalid id on non-focused node");
+ const combobox_entry = getNode("combobox_entry");
+ evtProm = PromEvents.waitForEvents({
+ expected: [[EVENT_FOCUS, combobox_entry]],
+ unexpected: [[EVENT_FOCUS, listbox]],
+ });
+ combobox_entry.focus();
+ listbox.setAttribute("aria-activedescendant", "invalid");
+ await evtProm;
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=429547"
+ title="Support aria-activedescendant usage in nsIAccesible::TakeFocus()">
+ Mozilla Bug 429547
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=761102"
+ title="Focus may be missed when ARIA active-descendant is changed on active composite widget">
+ Mozilla Bug 761102
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div role="listbox" aria-activedescendant="item1" id="listbox" tabindex="1"
+ aria-owns="item3">
+ <div role="listitem" id="item1">item1</div>
+ <div role="listitem" id="item2">item2</div>
+ <div role="listitem" id="roaming">roaming</div>
+ <div role="listitem" id="roaming2">roaming2</div>
+ </div>
+ <div role="listitem" id="item3">item3</div>
+
+ <div role="combobox" id="combobox">
+ <input id="combobox_entry">
+ <ul>
+ <li role="option" id="combobox_option1">option1</li>
+ <li role="option" id="combobox_option2">option2</li>
+ </ul>
+ </div>
+
+ <!-- aria-activedescendant targeting a non-descendant -->
+ <input id="activedesc_nondesc_input" aria-activedescendant="activedesc_nondesc_option">
+ <div role="listbox">
+ <div role="option" id="activedesc_nondesc_option">option</div>
+ </div>
+
+ <div id="shadow"></div>
+ <script>
+ let host = document.getElementById("shadow");
+ let shadow = host.attachShadow({mode: "open"});
+ let listbox = document.createElement("div");
+ listbox.id = "shadowListbox";
+ listbox.setAttribute("role", "listbox");
+ listbox.setAttribute("tabindex", "0");
+ shadow.appendChild(listbox);
+ let item = document.createElement("div");
+ item.id = "shadowItem1";
+ item.setAttribute("role", "option");
+ listbox.appendChild(item);
+ listbox.setAttribute("aria-activedescendant", "shadowItem1");
+ item = document.createElement("div");
+ item.id = "shadowItem2";
+ item.setAttribute("role", "option");
+ listbox.appendChild(item);
+ </script>
+
+ <div id="comboboxWithHiddenList" tabindex="0" role="combobox" aria-owns="hiddenList">
+ </div>
+ <div id="hiddenList" hidden role="listbox">
+ <div id="hiddenListOption" role="option"></div>
+ </div>
+</body>
+</html>
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..c7fdbbb6a5
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_autocomplete.html
@@ -0,0 +1,83 @@
+<!doctype html>
+
+<head>
+ <title>Form Autocomplete Tests</title>
+
+ <link rel="stylesheet"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script src="../common.js"></script>
+ <script src="../promisified-events.js"></script>
+ <script src="../role.js"></script>
+
+ <script type="application/javascript">
+ const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs");
+
+ async function waitForFocusOnOptionWithname(name) {
+ let event = await waitForEvent(
+ EVENT_FOCUS,
+ evt => evt.accessible.role == ROLE_COMBOBOX_OPTION
+ );
+ while (!event.accessible.name) {
+ // Sometimes, the name is null for a very short time after the focus
+ // event.
+ await waitForEvent(EVENT_NAME_CHANGE, event.accessible);
+ }
+ is(event.accessible.name, name, "Got focus on option with name " + name);
+ }
+
+ async function doTests() {
+ const input = getNode("input");
+ info("Focusing the input");
+ let focused = waitForEvent(EVENT_FOCUS, input);
+ input.focus();
+ await focused;
+
+ let shown = waitForEvent(EVENT_SHOW, event =>
+ event.accessible.role == ROLE_GROUPING &&
+ event.accessible.firstChild.role == ROLE_COMBOBOX_LIST);
+ info("Pressing ArrowDown to open the popup");
+ synthesizeKey("KEY_ArrowDown");
+ await shown;
+ // The popup still doesn't seem to be ready even once it's fired an a11y
+ // show event!
+ const controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+ info("Waiting for popup to be fully open and ready");
+ await TestUtils.waitForCondition(() => controller.input.popupOpen);
+
+ focused = waitForFocusOnOptionWithname("a");
+ info("Pressing ArrowDown to focus first item");
+ synthesizeKey("KEY_ArrowDown");
+ await focused;
+
+ focused = waitForFocusOnOptionWithname("b");
+ info("Pressing ArrowDown to focus the second item");
+ synthesizeKey("KEY_ArrowDown");
+ await focused;
+
+ focused = waitForEvent(EVENT_FOCUS, input);
+ info("Pressing enter to select the second item");
+ synthesizeKey("KEY_Enter");
+ await focused;
+ is(input.value, "b", "input value filled with second item");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+<body>
+ <input id="input" list="list">
+ <datalist id="list">
+ <option id="a" value="a">
+ <option id="b" value="b">
+ </datalist>
+</body>
+</html>
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 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<!-- Firefox searchbar -->
+<?xml-stylesheet href="chrome://browser/content/browser.css"
+ type="text/css"?>
+<!-- SeaMonkey searchbar -->
+<?xml-stylesheet href="chrome://navigator/content/navigator.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Accessible focus event testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript"
+ src="../autocomplete.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Hacky stuffs
+
+ // This is the hacks needed to use a searchbar without browser.js.
+ var BrowserSearch = {
+ updateOpenSearchBadge() {}
+ };
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function loadFormAutoComplete(aIFrameID)
+ {
+ this.iframeNode = getNode(aIFrameID);
+ this.iframe = getAccessible(aIFrameID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.iframe)
+ ];
+
+ this.invoke = function loadFormAutoComplete_invoke()
+ {
+ var url = "data:text/html,<html><body><form id='form'>" +
+ "<input id='input' name='a11ytest-formautocomplete'>" +
+ "</form></body></html>";
+ this.iframeNode.setAttribute("src", url);
+ }
+
+ this.getID = function loadFormAutoComplete_getID()
+ {
+ return "load form autocomplete page";
+ }
+ }
+
+ function initFormAutoCompleteBy(aIFrameID, aAutoCompleteValue)
+ {
+ this.iframe = getAccessible(aIFrameID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.iframe)
+ ];
+
+ this.invoke = function initFormAutoCompleteBy_invoke()
+ {
+ var iframeDOMDoc = getIFrameDOMDoc(aIFrameID);
+
+ var inputNode = iframeDOMDoc.getElementById("input");
+ inputNode.value = aAutoCompleteValue;
+ var formNode = iframeDOMDoc.getElementById("form");
+ formNode.submit();
+ }
+
+ this.getID = function initFormAutoCompleteBy_getID()
+ {
+ return "init form autocomplete by '" + aAutoCompleteValue + "'";
+ }
+ }
+
+ function loadHTML5ListAutoComplete(aIFrameID)
+ {
+ this.iframeNode = getNode(aIFrameID);
+ this.iframe = getAccessible(aIFrameID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.iframe)
+ ];
+
+ this.invoke = function loadHTML5ListAutoComplete_invoke()
+ {
+ var url = "data:text/html,<html><body>" +
+ "<datalist id='cities'><option>hello</option><option>hi</option></datalist>" +
+ "<input id='input' list='cities'>" +
+ "</body></html>";
+ this.iframeNode.setAttribute("src", url);
+ }
+
+ this.getID = function loadHTML5ListAutoComplete_getID()
+ {
+ return "load HTML5 list autocomplete page";
+ }
+ }
+
+ function removeChar(aID, aCheckerOrEventSeq)
+ {
+ this.__proto__ = new synthAction(aID, aCheckerOrEventSeq);
+
+ this.invoke = function removeChar_invoke()
+ {
+ synthesizeKey("KEY_ArrowLeft", {shiftKey: true});
+ synthesizeKey("KEY_Delete");
+ }
+
+ this.getID = function removeChar_getID()
+ {
+ return "remove char on " + prettyName(aID);
+ }
+ }
+
+ function replaceOnChar(aID, aChar, aCheckerOrEventSeq)
+ {
+ this.__proto__ = new synthAction(aID, aCheckerOrEventSeq);
+
+ this.invoke = function replaceOnChar_invoke()
+ {
+ this.DOMNode.select();
+ sendString(aChar);
+ }
+
+ this.getID = function replaceOnChar_getID()
+ {
+ return "replace on char '" + aChar + "' for" + prettyName(aID);
+ }
+ }
+
+ function focusOnMouseOver(aIDFunc, aIDFuncArg)
+ {
+ this.eventSeq = [ new focusChecker(aIDFunc, aIDFuncArg) ];
+
+ this.invoke = function focusOnMouseOver_invoke()
+ {
+ this.id = aIDFunc(aIDFuncArg);
+ this.node = getNode(this.id);
+ this.window = this.node.ownerGlobal;
+
+ if (this.node.localName == "tree") {
+ var tree = getAccessible(this.node);
+ var accessible = getAccessible(this.id);
+ if (tree != accessible) {
+ var itemX = {}, itemY = {}, treeX = {}, treeY = {};
+ accessible.getBounds(itemX, itemY, {}, {});
+ tree.getBounds(treeX, treeY, {}, {});
+ this.x = itemX.value - treeX.value;
+ this.y = itemY.value - treeY.value;
+ }
+ }
+
+ // Generate mouse move events in timeouts until autocomplete popup list
+ // doesn't have it, the reason is do that because autocomplete popup
+ // ignores mousemove events firing in too short range.
+ synthesizeMouse(this.node, this.x, this.y, { type: "mousemove" });
+ this.doMouseMoveFlood(this);
+ }
+
+ this.finalCheck = function focusOnMouseOver_getID()
+ {
+ this.isFlooding = false;
+ }
+
+ this.getID = function focusOnMouseOver_getID()
+ {
+ return "mouse over on " + prettyName(aIDFunc(aIDFuncArg));
+ }
+
+ this.doMouseMoveFlood = function focusOnMouseOver_doMouseMoveFlood(aThis)
+ {
+ synthesizeMouse(aThis.node, aThis.x + 1, aThis.y + 1,
+ { type: "mousemove" }, aThis.window);
+
+ if (aThis.isFlooding)
+ aThis.window.setTimeout(aThis.doMouseMoveFlood, 0, aThis);
+ }
+
+ this.id = null;
+ this.node = null;
+ this.window = null;
+
+ this.isFlooding = true;
+ this.x = 1;
+ this.y = 1;
+ }
+
+ function selectByClick(aIDFunc, aIDFuncArg,
+ aFocusTargetFunc, aFocusTargetFuncArg)
+ {
+ this.eventSeq = [ new focusChecker(aFocusTargetFunc, aFocusTargetFuncArg) ];
+
+ this.invoke = function selectByClick_invoke()
+ {
+ var id = aIDFunc(aIDFuncArg);
+ var node = getNode(id);
+ var targetWindow = node.ownerGlobal;
+
+ if (node.localName == "tree") {
+ var tree = getAccessible(node);
+ var accessible = getAccessible(id);
+ if (tree != accessible) {
+ var itemX = {}, itemY = {}, treeX = {}, treeY = {};
+ accessible.getBounds(itemX, itemY, {}, {});
+ tree.getBounds(treeX, treeY, {}, {});
+ this.x = itemX.value - treeX.value;
+ this.y = itemY.value - treeY.value;
+ }
+ }
+
+ synthesizeMouseAtCenter(node, {}, targetWindow);
+ }
+
+ this.getID = function selectByClick_getID()
+ {
+ return "select by click " + prettyName(aIDFunc(aIDFuncArg));
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Target getters
+
+ function getItem(aItemObj)
+ {
+ var autocompleteNode = aItemObj.autocompleteNode;
+
+ // XUL searchbar
+ if (autocompleteNode.localName == "searchbar") {
+ let popupNode = autocompleteNode._popup;
+ if (popupNode) {
+ let list = getAccessible(popupNode);
+ return list.getChildAt(aItemObj.index);
+ }
+ }
+
+ // XUL autocomplete
+ let popupNode = autocompleteNode.popup;
+ if (!popupNode) {
+ // HTML form autocomplete
+ var controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+ popupNode = controller.input.popup;
+ }
+
+ if (popupNode) {
+ if ("richlistbox" in popupNode) {
+ let list = getAccessible(popupNode.richlistbox);
+ return list.getChildAt(aItemObj.index);
+ }
+
+ let list = getAccessible(popupNode.tree);
+ return list.getChildAt(aItemObj.index + 1);
+ }
+ return null;
+ }
+
+ function getTextEntry(aID)
+ {
+ // For form autocompletes the autocomplete widget and text entry widget
+ // is the same widget, for XUL autocompletes the text entry is a first
+ // child.
+ var localName = getNode(aID).localName;
+
+ // HTML form autocomplete
+ if (localName == "input")
+ return getAccessible(aID);
+
+ // XUL searchbar
+ if (localName == "searchbar")
+ return getAccessible(getNode(aID).textbox);
+
+ return null;
+ }
+
+ function itemObj(aID, aIdx)
+ {
+ this.autocompleteNode = getNode(aID);
+
+ this.autocomplete = this.autocompleteNode.localName == "searchbar" ?
+ getAccessible(this.autocompleteNode.textbox) :
+ getAccessible(this.autocompleteNode);
+
+ this.index = aIdx;
+ }
+
+ function getIFrameDOMDoc(aIFrameID)
+ {
+ return getNode(aIFrameID).contentDocument;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test helpers
+
+ function queueAutoCompleteTests(aID)
+ {
+ // focus autocomplete text entry
+ gQueue.push(new synthFocus(aID, new focusChecker(getTextEntry, aID)));
+
+ // open autocomplete popup
+ gQueue.push(new synthDownKey(aID, new nofocusChecker()));
+
+ // select second option ('hi' option), focus on it
+ gQueue.push(new synthUpKey(aID,
+ new focusChecker(getItem, new itemObj(aID, 1))));
+
+ // choose selected option, focus on text entry
+ gQueue.push(new synthEnterKey(aID, new focusChecker(getTextEntry, aID)));
+
+ // remove char, autocomplete popup appears
+ gQueue.push(new removeChar(aID, new nofocusChecker()));
+
+ // select first option ('hello' option), focus on it
+ gQueue.push(new synthDownKey(aID,
+ new focusChecker(getItem, new itemObj(aID, 0))));
+
+ // mouse move on second option ('hi' option), focus on it
+ gQueue.push(new focusOnMouseOver(getItem, new itemObj(aID, 1)));
+
+ // autocomplete popup updated (no selected item), focus on textentry
+ gQueue.push(new synthKey(aID, "e", null, new focusChecker(getTextEntry, aID)));
+
+ // select first option ('hello' option), focus on it
+ gQueue.push(new synthDownKey(aID,
+ new focusChecker(getItem, new itemObj(aID, 0))));
+
+ // popup gets hidden, focus on textentry
+ gQueue.push(new synthRightKey(aID, new focusChecker(getTextEntry, aID)));
+
+ // popup gets open, no focus
+ gQueue.push(new synthOpenComboboxKey(aID, new nofocusChecker()));
+
+ // select first option again ('hello' option), focus on it
+ gQueue.push(new synthDownKey(aID,
+ new focusChecker(getItem, new itemObj(aID, 0))));
+
+ // no option is selected, focus on text entry
+ gQueue.push(new synthUpKey(aID, new focusChecker(getTextEntry, aID)));
+
+ // close popup, no focus
+ gQueue.push(new synthEscapeKey(aID, new nofocusChecker()));
+
+ // autocomplete popup appears (no selected item), focus stays on textentry
+ gQueue.push(new replaceOnChar(aID, "h", new nofocusChecker()));
+
+ // mouse move on first option ('hello' option), focus on it
+ gQueue.push(new focusOnMouseOver(getItem, new itemObj(aID, 0)));
+
+ // click first option ('hello' option), popup closes, focus on text entry
+ gQueue.push(new selectByClick(getItem, new itemObj(aID, 0), getTextEntry, aID));
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Tests
+
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var gInitQueue = null;
+ function initTests()
+ {
+ if (SEAMONKEY || MAC) {
+ todo(false, "Skipping this test on SeaMonkey ftb. (Bug 718237), and on Mac (bug 746177)");
+ shutdownAutoComplete();
+ SimpleTest.finish();
+ return;
+ }
+
+ gInitQueue = new eventQueue();
+ gInitQueue.push(new loadFormAutoComplete("iframe"));
+ gInitQueue.push(new initFormAutoCompleteBy("iframe", "hello"));
+ gInitQueue.push(new initFormAutoCompleteBy("iframe", "hi"));
+ gInitQueue.push(new loadHTML5ListAutoComplete("iframe2"));
+ gInitQueue.onFinish = function initQueue_onFinish()
+ {
+ SimpleTest.executeSoon(doTests);
+ return DO_NOT_FINISH_TEST;
+ }
+ gInitQueue.invoke();
+ }
+
+ var gQueue = null;
+ function doTests()
+ {
+ // Test focus events.
+ gQueue = new eventQueue();
+
+ ////////////////////////////////////////////////////////////////////////////
+ // tree popup autocomplete tests
+ queueAutoCompleteTests("autocomplete");
+
+ ////////////////////////////////////////////////////////////////////////////
+ // richlistbox popup autocomplete tests
+ queueAutoCompleteTests("richautocomplete");
+
+ ////////////////////////////////////////////////////////////////////////////
+ // HTML form autocomplete tests
+ queueAutoCompleteTests(getIFrameDOMDoc("iframe").getElementById("input"));
+
+ ////////////////////////////////////////////////////////////////////////////
+ // HTML5 list autocomplete tests
+ queueAutoCompleteTests(getIFrameDOMDoc("iframe2").getElementById("input"));
+
+ ////////////////////////////////////////////////////////////////////////////
+ // searchbar tests
+
+ // focus searchbar, focus on text entry
+ gQueue.push(new synthFocus("searchbar",
+ new focusChecker(getTextEntry, "searchbar")));
+ // open search engine popup, no focus
+ gQueue.push(new synthOpenComboboxKey("searchbar", new nofocusChecker()));
+ // select first item, focus on it
+ gQueue.push(new synthDownKey("searchbar",
+ new focusChecker(getItem, new itemObj("searchbar", 0))));
+ // mouse over on second item, focus on it
+ gQueue.push(new focusOnMouseOver(getItem, new itemObj("searchbar", 1)));
+ // press enter key, focus on text entry
+ gQueue.push(new synthEnterKey("searchbar",
+ new focusChecker(getTextEntry, "searchbar")));
+ // click on search button, open popup, focus goes to document
+ var searchBtn = getAccessible(getNode("searchbar").searchButton);
+ gQueue.push(new synthClick(searchBtn, new focusChecker(document)));
+ // select first item, focus on it
+ gQueue.push(new synthDownKey("searchbar",
+ new focusChecker(getItem, new itemObj("searchbar", 0))));
+ // close popup, focus goes on document
+ gQueue.push(new synthEscapeKey("searchbar", new focusChecker(document)));
+
+ gQueue.onFinish = function()
+ {
+ // unregister 'test-a11y-search' autocomplete search
+ shutdownAutoComplete();
+ }
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ // Register 'test-a11y-search' autocomplete search.
+ // XPFE AutoComplete needs to register early.
+ initAutoComplete([ "hello", "hi" ],
+ [ "Beep beep'm beep beep yeah", "Baby you can drive my car" ]);
+
+ addA11yLoadEvent(initTests);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=383759"
+ title="Focus event inconsistent for search box autocomplete">
+ Mozilla Bug 383759
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958"
+ title="Rework accessible focus handling">
+ Mozilla Bug 673958
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=559766"
+ title="Add accessibility support for @list on HTML input and for HTML datalist">
+ Mozilla Bug 559766
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <html:input is="autocomplete-input"
+ id="autocomplete"
+ autocompletesearch="test-a11y-search"/>
+
+ <html:input is="autocomplete-input"
+ id="richautocomplete"
+ autocompletesearch="test-a11y-search"
+ autocompletepopup="richpopup"/>
+ <panel is="autocomplete-richlistbox-popup"
+ id="richpopup"
+ type="autocomplete-richlistbox"
+ noautofocus="true"/>
+
+ <iframe id="iframe"/>
+
+ <iframe id="iframe2"/>
+
+ <searchbar id="searchbar"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_focus_canvas.html b/accessible/tests/mochitest/events/test_focus_canvas.html
new file mode 100644
index 0000000000..e2464e41a6
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_canvas.html
@@ -0,0 +1,58 @@
+<html>
+
+<head>
+ <title>Accessible focus testing in canvas subdom</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new synthFocus("button"));
+ gQueue.push(new synthTab("button", new focusChecker("textbox")));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ title="Expose content in Canvas element"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=495912">
+ Mozilla Bug 495912
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <canvas>
+ <input id="button" type="button">
+ <input id="textbox">
+ </canvas>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_contextmenu.xhtml b/accessible/tests/mochitest/events/test_focus_contextmenu.xhtml
new file mode 100644
index 0000000000..a0c92212dc
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_contextmenu.xhtml
@@ -0,0 +1,98 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Context menu focus testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ //gA11yEventDumpID = "eventdump"; // debug stuff
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var winLowerThanVista = navigator.platform.indexOf("Win") == 0;
+ if (winLowerThanVista) {
+ var version = Services.sysinfo.getProperty("version");
+ version = parseFloat(version);
+ winLowerThanVista = !(version >= 6.0);
+ }
+
+ var gQueue = null;
+ function doTests()
+ {
+ // bug 746183 - Whole file times out on OS X
+ if (MAC || winLowerThanVista) {
+ todo(false, "Reenable on mac after fixing bug 746183!");
+ SimpleTest.finish();
+ return;
+ }
+
+ // Test focus events.
+ gQueue = new eventQueue();
+
+ gQueue.push(new synthFocus("button"));
+ gQueue.push(new synthContextMenu("button", [
+ new invokerChecker(EVENT_MENUPOPUP_START, "contextmenu"),
+ new invokerChecker("popupshown", "contextmenu"),
+ ]));
+ gQueue.push(new synthDownKey("button", new focusChecker("item1")));
+ gQueue.push(new synthEscapeKey("contextmenu", new focusChecker("button")));
+
+ gQueue.push(new synthContextMenu("button",
+ new invokerChecker(EVENT_MENUPOPUP_START, "contextmenu")));
+ gQueue.push(new synthDownKey("contextmenu", new focusChecker("item1")));
+ gQueue.push(new synthDownKey("item1", new focusChecker("item2")));
+ gQueue.push(new synthRightKey("item2", new focusChecker("item2.1")));
+ if (WIN) {
+ todo(false, "synthEscapeKey for item2.1 and item2 disabled due to bug 691580");
+ } else {
+ gQueue.push(new synthEscapeKey("item2.1", new focusChecker("item2")));
+ gQueue.push(new synthEscapeKey("item2", new focusChecker("button")));
+ }
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958"
+ title="Rework accessible focus handling">
+ Mozilla Bug 673958
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <button id="button" context="contextmenu" label="button"/>
+ <menupopup id="contextmenu">
+ <menuitem id="item1" label="item1"/>
+ <menu id="item2" label="item2">
+ <menupopup>
+ <menuitem id="item2.1" label="item2.1"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+
+ <vbox id="eventdump"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_focus_controls.html b/accessible/tests/mochitest/events/test_focus_controls.html
new file mode 100644
index 0000000000..268ec5d0e4
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_controls.html
@@ -0,0 +1,76 @@
+<html>
+
+<head>
+ <title>Accessible focus testing on HTML controls</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue(EVENT_FOCUS);
+
+ gQueue.push(new synthFocus("textbox"));
+ gQueue.push(new synthFocus("textarea"));
+ gQueue.push(new synthFocus("button1"));
+ gQueue.push(new synthFocus("button2"));
+ gQueue.push(new synthFocus("checkbox"));
+ gQueue.push(new synthFocus("radio1"));
+ gQueue.push(new synthDownKey("radio1", new focusChecker("radio2")));
+
+ // no focus events for checkbox or radio inputs when they are checked
+ // programmatically
+ gQueue.push(new changeCurrentItem("checkbox"));
+ gQueue.push(new changeCurrentItem("radio1"));
+
+ let fileBrowseButton = getAccessible("file");
+ gQueue.push(new synthFocus("file", new focusChecker(fileBrowseButton)));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958"
+ title="Rework accessible focus handling">
+ Mozilla Bug 673958
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="textbox">
+ <textarea id="textarea"></textarea>
+
+ <input id="button1" type="button" value="button">
+ <button id="button2">button</button>
+ <input id="checkbox" type="checkbox">
+ <input id="radio1" type="radio" name="radiogroup">
+ <input id="radio2" type="radio" name="radiogroup">
+ <input id="file" type="file">
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_doc.html b/accessible/tests/mochitest/events/test_focus_doc.html
new file mode 100644
index 0000000000..a35fc06ed0
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_doc.html
@@ -0,0 +1,92 @@
+<html>
+
+<head>
+ <title>Accessible document focus event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ var gQueue = null;
+
+ // var gA11yEventDumpID = "eventdump";
+ // gA11yEventDumpToConsole = true;
+
+ function doTests() {
+ // setup
+ var frameDoc = document.getElementById("iframe").contentDocument;
+ frameDoc.designMode = "on";
+ var frameDocAcc = getAccessible(frameDoc, [nsIAccessibleDocument]);
+ var buttonAcc = getAccessible("b1");
+
+ var frame2Doc = document.getElementById("iframe2").contentDocument;
+ var frame2Input = frame2Doc.getElementById("input");
+ var frame2DocAcc = getAccessible(frame2Doc);
+ var frame2InputAcc = getAccessible(frame2Input);
+
+ // Test focus events.
+ gQueue = new eventQueue();
+
+ // try to give focus to contentEditable frame twice to cover bug 512059
+ gQueue.push(new synthFocus(buttonAcc));
+ gQueue.push(new synthTab(frameDocAcc, new focusChecker(frameDocAcc)));
+ gQueue.push(new synthFocus(buttonAcc));
+ gQueue.push(new synthTab(frameDocAcc, new focusChecker(frameDocAcc)));
+
+ // focus on not editable document
+ gQueue.push(new synthFocus(frame2InputAcc));
+ gQueue.push(new synthShiftTab(frame2DocAcc, new focusChecker(frame2DocAcc)));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=512058"
+ title="Can't set focus to designMode document via accessibility APIs">
+ Mozilla Bug 512058
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=512059"
+ title="Accessibility focus event never fired for designMode document after the first focus">
+ Mozilla Bug 512059
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=618046"
+ title="No focus change event when Shift+Tab at top of screen">
+ Mozilla Bug 618046
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="eventdump"></div>
+
+ <div id="testContainer">
+ <button id="b1">a button</button>
+ <iframe id="iframe" src="about:blank"></iframe>
+ <button id="b2">a button</button>
+ <iframe id="iframe2" src="data:text/html,<html><input id='input'></html>"></iframe>
+ </div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible focus testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ function focusElmWhileSubdocIsFocused(aID) {
+ this.DOMNode = getNode(aID);
+
+ this.invoke = function focusElmWhileSubdocIsFocused_invoke() {
+ this.DOMNode.focus();
+ };
+
+ this.eventSeq = [
+ new focusChecker(this.DOMNode),
+ ];
+
+ this.unexpectedEventSeq = [
+ new invokerChecker(EVENT_FOCUS, this.DOMNode.ownerDocument),
+ ];
+
+ this.getID = function focusElmWhileSubdocIsFocused_getID() {
+ return "Focus element while subdocument is focused " + prettyName(aID);
+ };
+ }
+
+ function imageMapChecker(aID) {
+ var node = getNode(aID);
+ this.type = EVENT_FOCUS;
+ this.match = function imageMapChecker_match(aEvent) {
+ return aEvent.DOMNode == node;
+ };
+ }
+
+ function topMenuChecker() {
+ this.type = EVENT_FOCUS;
+ this.match = function topMenuChecker_match(aEvent) {
+ return aEvent.accessible.role == ROLE_PARENT_MENUITEM;
+ };
+ }
+
+ function contextMenuChecker() {
+ this.type = EVENT_MENUPOPUP_START;
+ this.match = function contextMenuChecker_match(aEvent) {
+ return aEvent.accessible.role == ROLE_MENUPOPUP;
+ };
+ }
+
+ function focusContextMenuItemChecker() {
+ this.__proto__ = new focusChecker();
+
+ this.match = function focusContextMenuItemChecker_match(aEvent) {
+ return aEvent.accessible.role == ROLE_MENUITEM;
+ };
+ }
+
+ /**
+ * Do tests.
+ */
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+
+ function doTests() {
+ var frameDoc = document.getElementById("iframe").contentDocument;
+
+ var editableDoc = document.getElementById("editabledoc").contentDocument;
+ editableDoc.designMode = "on";
+
+ gQueue = new eventQueue();
+
+ gQueue.push(new synthFocus("editablearea"));
+ gQueue.push(new synthFocus("navarea"));
+ gQueue.push(new synthTab("navarea", new focusChecker(frameDoc)));
+ gQueue.push(new focusElmWhileSubdocIsFocused("link"));
+
+ gQueue.push(new synthTab(editableDoc, new focusChecker(editableDoc)));
+ if (WIN || LINUX) {
+ // Alt key is used to active menubar and focus menu item on Windows,
+ // other platforms requires setting a ui.key.menuAccessKeyFocuses
+ // preference.
+ gQueue.push(new toggleTopMenu(editableDoc, new topMenuChecker()));
+ gQueue.push(new toggleTopMenu(editableDoc, new focusChecker(editableDoc)));
+ }
+ if (!(MAC && Services.prefs.getBoolPref("widget.macos.native-context-menus", false))) {
+ // Context menu accessibility is handled natively and not testable when
+ // native context menus are used on macOS.
+ gQueue.push(new synthContextMenu(editableDoc, new contextMenuChecker()));
+ gQueue.push(new synthDownKey(editableDoc, new focusContextMenuItemChecker()));
+ gQueue.push(new synthEscapeKey(editableDoc, new focusChecker(editableDoc)));
+ } else {
+ // If this test is run as part of multiple tests, it is displayed in the test harness iframe.
+ // In the non-native context menu case, right-clicking the editableDoc causes the editableDoc
+ // to scroll fully into view, and as a side-effect, the img below it ends up on the screen.
+ // When we're skipping the context menu check, scroll img onto the screen manually, because
+ // otherwise it may remain out-of-view and clipped by the test harness iframe.
+ var img = document.querySelector("img");
+ gQueue.push(new scrollIntoView(img, new nofocusChecker(img)));
+ }
+ if (SEAMONKEY) {
+ todo(false, "shift tab from editable document fails on (Windows) SeaMonkey! (Bug 718235)");
+ } else if (LINUX || MAC) {
+ todo(false, "shift tab from editable document fails on linux and Mac, bug 746519!");
+ } else {
+ gQueue.push(new synthShiftTab("link", new focusChecker("link")));
+ } // ! SEAMONKEY
+
+ gQueue.push(new synthFocus("a", new imageMapChecker("a")));
+ gQueue.push(new synthFocus("b", new imageMapChecker("b")));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=352220"
+ title="Inconsistent focus events when returning to a document frame">
+ Mozilla Bug 352220
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=550338"
+ title="Broken focus when returning to editable documents from menus">
+ Mozilla Bug 550338
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958"
+ title="Rework accessible focus handling">
+ Mozilla Bug 673958
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=961696"
+ title="Accessible object:state-changed:focused events for imagemap links are broken">
+ Mozilla Bug 961696
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="editablearea" contentEditable="true">editable area</div>
+ <div id="navarea" tabindex="0">navigable area</div>
+ <iframe id="iframe" src="data:text/html,<html></html>"></iframe>
+ <a id="link" href="">link</a>
+ <iframe id="editabledoc" src="about:blank"></iframe>
+
+ <map name="atoz_map">
+ <area id="a" coords="0,0,13,14" shape="rect">
+ <area id="b" coords="17,0,30,14" shape="rect">
+ </map>
+ <img width="447" height="15" usemap="#atoz_map" src="../letters.gif">
+
+ <div id="eventdump"></div>
+</body>
+</html>
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 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Accessible focus event testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ //gA11yEventDumpID = "eventdump"; // debug stuff
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTests()
+ {
+ // Test focus events.
+ gQueue = new eventQueue();
+
+ gQueue.push(new synthFocus("textbox",
+ new focusChecker(getNode("textbox"))));
+ gQueue.push(new synthFocusOnFrame("editabledoc"));
+ gQueue.push(new synthFocus("radioclothes",
+ new focusChecker("radiosweater")));
+ gQueue.push(new synthDownKey("radiosweater",
+ new focusChecker("radiojacket")));
+ gQueue.push(new synthFocus("checkbox"));
+ gQueue.push(new synthFocus("button"));
+ gQueue.push(new synthFocus("checkbutton"));
+ gQueue.push(new synthFocus("radiobutton"));
+
+ // focus menubutton
+ gQueue.push(new synthFocus("menubutton"));
+ // click menubutton, open popup, focus stays on menu button
+ gQueue.push(new synthClick("menubutton", new nofocusChecker()));
+ // select first menu item ("item 1"), focus on menu item
+ gQueue.push(new synthDownKey("menubutton", new focusChecker("mb_item1")));
+ // choose select menu item, focus gets back to menubutton
+ gQueue.push(new synthEnterKey("mb_item1", new focusChecker("menubutton")));
+ // press enter to open popup, focus stays on menubutton
+ gQueue.push(new synthEnterKey("menubutton", new nofocusChecker()));
+ // select second menu item ("item 2"), focus on menu item
+ gQueue.push(new synthUpKey("menubutton", new focusChecker("mb_item2")));
+ // close the popup
+ gQueue.push(new synthEscapeKey("menubutton", new focusChecker("menubutton")));
+
+ // clicking on button having associated popup doesn't change focus
+ gQueue.push(new synthClick("popupbutton", [
+ new nofocusChecker(),
+ new invokerChecker("popupshown", "backpopup")
+ ]));
+
+ // select first menu item ("item 1"), focus on menu item
+ gQueue.push(new synthDownKey("popupbutton", new focusChecker("bp_item1")));
+ // choose select menu item, focus gets back to menubutton
+ gQueue.push(new synthEnterKey("bp_item1", new focusChecker("menubutton")));
+ // show popup again for the next test
+ gQueue.push(new synthClick("popupbutton", new nofocusChecker()));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=552368"
+ title=" fire focus event on document accessible whenever the root or body element is focused">
+ Mozilla Bug 552368
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <html:input id="textbox" value="hello"/>
+ <iframe id="editabledoc" src="focus.html"/>
+ <radiogroup id="radioclothes">
+ <radio id="radiosweater" label="radiosweater"/>
+ <radio id="radiocap" label="radiocap" disabled="true"/>
+ <radio id="radiojacket" label="radiojacket"/>
+ </radiogroup>
+ <checkbox id="checkbox" label="checkbox"/>
+ <button id="button" label="button"/>
+
+ <button id="menubutton" type="menu" label="menubutton">
+ <menupopup>
+ <menuitem id="mb_item1" label="item1"/>
+ <menuitem id="mb_item2" label="item2"/>
+ </menupopup>
+ </button>
+
+ <button id="checkbutton" type="checkbox" label="checkbutton"/>
+ <button id="radiobutton" type="radio" group="rbgroup" label="radio1"/>
+
+ <popupset>
+ <menupopup id="backpopup" position="after_start">
+ <menuitem id="bp_item1" label="Page 1"/>
+ <menuitem id="bp_item2" label="Page 2"/>
+ </menupopup>
+ </popupset>
+ <button id="popupbutton" label="Pop Me Up" popup="backpopup"/>
+
+ <vbox id="eventdump"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_focus_listcontrols.xhtml b/accessible/tests/mochitest/events/test_focus_listcontrols.xhtml
new file mode 100644
index 0000000000..848657d3b3
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_listcontrols.xhtml
@@ -0,0 +1,153 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible focus event testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ let PromEvents = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js",
+ PromEvents);
+ //gA11yEventDumpID = "eventdump"; // debug stuff
+ gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ async function doTests()
+ {
+ // Test focus events.
+ gQueue = new eventQueue();
+ // Later tests use await.
+ let queueFinished = new Promise(resolve => {
+ gQueue.onFinish = function() {
+ resolve();
+ return DO_NOT_FINISH_TEST;
+ };
+ });
+
+ gQueue.push(new synthFocus("richlistbox", new focusChecker("rlb_item1")));
+ gQueue.push(new synthDownKey("rlb_item1", new focusChecker("rlb_item2")));
+ gQueue.push(new synthFocus("multiselrichlistbox", new focusChecker("msrlb_item1")));
+ gQueue.push(new synthDownKey("msrlb_item1", new focusChecker("msrlb_item2"), { shiftKey: true }));
+ gQueue.push(new synthFocus("emptyrichlistbox", new focusChecker("emptyrichlistbox")));
+
+ gQueue.push(new synthFocus("menulist"));
+ gQueue.push(new synthClick("menulist", new focusChecker("ml_tangerine"),
+ { where: "center" }));
+ gQueue.push(new synthDownKey("ml_tangerine", new focusChecker("ml_marmalade")));
+ gQueue.push(new synthEscapeKey("ml_marmalade", new focusChecker("menulist")));
+
+ // On Windows, items get selected during navigation.
+ let expectedItem = WIN ? "ml_strawberry" : "ml_marmalade";
+ gQueue.push(new synthDownKey("menulist", new nofocusChecker(expectedItem)));
+ gQueue.push(new synthOpenComboboxKey("menulist", new focusChecker(expectedItem)));
+ gQueue.push(new synthEnterKey(expectedItem, new focusChecker("menulist")));
+
+ // no focus events for unfocused list controls when current item is
+ // changed.
+ gQueue.push(new synthFocus("emptyrichlistbox"));
+
+ gQueue.push(new changeCurrentItem("richlistbox", "rlb_item1"));
+ gQueue.push(new changeCurrentItem("menulist", WIN ? "ml_marmalade" : "ml_tangerine"));
+
+ gQueue.invoke();
+ await queueFinished;
+ // Tests beyond this point use await rather than eventQueue.
+
+ // When a menulist contains something other than XUL menuitems, we need
+ // to manage focus with aria-activedescendant.
+ info("Testing opening a menupopup with aria-activedescendant");
+ let popupDiv1 = getNode("menupopup_ad_div1");
+ let focused = PromEvents.waitForEvent(EVENT_FOCUS, popupDiv1);
+ let popup = getNode("menupopup_ad");
+ popup.openPopup();
+ await focused;
+ info("Testing removal of previous active descendant + setting new active descendant");
+ focused = PromEvents.waitForEvent(EVENT_FOCUS, "menupopup_ad_div2");
+ popupDiv1.remove();
+ popup.setAttribute("aria-activedescendant", "menupopup_ad_div2");
+ await focused;
+ popup.hidePopup();
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=433418"
+ title="Accessibles for focused HTML Select elements are not getting focused state">
+ Mozilla Bug 433418
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=474893"
+ title="List controls should fire a focus event on the selected child when tabbing or when the selected child changes while the list is focused">
+ Mozilla Bug 474893
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=552368"
+ title=" fire focus event on document accessible whenever the root or body element is focused">
+ Mozilla Bug 552368
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <richlistbox id="richlistbox">
+ <richlistitem id="rlb_item1">
+ <description>A XUL Description!</description>
+ </richlistitem>
+ <richlistitem id="rlb_item2">
+ <button label="A XUL Button"/>
+ </richlistitem>
+ </richlistbox>
+ <richlistbox id="multiselrichlistbox" seltype="multiple">
+ <richlistitem id="msrlb_item1">
+ <description>A XUL Description!</description>
+ </richlistitem>
+ <richlistitem id="msrlb_item2">
+ <button label="A XUL Button"/>
+ </richlistitem>
+ </richlistbox>
+ <richlistbox id="emptyrichlistbox" seltype="multiple"/>
+
+ <menulist id="menulist">
+ <menupopup>
+ <menuitem id="ml_tangerine" label="tangerine trees"/>
+ <menuitem id="ml_marmalade" label="marmalade skies"/>
+ <menuitem id="ml_strawberry" label="strawberry fields"/>
+ </menupopup>
+ </menulist>
+
+ <menulist>
+ <menupopup id="menupopup_ad" aria-activedescendant="menupopup_ad_div1">
+ <div id="menupopup_ad_div1" role="option"></div>
+ <div id="menupopup_ad_div2" role="option"></div>
+ </menupopup>
+ </menulist>
+
+ <vbox id="eventdump"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_focus_menu.xhtml b/accessible/tests/mochitest/events/test_focus_menu.xhtml
new file mode 100644
index 0000000000..dda10517eb
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_menu.xhtml
@@ -0,0 +1,117 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Menu focus testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ // gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTests()
+ {
+ // Test focus events.
+ gQueue = new eventQueue();
+
+ if (WIN) {
+ gQueue.push(new toggleTopMenu("fruit", new focusChecker("fruit")));
+ gQueue.push(new synthRightKey("fruit", new focusChecker("vehicle")));
+ gQueue.push(new synthEscapeKey("vehicle", new focusChecker(document)));
+ }
+
+ // mouse move activate items but no focus event until menubar is active
+ gQueue.push(new synthMouseMove("fruit", new nofocusChecker("apple")));
+
+ // mouseover and click on menuitem makes it active before menubar is
+ // active
+ gQueue.push(new synthClick("fruit", new focusChecker("fruit"), { where: "center" }));
+
+ // mouseover on menuitem when menubar is active
+ gQueue.push(new synthMouseMove("apple", new focusChecker("apple")));
+
+ // keydown on disabled menuitem (disabled items are skipped on linux)
+ if (WIN)
+ gQueue.push(new synthDownKey("apple", new focusChecker("orange")));
+
+ // menu and menuitem are both active
+ // XXX: intermitent failure because two focus events may be coalesced,
+ // think to workaround or fix this issue, when done enable queue invoker
+ // below and remove next two.
+ //gQueue.push(new synthRightKey("apple",
+ // [ new focusChecker("vehicle"),
+ // new focusChecker("cycle")]));
+ gQueue.push(new synthMouseMove("vehicle", new focusChecker("vehicle")));
+ gQueue.push(new synthDownKey("vehicle", new focusChecker("cycle")));
+
+ // open submenu
+ gQueue.push(new synthRightKey("cycle", new focusChecker("tricycle")));
+
+ // move to first menu in cycle, DOMMenuItemActive is fired for fruit,
+ // cycle and apple menuitems (bug 685191)
+ todo(false, "focus is fired for 'cycle' menuitem");
+ //gQueue.push(new synthRightKey("vehicle", new focusChecker("apple")));
+
+ // click menuitem to close menu, focus gets back to document
+ gQueue.push(new synthClick("tricycle", new focusChecker(document), { where: "center" }));
+
+ //enableLogging("focus,DOMEvents,tree"); // logging for bug708927
+ //gQueue.onFinish = function() { disableLogging(); }
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958"
+ title="Rework accessible focus handling">
+ Mozilla Bug 673958
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <menubar>
+ <menu id="fruit" label="Fruit">
+ <menupopup>
+ <menuitem id="apple" label="Apple"/>
+ <menuitem id="orange" label="Orange" disabled="true"/>
+ </menupopup>
+ </menu>
+ <menu id="vehicle" label="Vehicle">
+ <menupopup id="vehiclePopup">
+ <menu id="cycle" label="cycle">
+ <menupopup>
+ <menuitem id="tricycle" label="tricycle"/>
+ </menupopup>
+ </menu>
+ <menuitem id="car" label="Car" disabled="true"/>
+ </menupopup>
+ </menu>
+ </menubar>
+
+ <vbox id="eventdump"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_focus_name.html b/accessible/tests/mochitest/events/test_focus_name.html
new file mode 100644
index 0000000000..aa77923909
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_name.html
@@ -0,0 +1,116 @@
+<html>
+
+<head>
+ <title>Accessible name testing on focus</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ /**
+ * Checker for invokers.
+ */
+ function actionChecker(aID, aDescription) {
+ this.__proto__ = new invokerChecker(EVENT_FOCUS, aID);
+
+ this.check = function actionChecker_check(aEvent) {
+ var target = aEvent.accessible;
+ is(target.description, aDescription,
+ "Wrong description for " + prettyName(target));
+ };
+ }
+
+ var gFocusHandler = {
+ handleEvent: function gFocusHandler_handleEvent(aEvent) {
+ var elm = aEvent.target;
+ if (elm.nodeType != Node.ELEMENT_NODE)
+ return;
+
+ gTooltipElm.style.display = "block";
+
+ elm.setAttribute("aria-describedby", "tooltip");
+ },
+ };
+
+ var gBlurHandler = {
+ handleEvent: function gBlurHandler_handleEvent(aEvent) {
+ gTooltipElm.style.display = "none";
+
+ var elm = aEvent.target;
+ if (elm.nodeType == Node.ELEMENT_NODE)
+ elm.removeAttribute("aria-describedby");
+ },
+ };
+
+ /**
+ * Do tests.
+ */
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+
+ var gButtonElm = null;
+ var gTextboxElm = null;
+ var gTooltipElm = null;
+
+ function doTests() {
+ gButtonElm = getNode("button");
+ gTextboxElm = getNode("textbox");
+ gTooltipElm = getNode("tooltip");
+
+ gButtonElm.addEventListener("focus", gFocusHandler);
+ gButtonElm.addEventListener("blur", gBlurHandler);
+ gTextboxElm.addEventListener("focus", gFocusHandler);
+ gTextboxElm.addEventListener("blur", gBlurHandler);
+
+ // The aria-describedby is changed on DOM focus. Accessible description
+ // should be updated when a11y focus is fired.
+ gQueue = new eventQueue(nsIAccessibleEvent.EVENT_FOCUS);
+ gQueue.onFinish = function() {
+ gButtonElm.removeEventListener("focus", gFocusHandler);
+ gButtonElm.removeEventListener("blur", gBlurHandler);
+ gTextboxElm.removeEventListener("focus", gFocusHandler);
+ gTextboxElm.removeEventListener("blur", gBlurHandler);
+ };
+
+ var descr = "It's a tooltip";
+ gQueue.push(new synthFocus("button", new actionChecker("button", descr)));
+ gQueue.push(new synthTab("textbox", new actionChecker("textbox", descr)));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=520709"
+ title="mochitest to ensure name/description are updated on a11y focus if they were changed on DOM focus">
+ Mozilla Bug 520709
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="tooltip" style="display: none" aria-hidden="true">It's a tooltip</div>
+ <button id="button">button</button>
+ <input id="textbox">
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_removal.html b/accessible/tests/mochitest/events/test_focus_removal.html
new file mode 100644
index 0000000000..eb47b07075
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_removal.html
@@ -0,0 +1,95 @@
+<html>
+
+<head>
+ <title>Test removal of focused accessible</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ async function setFocus(aNodeToFocus, aExpectedFocus) {
+ let expected = aExpectedFocus || aNodeToFocus;
+ let focused = waitForEvent(EVENT_FOCUS, expected);
+ info("Focusing " + aNodeToFocus.id);
+ aNodeToFocus.focus();
+ await focused;
+ ok(true, expected.id + " focused after " +
+ aNodeToFocus.id + ".focus()");
+ }
+
+ async function expectFocusAfterRemove(aNodeToRemove, aExpectedFocus, aDisplayNone = false) {
+ let focused = waitForEvent(EVENT_FOCUS, aExpectedFocus);
+ info("Removing " + aNodeToRemove.id);
+ if (aDisplayNone) {
+ aNodeToRemove.style.display = "none";
+ } else {
+ aNodeToRemove.remove();
+ }
+ await focused;
+ let friendlyExpected = aExpectedFocus == document ?
+ "document" : aExpectedFocus.id;
+ ok(true, friendlyExpected + " focused after " +
+ aNodeToRemove.id + " removed");
+ }
+
+ async function doTests() {
+ info("Testing removal of focused node itself");
+ let button = getNode("button");
+ await setFocus(button);
+ await expectFocusAfterRemove(button, document);
+
+ info("Testing removal of focused node's parent");
+ let dialog = getNode("dialog");
+ let dialogButton = getNode("dialogButton");
+ await setFocus(dialogButton);
+ await expectFocusAfterRemove(dialog, document);
+
+ info("Testing removal of aria-activedescendant target");
+ let listbox = getNode("listbox");
+ let option = getNode("option");
+ await setFocus(listbox, option);
+ await expectFocusAfterRemove(option, listbox);
+
+ info("Test hiding focused element with display: none");
+ let groupingButton = getNode("groupingButton");
+ await setFocus(groupingButton);
+ await expectFocusAfterRemove(groupingButton, document, true);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <button id="button"></button>
+
+ <div role="dialog" id="dialog">
+ <button id="dialogButton"></button>
+ </div>
+
+ <div role="listbox" id="listbox" tabindex="0" aria-activedescendant="option">
+ <div role="option" id="option"></div>
+ </div>
+
+ <div role="grouping" id="grouping">
+ <button id="groupingButton">
+ </div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_selects.html b/accessible/tests/mochitest/events/test_focus_selects.html
new file mode 100644
index 0000000000..2346691dfd
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_selects.html
@@ -0,0 +1,190 @@
+<html>
+
+<head>
+ <title>Accessible focus testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+ var gQueue = null;
+
+ async function doTests() {
+ // Bug 746534 - File causes crash or hang on OS X
+ if (MAC) {
+ todo(false, "Bug 746534 - test file causes crash or hang on OS X");
+ SimpleTest.finish();
+ return;
+ }
+
+ let p = waitForEvent(EVENT_FOCUS, "orange");
+ // first item is focused until there's selection
+ getNode("list").focus();
+ await p;
+
+ p = waitForEvents({
+ expected: [[EVENT_SELECTION, "orange"]],
+ unexpected: [
+ [EVENT_FOCUS],
+ stateChangeEventArgs("orange", EXT_STATE_ACTIVE, true, true),
+ ],
+ });
+ // item is selected and stays focused and active
+ synthesizeKey("VK_DOWN");
+ await p;
+
+ p = waitForEvents([
+ stateChangeEventArgs("orange", EXT_STATE_ACTIVE, false, true),
+ stateChangeEventArgs("apple", EXT_STATE_ACTIVE, true, true),
+ [EVENT_FOCUS, "apple"],
+ ]);
+ // last selected item is focused
+ synthesizeKey("VK_DOWN", { shiftKey: true });
+ await p;
+
+ p = waitForEvents({
+ expected: [
+ [EVENT_FOCUS, "orange"],
+ stateChangeEventArgs("orange", EXT_STATE_ACTIVE, true, true),
+ ],
+ unexpected: [
+ [EVENT_FOCUS, "apple"],
+ stateChangeEventArgs("apple", EXT_STATE_ACTIVE, true, true),
+ ],
+ });
+ // no focus event if nothing is changed
+ synthesizeKey("VK_DOWN");
+ // current item is focused
+ synthesizeKey("VK_UP", { ctrlKey: true });
+ await p;
+
+ p = waitForEvent(EVENT_FOCUS, "emptylist");
+ // focus on empty list (no items to be focused)
+ synthesizeKey("VK_TAB");
+ await p;
+
+ p = waitForEvents({
+ expected: [[EVENT_FOCUS, "orange"]],
+ unexpected: [stateChangeEventArgs("orange", EXT_STATE_ACTIVE, true, true)],
+ });
+ // current item is focused
+ synthesizeKey("VK_TAB", { shiftKey: true });
+ await p;
+
+ p = waitForEvent(EVENT_FOCUS, "combobox");
+ getNode("combobox").focus();
+ await p;
+
+ p = waitForEvents({
+ expected: [[EVENT_SELECTION, "cb_apple"]],
+ unexpected: [
+ [EVENT_FOCUS],
+ stateChangeEventArgs("cb_apple", EXT_STATE_ACTIVE, true, true),
+ ],
+ });
+ // collapsed combobox keeps a focus
+ synthesizeKey("VK_DOWN");
+ await p;
+
+ // no focus events for unfocused list controls when current item is
+ // changed
+
+ p = waitForEvent(EVENT_FOCUS, "emptylist");
+ getNode("emptylist").focus();
+ await p;
+
+ p = waitForEvents({
+ expected: [[EVENT_SELECTION, "orange"]],
+ unexpected: [
+ [EVENT_FOCUS],
+ stateChangeEventArgs("orange", EXT_STATE_ACTIVE, true, true),
+ ],
+ });
+ // An unfocused selectable list gets selection change events,
+ // but not active or focus change events.
+ getNode("list").selectedIndex = getNode("orange").index;
+ await p;
+
+ p = waitForEvents({
+ expected: [[EVENT_SELECTION, "cb_orange"]],
+ unexpected: [
+ [EVENT_FOCUS],
+ stateChangeEventArgs("cb_orange", EXT_STATE_ACTIVE, true, true),
+ ],
+ });
+ // An unfocused selectable combobox gets selection change events,
+ // but not focus events nor active state change events.
+ getNode("cb_orange").selected = true;
+ await p;
+
+ // Bug 1838983: Make sure we don't fail a C++ assertion when the focused
+ // active item is destroyed.
+ info("Focusing recreate");
+ p = waitForEvent(EVENT_FOCUS, "recreateA");
+ const recreate = getNode("recreate");
+ recreate.focus();
+ await p;
+ info("Changing recreate size");
+ p = waitForEvent(EVENT_FOCUS, recreate);
+ // This will recreate the select and its children.
+ recreate.size = 1;
+ await p;
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=433418"
+ title="Accessibles for focused HTML Select elements are not getting focused state">
+ Mozilla Bug 433418
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=474893"
+ title="List controls should fire a focus event on the selected child when tabbing or when the selected child changes while the list is focused">
+ Mozilla Bug 474893
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <select id="list" size="5" multiple="">
+ <option id="orange">Orange</option>
+ <option id="apple">Apple</option>
+ </select>
+
+ <select id="emptylist" size="5"></select>
+
+ <select id="combobox">
+ <option id="cb_orange">Orange</option>
+ <option id="cb_apple">Apple</option>
+ </select>
+
+ <select id="recreate" size="5">
+ <option id="recreateA">a</option>
+ </select>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_focus_tabbox.xhtml b/accessible/tests/mochitest/events/test_focus_tabbox.xhtml
new file mode 100644
index 0000000000..1b808831dc
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_tabbox.xhtml
@@ -0,0 +1,102 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Tabbox focus testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ //gA11yEventDumpID = "eventdump"; // debug stuff
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTests()
+ {
+ if (MAC) {
+ todo(false, "Tests disabled because of imminent failure.");
+ SimpleTest.finish();
+ return;
+ }
+
+ // Test focus events.
+ gQueue = new eventQueue();
+
+ var input = getNode("input");
+ gQueue.push(new synthClick("tab1", new focusChecker("tab1")));
+ gQueue.push(new synthTab("tab1", new focusChecker("checkbox1")));
+ gQueue.push(new synthKey("tab1", "VK_TAB", { ctrlKey: true },
+ new focusChecker(input)));
+ gQueue.push(new synthKey("tab2", "VK_TAB", { ctrlKey: true },
+ new focusChecker("tab3")));
+ gQueue.push(new synthKey("tab3", "VK_TAB", { ctrlKey: true },
+ new focusChecker("tab1")));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=370396"
+ title="Control+Tab to an empty tab panel in a tabbox causes focus to leave the tabbox">
+ Mozilla Bug 370396
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <tabbox>
+ <tabs>
+ <tab id="tab1" label="Tab1" selected="true"/>
+ <tab id="tab2" label="Tab2" />
+ <tab id="tab3" label="Tab3" />
+ </tabs>
+ <tabpanels>
+ <tabpanel orient="vertical">
+ <groupbox orient="vertical">
+ <checkbox id="checkbox1" label="Monday" width="75"/>
+ <checkbox label="Tuesday" width="75"/>
+ <checkbox label="Wednesday" width="75"/>
+ <checkbox label="Thursday" width="75"/>
+ <checkbox label="Friday" width="75"/>
+ <checkbox label="Saturday" width="75"/>
+ <checkbox label="Sunday" width="75"/>
+ </groupbox>
+
+ <spacer style="height: 10px" />
+ <label value="Label After checkboxes" />
+ </tabpanel>
+ <tabpanel orient="vertical">
+ <html:input id="input" />
+ </tabpanel>
+ <tabpanel orient="vertical">
+ <description>Tab 3 content</description>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+
+ <vbox id="eventdump"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_focus_tree.xhtml b/accessible/tests/mochitest/events/test_focus_tree.xhtml
new file mode 100644
index 0000000000..f36816c788
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focus_tree.xhtml
@@ -0,0 +1,117 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="XUL tree focus testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function focusTree(aTreeID)
+ {
+ var checker = new focusChecker(getFirstTreeItem, aTreeID);
+ this.__proto__ = new synthFocus(aTreeID, [ checker ]);
+ }
+
+ function moveToNextItem(aTreeID)
+ {
+ var checker = new focusChecker(getSecondTreeItem, aTreeID);
+ this.__proto__ = new synthDownKey(aTreeID, [ checker ]);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Helpers
+
+ function getTreeItemAt(aTreeID, aIdx)
+ { return getAccessible(aTreeID).getChildAt(aIdx + 1); }
+
+ function getFirstTreeItem(aTreeID)
+ { return getTreeItemAt(aTreeID, 0); }
+
+ function getSecondTreeItem(aTreeID)
+ { return getTreeItemAt(aTreeID, 1); }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ var gQueue = null;
+
+ //gA11yEventDumpID = "debug"; // debugging
+ //gA11yEventDumpToConsole = true; // debugging
+
+ function doTest()
+ {
+ gQueue = new eventQueue();
+
+ gQueue.push(new focusTree("tree"));
+ gQueue.push(new moveToNextItem("tree"));
+ gQueue.push(new synthFocus("emptytree"));
+
+ // no focus event for changed current item for unfocused tree
+ gQueue.push(new changeCurrentItem("tree", 0));
+
+ gQueue.invoke();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yXULTreeLoadEvent(doTest, "tree", new nsTableTreeView(5));
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=386821"
+ title="Need better solution for firing delayed event against xul tree">
+ Mozilla Bug 386821
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=406308"
+ title="Don't fire accessible focus events if widget is not actually in focus, confuses screen readers">
+ Mozilla Bug 406308
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="debug"/>
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col1" flex="1" primary="true" label="column"/>
+ <treecol id="col2" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren id="treechildren"/>
+ </tree>
+ <tree id="emptytree" flex="1">
+ <treecols>
+ <treecol id="emptytree_col1" flex="1" primary="true" label="column"/>
+ <treecol id="emptytree_col2" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren id="emptytree_treechildren"/>
+ </tree>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/events/test_focusable_statechange.html b/accessible/tests/mochitest/events/test_focusable_statechange.html
new file mode 100644
index 0000000000..03f4f12587
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_focusable_statechange.html
@@ -0,0 +1,122 @@
+<html>
+
+<head>
+ <title>Test removal of focused accessible</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ function focusableStateChange(id, enabled) {
+ return [EVENT_STATE_CHANGE, e => {
+ e.QueryInterface(nsIAccessibleStateChangeEvent);
+ return getAccessible(id) == e.accessible &&
+ e.state == STATE_FOCUSABLE && (enabled == undefined || e.isEnabled == enabled);
+ }];
+ }
+
+ function editableStateChange(id, enabled) {
+ return [EVENT_STATE_CHANGE, e => {
+ e.QueryInterface(nsIAccessibleStateChangeEvent);
+ return getAccessible(id) == e.accessible &&
+ e.state == EXT_STATE_EDITABLE && e.isExtraState &&
+ (enabled == undefined || e.isEnabled == enabled);
+ }];
+ }
+
+ async function doTests() {
+ info("disable buttons.");
+ // Expect focusable change with 'disabled',
+ // and don't expect it with 'aria-disabled'.
+ let p = waitForEvents({
+ expected: [focusableStateChange("button2", false)],
+ unexpected: [focusableStateChange("button1")]
+ });
+ getNode("button1").setAttribute("aria-disabled", "true");
+ getNode("button2").disabled = true;
+ await p;
+
+ info("re-enable button");
+ // Expect focusable change with 'disabled',
+ // and don't expect it with 'aria-disabled'.
+ p = waitForEvents({
+ expected: [focusableStateChange("button2", true)],
+ unexpected: [focusableStateChange("button1")]
+ });
+ getNode("button1").setAttribute("aria-disabled", "false");
+ getNode("button2").disabled = false;
+ await p;
+
+ info("add tabindex");
+ // Expect focusable change on non-input,
+ // and don't expect event on an already focusable input.
+ p = waitForEvents({
+ expected: [focusableStateChange("div", true)],
+ unexpected: [focusableStateChange("button2")]
+ });
+ getNode("button2").tabIndex = "0";
+ getNode("div").tabIndex = "0";
+ await p;
+
+ info("remove tabindex");
+ // Expect focusable change when removing tabindex.
+ p = waitForEvent(...focusableStateChange("div", false));
+ getNode("div").removeAttribute("tabindex");
+ await p;
+
+ info("add contenteditable");
+ // Expect editable change on non-input,
+ // and don't expect event on a native input.
+ p = waitForEvents({
+ expected: [focusableStateChange("div", true), editableStateChange("div", true)],
+ unexpected: [focusableStateChange("input"), editableStateChange("input")]
+ });
+ getNode("input").contentEditable = true;
+ getNode("div").contentEditable = true;
+ await p;
+
+ info("remove contenteditable");
+ // Expect editable change on non-input,
+ // and don't expect event on a native input.
+ p = waitForEvents({
+ expected: [focusableStateChange("div", false), editableStateChange("div", false)],
+ unexpected: [focusableStateChange("input"), editableStateChange("input")]
+ });
+ getNode("input").contentEditable = false;
+ getNode("div").contentEditable = false;
+ await p;
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <button id="button1"></button>
+ <button id="button2"></button>
+
+ <div id="div">Hello</div>
+
+ <input id="input" value="Hello">
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_fromUserInput.html b/accessible/tests/mochitest/events/test_fromUserInput.html
new file mode 100644
index 0000000000..b3617358cf
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_fromUserInput.html
@@ -0,0 +1,112 @@
+<html>
+
+<head>
+ <title>Testing of isFromUserInput in text events</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ /**
+ * Remove text data from HTML input.
+ */
+ function removeTextFromInput(aID, aStart, aEnd, aText, aFromUser) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new textChangeChecker(aID, aStart, aEnd, aText, false, aFromUser),
+ ];
+
+ this.invoke = function removeTextFromInput_invoke() {
+ this.DOMNode.focus();
+ this.DOMNode.setSelectionRange(aStart, aEnd);
+
+ synthesizeKey("KEY_Delete");
+ };
+
+ this.getID = function removeTextFromInput_getID() {
+ return "Remove text from " + aStart + " to " + aEnd + " for " +
+ prettyName(aID);
+ };
+ }
+
+ /**
+ * Remove text data from text node.
+ */
+ function removeTextFromContentEditable(aID, aStart, aEnd, aText, aFromUser) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new textChangeChecker(aID, aStart, aEnd, aText, false, aFromUser),
+ ];
+
+ this.invoke = function removeTextFromContentEditable_invoke() {
+ this.DOMNode.focus();
+ this.textNode = getNode(aID).firstChild;
+ var selection = window.getSelection();
+ var range = document.createRange();
+ range.setStart(this.textNode, aStart);
+ range.setEnd(this.textNode, aEnd);
+ selection.addRange(range);
+
+ synthesizeKey("KEY_Delete");
+ };
+
+ this.getID = function removeTextFromContentEditable_getID() {
+ return "Remove text from " + aStart + " to " + aEnd + " for " +
+ prettyName(aID);
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+
+ var gQueue = null;
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ // Focused editable text node
+ gQueue.push(new removeTextFromContentEditable("div", 0, 3, "hel", true));
+
+ // Focused editable HTML input
+ gQueue.push(new removeTextFromInput("input", 1, 2, "n", true));
+
+ gQueue.invoke(); // Will call SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+
+ </script>
+</head>
+
+
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=686909"
+ title="isFromUserInput flag on accessible text change events not correct">
+ Mozilla Bug 686909
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ <div id="eventdump"></div>
+
+ <div id="div" contentEditable="true">hello</div>
+ <input id="input" value="input">
+
+</body>
+
+</html>
diff --git a/accessible/tests/mochitest/events/test_label.xhtml b/accessible/tests/mochitest/events/test_label.xhtml
new file mode 100644
index 0000000000..5780629dc6
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_label.xhtml
@@ -0,0 +1,178 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tests: accessible XUL label/description events">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ const kRecreated = 0;
+ const kTextRemoved = 1;
+ const kTextChanged = 2;
+
+ const kNoValue = 0;
+
+ /**
+ * Set/remove @value attribute.
+ */
+ function setValue(aID, aValue, aResult, aOldValue)
+ {
+ this.labelNode = getNode(aID);
+
+ this.eventSeq = [];
+
+ switch (aResult) {
+ case kRecreated:
+ this.eventSeq.push(new invokerChecker(EVENT_HIDE, this.labelNode));
+ this.eventSeq.push(new invokerChecker(EVENT_SHOW, this.labelNode));
+ break;
+ case kTextRemoved:
+ this.eventSeq.push(
+ new textChangeChecker(this.labelNode, 0, aOldValue.length,
+ aOldValue, false));
+ break;
+ case kTextChanged:
+ this.eventSeq.push(
+ new textChangeChecker(this.labelNode, 0, aOldValue.length,
+ aOldValue, false));
+ this.eventSeq.push(
+ new textChangeChecker(this.labelNode, 0, aValue.length,
+ aValue, true));
+ break;
+ }
+
+ this.invoke = function setValue_invoke()
+ {
+ if (aValue === kNoValue)
+ this.labelNode.removeAttribute("value");
+ else
+ this.labelNode.setAttribute("value", aValue);
+ }
+
+ this.finalCheck = function setValue_finalCheck()
+ {
+ let tree =
+ { LABEL: [] };
+
+ const expectChild = (() => {
+ if (aValue === kNoValue) {
+ return false;
+ }
+ if (aValue === "") {
+ return this.labelNode.nodeName == "label";
+ }
+ return true;
+ })();
+
+ if (expectChild) {
+ tree.LABEL.push({ STATICTEXT: [ ] });
+ }
+ testAccessibleTree(aID, tree);
+ }
+
+ this.getID = function setValue_getID()
+ {
+ return "set @value='" + aValue + "' for label " + prettyName(aID);
+ }
+ }
+
+ /**
+ * Change @crop attribute.
+ */
+ function setCrop(aID, aCropValue, aRemovedText, aInsertedText)
+ {
+ this.labelNode = getNode(aID);
+ this.width = this.labelNode.getBoundingClientRect().width;
+ this.charWidth = this.width / this.labelNode.value.length;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.labelNode),
+ new invokerChecker(EVENT_SHOW, this.labelNode),
+ ];
+
+ this.invoke = function setCrop_invoke()
+ {
+ if (!this.labelNode.hasAttribute("crop"))
+ this.labelNode.style.width = Math.floor(this.width - 2 * this.charWidth) + "px";
+
+ this.labelNode.setAttribute("crop", aCropValue);
+ }
+
+ this.getID = function setCrop_finalCheck()
+ {
+ return "set crop " + aCropValue;
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+ function doTest()
+ {
+ gQueue = new eventQueue();
+
+ gQueue.push(new setValue("label", "shiroka strana", kRecreated));
+ gQueue.push(new setValue("label", "?<>!+_", kTextChanged, "shiroka strana"));
+ gQueue.push(new setValue("label", "", kRecreated));
+ gQueue.push(new setValue("label", kNoValue, kRecreated));
+
+ gQueue.push(new setValue("descr", "hello world", kRecreated));
+ gQueue.push(new setValue("descr", "si_ya", kTextChanged, "hello world"));
+ gQueue.push(new setValue("descr", "", kTextRemoved, "si_ya"));
+ gQueue.push(new setValue("descr", kNoValue, kRecreated));
+
+ gQueue.push(new setCrop("croplabel", "center"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=396166"
+ title="xul:label@value accessible should implement nsIAccessibleText">
+ Bug 396166
+ </a>
+ <br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <label id="label"/>
+ <description id="descr"/>
+
+ <hbox>
+ <label id="croplabel" value="valuetocro"
+ style="font-family: monospace;"/>
+ </hbox>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/events/test_menu.xhtml b/accessible/tests/mochitest/events/test_menu.xhtml
new file mode 100644
index 0000000000..271831ba83
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_menu.xhtml
@@ -0,0 +1,200 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible menu events testing for XUL menu">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script><![CDATA[
+ function openFileMenu()
+ {
+ this.eventSeq = [
+ new invokerChecker(EVENT_MENU_START, "menubar"),
+ new invokerChecker("popupshown", "menupopup-file")
+ // new invokerChecker(EVENT_FOCUS, getNode("menuitem-newtab")) intermitent failure
+ ];
+
+ this.invoke = function openFileMenu_invoke()
+ {
+ synthesizeKey("F", {altKey: true, shiftKey: true});
+ }
+
+ this.getID = function openFileMenu_getID()
+ {
+ return "open file menu by alt+F press";
+ }
+ }
+
+ function openEditMenu()
+ {
+ this.eventSeq = [
+ new invokerChecker("popuphidden", "menupopup-file"),
+ new invokerChecker("popupshown", "menupopup-edit"),
+ // new invokerChecker(EVENT_FOCUS, getNode("menuitem-undo")) intermitent failure
+ ];
+
+ this.invoke = function openEditMenu_invoke()
+ {
+ synthesizeKey("KEY_ArrowRight");
+ }
+
+ this.getID = function openEditMenu_getID()
+ {
+ return "open edit menu by lef arrow press";
+ }
+ }
+
+ function closeEditMenu()
+ {
+ this.eventSeq = [
+ //new invokerChecker(EVENT_FOCUS, document), intermitent failure
+ new invokerChecker("popuphidden", "menupopup-edit"),
+ ];
+
+ this.invoke = function closeEditMenu_invoke()
+ {
+ synthesizeKey("KEY_Escape");
+ }
+
+ this.getID = function closeEditMenu_getID()
+ {
+ return "close edit menu";
+ }
+ }
+
+ function focusFileMenu()
+ {
+ this.eventSeq = [
+ new invokerChecker(EVENT_MENU_START, getNode("menubar"))
+ // new invokerChecker(EVENT_FOCUS, getNode("menuitem-file")) //intermitent failure
+ ];
+
+ this.invoke = function focusFileMenu_invoke()
+ {
+ synthesizeKey("KEY_Alt");
+ }
+
+ this.getID = function focusFileMenu_getID()
+ {
+ return "activate menubar, focus file menu (atl press)";
+ }
+ }
+
+ function focusEditMenu()
+ {
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, getNode("menuitem-edit"))
+ ];
+
+ this.invoke = function focusEditMenu_invoke()
+ {
+ synthesizeKey("KEY_ArrowRight");
+ }
+
+ this.getID = function focusEditMenu_getID()
+ {
+ return "focus edit menu";
+ }
+ }
+
+ function leaveMenubar()
+ {
+ this.eventSeq = [
+ //new invokerChecker(EVENT_FOCUS, document), intermitent failure
+ new invokerChecker(EVENT_MENU_END, "menubar")
+ ];
+
+ this.invoke = function leaveMenubar_invoke()
+ {
+ synthesizeKey("KEY_Escape");
+ }
+
+ this.getID = function leaveMenubar_getID()
+ {
+ return "leave menubar";
+ }
+ }
+
+ /**
+ * Do tests.
+ */
+
+ //gA11yEventDumpID = "eventdump";
+ //gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+
+ function doTests()
+ {
+ if (!WIN && !LINUX) {
+ todo(false, "Enable this test on other platforms.");
+ SimpleTest.finish();
+ return;
+ }
+
+ todo(false,
+ "Fix intermitent failures. Focus may randomly occur before or after menupopup events!");
+
+ gQueue = new eventQueue();
+
+ gQueue.push(new openFileMenu());
+ gQueue.push(new openEditMenu());
+ gQueue.push(new closeEditMenu());
+ gQueue.push(new leaveMenubar());
+
+ // Alt key is used to active menubar and focus menu item on Windows,
+ // other platforms requires setting a ui.key.menuAccessKeyFocuses
+ // preference.
+ if (WIN || LINUX) {
+ gQueue.push(new focusFileMenu());
+ gQueue.push(new focusEditMenu());
+ gQueue.push(new leaveMenubar());
+ }
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ ]]></script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=615189"
+ title="Clean up FireAccessibleFocusEvent">
+ Mozilla Bug 615189
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <menubar id="menubar">
+ <menu id="menuitem-file" label="File" accesskey="F">
+ <menupopup id="menupopup-file">
+ <menuitem id="menuitem-newtab" label="New Tab"/>
+ </menupopup>
+ </menu>
+ <menu id="menuitem-edit" label="Edit" accesskey="E">
+ <menupopup id="menupopup-edit">
+ <menuitem id="menuitem-undo" label="Undo"/>
+ </menupopup>
+ </menu>
+ </menubar>
+
+ <vbox id="eventdump" role="log"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_mutation.html b/accessible/tests/mochitest/events/test_mutation.html
new file mode 100644
index 0000000000..7ee876570b
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_mutation.html
@@ -0,0 +1,580 @@
+<html>
+
+<head>
+ <title>Accessible mutation events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <style>
+ div.displayNone a { display:none; }
+ div.visibilityHidden a { visibility:hidden; }
+</style>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ /**
+ * Invokers.
+ */
+ var kNoEvents = 0;
+
+ var kShowEvent = 1;
+ var kHideEvent = 2;
+ var kReorderEvent = 4;
+ var kShowEvents = kShowEvent | kReorderEvent;
+ var kHideEvents = kHideEvent | kReorderEvent;
+ var kHideAndShowEvents = kHideEvents | kShowEvent;
+
+ /**
+ * Base class to test mutation a11y events.
+ *
+ * @param aNodeOrID [in] node invoker's action is executed for
+ * @param aEventTypes [in] events to register (see constants above)
+ * @param aDoNotExpectEvents [in] boolean indicates if events are expected
+ */
+ function mutateA11yTree(aNodeOrID, aEventTypes, aDoNotExpectEvents) {
+ // Interface
+ this.DOMNode = getNode(aNodeOrID);
+ this.doNotExpectEvents = aDoNotExpectEvents;
+ this.eventSeq = [];
+ this.unexpectedEventSeq = [];
+
+ /**
+ * Change default target (aNodeOrID) registered for the given event type.
+ */
+ this.setTarget = function mutateA11yTree_setTarget(aEventType, aTarget) {
+ var type = this.getA11yEventType(aEventType);
+ for (var idx = 0; idx < this.getEventSeq().length; idx++) {
+ if (this.getEventSeq()[idx].type == type) {
+ this.getEventSeq()[idx].target = aTarget;
+ return idx;
+ }
+ }
+ return -1;
+ };
+
+ /**
+ * Replace the default target currently registered for a given event type
+ * with the nodes in the targets array.
+ */
+ this.setTargets = function mutateA11yTree_setTargets(aEventType, aTargets) {
+ var targetIdx = this.setTarget(aEventType, aTargets[0]);
+
+ var type = this.getA11yEventType(aEventType);
+ for (var i = 1; i < aTargets.length; i++) {
+ let checker = new invokerChecker(type, aTargets[i]);
+ this.getEventSeq().splice(++targetIdx, 0, checker);
+ }
+ };
+
+ // Implementation
+ this.getA11yEventType = function mutateA11yTree_getA11yEventType(aEventType) {
+ if (aEventType == kReorderEvent)
+ return nsIAccessibleEvent.EVENT_REORDER;
+
+ if (aEventType == kHideEvent)
+ return nsIAccessibleEvent.EVENT_HIDE;
+
+ if (aEventType == kShowEvent)
+ return nsIAccessibleEvent.EVENT_SHOW;
+
+ return 0;
+ };
+
+ this.getEventSeq = function mutateA11yTree_getEventSeq() {
+ return this.doNotExpectEvents ? this.unexpectedEventSeq : this.eventSeq;
+ };
+
+ if (aEventTypes & kHideEvent) {
+ let checker = new invokerChecker(this.getA11yEventType(kHideEvent),
+ this.DOMNode);
+ this.getEventSeq().push(checker);
+ }
+
+ if (aEventTypes & kShowEvent) {
+ let checker = new invokerChecker(this.getA11yEventType(kShowEvent),
+ this.DOMNode);
+ this.getEventSeq().push(checker);
+ }
+
+ if (aEventTypes & kReorderEvent) {
+ let checker = new invokerChecker(this.getA11yEventType(kReorderEvent),
+ this.DOMNode.parentNode);
+ this.getEventSeq().push(checker);
+ }
+ }
+
+ /**
+ * Change CSS style for the given node.
+ */
+ function changeStyle(aNodeOrID, aProp, aValue, aEventTypes) {
+ this.__proto__ = new mutateA11yTree(aNodeOrID, aEventTypes, false);
+
+ this.invoke = function changeStyle_invoke() {
+ this.DOMNode.style[aProp] = aValue;
+ };
+
+ this.getID = function changeStyle_getID() {
+ return aNodeOrID + " change style " + aProp + " on value " + aValue;
+ };
+ }
+
+ /**
+ * Change class name for the given node.
+ */
+ function changeClass(aParentNodeOrID, aNodeOrID, aClassName, aEventTypes) {
+ this.__proto__ = new mutateA11yTree(aNodeOrID, aEventTypes, false);
+
+ this.invoke = function changeClass_invoke() {
+ this.parentDOMNode.className = aClassName;
+ };
+
+ this.getID = function changeClass_getID() {
+ return aNodeOrID + " change class " + aClassName;
+ };
+
+ this.parentDOMNode = getNode(aParentNodeOrID);
+ }
+
+ /**
+ * Clone the node and append it to its parent.
+ */
+ function cloneAndAppendToDOM(aNodeOrID, aEventTypes,
+ aTargetsFunc, aReorderTargetFunc) {
+ var eventTypes = aEventTypes || kShowEvents;
+ var doNotExpectEvents = (aEventTypes == kNoEvents);
+
+ this.__proto__ = new mutateA11yTree(aNodeOrID, eventTypes,
+ doNotExpectEvents);
+
+ this.invoke = function cloneAndAppendToDOM_invoke() {
+ var newElm = this.DOMNode.cloneNode(true);
+ newElm.removeAttribute("id");
+
+ var targets = aTargetsFunc ?
+ aTargetsFunc(newElm) : [newElm];
+ this.setTargets(kShowEvent, targets);
+
+ if (aReorderTargetFunc) {
+ var reorderTarget = aReorderTargetFunc(this.DOMNode);
+ this.setTarget(kReorderEvent, reorderTarget);
+ }
+
+ this.DOMNode.parentNode.appendChild(newElm);
+ };
+
+ this.getID = function cloneAndAppendToDOM_getID() {
+ return aNodeOrID + " clone and append to DOM.";
+ };
+ }
+
+ /**
+ * Removes the node from DOM.
+ */
+ function removeFromDOM(aNodeOrID, aEventTypes,
+ aTargetsFunc, aReorderTargetFunc) {
+ var eventTypes = aEventTypes || kHideEvents;
+ var doNotExpectEvents = (aEventTypes == kNoEvents);
+
+ this.__proto__ = new mutateA11yTree(aNodeOrID, eventTypes,
+ doNotExpectEvents);
+
+ this.invoke = function removeFromDOM_invoke() {
+ this.DOMNode.remove();
+ };
+
+ this.getID = function removeFromDOM_getID() {
+ return prettyName(aNodeOrID) + " remove from DOM.";
+ };
+
+ if (aTargetsFunc && (eventTypes & kHideEvent))
+ this.setTargets(kHideEvent, aTargetsFunc(this.DOMNode));
+
+ if (aReorderTargetFunc && (eventTypes & kReorderEvent))
+ this.setTarget(kReorderEvent, aReorderTargetFunc(this.DOMNode));
+ }
+
+ /**
+ * Clone the node and replace the original node by cloned one.
+ */
+ function cloneAndReplaceInDOM(aNodeOrID) {
+ this.__proto__ = new mutateA11yTree(aNodeOrID, kHideAndShowEvents,
+ false);
+
+ this.invoke = function cloneAndReplaceInDOM_invoke() {
+ this.DOMNode.parentNode.replaceChild(this.newElm, this.DOMNode);
+ };
+
+ this.getID = function cloneAndReplaceInDOM_getID() {
+ return aNodeOrID + " clone and replace in DOM.";
+ };
+
+ this.newElm = this.DOMNode.cloneNode(true);
+ this.newElm.removeAttribute("id");
+ this.setTarget(kShowEvent, this.newElm);
+ }
+
+ /**
+ * Trigger content insertion (flush layout), removal and insertion of
+ * the same element for the same parent.
+ */
+ function test1(aContainerID) {
+ this.divNode = document.createElement("div");
+ this.divNode.setAttribute("id", "div-test1");
+ this.containerNode = getNode(aContainerID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.divNode),
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.invoke = function test1_invoke() {
+ this.containerNode.appendChild(this.divNode);
+ getComputedStyle(this.divNode, "").color;
+ this.containerNode.removeChild(this.divNode);
+ this.containerNode.appendChild(this.divNode);
+ };
+
+ this.getID = function test1_getID() {
+ return "fuzzy test #1: content insertion (flush layout), removal and" +
+ "reinsertion";
+ };
+ }
+
+ /**
+ * Trigger content insertion (flush layout), removal and insertion of
+ * the same element for the different parents.
+ */
+ function test2(aContainerID, aTmpContainerID) {
+ this.divNode = document.createElement("div");
+ this.divNode.setAttribute("id", "div-test2");
+ this.containerNode = getNode(aContainerID);
+ this.tmpContainerNode = getNode(aTmpContainerID);
+ this.container = getAccessible(this.containerNode);
+ this.tmpContainer = getAccessible(this.tmpContainerNode);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.divNode),
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.unexpectedEventSeq = [
+ new invokerChecker(EVENT_REORDER, this.tmpContainerNode),
+ ];
+
+ this.invoke = function test2_invoke() {
+ this.tmpContainerNode.appendChild(this.divNode);
+ getComputedStyle(this.divNode, "").color;
+ this.tmpContainerNode.removeChild(this.divNode);
+ this.containerNode.appendChild(this.divNode);
+ };
+
+ this.getID = function test2_getID() {
+ return "fuzzy test #2: content insertion (flush layout), removal and" +
+ "reinsertion under another container";
+ };
+ }
+
+ /**
+ * Content insertion (flush layout) and then removal (nothing was changed).
+ */
+ function test3(aContainerID) {
+ this.divNode = document.createElement("div");
+ this.divNode.setAttribute("id", "div-test3");
+ this.containerNode = getNode(aContainerID);
+
+ this.unexpectedEventSeq = [
+ new invokerChecker(EVENT_SHOW, this.divNode),
+ new invokerChecker(EVENT_HIDE, this.divNode),
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.invoke = function test3_invoke() {
+ this.containerNode.appendChild(this.divNode);
+ getComputedStyle(this.divNode, "").color;
+ this.containerNode.removeChild(this.divNode);
+ };
+
+ this.getID = function test3_getID() {
+ return "fuzzy test #3: content insertion (flush layout) and removal";
+ };
+ }
+
+ function insertReferredElm(aContainerID) {
+ this.containerNode = getNode(aContainerID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, function(aNode) { return aNode.firstChild; }, this.containerNode),
+ new invokerChecker(EVENT_SHOW, function(aNode) { return aNode.lastChild; }, this.containerNode),
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.invoke = function insertReferredElm_invoke() {
+ let span = document.createElement("span");
+ span.setAttribute("id", "insertReferredElms_span");
+ let input = document.createElement("input");
+ input.setAttribute("aria-labelledby", "insertReferredElms_span");
+ this.containerNode.appendChild(span);
+ this.containerNode.appendChild(input);
+ };
+
+ this.getID = function insertReferredElm_getID() {
+ return "insert inaccessible element and then insert referring element to make it accessible";
+ };
+ }
+
+ function showHiddenParentOfVisibleChild() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("c4_child")),
+ new invokerChecker(EVENT_SHOW, getNode("c4_middle")),
+ new invokerChecker(EVENT_REORDER, getNode("c4")),
+ ];
+
+ this.invoke = function showHiddenParentOfVisibleChild_invoke() {
+ getNode("c4_middle").style.visibility = "visible";
+ };
+
+ this.getID = function showHiddenParentOfVisibleChild_getID() {
+ return "show hidden parent of visible child";
+ };
+ }
+
+ function hideNDestroyDoc() {
+ this.txt = null;
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, () => { return this.txt; }),
+ ];
+
+ this.invoke = function hideNDestroyDoc_invoke() {
+ this.txt = getAccessible("c5").firstChild.firstChild;
+ this.txt.DOMNode.remove();
+ };
+
+ this.check = function hideNDestroyDoc_check() {
+ getNode("c5").remove();
+ };
+
+ this.getID = function hideNDestroyDoc_getID() {
+ return "remove text node and destroy a document on hide event";
+ };
+ }
+
+ function hideHideNDestroyDoc() {
+ this.target = null;
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, () => { return this.target; }),
+ ];
+
+ this.invoke = function hideHideNDestroyDoc_invoke() {
+ var doc = getAccessible("c6").firstChild;
+ var l1 = doc.firstChild;
+ this.target = l1.firstChild;
+ var l2 = doc.lastChild;
+ l1.DOMNode.firstChild.remove();
+ l2.DOMNode.firstChild.remove();
+ };
+
+ this.check = function hideHideNDestroyDoc_check() {
+ getNode("c6").remove();
+ };
+
+ this.getID = function hideHideNDestroyDoc_getID() {
+ return "remove text nodes (2 events in the queue) and destroy a document on first hide event";
+ };
+ }
+
+ /**
+ * Target getters.
+ */
+ function getFirstChild(aNode) {
+ return [aNode.firstChild];
+ }
+ function getLastChild(aNode) {
+ return [aNode.lastChild];
+ }
+
+ function getNEnsureFirstChild(aNode) {
+ var node = aNode.firstChild;
+ getAccessible(node);
+ return [node];
+ }
+
+ function getNEnsureChildren(aNode) {
+ var children = [];
+ var node = aNode.firstChild;
+ do {
+ children.push(node);
+ getAccessible(node);
+ node = node.nextSibling;
+ } while (node);
+
+ return children;
+ }
+
+ function getParent(aNode) {
+ return aNode.parentNode;
+ }
+
+ // gA11yEventDumpToConsole = true; // debug stuff
+ // enableLogging("events,verbose");
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ // Show/hide events by changing of display style of accessible DOM node
+ // from 'inline' to 'none', 'none' to 'inline'.
+ let id = "link1";
+ getAccessible(id); // ensure accessible is created
+ gQueue.push(new changeStyle(id, "display", "none", kHideEvents));
+ gQueue.push(new changeStyle(id, "display", "inline", kShowEvents));
+
+ // Show/hide events by changing of visibility style of accessible DOM node
+ // from 'visible' to 'hidden', 'hidden' to 'visible'.
+ id = "link2";
+ getAccessible(id);
+ gQueue.push(new changeStyle(id, "visibility", "hidden", kHideEvents));
+ gQueue.push(new changeStyle(id, "visibility", "visible", kShowEvents));
+
+ // Show/hide events by changing of visibility style of accessible DOM node
+ // from 'collapse' to 'visible', 'visible' to 'collapse'.
+ id = "link4";
+ gQueue.push(new changeStyle(id, "visibility", "visible", kShowEvents));
+ gQueue.push(new changeStyle(id, "visibility", "collapse", kHideEvents));
+
+ // Show/hide events by adding new accessible DOM node and removing old one.
+ id = "link5";
+ gQueue.push(new cloneAndAppendToDOM(id));
+ gQueue.push(new removeFromDOM(id));
+
+ // No show/hide events by adding new not accessible DOM node and removing
+ // old one, no reorder event for their parent.
+ id = "child1";
+ gQueue.push(new cloneAndAppendToDOM(id, kNoEvents));
+ gQueue.push(new removeFromDOM(id, kNoEvents));
+
+ // Show/hide events by adding new accessible DOM node and removing
+ // old one, there is reorder event for their parent.
+ id = "child2";
+ gQueue.push(new cloneAndAppendToDOM(id));
+ gQueue.push(new removeFromDOM(id));
+
+ // Show/hide events by adding new DOM node containing accessible DOM and
+ // removing old one, there is reorder event for their parent.
+ id = "child3";
+ gQueue.push(new cloneAndAppendToDOM(id, kShowEvents, getFirstChild,
+ getParent));
+
+ // Hide event for accessible child of unaccessible removed DOM node and
+ // reorder event for its parent.
+ gQueue.push(new removeFromDOM(id, kHideEvents,
+ getNEnsureFirstChild, getParent));
+
+ // Hide events for accessible children of unaccessible removed DOM node
+ // and reorder event for its parent.
+ gQueue.push(new removeFromDOM("child4", kHideEvents,
+ getNEnsureChildren, getParent));
+
+ // Show/hide events by creating new accessible DOM node and replacing
+ // old one.
+ getAccessible("link6"); // ensure accessible is created
+ gQueue.push(new cloneAndReplaceInDOM("link6"));
+
+ // Show/hide events by changing class name on the parent node.
+ gQueue.push(new changeClass("container2", "link7", "", kShowEvents));
+ gQueue.push(new changeClass("container2", "link7", "displayNone",
+ kHideEvents));
+
+ gQueue.push(new changeClass("container3", "link8", "", kShowEvents));
+ gQueue.push(new changeClass("container3", "link8", "visibilityHidden",
+ kHideEvents));
+
+ gQueue.push(new test1("testContainer"));
+ gQueue.push(new test2("testContainer", "testContainer2"));
+ gQueue.push(new test2("testContainer", "testNestedContainer"));
+ gQueue.push(new test3("testContainer"));
+ gQueue.push(new insertReferredElm("testContainer3"));
+ gQueue.push(new showHiddenParentOfVisibleChild());
+
+ gQueue.push(new hideNDestroyDoc());
+ gQueue.push(new hideHideNDestroyDoc());
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=469985"
+ title=" turn the test from bug 354745 into mochitest">
+ Mozilla Bug 469985</a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=472662"
+ title="no reorder event when html:link display property is changed from 'none' to 'inline'">
+ Mozilla Bug 472662</a>
+ <a target="_blank"
+ title="Rework accessible tree update code"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275">
+ Mozilla Bug 570275</a>
+ <a target="_blank"
+ title="Develop a way to handle visibility style"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=606125">
+ Mozilla Bug 606125</a>
+ <a target="_blank"
+ title="Update accessible tree on content insertion after layout"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=498015">
+ Mozilla Bug 498015</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="eventdump"></div>
+
+ <div id="testContainer">
+ <a id="link1" href="http://www.google.com">Link #1</a>
+ <a id="link2" href="http://www.google.com">Link #2</a>
+ <a id="link3" href="http://www.google.com">Link #3</a>
+ <a id="link4" href="http://www.google.com" style="visibility:collapse">Link #4</a>
+ <a id="link5" href="http://www.google.com">Link #5</a>
+
+ <div id="container" role="list">
+ <span id="child1"></span>
+ <span id="child2" role="listitem"></span>
+ <span id="child3"><span role="listitem"></span></span>
+ <span id="child4"><span id="child4_1" role="listitem"></span><span id="child4_2" role="listitem"></span></span>
+ </div>
+
+ <a id="link6" href="http://www.google.com">Link #6</a>
+
+ <div id="container2" class="displayNone"><a id="link7">Link #7</a></div>
+ <div id="container3" class="visibilityHidden"><a id="link8">Link #8</a></div>
+ <div id="testNestedContainer"></div>
+ </div>
+ <div id="testContainer2"></div>
+ <div id="testContainer3"></div>
+
+ <div id="c4">
+ <div style="visibility:hidden" id="c4_middle">
+ <div style="visibility:visible" id="c4_child"></div>
+ </div>
+
+ <iframe id="c5" src="data:text/html,hey"></iframe>
+ <iframe id="c6" src="data:text/html,<label>l</label><label>l</label>"></iframe>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible name change event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ let PromEvents = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js",
+ PromEvents);
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function setAttr(aID, aAttr, aValue, aChecker) {
+ this.eventSeq = [ aChecker ];
+ this.invoke = function setAttr_invoke() {
+ getNode(aID).setAttribute(aAttr, aValue);
+ };
+
+ this.getID = function setAttr_getID() {
+ return "set attr '" + aAttr + "', value '" + aValue + "'";
+ };
+ }
+
+ /**
+ * No name change on an accessible, because the accessible is recreated.
+ */
+ function setAttr_recreate(aID, aAttr, aValue) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getAccessible(aID)),
+ new invokerChecker(EVENT_SHOW, aID),
+ ];
+ this.invoke = function setAttr_recreate_invoke() {
+ todo(false, "No accessible recreation should happen, just name change event");
+ getNode(aID).setAttribute(aAttr, aValue);
+ };
+
+ this.getID = function setAttr_recreate_getID() {
+ return "set attr '" + aAttr + "', value '" + aValue + "'";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ // gA11yEventDumpToConsole = true; // debuggin
+
+ var gQueue = null;
+ async function doTests() {
+ gQueue = new eventQueue();
+ // Later tests use await.
+ let queueFinished = new Promise(resolve => {
+ gQueue.onFinish = function() {
+ resolve();
+ return DO_NOT_FINISH_TEST;
+ };
+ });
+
+ gQueue.push(new setAttr("tst1", "aria-label", "hi",
+ new invokerChecker(EVENT_NAME_CHANGE, "tst1")));
+ gQueue.push(new setAttr("tst1", "alt", "alt",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst1")));
+ gQueue.push(new setAttr("tst1", "title", "title",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst1")));
+ gQueue.push(new setAttr("tst1", "aria-labelledby", "display",
+ new invokerChecker(EVENT_NAME_CHANGE, "tst1")));
+
+ gQueue.push(new setAttr("tst2", "aria-labelledby", "display",
+ new invokerChecker(EVENT_NAME_CHANGE, "tst2")));
+ gQueue.push(new setAttr("tst2", "alt", "alt",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2")));
+ gQueue.push(new setAttr("tst2", "title", "title",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2")));
+ gQueue.push(new setAttr("tst2", "aria-label", "hi",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2")));
+
+ // When `alt` attribute is added or removed from a broken img,
+ // the accessible is recreated.
+ gQueue.push(new setAttr_recreate("tst3", "alt", "one"));
+ // When an `alt` attribute is changed, there is a name change event.
+ gQueue.push(new setAttr("tst3", "alt", "two",
+ new invokerChecker(EVENT_NAME_CHANGE, "tst3")));
+ gQueue.push(new setAttr("tst3", "title", "title",
+ new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst3")));
+
+ gQueue.push(new setAttr("tst4", "title", "title",
+ new invokerChecker(EVENT_NAME_CHANGE, "tst4")));
+
+ gQueue.invoke();
+ await queueFinished;
+ // Tests beyond this point use await rather than eventQueue.
+
+ const labelledBy = getNode("labelledBy");
+ const label = getNode("label");
+ let nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, labelledBy);
+ info("Changing text of aria-labelledby target");
+ label.textContent = "l2";
+ await nameChanged;
+ nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, labelledBy);
+ info("Adding node to aria-labelledby target");
+ label.innerHTML = '<p id="labelChild">l3</p>';
+ await nameChanged;
+ nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, labelledBy);
+ info("Changing text of aria-labelledby target's child");
+ getNode("labelChild").textContent = "l4";
+ await nameChanged;
+
+ const lateLabelledBy = getNode("lateLabelledBy");
+ nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, lateLabelledBy);
+ info("Setting aria-labelledby");
+ lateLabelledBy.setAttribute("aria-labelledby", "lateLabel");
+ await nameChanged;
+ nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, lateLabelledBy);
+ info("Changing text of late aria-labelledby target's child");
+ getNode("lateLabelChild").textContent = "l2";
+ await nameChanged;
+
+ nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, "listitem");
+ info("Changing textContent of listitem child");
+ // Changing textContent replaces the text leaf with a new one.
+ getNode("listitem").textContent = "world";
+ await nameChanged;
+
+ nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, "button");
+ info("Changing text of button's text leaf");
+ // Changing the text node's data changes the text without replacing the
+ // leaf.
+ getNode("button").firstChild.data = "after";
+ await nameChanged;
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=991969"
+ title="Event not fired when description changes">
+ Bug 991969
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <img id="tst1" alt="initial" src="../moz.png">
+ <img id="tst2" src="../moz.png">
+ <img id="tst3">
+ <img id="tst4" src="../moz.png">
+
+ <div id="labelledBy" aria-labelledby="label"></div>
+ <div id="label">l1</div>
+
+ <div id="lateLabelledBy"></div>
+ <div id="lateLabel"><p id="lateLabelChild">l1</p></div>
+
+ <ul><li id="listitem">hello</li></ul>
+
+ <button id="button">before</button>
+
+ <div id="eventdump"></div>
+</body>
+</html>
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 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/chrome-harness.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ /**
+ * Check name changed a11y event.
+ */
+ function nameChangeChecker(aMsg, aID)
+ {
+ this.type = EVENT_NAME_CHANGE;
+
+ function targetGetter()
+ {
+ return getAccessible(aID);
+ }
+ Object.defineProperty(this, "target", { get: targetGetter });
+
+ this.getID = function getID()
+ {
+ return aMsg + " name changed";
+ }
+ }
+
+ function changeRichListItemChild()
+ {
+ this.invoke = function changeRichListItemChild_invoke()
+ {
+ getNode('childcontent').setAttribute('value', 'Changed.');
+ }
+
+ this.eventSeq =
+ [
+ new nameChangeChecker("changeRichListItemChild: ", "listitem")
+ ];
+
+ this.getID = function changeRichListItemChild_getID()
+ {
+ return "changeRichListItemChild";
+ }
+ }
+
+ function doTest()
+ {
+ var queue = new eventQueue();
+ queue.push(new changeRichListItemChild());
+ queue.invoke();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <vbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=986054"
+ title="Propagate name change events">
+ Mozilla Bug 986054
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <richlistbox>
+ <richlistitem id="listitem">
+ <description id="childcontent" value="This will be changed."/>
+ </richlistitem>
+ </richlistbox>
+ </vbox>
+</window>
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 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/chrome-harness.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../promisified-events.js" />
+ <script type="application/javascript"
+ src="../browser.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Tests
+
+ function matchesAnchorJumpInTabDocument(aTabIdx) {
+ return evt => {
+ let tabDoc = aTabIdx ? tabDocumentAt(aTabIdx) : currentTabDocument();
+ let anchorAcc = getAccessible(tabDoc.querySelector("a[name='link1']"));
+ return anchorAcc == evt.accessible;
+ }
+ }
+
+ function matchesTabDocumentAt(aTabIdx) {
+ return evt => {
+ let tabDoc = aTabIdx ? tabDocumentAt(aTabIdx) : currentTabDocument();
+ return getAccessible(tabDoc) == evt.accessible;
+ }
+ }
+
+ async function doTest() {
+ const kURL = "http://mochi.test:8888/a11y/accessible/tests/mochitest/events/scroll.html#link1";
+ let evtProm = waitForEvents([
+ [EVENT_DOCUMENT_LOAD_COMPLETE, currentTabDocument],
+ [EVENT_SCROLLING_START, matchesAnchorJumpInTabDocument()],
+ ], "Load foreground tab then scroll to anchor");
+
+ tabBrowser().loadURI(Services.io.newURI(kURL), {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ // Flush layout, so as to guarantee that the a11y tree is constructed.
+ browserDocument().documentElement.getBoundingClientRect();
+ await evtProm;
+
+
+ evtProm = waitForEvents({
+ expected: [[EVENT_DOCUMENT_LOAD_COMPLETE, matchesTabDocumentAt(1)]],
+ unexpected: [[EVENT_SCROLLING_START, matchesAnchorJumpInTabDocument(1)]],
+ }, "Load background tab, don't dispatch scroll to anchor event");
+
+ tabBrowser().addTab(kURL, {
+ referrerURI: null,
+ charset: "",
+ postData: null,
+ inBackground: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ // Flush layout, so as to guarantee that the a11y tree is constructed.
+ browserDocument().documentElement.getBoundingClientRect();
+ await evtProm;
+
+ evtProm = waitForEvent(EVENT_SCROLLING_START,
+ matchesAnchorJumpInTabDocument(),
+ "Scroll to anchor event dispatched when switching to loaded doc.");
+ tabBrowser().selectTabAtIndex(1);
+ await evtProm;
+
+ closeBrowserWindow();
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ openBrowserWindow(doTest);
+ ]]>
+ </script>
+
+ <vbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=691734"
+ title="Make sure scrolling start event is fired when document receive focus">
+ Mozilla Bug 691734
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="eventdump"></vbox>
+ </vbox>
+</window>
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 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/chrome-harness.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../browser.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Tests
+
+ function getAnchorJumpInTabDocument(aTabIdx)
+ {
+ var tabDoc = aTabIdx ? tabDocumentAt(aTabIdx) : currentTabDocument();
+ return tabDoc.querySelector("h1[id='heading_1']");
+ }
+
+ function loadTab(aURL)
+ {
+ this.eventSeq = [
+ new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, currentTabDocument),
+ new asyncCaretMoveChecker(0, getAnchorJumpInTabDocument)
+ ];
+
+ this.invoke = function loadTab_invoke()
+ {
+ tabBrowser().loadURI(Services.io.newURI(aURL), {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ }
+
+ this.getID = function loadTab_getID()
+ {
+ return "load tab: " + aURL;
+ }
+ }
+
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTest()
+ {
+ gQueue = new eventQueue();
+
+ var url = "http://mochi.test:8888/a11y/accessible/tests/mochitest/events/scroll.html#heading_1";
+ gQueue.push(new loadTab(url));
+ gQueue.onFinish = function() { closeBrowserWindow(); }
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ openBrowserWindow(doTest);
+ ]]>
+ </script>
+
+ <vbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1056459"
+ title="Make sure caret move event is fired when document receive focus">
+ Mozilla Bug 1056459
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="eventdump"></vbox>
+ </vbox>
+</window>
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 @@
+<html>
+
+<head>
+ <title>Accessible selection event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ // gA11yEventDumpToConsole = true; // debuggin
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+
+ // closed combobox
+ gQueue.push(new synthFocus("combobox"));
+ gQueue.push(new synthDownKey("cb1_item1",
+ selChangeSeq("cb1_item1", "cb1_item2")));
+
+ // listbox
+ gQueue.push(new synthClick("lb1_item1",
+ new invokerChecker(EVENT_SELECTION, "lb1_item1")));
+ gQueue.push(new synthDownKey("lb1_item1",
+ selChangeSeq("lb1_item1", "lb1_item2")));
+
+ // multiselectable listbox
+ gQueue.push(new synthClick("lb2_item1",
+ selChangeSeq(null, "lb2_item1")));
+ gQueue.push(new synthDownKey("lb2_item1",
+ selAddSeq("lb2_item2"),
+ { shiftKey: true }));
+ gQueue.push(new synthUpKey("lb2_item2",
+ selRemoveSeq("lb2_item2"),
+ { shiftKey: true }));
+ gQueue.push(new synthKey("lb2_item1", " ", { ctrlKey: true },
+ selRemoveSeq("lb2_item1")));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=414302"
+ title="Incorrect selection events in HTML, XUL and ARIA">
+ Bug 414302
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=810268"
+ title="There's no way to know unselected item when selection in single selection was changed">
+ Bug 810268
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <select id="combobox">
+ <option id="cb1_item1" value="mushrooms">mushrooms
+ <option id="cb1_item2" value="greenpeppers">green peppers
+ <option id="cb1_item3" value="onions" id="onions">onions
+ <option id="cb1_item4" value="tomatoes">tomatoes
+ <option id="cb1_item5" value="olives">olives
+ </select>
+
+ <select id="listbox" size=5>
+ <option id="lb1_item1" value="mushrooms">mushrooms
+ <option id="lb1_item2" value="greenpeppers">green peppers
+ <option id="lb1_item3" value="onions" id="onions">onions
+ <option id="lb1_item4" value="tomatoes">tomatoes
+ <option id="lb1_item5" value="olives">olives
+ </select>
+
+ <p>Pizza</p>
+ <select id="listbox2" multiple size=5>
+ <option id="lb2_item1" value="mushrooms">mushrooms
+ <option id="lb2_item2" value="greenpeppers">green peppers
+ <option id="lb2_item3" value="onions" id="onions">onions
+ <option id="lb2_item4" value="tomatoes">tomatoes
+ <option id="lb2_item5" value="olives">olives
+ </select>
+</body>
+</html>
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 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Selection event tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ function advanceTab(aTabsID, aDirection, aNextTabID)
+ {
+ var eventSeq1 = [
+ new invokerChecker(EVENT_SELECTION, aNextTabID)
+ ]
+ defineScenario(this, eventSeq1);
+
+ var eventSeq2 = [
+ new invokerChecker(EVENT_HIDE, getAccessible(aNextTabID)),
+ new invokerChecker(EVENT_SHOW, aNextTabID)
+ ];
+ defineScenario(this, eventSeq2);
+
+ this.invoke = function advanceTab_invoke()
+ {
+ todo(false, "No accessible recreation should happen, just selection event");
+ getNode(aTabsID).advanceSelectedTab(aDirection, true);
+ }
+
+ this.getID = function synthFocus_getID()
+ {
+ return "advanceTab on " + prettyName(aTabsID) + " to " + prettyName(aNextTabID);
+ }
+ }
+
+ function select4FirstItems(aID)
+ {
+ this.listboxNode = getNode(aID);
+ this.eventSeq = [
+ new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(0)),
+ new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(1)),
+ new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(2)),
+ new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(3))
+ ];
+
+ this.invoke = function select4FirstItems_invoke()
+ {
+ synthesizeKey("VK_DOWN", { shiftKey: true }); // selects two items
+ synthesizeKey("VK_DOWN", { shiftKey: true });
+ synthesizeKey("VK_DOWN", { shiftKey: true });
+ }
+
+ this.getID = function select4FirstItems_getID()
+ {
+ return "select 4 first items for " + prettyName(aID);
+ }
+ }
+
+ function unselect4FirstItems(aID)
+ {
+ this.listboxNode = getNode(aID);
+ this.eventSeq = [
+ new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(3)),
+ new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(2)),
+ new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(1)),
+ new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(0))
+ ];
+
+ this.invoke = function unselect4FirstItems_invoke()
+ {
+ synthesizeKey("VK_UP", { shiftKey: true });
+ synthesizeKey("VK_UP", { shiftKey: true });
+ synthesizeKey("VK_UP", { shiftKey: true });
+ synthesizeKey(" ", { ctrlKey: true }); // unselect first item
+ }
+
+ this.getID = function unselect4FirstItems_getID()
+ {
+ return "unselect 4 first items for " + prettyName(aID);
+ }
+ }
+
+ function selectAllItems(aID)
+ {
+ this.listboxNode = getNode(aID);
+ this.eventSeq = [
+ new invokerChecker(EVENT_SELECTION_WITHIN, getAccessible(this.listboxNode))
+ ];
+
+ this.invoke = function selectAllItems_invoke()
+ {
+ synthesizeKey("VK_END", { shiftKey: true });
+ }
+
+ this.getID = function selectAllItems_getID()
+ {
+ return "select all items for " + prettyName(aID);
+ }
+ }
+
+ function unselectAllItemsButFirst(aID)
+ {
+ this.listboxNode = getNode(aID);
+ this.eventSeq = [
+ new invokerChecker(EVENT_SELECTION_WITHIN, getAccessible(this.listboxNode))
+ ];
+
+ this.invoke = function unselectAllItemsButFirst_invoke()
+ {
+ synthesizeKey("VK_HOME", { shiftKey: true });
+ }
+
+ this.getID = function unselectAllItemsButFirst_getID()
+ {
+ return "unselect all items for " + prettyName(aID);
+ }
+ }
+
+ function unselectSelectItem(aID)
+ {
+ this.listboxNode = getNode(aID);
+ this.eventSeq = [
+ new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(0)),
+ new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(0))
+ ];
+
+ this.invoke = function unselectSelectItem_invoke()
+ {
+ synthesizeKey(" ", { ctrlKey: true }); // select item
+ synthesizeKey(" ", { ctrlKey: true }); // unselect item
+ }
+
+ this.getID = function unselectSelectItem_getID()
+ {
+ return "unselect and then select first item for " + prettyName(aID);
+ }
+ }
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ //enableLogging("events");
+ //gA11yEventDumpToConsole = true; // debuggin
+
+ function doTests()
+ {
+ gQueue = new eventQueue();
+
+ //////////////////////////////////////////////////////////////////////////
+ // tabbox
+ gQueue.push(new advanceTab("tabs", 1, "tab3"));
+
+ //////////////////////////////////////////////////////////////////////////
+ // single selection listbox, the first item is selected by default
+
+ gQueue.push(new synthClick("lb1_item2",
+ new invokerChecker(EVENT_SELECTION, "lb1_item2")));
+ gQueue.push(new synthUpKey("lb1_item2",
+ new invokerChecker(EVENT_SELECTION, "lb1_item1")));
+ gQueue.push(new synthDownKey("lb1_item1",
+ new invokerChecker(EVENT_SELECTION, "lb1_item2")));
+
+ //////////////////////////////////////////////////////////////////////////
+ // multiselectable listbox
+ gQueue.push(new synthClick("lb2_item1",
+ new invokerChecker(EVENT_SELECTION, "lb2_item1")));
+ gQueue.push(new synthDownKey("lb2_item1",
+ new invokerChecker(EVENT_SELECTION_ADD, "lb2_item2"),
+ { shiftKey: true }));
+ gQueue.push(new synthUpKey("lb2_item2",
+ new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item2"),
+ { shiftKey: true }));
+ gQueue.push(new synthKey("lb2_item1", " ", { ctrlKey: true },
+ new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item1")));
+
+ //////////////////////////////////////////////////////////////////////////
+ // selection event coalescence
+
+ // fire 4 selection_add events
+ gQueue.push(new select4FirstItems("listbox2"));
+ // fire 4 selection_remove events
+ gQueue.push(new unselect4FirstItems("listbox2"));
+ // fire selection_within event
+ gQueue.push(new selectAllItems("listbox2"));
+ // fire selection_within event
+ gQueue.push(new unselectAllItemsButFirst("listbox2"));
+ // fire selection_remove/add events
+ gQueue.push(new unselectSelectItem("listbox2"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=414302"
+ title="Incorrect selection events in HTML, XUL and ARIA">
+ Mozilla Bug 414302
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <tabbox id="tabbox" selectedIndex="1">
+ <tabs id="tabs">
+ <tab id="tab1" label="tab1"/>
+ <tab id="tab2" label="tab2"/>
+ <tab id="tab3" label="tab3"/>
+ <tab id="tab4" label="tab4"/>
+ </tabs>
+ <tabpanels>
+ <tabpanel><!-- tabpanel First elements go here --></tabpanel>
+ <tabpanel><button id="b1" label="b1"/></tabpanel>
+ <tabpanel><button id="b2" label="b2"/></tabpanel>
+ <tabpanel></tabpanel>
+ </tabpanels>
+ </tabbox>
+
+ <richlistbox id="listbox">
+ <richlistitem id="lb1_item1"><label value="item1"/></richlistitem>
+ <richlistitem id="lb1_item2"><label value="item2"/></richlistitem>
+ </richlistbox>
+
+ <richlistbox id="listbox2" seltype="multiple">
+ <richlistitem id="lb2_item1"><label value="item1"/></richlistitem>
+ <richlistitem id="lb2_item2"><label value="item2"/></richlistitem>
+ <richlistitem id="lb2_item3"><label value="item3"/></richlistitem>
+ <richlistitem id="lb2_item4"><label value="item4"/></richlistitem>
+ <richlistitem id="lb2_item5"><label value="item5"/></richlistitem>
+ <richlistitem id="lb2_item6"><label value="item6"/></richlistitem>
+ <richlistitem id="lb2_item7"><label value="item7"/></richlistitem>
+ </richlistbox>
+
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_selection_aria.html b/accessible/tests/mochitest/events/test_selection_aria.html
new file mode 100644
index 0000000000..c479868e03
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_selection_aria.html
@@ -0,0 +1,122 @@
+<html>
+
+<head>
+ <title>ARIA selection event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function selectItem(aSelectID, aItemID) {
+ this.selectNode = getNode(aSelectID);
+ this.itemNode = getNode(aItemID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SELECTION, aItemID),
+ ];
+
+ this.invoke = function selectItem_invoke() {
+ var itemNode = this.selectNode.querySelector("*[aria-selected='true']");
+ if (itemNode)
+ itemNode.removeAttribute("aria-selected");
+
+ this.itemNode.setAttribute("aria-selected", "true");
+ };
+
+ this.getID = function selectItem_getID() {
+ return "select item " + prettyName(aItemID);
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ var gQueue = null;
+
+ // gA11yEventDumpToConsole = true; // debug stuff
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new selectItem("tablist", "tab1"));
+ gQueue.push(new selectItem("tablist", "tab2"));
+
+ gQueue.push(new selectItem("tree", "treeitem1"));
+ gQueue.push(new selectItem("tree", "treeitem1a"));
+ gQueue.push(new selectItem("tree", "treeitem1a1"));
+
+ gQueue.push(new selectItem("tree2", "tree2item1"));
+ gQueue.push(new selectItem("tree2", "tree2item1a"));
+ gQueue.push(new selectItem("tree2", "tree2item1a1"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=569653"
+ title="Make selection events async">
+ Mozilla Bug 569653
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=804040"
+ title="Selection event not fired when selection of ARIA tab changes">
+ Mozilla Bug 804040
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div role="tablist" id="tablist">
+ <div role="tab" id="tab1">tab1</div>
+ <div role="tab" id="tab2">tab2</div>
+ </div>
+
+ <div id="tree" role="tree">
+ <div id="treeitem1" role="treeitem">Canada
+ <div id="treeitem1a" role="treeitem">- Ontario
+ <div id="treeitem1a1" role="treeitem">-- Toronto</div>
+ </div>
+ <div id="treeitem1b" role="treeitem">- Manitoba</div>
+ </div>
+ <div id="treeitem2" role="treeitem">Germany</div>
+ <div id="treeitem3" role="treeitem">Russia</div>
+ </div>
+
+ <div id="tree2" role="tree" aria-multiselectable="true">
+ <div id="tree2item1" role="treeitem">Canada
+ <div id="tree2item1a" role="treeitem">- Ontario
+ <div id="tree2item1a1" role="treeitem">-- Toronto</div>
+ </div>
+ <div id="tree2item1b" role="treeitem">- Manitoba</div>
+ </div>
+ <div id="tree2item2" role="treeitem">Germany</div>
+ <div id="tree2item3" role="treeitem">Russia</div>
+ </div>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_statechange.html b/accessible/tests/mochitest/events/test_statechange.html
new file mode 100644
index 0000000000..0642851408
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_statechange.html
@@ -0,0 +1,565 @@
+<html>
+
+<head>
+ <title>Accessible state change event testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ async function openNode(aIDDetails, aIDSummary, aIsOpen) {
+ let p = waitForStateChange(aIDSummary, STATE_EXPANDED, aIsOpen, false);
+ if (aIsOpen) {
+ getNode(aIDDetails).setAttribute("open", "");
+ } else {
+ getNode(aIDDetails).removeAttribute("open");
+ }
+ await p;
+ }
+
+ async function makeEditableDoc(aDocNode, aIsEnabled) {
+ let p = waitForStateChange(aDocNode, EXT_STATE_EDITABLE, true, true);
+ aDocNode.designMode = "on";
+ await p;
+ }
+
+ async function invalidInput(aNodeOrID) {
+ let p = waitForStateChange(aNodeOrID, STATE_INVALID, true, false);
+ getNode(aNodeOrID).value = "I am not an email";
+ await p;
+ }
+
+ async function changeCheckInput(aID, aIsChecked) {
+ let p = waitForStateChange(aID, STATE_CHECKED, aIsChecked, false);
+ getNode(aID).checked = aIsChecked;
+ await p;
+ }
+
+ async function changeRequiredState(aID, aIsRequired) {
+ let p = waitForStateChange(aID, STATE_REQUIRED, aIsRequired, false);
+ getNode(aID).required = aIsRequired;
+ await p;
+ }
+
+ async function stateChangeOnFileInput(aID, aAttr, aValue,
+ aState, aIsExtraState, aIsEnabled) {
+ let fileControlNode = getNode(aID);
+ let browseButton = getAccessible(fileControlNode);
+ let p = waitForStateChange(
+ browseButton, aState, aIsEnabled, aIsExtraState
+ );
+ fileControlNode.setAttribute(aAttr, aValue);
+ await p;
+ }
+
+ function toggleSentinel() {
+ let sentinel = getNode("sentinel");
+ if (sentinel.hasAttribute("aria-busy")) {
+ sentinel.removeAttribute("aria-busy");
+ } else {
+ sentinel.setAttribute("aria-busy", "true");
+ }
+ }
+
+ async function toggleStateChange(aID, aAttr, aState, aIsExtraState) {
+ let p = waitForEvents([
+ stateChangeEventArgs(aID, aState, true, aIsExtraState),
+ [EVENT_STATE_CHANGE, "sentinel"]
+ ]);
+ getNode(aID).setAttribute(aAttr, "true");
+ toggleSentinel();
+ await p;
+ p = waitForEvents([
+ stateChangeEventArgs(aID, aState, false, aIsExtraState),
+ [EVENT_STATE_CHANGE, "sentinel"]
+ ]);
+ getNode(aID).setAttribute(aAttr, "false");
+ toggleSentinel();
+ await p;
+ }
+
+ async function dupeStateChange(aID, aAttr, aValue,
+ aState, aIsExtraState, aIsEnabled) {
+ let p = waitForEvents([
+ stateChangeEventArgs(aID, aState, aIsEnabled, aIsExtraState),
+ [EVENT_STATE_CHANGE, "sentinel"]
+ ]);
+ getNode(aID).setAttribute(aAttr, aValue);
+ getNode(aID).setAttribute(aAttr, aValue);
+ toggleSentinel();
+ await p;
+ }
+
+ async function oppositeStateChange(aID, aAttr, aState, aIsExtraState) {
+ let p = waitForEvents({
+ expected: [[EVENT_STATE_CHANGE, "sentinel"]],
+ unexpected: [
+ stateChangeEventArgs(aID, aState, false, aIsExtraState),
+ stateChangeEventArgs(aID, aState, true, aIsExtraState)
+ ]
+ });
+ getNode(aID).setAttribute(aAttr, "false");
+ getNode(aID).setAttribute(aAttr, "true");
+ toggleSentinel();
+ await p;
+ }
+
+ /**
+ * Change concomitant ARIA and native attribute at once.
+ */
+ async function echoingStateChange(aID, aARIAAttr, aAttr, aValue,
+ aState, aIsExtraState, aIsEnabled) {
+ let p = waitForStateChange(aID, aState, aIsEnabled, aIsExtraState);
+ if (aValue == null) {
+ getNode(aID).removeAttribute(aARIAAttr);
+ getNode(aID).removeAttribute(aAttr);
+ } else {
+ getNode(aID).setAttribute(aARIAAttr, aValue);
+ getNode(aID).setAttribute(aAttr, aValue);
+ }
+ await p;
+ }
+
+ async function testHasPopup() {
+ let p = waitForStateChange("popupButton", STATE_HASPOPUP, true, false);
+ getNode("popupButton").setAttribute("aria-haspopup", "true");
+ await p;
+
+ p = waitForStateChange("popupButton", STATE_HASPOPUP, false, false);
+ getNode("popupButton").setAttribute("aria-haspopup", "false");
+ await p;
+
+ p = waitForStateChange("popupButton", STATE_HASPOPUP, true, false);
+ getNode("popupButton").setAttribute("aria-haspopup", "true");
+ await p;
+
+ p = waitForStateChange("popupButton", STATE_HASPOPUP, false, false);
+ getNode("popupButton").removeAttribute("aria-haspopup");
+ await p;
+ }
+
+ async function testDefaultSubmitChange() {
+ testStates("default-button",
+ STATE_DEFAULT, 0,
+ 0, 0,
+ "button should have DEFAULT state");
+ let button = document.createElement("button");
+ button.textContent = "new default";
+ let p = waitForStateChange("default-button", STATE_DEFAULT, false, false);
+ getNode("default-button").before(button);
+ await p;
+ testStates("default-button",
+ 0, 0,
+ STATE_DEFAULT, 0,
+ "button should not have DEFAULT state");
+ p = waitForStateChange("default-button", STATE_DEFAULT, true, false);
+ button.remove();
+ await p;
+ testStates("default-button",
+ STATE_DEFAULT, 0,
+ 0, 0,
+ "button should have DEFAULT state");
+ }
+
+ async function testReadOnly() {
+ let p = waitForStateChange("email", STATE_READONLY, true, false);
+ getNode("email").setAttribute("readonly", "true");
+ await p;
+ p = waitForStateChange("email", STATE_READONLY, false, false);
+ getNode("email").removeAttribute("readonly");
+ await p;
+ }
+
+ async function testReadonlyUntilEditable() {
+ testStates("article",
+ STATE_READONLY, 0,
+ 0, EXT_STATE_EDITABLE,
+ "article is READONLY and not EDITABLE");
+ let p = waitForEvents([
+ stateChangeEventArgs("article", STATE_READONLY, false, false),
+ stateChangeEventArgs("article", EXT_STATE_EDITABLE, true, true)]);
+ getNode("article").contentEditable = "true";
+ await p;
+ testStates("article",
+ 0, EXT_STATE_EDITABLE,
+ STATE_READONLY, 0,
+ "article is EDITABLE and not READONLY");
+ p = waitForEvents([
+ stateChangeEventArgs("article", STATE_READONLY, true, false),
+ stateChangeEventArgs("article", EXT_STATE_EDITABLE, false, true)]);
+ getNode("article").contentEditable = "false";
+ await p;
+ testStates("article",
+ STATE_READONLY, 0,
+ 0, EXT_STATE_EDITABLE,
+ "article is READONLY and not EDITABLE");
+ }
+
+ async function testAnimatedImage() {
+ testStates("animated-image",
+ STATE_ANIMATED, 0,
+ 0, 0,
+ "image should be animated 1");
+ let p = waitForStateChange("animated-image", STATE_ANIMATED, false, false);
+ getNode("animated-image").src = "../animated-gif-finalframe.gif";
+ await p;
+ testStates("animated-image",
+ 0, 0,
+ STATE_ANIMATED, 0,
+ "image should not be animated 2");
+ p = waitForStateChange("animated-image", STATE_ANIMATED, true, false);
+ getNode("animated-image").src = "../animated-gif.gif";
+ await p;
+ testStates("animated-image",
+ STATE_ANIMATED, 0,
+ 0, 0,
+ "image should be animated 3");
+ }
+
+ async function testImageLoad() {
+ let img = document.createElement("img");
+ img.id = "image";
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ img.src = "http://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs";
+ let p = waitForEvent(EVENT_SHOW, "image");
+ getNode("eventdump").before(img);
+ await p;
+ testStates("image",
+ STATE_INVISIBLE, 0,
+ 0, 0,
+ "image should be invisible");
+ p = waitForStateChange("image", STATE_INVISIBLE, false, false);
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ await fetch("http://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs?complete");
+ await p;
+ testStates("image",
+ 0, 0,
+ STATE_INVISIBLE, 0,
+ "image should be invisible");
+ }
+
+ async function testMultiSelectable(aID, aAttribute) {
+ testStates(aID,
+ 0, 0,
+ STATE_MULTISELECTABLE | STATE_EXTSELECTABLE, 0,
+ `${aID} should not be multiselectable`);
+ let p = waitForEvents([
+ stateChangeEventArgs(aID, STATE_MULTISELECTABLE, true, false),
+ stateChangeEventArgs(aID, STATE_EXTSELECTABLE, true, false),
+ ]);
+ getNode(aID).setAttribute(aAttribute, true);
+ await p;
+ testStates(aID,
+ STATE_MULTISELECTABLE | STATE_EXTSELECTABLE, 0,
+ 0, 0,
+ `${aID} should not be multiselectable`);
+ p = waitForEvents([
+ stateChangeEventArgs(aID, STATE_MULTISELECTABLE, false, false),
+ stateChangeEventArgs(aID, STATE_EXTSELECTABLE, false, false),
+ ]);
+ getNode(aID).removeAttribute(aAttribute);
+ await p;
+ testStates(aID,
+ 0, 0,
+ STATE_MULTISELECTABLE | STATE_EXTSELECTABLE, 0,
+ `${aID} should not be multiselectable`);
+ }
+
+ async function testAutocomplete() {
+ // A text input will have autocomplete via browser's form autofill...
+ testStates("input",
+ 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION,
+ 0, 0,
+ "input supports autocompletion");
+ // unless it is explicitly turned off.
+ testStates("input-autocomplete-off",
+ 0, 0,
+ 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION,
+ "input-autocomplete-off does not support autocompletion");
+ // An input with a datalist will always have autocomplete.
+ testStates("input-list",
+ 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION,
+ 0, 0,
+ "input-list supports autocompletion");
+ // password fields don't get autocomplete.
+ testStates("input-password",
+ 0, 0,
+ 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION,
+ "input-autocomplete-off does not support autocompletion");
+
+ let p = waitForEvents({
+ expected: [
+ // Setting the form's autocomplete attribute to "off" will cause
+ // "input" to lost its autocomplete state.
+ stateChangeEventArgs("input", EXT_STATE_SUPPORTS_AUTOCOMPLETION, false, true)
+ ],
+ unexpected: [
+ // "input-list" should preserve its autocomplete state regardless of
+ // forms "autocomplete" attribute
+ [EVENT_STATE_CHANGE, "input-list"],
+ // "input-autocomplete-off" already has its autocomplte off, so no state
+ // change here.
+ [EVENT_STATE_CHANGE, "input-autocomplete-off"],
+ // passwords never get autocomplete
+ [EVENT_STATE_CHANGE, "input-password"],
+ ]
+ });
+
+ getNode("form").setAttribute("autocomplete", "off");
+
+ await p;
+
+ // Same when we remove the form's autocomplete attribute.
+ p = waitForEvents({
+ expected: [stateChangeEventArgs("input", EXT_STATE_SUPPORTS_AUTOCOMPLETION, true, true)],
+ unexpected: [
+ [EVENT_STATE_CHANGE, "input-list"],
+ [EVENT_STATE_CHANGE, "input-autocomplete-off"],
+ [EVENT_STATE_CHANGE, "input-password"],
+ ]
+ });
+
+ getNode("form").removeAttribute("autocomplete");
+
+ await p;
+
+ p = waitForEvents({
+ expected: [
+ // Forcing autocomplete off on an input will cause a state change
+ stateChangeEventArgs("input", EXT_STATE_SUPPORTS_AUTOCOMPLETION, false, true),
+ // Associating a datalist with an autocomplete=off input
+ // will give it an autocomplete state, regardless.
+ stateChangeEventArgs("input-autocomplete-off", EXT_STATE_SUPPORTS_AUTOCOMPLETION, true, true),
+ // XXX: datalist inputs also get a HASPOPUP state, the inconsistent
+ // use of that state is inexplicable, but lets make sure we fire state
+ // change events for it anyway.
+ stateChangeEventArgs("input-autocomplete-off", STATE_HASPOPUP, true, false),
+ ],
+ unexpected: [
+ // Forcing autocomplete off with a dataset input does nothing.
+ [EVENT_STATE_CHANGE, "input-list"],
+ // passwords never get autocomplete
+ [EVENT_STATE_CHANGE, "input-password"],
+ ]
+ });
+
+ getNode("input").setAttribute("autocomplete", "off");
+ getNode("input-list").setAttribute("autocomplete", "off");
+ getNode("input-autocomplete-off").setAttribute("list", "browsers");
+ getNode("input-password").setAttribute("autocomplete", "off");
+
+ await p;
+ }
+
+ async function doTests() {
+ // Disable mixed-content upgrading as this test is expecting an HTTP load
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.mixed_content.upgrade_display_content", false]]
+ });
+
+ // Test opening details objects
+ await openNode("detailsOpen", "summaryOpen", true);
+ await openNode("detailsOpen", "summaryOpen", false);
+ await openNode("detailsOpen1", "summaryOpen1", true);
+ await openNode("detailsOpen2", "summaryOpen2", true);
+ await openNode("detailsOpen3", "summaryOpen3", true);
+ await openNode("detailsOpen4", "summaryOpen4", true);
+ await openNode("detailsOpen5", "summaryOpen5", true);
+ await openNode("detailsOpen6", "summaryOpen6", true);
+
+ // Test delayed editable state change
+ var doc = document.getElementById("iframe").contentDocument;
+ await makeEditableDoc(doc);
+
+ // invalid state change
+ await invalidInput("email");
+
+ // checked state change
+ await changeCheckInput("checkbox", true);
+ await changeCheckInput("checkbox", false);
+ await changeCheckInput("radio", true);
+ await changeCheckInput("radio", false);
+
+ // required state change
+ await changeRequiredState("checkbox", true);
+
+ // file input inherited state changes
+ await stateChangeOnFileInput("file", "aria-busy", "true",
+ STATE_BUSY, false, true);
+ await stateChangeOnFileInput("file", "aria-required", "true",
+ STATE_REQUIRED, false, true);
+ await stateChangeOnFileInput("file", "aria-invalid", "true",
+ STATE_INVALID, false, true);
+
+ await dupeStateChange("div", "aria-busy", "true",
+ STATE_BUSY, false, true);
+ await oppositeStateChange("div", "aria-busy",
+ STATE_BUSY, false);
+
+ await echoingStateChange("text1", "aria-disabled", "disabled", "true",
+ EXT_STATE_ENABLED, true, false);
+ await echoingStateChange("text1", "aria-disabled", "disabled", null,
+ EXT_STATE_ENABLED, true, true);
+
+ await testReadOnly();
+
+ await testReadonlyUntilEditable();
+
+ await testHasPopup();
+
+ await toggleStateChange("textbox", "aria-multiline", EXT_STATE_MULTI_LINE, true);
+
+ await testDefaultSubmitChange();
+
+ await testAnimatedImage();
+
+ await testImageLoad();
+
+ await testMultiSelectable("listbox", "aria-multiselectable");
+
+ await testMultiSelectable("select", "multiple");
+
+ await testAutocomplete();
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+<style>
+ details.openBefore::before{
+ content: "before detail content: ";
+ background: blue;
+ }
+ summary.openBefore::before{
+ content: "before summary content: ";
+ background: green;
+ }
+ details.openAfter::after{
+ content: " :after detail content";
+ background: blue;
+ }
+ summary.openAfter::after{
+ content: " :after summary content";
+ background: green;
+ }
+</style>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=564471"
+ title="Make state change events async">
+ Bug 564471
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=555728"
+ title="Fire a11y event based on HTML5 constraint validation">
+ Bug 555728
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=699017"
+ title="File input control should be propogate states to descendants">
+ Bug 699017
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=788389"
+ title="Fire statechange event whenever checked state is changed not depending on focused state">
+ Bug 788389
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=926812"
+ title="State change event not fired when both disabled and aria-disabled are toggled">
+ Bug 926812
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- open -->
+ <details id="detailsOpen"><summary id="summaryOpen">open</summary>details can be opened</details>
+ <details id="detailsOpen1">order doesn't matter<summary id="summaryOpen1">open</summary></details>
+ <details id="detailsOpen2"><div>additional elements don't matter</div><summary id="summaryOpen2">open</summary></details>
+ <details id="detailsOpen3" class="openBefore"><summary id="summaryOpen3">summary</summary>content</details>
+ <details id="detailsOpen4" class="openAfter"><summary id="summaryOpen4">summary</summary>content</details>
+ <details id="detailsOpen5"><summary id="summaryOpen5" class="openBefore">summary</summary>content</details>
+ <details id="detailsOpen6"><summary id="summaryOpen6" class="openAfter">summary</summary>content</details>
+
+
+ <div id="testContainer">
+ <iframe id="iframe"></iframe>
+ </div>
+
+ <input id="email" type='email'>
+
+ <input id="checkbox" type="checkbox">
+ <input id="radio" type="radio">
+
+ <input id="file" type="file">
+
+ <div id="div"></div>
+
+ <!-- A sentinal guards from events of interest being fired after it emits a state change -->
+ <div id="sentinel"></div>
+
+ <input id="text1">
+
+ <div id="textbox" role="textbox" aria-multiline="false">hello</div>
+
+ <form id="form">
+ <button id="default-button">hello</button>
+ <button>world</button>
+ <input id="input">
+ <input id="input-autocomplete-off" autocomplete="off">
+ <input id="input-list" list="browsers">
+ <input id="input-password" type="password">
+ <datalist id="browsers">
+ <option value="Internet Explorer">
+ <option value="Firefox">
+ <option value="Google Chrome">
+ <option value="Opera">
+ <option value="Safari">
+ </datalist>
+ </form>
+
+ <div id="article" role="article">hello</div>
+
+ <img id="animated-image" src="../animated-gif.gif">
+
+ <ul id="listbox" role="listbox">
+ <li role="option">one</li>
+ <li role="option">two</li>
+ <li role="option">three</li>
+ <li role="option">four</li>
+ <li role="option">five</li>
+ </ul>
+
+ <select id="select" size="2">
+ <option>one</option>
+ <option>two</option>
+ <option>three</option>
+ <option>four</option>
+ <option>five</option>
+ <option>size</option>
+ </select>
+
+ <div id="eventdump"></div>
+
+ <div id="eventdump"></div>
+ <button id="popupButton">action</button>
+</body>
+</html>
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 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="XUL state change event tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../promisified-events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function offscreenChangeEvent(acc, enabled) {
+ return [
+ EVENT_STATE_CHANGE,
+ event => {
+ const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent);
+ return event.accessible == acc &&
+ scEvent.state == STATE_OFFSCREEN &&
+ scEvent.isEnabled == enabled;
+ }
+ ];
+ }
+
+ async function testTabpanels() {
+ const tabs = getNode("tabs");
+ is(tabs.selectedIndex, 0, "tab1 initially selected");
+ const panel1 = getAccessible("panel1");
+ testStates(panel1, 0, 0, STATE_OFFSCREEN);
+ const panel2 = getAccessible("panel2");
+ testStates(panel2, STATE_OFFSCREEN);
+ const panel3 = getAccessible("panel3");
+ testStates(panel3, STATE_OFFSCREEN);
+
+ let events = waitForEvents([
+ offscreenChangeEvent(panel1, true),
+ offscreenChangeEvent(panel2, false)
+ ]);
+ info("Selecting tab2");
+ tabs.selectedIndex = 1;
+ await events;
+
+ events = waitForEvents([
+ offscreenChangeEvent(panel2, true),
+ offscreenChangeEvent(panel3, false)
+ ]);
+ info("Selecting tab3");
+ tabs.selectedIndex = 2;
+ await events;
+
+ events = waitForEvents([
+ offscreenChangeEvent(panel3, true),
+ offscreenChangeEvent(panel1, false)
+ ]);
+ info("Selecting tab1");
+ tabs.selectedIndex = 0;
+ await events;
+ }
+
+ async function testPressed() {
+ const toolbarbuttonCheckbox = getNode("toolbarbuttonCheckbox");
+ testStates(toolbarbuttonCheckbox, 0, 0, STATE_PRESSED);
+ info("Checking toolbarbuttonCheckbox");
+ let changed = waitForStateChange(toolbarbuttonCheckbox, STATE_PRESSED, true);
+ toolbarbuttonCheckbox.setAttribute("checked", true);
+ await changed;
+ info("Unchecking toolbarbuttonCheckbox");
+ changed = waitForStateChange(toolbarbuttonCheckbox, STATE_PRESSED, false);
+ toolbarbuttonCheckbox.removeAttribute("checked");
+ await changed;
+ }
+
+ async function doTests() {
+ await testTabpanels();
+ await testPressed();
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <tabbox id="tabbox" selectedIndex="0">
+ <tabs id="tabs">
+ <tab id="tab1" label="tab1"/>
+ <tab id="tab2" label="tab2"/>
+ <tab id="tab3" label="tab3"/>
+ </tabs>
+ <tabpanels>
+ <hbox id="panel1"><button label="b1"/></hbox>
+ <hbox id="panel2"><button label="b2"/></hbox>
+ <hbox id="panel3"><button label="b3"/></hbox>
+ </tabpanels>
+ </tabbox>
+
+ <toolbarbutton id="toolbarbuttonCheckbox" type="checkbox">toolbarbuttonCheckbox</toolbarbutton>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/events/test_text.html b/accessible/tests/mochitest/events/test_text.html
new file mode 100644
index 0000000000..8a0bd7f9a4
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_text.html
@@ -0,0 +1,310 @@
+<html>
+
+<head>
+ <title>Accessible mutation events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ /**
+ * Base text remove invoker and checker.
+ */
+ function textRemoveInvoker(aID, aStart, aEnd, aText) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new textChangeChecker(aID, aStart, aEnd, aText, false),
+ ];
+ }
+
+ function textInsertInvoker(aID, aStart, aEnd, aText) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new textChangeChecker(aID, aStart, aEnd, aText, true),
+ ];
+ }
+
+ /**
+ * Remove inaccessible child node containing accessibles.
+ */
+ function removeChildSpan(aID) {
+ this.__proto__ = new textRemoveInvoker(aID, 0, 5, "33322");
+
+ this.invoke = function removeChildSpan_invoke() {
+ // remove HTML span, a first child of the node
+ this.DOMNode.firstChild.remove();
+ };
+
+ this.getID = function removeChildSpan_getID() {
+ return "Remove inaccessible span containing accessible nodes" + prettyName(aID);
+ };
+ }
+
+ /**
+ * Insert inaccessible child node containing accessibles.
+ */
+ function insertChildSpan(aID, aInsertAllTogether) {
+ this.__proto__ = new textInsertInvoker(aID, 0, 5, "33322");
+
+ this.invoke = function insertChildSpan_invoke() {
+ // <span><span>333</span><span>22</span></span>
+ if (aInsertAllTogether) {
+ let topSpan = document.createElement("span");
+ let fSpan = document.createElement("span");
+ fSpan.textContent = "333";
+ topSpan.appendChild(fSpan);
+ let sSpan = document.createElement("span");
+ sSpan.textContent = "22";
+ topSpan.appendChild(sSpan);
+
+ this.DOMNode.insertBefore(topSpan, this.DOMNode.childNodes[0]);
+ } else {
+ let topSpan = document.createElement("span");
+ this.DOMNode.insertBefore(topSpan, this.DOMNode.childNodes[0]);
+
+ let fSpan = document.createElement("span");
+ fSpan.textContent = "333";
+ topSpan.appendChild(fSpan);
+
+ let sSpan = document.createElement("span");
+ sSpan.textContent = "22";
+ topSpan.appendChild(sSpan);
+ }
+ };
+
+ this.getID = function insertChildSpan_getID() {
+ return "Insert inaccessible span containing accessibles" +
+ prettyName(aID);
+ };
+ }
+
+ /**
+ * Remove child embedded accessible.
+ */
+ function removeChildDiv(aID) {
+ this.__proto__ = new textRemoveInvoker(aID, 5, 6, kEmbedChar);
+
+ this.invoke = function removeChildDiv_invoke() {
+ var childDiv = this.DOMNode.childNodes[1];
+
+ // Ensure accessible is created to get text remove event when it's
+ // removed.
+ getAccessible(childDiv);
+
+ this.DOMNode.removeChild(childDiv);
+ };
+
+ this.getID = function removeChildDiv_getID() {
+ return "Remove accessible div from the middle of text accessible " +
+ prettyName(aID);
+ };
+ }
+
+ /**
+ * Insert child embedded accessible.
+ */
+ function insertChildDiv(aID) {
+ this.__proto__ = new textInsertInvoker(aID, 5, 6, kEmbedChar);
+
+ this.invoke = function insertChildDiv_invoke() {
+ var childDiv = document.createElement("div");
+ // Note after bug 646216, a sole div without text won't be accessible
+ // and would not result in an embedded character.
+ // Therefore, add some text.
+ childDiv.textContent = "hello";
+ this.DOMNode.insertBefore(childDiv, this.DOMNode.childNodes[1]);
+ };
+
+ this.getID = function insertChildDiv_getID() {
+ return "Insert accessible div into the middle of text accessible " +
+ prettyName(aID);
+ };
+ }
+
+ /**
+ * Remove children from text container from first to last child or vice
+ * versa.
+ */
+ function removeChildren(aID, aLastToFirst, aStart, aEnd, aText) {
+ this.__proto__ = new textRemoveInvoker(aID, aStart, aEnd, aText);
+
+ this.invoke = function removeChildren_invoke() {
+ if (aLastToFirst) {
+ while (this.DOMNode.firstChild)
+ this.DOMNode.removeChild(this.DOMNode.lastChild);
+ } else {
+ while (this.DOMNode.firstChild)
+ this.DOMNode.firstChild.remove();
+ }
+ };
+
+ this.getID = function removeChildren_getID() {
+ return "remove children of " + prettyName(aID) +
+ (aLastToFirst ? " from last to first" : " from first to last");
+ };
+ }
+
+ /**
+ * Remove text from HTML input.
+ */
+ function removeTextFromInput(aID, aStart, aEnd, aText) {
+ this.__proto__ = new textRemoveInvoker(aID, aStart, aEnd, aText);
+
+ this.eventSeq.push(new invokerChecker(EVENT_TEXT_VALUE_CHANGE,
+ this.DOMNode));
+
+ this.invoke = function removeTextFromInput_invoke() {
+ this.DOMNode.focus();
+ this.DOMNode.setSelectionRange(aStart, aEnd);
+
+ synthesizeKey("KEY_Delete");
+ };
+
+ this.getID = function removeTextFromInput_getID() {
+ return "Remove text from " + aStart + " to " + aEnd + " for " +
+ prettyName(aID);
+ };
+ }
+
+ /**
+ * Add text into HTML input.
+ */
+ function insertTextIntoInput(aID, aStart, aEnd, aText) {
+ this.__proto__ = new textInsertInvoker(aID, aStart, aEnd, aText);
+
+ this.eventSeq.push(new invokerChecker(EVENT_TEXT_VALUE_CHANGE,
+ this.DOMNode));
+
+ this.invoke = function insertTextIntoInput_invoke() {
+ this.DOMNode.focus();
+ sendString("a");
+ };
+
+ this.getID = function insertTextIntoInput_getID() {
+ return "Insert text to " + aStart + " for " + prettyName(aID);
+ };
+ }
+
+ /**
+ * Remove text data from text node of editable area.
+ */
+ function removeTextFromEditable(aID, aStart, aEnd, aText, aTextNode) {
+ this.__proto__ = new textRemoveInvoker(aID, aStart, aEnd, aText);
+
+ this.invoke = function removeTextFromEditable_invoke() {
+ this.DOMNode.focus();
+
+ var selection = window.getSelection();
+ var range = document.createRange();
+ range.setStart(this.textNode, aStart);
+ range.setEnd(this.textNode, aEnd);
+ selection.addRange(range);
+
+ synthesizeKey("KEY_Delete");
+ };
+
+ this.getID = function removeTextFromEditable_getID() {
+ return "Remove text from " + aStart + " to " + aEnd + " for " +
+ prettyName(aID);
+ };
+
+ this.textNode = getNode(aTextNode);
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+ gA11yEventDumpToConsole = true; // debugging
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ // Text remove event on inaccessible child HTML span removal containing
+ // accessible text nodes.
+ gQueue.push(new removeChildSpan("p"));
+ gQueue.push(new insertChildSpan("p"), true);
+ gQueue.push(new insertChildSpan("p"), false);
+
+ // Remove embedded character.
+ gQueue.push(new removeChildDiv("div"));
+ gQueue.push(new insertChildDiv("div"));
+
+ // Remove all children.
+ var text = kEmbedChar + "txt" + kEmbedChar;
+ gQueue.push(new removeChildren("div2", true, 0, 5, text));
+ gQueue.push(new removeChildren("div3", false, 0, 5, text));
+
+ // Text remove from text node within hypertext accessible.
+ gQueue.push(new removeTextFromInput("input", 1, 3, "al"));
+ gQueue.push(new insertTextIntoInput("input", 1, 2, "a"));
+
+ // bug 570691
+ todo(false, "Fix text change events from editable area, see bug 570691");
+ // var textNode = getNode("editable").firstChild;
+ // gQueue.push(new removeTextFromEditable("editable", 1, 3, "al", textNode));
+ // textNode = getNode("editable2").firstChild.firstChild;
+ // gQueue.push(new removeTextFromEditable("editable2", 1, 3, "al", textNode));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=566293"
+ title=" wrong length of text remove event when inaccessible node containing accessible nodes is removed">
+ Mozilla Bug 566293
+ </a><br>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=570710"
+ title="Avoid extra array traversal during text event creation">
+ Mozilla Bug 570710
+ </a><br>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=574003"
+ title="Coalesce text events on nodes removal">
+ Mozilla Bug 574003
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=575052"
+ title="Cache text offsets within hypertext accessible">
+ Mozilla Bug 575052
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275"
+ title="Rework accessible tree update code">
+ Mozilla Bug 570275
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <p id="p"><span><span>333</span><span>22</span></span>1111</p>
+ <div id="div">hello<div>hello</div>hello</div>
+ <div id="div2"><div>txt</div>txt<div>txt</div></div>
+ <div id="div3"><div>txt</div>txt<div>txt</div></div>
+ <input id="input" value="value">
+ <div contentEditable="true" id="editable">value</div>
+ <div contentEditable="true" id="editable2"><span>value</span></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_text_alg.html b/accessible/tests/mochitest/events/test_text_alg.html
new file mode 100644
index 0000000000..f9b55c8b23
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_text_alg.html
@@ -0,0 +1,249 @@
+<html>
+
+<head>
+ <title>Accessible text update algorithm testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ const kRemoval = false;
+ const kInsertion = true;
+ const kUnexpected = true;
+
+ function changeText(aContainerID, aValue, aEventList) {
+ this.containerNode = getNode(aContainerID);
+ this.textNode = this.containerNode.firstChild;
+ this.textData = this.textNode.data;
+
+ this.eventSeq = [ ];
+ this.unexpectedEventSeq = [ ];
+
+ for (var i = 0; i < aEventList.length; i++) {
+ var event = aEventList[i];
+
+ var isInserted = event[0];
+ var str = event[1];
+ var offset = event[2];
+ var checker = new textChangeChecker(this.containerNode, offset,
+ offset + str.length, str,
+ isInserted);
+
+ if (event[3] == kUnexpected)
+ this.unexpectedEventSeq.push(checker);
+ else
+ this.eventSeq.push(checker);
+ }
+
+ this.invoke = function changeText_invoke() {
+ this.textNode.data = aValue;
+ };
+
+ this.getID = function changeText_getID() {
+ return "change text '" + shortenString(this.textData) + "' -> '" +
+ shortenString(this.textNode.data) + "' for " +
+ prettyName(this.containerNode);
+ };
+ }
+
+ function expStr(x, doublings) {
+ for (var i = 0; i < doublings; ++i)
+ x = x + x;
+ return x;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+
+ // ////////////////////////////////////////////////////////////////////////
+ // wqrema -> tqb: substitution coalesced with removal
+
+ var events = [
+ [ kRemoval, "w", 0 ], // wqrema -> qrema
+ [ kInsertion, "t", 0], // qrema -> tqrema
+ [ kRemoval, "rema", 2 ], // tqrema -> tq
+ [ kInsertion, "b", 2], // tq -> tqb
+ ];
+ gQueue.push(new changeText("p1", "tqb", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // b -> insa: substitution coalesced with insertion (complex substitution)
+
+ events = [
+ [ kRemoval, "b", 0 ], // b ->
+ [ kInsertion, "insa", 0], // -> insa
+ ];
+ gQueue.push(new changeText("p2", "insa", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // abc -> def: coalesced substitutions
+
+ events = [
+ [ kRemoval, "abc", 0 ], // abc ->
+ [ kInsertion, "def", 0], // -> def
+ ];
+ gQueue.push(new changeText("p3", "def", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // abcabc -> abcDEFabc: coalesced insertions
+
+ events = [
+ [ kInsertion, "DEF", 3], // abcabc -> abcDEFabc
+ ];
+ gQueue.push(new changeText("p4", "abcDEFabc", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // abc -> defabc: insertion into begin
+
+ events = [
+ [ kInsertion, "def", 0], // abc -> defabc
+ ];
+ gQueue.push(new changeText("p5", "defabc", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // abc -> abcdef: insertion into end
+
+ events = [
+ [ kInsertion, "def", 3], // abc -> abcdef
+ ];
+ gQueue.push(new changeText("p6", "abcdef", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // defabc -> abc: removal from begin
+
+ events = [
+ [ kRemoval, "def", 0], // defabc -> abc
+ ];
+ gQueue.push(new changeText("p7", "abc", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // abcdef -> abc: removal from the end
+
+ events = [
+ [ kRemoval, "def", 3], // abcdef -> abc
+ ];
+ gQueue.push(new changeText("p8", "abc", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // abcDEFabc -> abcabc: coalesced removals
+
+ events = [
+ [ kRemoval, "DEF", 3], // abcDEFabc -> abcabc
+ ];
+ gQueue.push(new changeText("p9", "abcabc", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // !abcdef@ -> @axbcef!: insertion, deletion and substitutions
+
+ events = [
+ [ kRemoval, "!", 0 ], // !abcdef@ -> abcdef@
+ [ kInsertion, "@", 0], // abcdef@ -> @abcdef@
+ [ kInsertion, "x", 2 ], // @abcdef@ -> @axbcdef@
+ [ kRemoval, "d", 5], // @axbcdef@ -> @axbcef@
+ [ kRemoval, "@", 7 ], // @axbcef@ -> @axbcef
+ [ kInsertion, "!", 7 ], // @axbcef -> @axbcef!
+ ];
+ gQueue.push(new changeText("p10", "@axbcef!", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // meilenstein -> levenshtein: insertion, complex and simple substitutions
+
+ events = [
+ [ kRemoval, "m", 0 ], // meilenstein -> eilenstein
+ [ kInsertion, "l", 0], // eilenstein -> leilenstein
+ [ kRemoval, "il", 2 ], // leilenstein -> leenstein
+ [ kInsertion, "v", 2], // leenstein -> levenstein
+ [ kInsertion, "h", 6 ], // levenstein -> levenshtein
+ ];
+ gQueue.push(new changeText("p11", "levenshtein", events));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // long strings, remove/insert pair as the old string was replaced on
+ // new one
+
+ var longStr1 = expStr("x", 16);
+ var longStr2 = expStr("X", 16);
+
+ var newStr = "a" + longStr1 + "b", insStr = longStr1, rmStr = "";
+ events = [
+ [ kRemoval, rmStr, 1, kUnexpected ],
+ [ kInsertion, insStr, 1 ],
+ ];
+ gQueue.push(new changeText("p12", newStr, events));
+
+ newStr = "a" + longStr2 + "b";
+ insStr = longStr2;
+ rmStr = longStr1;
+ events = [
+ [ kRemoval, rmStr, 1 ],
+ [ kInsertion, insStr, 1],
+ ];
+ gQueue.push(new changeText("p12", newStr, events));
+
+ newStr = "ab";
+ insStr = "";
+ rmStr = longStr2;
+ events = [
+ [ kRemoval, rmStr, 1 ],
+ [ kInsertion, insStr, 1, kUnexpected ],
+ ];
+ gQueue.push(new changeText("p12", newStr, events));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=626660"
+ title="Cache rendered text on a11y side">
+ Mozilla Bug 626660
+ </a>
+ <br>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="eventdump"></div>
+
+ <!-- Note: only editable text gets diffed this way. -->
+ <div contenteditable="true">
+ <p id="p1">wqrema</p>
+ <p id="p2">b</p>
+ <p id="p3">abc</p>
+ <p id="p4">abcabc</p>
+ <p id="p5">abc</p>
+ <p id="p6">abc</p>
+ <p id="p7">defabc</p>
+ <p id="p8">abcdef</p>
+ <p id="p9">abcDEFabc</p>
+ <p id="p10">!abcdef@</p>
+ <p id="p11">meilenstein</p>
+ <p id="p12">ab</p>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_textattrchange.html b/accessible/tests/mochitest/events/test_textattrchange.html
new file mode 100644
index 0000000000..b46e1ef399
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_textattrchange.html
@@ -0,0 +1,107 @@
+<html>
+
+<head>
+ <title>Text attribute changed event for misspelled text</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+
+ const {InlineSpellChecker} = ChromeUtils.importESModule(
+ "resource://gre/modules/InlineSpellChecker.sys.mjs"
+ );
+
+ function spelledTextInvoker(aID) {
+ this.DOMNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_TEXT_ATTRIBUTE_CHANGED, this.DOMNode),
+ ];
+
+ this.invoke = function spelledTextInvoker_invoke() {
+ var editor = this.DOMNode.editor;
+ var spellChecker = new InlineSpellChecker(editor);
+ spellChecker.enabled = true;
+
+ // var spellchecker = editor.getInlineSpellChecker(true);
+ // spellchecker.enableRealTimeSpell = true;
+
+ this.DOMNode.value = "valid text inalid tixt";
+ };
+
+ this.finalCheck = function spelledTextInvoker_finalCheck() {
+ var defAttrs = buildDefaultTextAttrs(this.DOMNode, kInputFontSize,
+ kNormalFontWeight,
+ kInputFontFamily);
+ testDefaultTextAttrs(aID, defAttrs);
+
+ var attrs = { };
+ var misspelledAttrs = {
+ "invalid": "spelling",
+ };
+
+ testTextAttrs(aID, 0, attrs, defAttrs, 0, 11);
+ testTextAttrs(aID, 11, misspelledAttrs, defAttrs, 11, 17);
+ testTextAttrs(aID, 17, attrs, defAttrs, 17, 18);
+ testTextAttrs(aID, 18, misspelledAttrs, defAttrs, 18, 22);
+ };
+
+ this.getID = function spelledTextInvoker_getID() {
+ return "text attribute change for misspelled text";
+ };
+ }
+
+ /**
+ * Do tests.
+ */
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+ function doTests() {
+ // Synth focus before spellchecking turning on to make sure editor
+ // gets a time for initialization.
+
+ gQueue = new eventQueue();
+ gQueue.push(new synthFocus("input"));
+ gQueue.push(new spelledTextInvoker("input"));
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=345759"
+ title="Implement text attributes">
+ Mozilla Bug 345759
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="input"/>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_textselchange.html b/accessible/tests/mochitest/events/test_textselchange.html
new file mode 100644
index 0000000000..3dce0760eb
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_textselchange.html
@@ -0,0 +1,82 @@
+<html>
+
+<head>
+ <title>Accessible text selection change events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ var gQueue = null;
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ function getOnclickSeq(aID) {
+ return [
+ new caretMoveChecker(0, true, aID),
+ new unexpectedInvokerChecker(EVENT_TEXT_SELECTION_CHANGED, aID),
+ ];
+ }
+
+ function doTests() {
+ // test caret move events and caret offsets
+ gQueue = new eventQueue();
+
+ gQueue.push(new synthClick("c1_p1", getOnclickSeq("c1_p1")));
+ gQueue.push(new synthDownKey("c1", new textSelectionChecker("c1", 0, 1, "c1_p1", 0, "c1_p2", 0), { shiftKey: true }));
+ gQueue.push(new synthDownKey("c1", new textSelectionChecker("c1", 0, 2, "c1_p1", 0, "c1_p2", 9), { shiftKey: true }));
+
+ gQueue.push(new synthClick("ta1", getOnclickSeq("ta1")));
+ gQueue.push(new synthRightKey("ta1",
+ new textSelectionChecker("ta1", 0, 1, "ta1", 0, "ta1", 1),
+ { shiftKey: true }));
+ gQueue.push(new synthLeftKey("ta1",
+ [new textSelectionChecker("ta1", 0, 0, "ta1", 0, "ta1", 0),
+ new caretMoveChecker(0, true, "ta1")]));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=762934"
+ title="Text selection change event has a wrong target when selection is spanned through several objects">
+ Bug 762934
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=956032"
+ title="Text selection change event missed when selected text becomes unselected">
+ Bug 956032
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="c1" contentEditable="true">
+ <p id="c1_p1">paragraph</p>
+ <p id="c1_p2">paragraph</p>
+ </div>
+
+ <textarea id="ta1">Hello world</textarea>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/events/test_tree.xhtml b/accessible/tests/mochitest/events/test_tree.xhtml
new file mode 100644
index 0000000000..af7feafde8
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_tree.xhtml
@@ -0,0 +1,358 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="DOM TreeRowCountChanged and a11y name change events.">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ var gView;
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Invoker's checkers
+
+ /**
+ * Check TreeRowCountChanged event.
+ */
+ function rowCountChangedChecker(aMsg, aIdx, aCount)
+ {
+ this.type = "TreeRowCountChanged";
+ this.target = gTree;
+ this.check = function check(aEvent)
+ {
+ var propBag = aEvent.detail.QueryInterface(Ci.nsIPropertyBag2);
+ var index = propBag.getPropertyAsInt32("index");
+ is(index, aIdx, "Wrong 'index' data of 'treeRowCountChanged' event.");
+
+ var count = propBag.getPropertyAsInt32("count");
+ is(count, aCount, "Wrong 'count' data of 'treeRowCountChanged' event.");
+ }
+ this.getID = function getID()
+ {
+ return aMsg + "TreeRowCountChanged";
+ }
+ }
+
+ /**
+ * Check TreeInvalidated event.
+ */
+ function treeInvalidatedChecker(aMsg, aStartRow, aEndRow, aStartCol, aEndCol)
+ {
+ this.type = "TreeInvalidated";
+ this.target = gTree;
+ this.check = function check(aEvent)
+ {
+ var propBag = aEvent.detail.QueryInterface(Ci.nsIPropertyBag2);
+ try {
+ var startRow = propBag.getPropertyAsInt32("startrow");
+ } catch (e) {
+ if (e.name != 'NS_ERROR_NOT_AVAILABLE') {
+ throw e;
+ }
+ startRow = null;
+ }
+ is(startRow, aStartRow,
+ "Wrong 'startrow' of 'treeInvalidated' event on " + aMsg);
+
+ try {
+ var endRow = propBag.getPropertyAsInt32("endrow");
+ } catch (e) {
+ if (e.name != 'NS_ERROR_NOT_AVAILABLE') {
+ throw e;
+ }
+ endRow = null;
+ }
+ is(endRow, aEndRow,
+ "Wrong 'endrow' of 'treeInvalidated' event on " + aMsg);
+
+ try {
+ var startCol = propBag.getPropertyAsInt32("startcolumn");
+ } catch (e) {
+ if (e.name != 'NS_ERROR_NOT_AVAILABLE') {
+ throw e;
+ }
+ startCol = null;
+ }
+ is(startCol, aStartCol,
+ "Wrong 'startcolumn' of 'treeInvalidated' event on " + aMsg);
+
+ try {
+ var endCol = propBag.getPropertyAsInt32("endcolumn");
+ } catch (e) {
+ if (e.name != 'NS_ERROR_NOT_AVAILABLE') {
+ throw e;
+ }
+ endCol = null;
+ }
+ is(endCol, aEndCol,
+ "Wrong 'endcolumn' of 'treeInvalidated' event on " + aMsg);
+ }
+ this.getID = function getID()
+ {
+ return "TreeInvalidated on " + aMsg;
+ }
+ }
+
+ /**
+ * Check name changed a11y event.
+ */
+ function nameChangeChecker(aMsg, aRow, aCol)
+ {
+ this.type = EVENT_NAME_CHANGE;
+
+ function targetGetter()
+ {
+ var acc = getAccessible(gTree);
+
+ var tableAcc = getAccessible(acc, [nsIAccessibleTable]);
+ return tableAcc.getCellAt(aRow, aCol);
+ }
+ Object.defineProperty(this, "target", { get: targetGetter });
+
+ this.getID = function getID()
+ {
+ return aMsg + "name changed";
+ }
+ }
+
+ /**
+ * Check name changed a11y event for a row.
+ */
+ function rowNameChangeChecker(aMsg, aRow)
+ {
+ this.type = EVENT_NAME_CHANGE;
+
+ function targetGetter()
+ {
+ var acc = getAccessible(gTree);
+ return acc.getChildAt(aRow + 1);
+ }
+ Object.defineProperty(this, "target", { get: targetGetter });
+
+ this.getID = function getID()
+ {
+ return aMsg + "name changed";
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ /**
+ * Set tree view.
+ */
+ function setTreeView()
+ {
+ this.invoke = function setTreeView_invoke()
+ {
+ gTree.view = gView;
+ }
+
+ this.getID = function setTreeView_getID() { return "set tree view"; }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, gTree)
+ ];
+ };
+
+ /**
+ * Insert row at 0 index and checks TreeRowCountChanged and TreeInvalidated
+ * event.
+ */
+ function insertRow()
+ {
+ this.invoke = function insertRow_invoke()
+ {
+ gView.appendItem("last");
+ gTree.rowCountChanged(0, 1);
+ }
+
+ this.eventSeq =
+ [
+ new rowCountChangedChecker("insertRow: ", 0, 1),
+ new treeInvalidatedChecker("insertRow", 0, 5, null, null)
+ ];
+
+ this.getID = function insertRow_getID()
+ {
+ return "insert row";
+ }
+ }
+
+ /**
+ * Invalidates first column and checks six name changed events for each
+ * treeitem plus TreeInvalidated event.
+ */
+ function invalidateColumn()
+ {
+ this.invoke = function invalidateColumn_invoke()
+ {
+ // Make sure accessible subtree of XUL tree is created otherwise no
+ // name change events for cell accessibles are emitted.
+ var tree = getAccessible(gTree);
+ var child = tree.firstChild;
+ var walkDown = true;
+ while (child != tree) {
+ if (walkDown) {
+ var grandChild = child.firstChild;
+ if (grandChild) {
+ child = grandChild;
+ continue;
+ }
+ }
+
+ var sibling = child.nextSibling;
+ if (sibling) {
+ child = sibling;
+ walkDown = true;
+ continue;
+ }
+
+ child = child.parent;
+ walkDown = false;
+ }
+
+ // Fire 'TreeInvalidated' event by InvalidateColumn()
+ var firstCol = gTree.columns.getFirstColumn();
+ for (var i = 0; i < gView.rowCount; i++)
+ gView.setCellText(i, firstCol, "hey " + String(i) + "x0");
+
+ gTree.invalidateColumn(firstCol);
+ }
+
+ this.eventSeq =
+ [
+ new nameChangeChecker("invalidateColumn: ", 0, 0),
+ new nameChangeChecker("invalidateColumn: ", 1, 0),
+ new nameChangeChecker("invalidateColumn: ", 2, 0),
+ new nameChangeChecker("invalidateColumn: ", 3, 0),
+ new nameChangeChecker("invalidateColumn: ", 4, 0),
+ new nameChangeChecker("invalidateColumn: ", 5, 0),
+ new treeInvalidatedChecker("invalidateColumn", null, null, 0, 0)
+ ];
+
+ this.getID = function invalidateColumn_getID()
+ {
+ return "invalidate column";
+ }
+ }
+
+ /**
+ * Invalidates second row and checks name changed event for first treeitem
+ * (note, there are two name changed events on linux due to different
+ * accessible tree for xul:tree element) plus TreeInvalidated event.
+ */
+ function invalidateRow()
+ {
+ this.invoke = function invalidateRow_invoke()
+ {
+ // Fire 'TreeInvalidated' event by InvalidateRow()
+ // eslint-disable-next-line no-unused-vars
+ var colCount = gTree.columns.count;
+ var column = gTree.columns.getFirstColumn();
+ while (column) {
+ gView.setCellText(1, column, "aloha 1x" + String(column.index));
+ column = column.getNext();
+ }
+
+ gTree.invalidateRow(1);
+ }
+
+ this.eventSeq =
+ [
+ new nameChangeChecker("invalidateRow: ", 1, 0),
+ new nameChangeChecker("invalidateRow: ", 1, 1),
+ new rowNameChangeChecker("invalidateRow: ", 1),
+ new treeInvalidatedChecker("invalidateRow", 1, 1, null, null)
+ ];
+
+ this.getID = function invalidateRow_getID()
+ {
+ return "invalidate row";
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ var gTree = null;
+ var gTreeView = null;
+ var gQueue = null;
+
+ // gA11yEventDumpID = "debug";
+ gA11yEventDumpToConsole = true; // debuggin
+
+ function doTest()
+ {
+ // Initialize the tree
+ gTree = document.getElementById("tree");
+ gView = new nsTableTreeView(5);
+
+ // Perform actions
+ gQueue = new eventQueue();
+
+ gQueue.push(new setTreeView());
+ gQueue.push(new insertRow());
+ gQueue.push(new invalidateColumn());
+ gQueue.push(new invalidateRow());
+
+ gQueue.invoke();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=368835"
+ title="Fire TreeViewChanged/TreeRowCountChanged events.">
+ Mozilla Bug 368835
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=308564"
+ title="No accessibility events when data in a tree row changes.">
+ Mozilla Bug 308564
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=739524"
+ title="replace TreeViewChanged DOM event on direct call from XUL tree.">
+ Mozilla Bug 739524
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=743568"
+ title="Thunderbird message list tree emitting incorrect focus signals after message deleted.">
+ Mozilla Bug 743568
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="debug"/>
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col1" flex="1" primary="true" label="column"/>
+ <treecol id="col2" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren id="treechildren"/>
+ </tree>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/events/test_valuechange.html b/accessible/tests/mochitest/events/test_valuechange.html
new file mode 100644
index 0000000000..1ad3c0359d
--- /dev/null
+++ b/accessible/tests/mochitest/events/test_valuechange.html
@@ -0,0 +1,315 @@
+<html>
+
+<head>
+ <title>Accessible value change events testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript"
+ src="../value.js"></script>
+
+ <script type="application/javascript">
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ // Value change invoker
+ function changeARIAValue(aNodeOrID, aValuenow, aValuetext) {
+ this.DOMNode = getNode(aNodeOrID);
+ this.eventSeq = [ new invokerChecker(aValuetext ?
+ EVENT_TEXT_VALUE_CHANGE :
+ EVENT_VALUE_CHANGE, this.DOMNode),
+ ];
+
+ this.invoke = function changeARIAValue_invoke() {
+ // Note: this should not fire an EVENT_VALUE_CHANGE when aria-valuetext
+ // is not empty
+ if (aValuenow != undefined)
+ this.DOMNode.setAttribute("aria-valuenow", aValuenow);
+
+ // Note: this should always fire an EVENT_VALUE_CHANGE
+ if (aValuetext != undefined)
+ this.DOMNode.setAttribute("aria-valuetext", aValuetext);
+ };
+
+ this.check = function changeARIAValue_check() {
+ var acc = getAccessible(aNodeOrID, [nsIAccessibleValue]);
+ if (!acc)
+ return;
+
+ // Note: always test against valuetext first because the existence of
+ // aria-valuetext takes precedence over aria-valuenow in gecko.
+ is(acc.value, (aValuetext != undefined) ? aValuetext : aValuenow,
+ "Wrong value of " + prettyName(aNodeOrID));
+ };
+
+ this.getID = function changeARIAValue_getID() {
+ return prettyName(aNodeOrID) + " value changed";
+ };
+ }
+
+ function changeValue(aID, aValue) {
+ this.DOMNode = getNode(aID);
+ this.eventSeq = [new invokerChecker(EVENT_TEXT_VALUE_CHANGE,
+ this.DOMNode),
+ ];
+
+ this.invoke = function changeValue_invoke() {
+ this.DOMNode.value = aValue;
+ };
+
+ this.check = function changeValue_check() {
+ var acc = getAccessible(this.DOMNode);
+ is(acc.value, aValue, "Wrong value for " + prettyName(aID));
+ };
+
+ this.getID = function changeValue_getID() {
+ return prettyName(aID) + " value changed";
+ };
+ }
+
+ function changeProgressValue(aID, aValue) {
+ this.DOMNode = getNode(aID);
+ this.eventSeq = [new invokerChecker(EVENT_VALUE_CHANGE, this.DOMNode)];
+
+ this.invoke = function changeProgressValue_invoke() {
+ this.DOMNode.value = aValue;
+ };
+
+ this.check = function changeProgressValue_check() {
+ var acc = getAccessible(this.DOMNode);
+ is(acc.value, aValue + "%", "Wrong value for " + prettyName(aID));
+ };
+
+ this.getID = function changeProgressValue_getID() {
+ return prettyName(aID) + " value changed";
+ };
+ }
+
+ function changeRangeValueWithMouse(aID) {
+ this.DOMNode = getNode(aID);
+ this.eventSeq = [new invokerChecker(EVENT_VALUE_CHANGE, this.DOMNode)];
+
+ this.invoke = function changeRangeValue_invoke() {
+ synthesizeMouse(getNode(aID), 5, 5, { });
+ };
+
+ this.finalCheck = function changeRangeValue_finalCheck() {
+ var acc = getAccessible(this.DOMNode);
+ is(acc.value, "0", "Wrong value for " + prettyName(aID));
+ };
+
+ this.getID = function changeRangeValue_getID() {
+ return prettyName(aID) + " range value changed";
+ };
+ }
+
+ function changeRangeValueWithA11yAPI(aID) {
+ this.DOMNode = getNode(aID);
+ let inputChecker = new invokerChecker("input", this.DOMNode);
+ inputChecker.eventTarget = "element";
+
+ let changeChecker = new invokerChecker("change", this.DOMNode);
+ changeChecker.eventTarget = "element";
+
+ this.eventSeq = [
+ inputChecker,
+ changeChecker,
+ new invokerChecker(EVENT_VALUE_CHANGE, this.DOMNode),
+ ];
+
+ this.invoke = function changeRangeValue_invoke() {
+ this.DOMNode.focus();
+ let acc = getAccessible(this.DOMNode, [nsIAccessibleValue]);
+ acc.currentValue = 0;
+ this.DOMNode.blur();
+ };
+
+ this.finalCheck = function changeRangeValue_finalCheck() {
+ var acc = getAccessible(this.DOMNode);
+ is(acc.value, "0", "Wrong value for " + prettyName(aID));
+ };
+
+ this.getID = function changeRangeValue_getID() {
+ return prettyName(aID) + " range value changed";
+ };
+ }
+
+ function changeSelectValue(aID, aKey, aValue) {
+ this.eventSeq =
+ [ new invokerChecker(EVENT_TEXT_VALUE_CHANGE, getAccessible(aID)) ];
+
+ this.invoke = function changeSelectValue_invoke() {
+ getAccessible(aID).takeFocus();
+ synthesizeKey(aKey, {}, window);
+ };
+
+ this.finalCheck = function changeSelectValue_finalCheck() {
+ is(getAccessible(aID).value, aValue, "Wrong value for " + prettyName(aID));
+ };
+
+ this.getID = function changeSelectValue_getID() {
+ return `${prettyName(aID)} closed select value change on '${aKey}'' key press`;
+ };
+ }
+
+ // enableLogging("DOMEvents");
+ // gA11yEventDumpToConsole = true;
+ function doTests() {
+
+ // Test initial values
+ testValue("slider_vn", "5", 5, 0, 1000, 0);
+ testValue("slider_vnvt", "plain", 0, 0, 5, 0);
+ testValue("slider_vt", "hi", 1.5, 0, 3, 0);
+ testValue("scrollbar", "5", 5, 0, 1000, 0);
+ testValue("splitter", "5", 5, 0, 1000, 0);
+ testValue("progress", "22%", 22, 0, 100, 0);
+ testValue("range", "6", 6, 0, 10, 1);
+ testValue("range2", "6", 6, 0, 10, 1);
+
+ // Test that elements which should not expose values do not
+ let separatorVal = getAccessible("separator", [nsIAccessibleValue], null, DONOTFAIL_IF_NO_INTERFACE);
+ ok(!separatorVal, "value interface is not exposed for separator");
+ let separatorAcc = getAccessible("separator");
+ ok(!separatorAcc.value, "Value text is not exposed for separator");
+
+ // Test value change events
+ gQueue = new eventQueue();
+
+ gQueue.push(new changeARIAValue("slider_vn", "6", undefined));
+ gQueue.push(new changeARIAValue("slider_vt", undefined, "hey!"));
+ gQueue.push(new changeARIAValue("slider_vnvt", "3", "sweet"));
+ gQueue.push(new changeARIAValue("scrollbar", "6", undefined));
+ gQueue.push(new changeARIAValue("splitter", "6", undefined));
+
+ gQueue.push(new changeValue("combobox", "hello"));
+
+ gQueue.push(new changeProgressValue("progress", "50"));
+ gQueue.push(new changeRangeValueWithMouse("range"));
+ gQueue.push(new changeRangeValueWithA11yAPI("range2"));
+
+ gQueue.push(new changeSelectValue("select", "VK_DOWN", "2nd"));
+ gQueue.push(new changeSelectValue("select", "3", "3rd"));
+
+ let iframeSelect = getAccessible("selectIframe").firstChild.firstChild;
+ gQueue.push(new changeSelectValue(iframeSelect, "VK_DOWN", "2"));
+
+ let shadowSelect = getAccessible("selectShadow").firstChild;
+ gQueue.push(new changeSelectValue(shadowSelect, "VK_DOWN", "2"));
+ gQueue.push(new changeValue("number", "2"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=478032"
+ title=" Fire delayed value changed event for aria-valuetext changes">
+ Mozilla Bug 478032
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=529289"
+ title="We dont expose new aria role 'scrollbar' and property aria-orientation">
+ Mozilla Bug 529289
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=559764"
+ title="Make HTML5 input@type=range element accessible">
+ Mozilla Bug 559764
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=703202"
+ title="ARIA comboboxes don't fire value change events">
+ Mozilla Bug 703202
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=761901"
+ title=" HTML5 progress accessible should fire value change event">
+ Mozilla Bug 761901
+ </a>
+
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="eventdump"></div>
+
+ <!-- ARIA sliders -->
+ <div id="slider_vn" role="slider" aria-valuenow="5"
+ aria-valuemin="0" aria-valuemax="1000">slider</div>
+
+ <div id="slider_vt" role="slider" aria-valuetext="hi"
+ aria-valuemin="0" aria-valuemax="3">greeting slider</div>
+
+ <div id="slider_vnvt" role="slider" aria-valuenow="0" aria-valuetext="plain"
+ aria-valuemin="0" aria-valuemax="5">sweetness slider</div>
+
+ <!-- ARIA scrollbar -->
+ <div id="scrollbar" role="scrollbar" aria-valuenow="5"
+ aria-valuemin="0" aria-valuemax="1000">slider</div>
+
+ <!-- ARIA separator which is focusable (i.e. a splitter) -->
+ <div id="splitter" role="separator" tabindex="0" aria-valuenow="5"
+ aria-valuemin="0" aria-valuemax="1000">splitter</div>
+
+ <!-- ARIA separator which is not focusable and should not expose values -->
+ <div id="separator" role="separator" aria-valuenow="5"
+ aria-valuemin="0" aria-valuemax="1000">splitter</div>
+
+ <!-- ARIA combobox -->
+ <input id="combobox" role="combobox" aria-autocomplete="inline">
+
+ <!-- progress bar -->
+ <progress id="progress" value="22" max="100"></progress>
+
+ <!-- input@type="range" -->
+ <input type="range" id="range" min="0" max="10" value="6">
+
+ <!-- input@type="range" -->
+ <input type="range" id="range2" min="0" max="10" value="6">
+
+ <select id="select">
+ <option>1st</option>
+ <option>2nd</option>
+ <option>3rd</option>
+ </select>
+
+ <iframe id="selectIframe"
+ src="data:text/html,<select id='iframeSelect'><option>1</option><option>2</option></select>">
+ </iframe>
+
+ <div id="selectShadow"></div>
+ <script>
+ let host = document.getElementById("selectShadow");
+ let shadow = host.attachShadow({mode: "open"});
+ let select = document.createElement("select");
+ select.id = "shadowSelect";
+ let option = document.createElement("option");
+ option.textContent = "1";
+ select.appendChild(option);
+ option = document.createElement("option");
+ option.textContent = "2";
+ select.appendChild(option);
+ shadow.appendChild(select);
+ </script>
+
+ <input type="number" id="number" value="1">
+</body>
+</html>
diff --git a/accessible/tests/mochitest/focus/a11y.toml b/accessible/tests/mochitest/focus/a11y.toml
new file mode 100644
index 0000000000..fd33c080e7
--- /dev/null
+++ b/accessible/tests/mochitest/focus/a11y.toml
@@ -0,0 +1,10 @@
+[DEFAULT]
+support-files = "!/accessible/tests/mochitest/*.js"
+
+["test_focus_radio.xhtml"]
+
+["test_focusedChild.html"]
+
+["test_takeFocus.html"]
+
+["test_takeFocus.xhtml"]
diff --git a/accessible/tests/mochitest/focus/test_focus_radio.xhtml b/accessible/tests/mochitest/focus/test_focus_radio.xhtml
new file mode 100644
index 0000000000..717d9976b6
--- /dev/null
+++ b/accessible/tests/mochitest/focus/test_focus_radio.xhtml
@@ -0,0 +1,84 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tests for Accessible TakeFocus on Radio Elements">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../promisified-events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ async function doTests() {
+ let radio1 = getAccessible("radio1");
+ let focused = waitForEvent(EVENT_FOCUS, radio1);
+ radio1.takeFocus();
+ await focused;
+ // radio1 wasn't selected. Ensure that takeFocus didn't change that.
+ testStates(radio1, STATE_FOCUSED, 0, STATE_CHECKED);
+
+ // Test focusing another radio in the group while the group is still
+ // focused.
+ let radio2 = getAccessible("radio2");
+ focused = waitForEvent(EVENT_FOCUS, radio2);
+ radio2.takeFocus();
+ await focused;
+ testStates(radio2, STATE_FOCUSED | STATE_CHECKED);
+
+ let groupEl = document.getElementById("radiogroup");
+ // Selecting an item also focuses it.
+ focused = waitForEvent(EVENT_FOCUS, radio1);
+ groupEl.value = "1";
+ await focused;
+ testStates(radio1, STATE_FOCUSED | STATE_CHECKED);
+
+ // If an item is already selected but not focused, selecting it again
+ // focuses it.
+ focused = waitForEvent(EVENT_FOCUS, radio2);
+ radio2.takeFocus();
+ await focused;
+ testStates(radio2, STATE_FOCUSED, 0, STATE_CHECKED);
+ // radio1 is selected but not focused.
+ // Select radio1 again, which should focus it.
+ focused = waitForEvent(EVENT_FOCUS, radio1);
+ groupEl.value = "1";
+ await focused;
+ testStates(radio1, STATE_FOCUSED | STATE_CHECKED);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <radiogroup id="radiogroup" value="2">
+ <radio id="radio1" value="1"/>
+ <radio id="radio2" value="2"/>
+ </radiogroup>
+
+ <vbox id="eventdump"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/focus/test_focusedChild.html b/accessible/tests/mochitest/focus/test_focusedChild.html
new file mode 100644
index 0000000000..d12e229b48
--- /dev/null
+++ b/accessible/tests/mochitest/focus/test_focusedChild.html
@@ -0,0 +1,81 @@
+<html>
+
+<head>
+ <title>nsIAccessible::focusedChild testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function openWnd() {
+ this.eventSeq = [ new invokerChecker(EVENT_FOCUS,
+ getDialogAccessible,
+ this) ];
+
+ this.invoke = function openWnd_invoke() {
+ this.dialog = window.browsingContext.topChromeWindow
+ .openDialog("about:mozilla",
+ "AboutMozilla",
+ "chrome,width=600,height=600");
+ };
+
+ this.finalCheck = function openWnd_finalCheck() {
+ var app = getApplicationAccessible();
+ is(app.focusedChild, getDialogAccessible(this),
+ "Wrong focused child");
+
+ this.dialog.close();
+ };
+
+ this.getID = function openWnd_getID() {
+ return "focusedChild for application accessible";
+ };
+
+ function getDialogAccessible(aInvoker) {
+ return getAccessible(aInvoker.dialog.document);
+ }
+ }
+
+ gA11yEventDumpToConsole = true;
+ var gQueue = null;
+
+ function doTest() {
+ enableLogging("focus,doclifecycle");
+ gQueue = new eventQueue();
+
+ gQueue.push(new openWnd());
+
+ gQueue.onFinish = function() { disableLogging(); };
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=677467"
+ title="focusedChild crashes on application accessible">
+ Mozilla Bug 677467
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/focus/test_takeFocus.html b/accessible/tests/mochitest/focus/test_takeFocus.html
new file mode 100644
index 0000000000..752ed66b36
--- /dev/null
+++ b/accessible/tests/mochitest/focus/test_takeFocus.html
@@ -0,0 +1,109 @@
+<html>
+
+<head>
+ <title>nsIAccessible::takeFocus testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function takeFocusInvoker(aID) {
+ this.accessible = getAccessible(aID);
+
+ this.eventSeq = [ new focusChecker(this.accessible) ];
+
+ this.invoke = function takeFocusInvoker_invoke() {
+ this.accessible.takeFocus();
+ };
+
+ this.getID = function takeFocusInvoker_getID() {
+ return "takeFocus for " + prettyName(aID);
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTest() {
+ disableLogging(); // from test_focusedChild
+ gQueue = new eventQueue();
+
+ gQueue.push(new takeFocusInvoker("aria-link"));
+ gQueue.push(new takeFocusInvoker("aria-link2"));
+ gQueue.push(new takeFocusInvoker("link"));
+ gQueue.push(new takeFocusInvoker("item2"));
+ gQueue.push(new takeFocusInvoker(document));
+ gQueue.push(new takeFocusInvoker("lb_item2"));
+ gQueue.push(new takeFocusInvoker(document));
+ gQueue.push(new takeFocusInvoker("lb_item3.2"));
+ gQueue.push(new takeFocusInvoker(document));
+ gQueue.push(new takeFocusInvoker("lb_item3.1"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=429547"
+ title="Support aria-activedescendant usage in nsIAccesible::TakeFocus()">
+ Mozilla Bug 429547
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=452710"
+ title="nsIAccessible::takeFocus testing">
+ Mozilla Bug 452710
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=706067"
+ title="Make takeFocus work on widget items">
+ Mozilla Bug 706067
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <span id="aria-link" role="link" tabindex="0">link</span>
+ <span id="aria-link2" role="link" tabindex="0">link</span>
+
+ <a id="link" href="">link</a>
+
+ <div role="listbox" aria-activedescendant="item1" id="container" tabindex="1">
+ <div role="option" id="item1">item1</div>
+ <div role="option" id="item2">item2</div>
+ <div role="option" id="item3">item3</div>
+ </div>
+
+ <select id="listbox" size="5">
+ <option id="lb_item1">item1</option>
+ <option id="lb_item2">item2</option>
+ <optgroup>
+ <option id="lb_item3.1">item 3.1</option>
+ <option id="lb_item3.2">item 3.2</option>
+ </optgroup>
+ </select>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/focus/test_takeFocus.xhtml b/accessible/tests/mochitest/focus/test_takeFocus.xhtml
new file mode 100644
index 0000000000..127f47d067
--- /dev/null
+++ b/accessible/tests/mochitest/focus/test_takeFocus.xhtml
@@ -0,0 +1,104 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible focus testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function takeFocusInvoker(aID, aArgConverterFunc)
+ {
+ this.targetFunc = aArgConverterFunc ? aArgConverterFunc : getAccessible;
+
+ this.eventSeq = [ new focusChecker(this.targetFunc, aID) ];
+
+ this.invoke = function takeFocusInvoker_invoke()
+ {
+ this.targetFunc.call(null, aID).takeFocus();
+ }
+
+ this.getID = function takeFocusInvoker_getID()
+ {
+ return "takeFocus for " + prettyName(aID);
+ }
+ }
+
+ function getLastChild(aID)
+ {
+ return getAccessible(aID).lastChild;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Tests
+
+ //gA11yEventDumpID = "eventdump"; // debug stuff
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTests()
+ {
+ // Test focus events.
+ gQueue = new eventQueue();
+
+ gQueue.push(new takeFocusInvoker("tree", getLastChild));
+ gQueue.push(new takeFocusInvoker("listitem2"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yXULTreeLoadEvent(doTests, "tree", new nsTableTreeView(5));
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=706067"
+ title="Make takeFocus work on widget items">
+ Mozilla Bug 706067
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col1" flex="1" primary="true" label="column"/>
+ <treecol id="col2" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren id="treechildren"/>
+ </tree>
+
+ <richlistbox id="listbox">
+ <richlistitem id="listitem1"><label value="item1"/></richlistitem>
+ <richlistitem id="listitem2"><label value="item2"/></richlistitem>
+ </richlistbox>
+
+ <vbox id="eventdump"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/formimage.png b/accessible/tests/mochitest/formimage.png
new file mode 100644
index 0000000000..10e44bf920
--- /dev/null
+++ b/accessible/tests/mochitest/formimage.png
Binary files differ
diff --git a/accessible/tests/mochitest/grid.js b/accessible/tests/mochitest/grid.js
new file mode 100644
index 0000000000..4a2e01ee9b
--- /dev/null
+++ b/accessible/tests/mochitest/grid.js
@@ -0,0 +1,142 @@
+/* import-globals-from common.js */
+
+/**
+ * Create grid object based on HTML table.
+ */
+function grid(aTableIdentifier) {
+ this.getRowCount = function getRowCount() {
+ return this.table.rows.length - (this.table.tHead ? 1 : 0);
+ };
+ this.getColsCount = function getColsCount() {
+ return this.table.rows[0].cells.length;
+ };
+
+ this.getRowAtIndex = function getRowAtIndex(aIndex) {
+ return this.table.rows[this.table.tHead ? aIndex + 1 : aIndex];
+ };
+
+ this.getMaxIndex = function getMaxIndex() {
+ return this.getRowCount() * this.getColsCount() - 1;
+ };
+
+ this.getCellAtIndex = function getCellAtIndex(aIndex) {
+ var colsCount = this.getColsCount();
+
+ var rowIdx = Math.floor(aIndex / colsCount);
+ var colIdx = aIndex % colsCount;
+
+ var row = this.getRowAtIndex(rowIdx);
+ return row.cells[colIdx];
+ };
+
+ this.getIndexByCell = function getIndexByCell(aCell) {
+ var colIdx = aCell.cellIndex;
+
+ var rowIdx = aCell.parentNode.rowIndex;
+ if (this.table.tHead) {
+ rowIdx -= 1;
+ }
+
+ var colsCount = this.getColsCount();
+ return rowIdx * colsCount + colIdx;
+ };
+
+ this.getCurrentCell = function getCurrentCell() {
+ var rowCount = this.table.rows.length;
+ var colsCount = this.getColsCount();
+ for (var rowIdx = 0; rowIdx < rowCount; rowIdx++) {
+ for (var colIdx = 0; colIdx < colsCount; colIdx++) {
+ var cell = this.table.rows[rowIdx].cells[colIdx];
+ if (cell.hasAttribute("tabindex")) {
+ return cell;
+ }
+ }
+ }
+ return null;
+ };
+
+ this.initGrid = function initGrid() {
+ this.table.addEventListener("keypress", this);
+ this.table.addEventListener("click", this);
+ };
+
+ this.handleEvent = function handleEvent(aEvent) {
+ if (aEvent instanceof KeyboardEvent) {
+ this.handleKeyEvent(aEvent);
+ } else {
+ this.handleClickEvent(aEvent);
+ }
+ };
+
+ this.handleKeyEvent = function handleKeyEvent(aEvent) {
+ if (aEvent.target.localName != "td") {
+ return;
+ }
+
+ var cell = aEvent.target;
+ switch (aEvent.keyCode) {
+ case KeyboardEvent.DOM_VK_UP: {
+ let colsCount = this.getColsCount();
+ let idx = this.getIndexByCell(cell);
+ var upidx = idx - colsCount;
+ if (upidx >= 0) {
+ cell.removeAttribute("tabindex");
+ var upcell = this.getCellAtIndex(upidx);
+ upcell.setAttribute("tabindex", "0");
+ upcell.focus();
+ }
+ break;
+ }
+ case KeyboardEvent.DOM_VK_DOWN: {
+ let colsCount = this.getColsCount();
+ let idx = this.getIndexByCell(cell);
+ var downidx = idx + colsCount;
+ if (downidx <= this.getMaxIndex()) {
+ cell.removeAttribute("tabindex");
+ var downcell = this.getCellAtIndex(downidx);
+ downcell.setAttribute("tabindex", "0");
+ downcell.focus();
+ }
+ break;
+ }
+ case KeyboardEvent.DOM_VK_LEFT: {
+ let idx = this.getIndexByCell(cell);
+ if (idx > 0) {
+ cell.removeAttribute("tabindex");
+ var prevcell = this.getCellAtIndex(idx - 1);
+ prevcell.setAttribute("tabindex", "0");
+ prevcell.focus();
+ }
+ break;
+ }
+ case KeyboardEvent.DOM_VK_RIGHT: {
+ let idx = this.getIndexByCell(cell);
+ if (idx < this.getMaxIndex()) {
+ cell.removeAttribute("tabindex");
+ var nextcell = this.getCellAtIndex(idx + 1);
+ nextcell.setAttribute("tabindex", "0");
+ nextcell.focus();
+ }
+ break;
+ }
+ }
+ };
+
+ this.handleClickEvent = function handleClickEvent(aEvent) {
+ if (aEvent.target.localName != "td") {
+ return;
+ }
+
+ var curCell = this.getCurrentCell();
+ var cell = aEvent.target;
+
+ if (cell != curCell) {
+ curCell.removeAttribute("tabindex");
+ cell.setAttribute("tabindex", "0");
+ cell.focus();
+ }
+ };
+
+ this.table = getNode(aTableIdentifier);
+ this.initGrid();
+}
diff --git a/accessible/tests/mochitest/hittest/a11y.toml b/accessible/tests/mochitest/hittest/a11y.toml
new file mode 100644
index 0000000000..b17ffbc289
--- /dev/null
+++ b/accessible/tests/mochitest/hittest/a11y.toml
@@ -0,0 +1,19 @@
+[DEFAULT]
+support-files = [ "zoom_tree.xhtml",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/letters.gif"]
+
+["test_browser.html"]
+
+["test_general.html"]
+
+["test_menu.xhtml"]
+
+["test_shadowroot.html"]
+support-files = "test_shadowroot_subframe.html"
+
+["test_zoom.html"]
+
+["test_zoom_text.html"]
+
+["test_zoom_tree.xhtml"]
diff --git a/accessible/tests/mochitest/hittest/test_browser.html b/accessible/tests/mochitest/hittest/test_browser.html
new file mode 100644
index 0000000000..c14df7d736
--- /dev/null
+++ b/accessible/tests/mochitest/hittest/test_browser.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>nsIAccessible::childAtPoint() from browser tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../layout.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // Hit testing. See bug #726097
+ getNode("hittest").scrollIntoView(true);
+
+ var hititem = getAccessible("hititem");
+ var hittest = getAccessible("hittest");
+
+ var [hitX, hitY, hitWidth, hitHeight] = getBounds(hititem);
+ var tgtX = hitX + hitWidth / 2;
+ var tgtY = hitY + hitHeight / 2;
+
+ var rootAcc = getRootAccessible();
+ var docAcc = getAccessible(document);
+ var outerDocAcc = docAcc.parent;
+
+ var hitAcc = rootAcc.getDeepestChildAtPoint(tgtX, tgtY);
+ is(hitAcc, hititem, "Hit match at " + tgtX + "," + tgtY +
+ ". Found: " + prettyName(hitAcc));
+ var hitAcc2 = docAcc.getDeepestChildAtPoint(tgtX, tgtY);
+ is(hitAcc, hitAcc2, "Hit match at " + tgtX + "," + tgtY +
+ ". Found: " + prettyName(hitAcc2));
+
+ hitAcc = outerDocAcc.getChildAtPoint(tgtX, tgtY);
+ is(hitAcc, docAcc, "Hit match at " + tgtX + "," + tgtY +
+ ". Found: " + prettyName(hitAcc));
+ hitAcc = docAcc.getChildAtPoint(tgtX, tgtY);
+ is(hitAcc, hittest, "Hit match at " + tgtX + "," + tgtY +
+ ". Found: " + prettyName(hitAcc));
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=726097"
+ title="nsIAccessible::childAtPoint() from browser tests">Mozilla Bug 726097</a>
+
+ <div id="hittest">
+ <div id="hititem"><span role="image">img</span>item</div>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/hittest/test_general.html b/accessible/tests/mochitest/hittest/test_general.html
new file mode 100644
index 0000000000..f5afd18446
--- /dev/null
+++ b/accessible/tests/mochitest/hittest/test_general.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>nsIAccessible::childAtPoint() tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../layout.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function doPreTest() {
+ waitForImageMap("imgmap", doTest);
+ }
+
+ function doTest() {
+ // Not specific case, child and deepchild testing.
+ var list = getAccessible("list");
+ var listitem = getAccessible("listitem");
+ var image = getAccessible("image");
+if (!MAC) {
+ testChildAtPoint(list, 1, 1, listitem, image.firstChild);
+} else {
+ todo(false, "Bug 746974 - children must match on all platforms, disable failing test on Mac");
+}
+
+ // ::MustPrune case (in this case childAtPoint doesn't look inside a
+ // textbox), point is inside of textbox.
+ var txt = getAccessible("txt");
+ testChildAtPoint(txt, 1, 1, txt, txt);
+
+ // ::MustPrune case, point is outside of textbox accessible but is in
+ // document.
+ testChildAtPoint(txt, -1, 1, null, null);
+
+ // ::MustPrune case, point is outside of root accessible.
+ testChildAtPoint(txt, -10000, 10000, null, null);
+
+ // Not specific case, point is inside of btn accessible.
+ var btn = getAccessible("btn");
+ testChildAtPoint(btn, 1, 1, btn, btn);
+
+ // Not specific case, point is outside of btn accessible.
+ testChildAtPoint(btn, -1, 1, null, null);
+
+ // Out of flow accessible testing, do not return out of flow accessible
+ // because it's not a child of the accessible even visually it is.
+ var rectArea = getNode("area").getBoundingClientRect();
+ var outOfFlow = getNode("outofflow");
+ outOfFlow.style.left = rectArea.left + "px";
+ outOfFlow.style.top = rectArea.top + "px";
+
+ testChildAtPoint("area", 1, 1, "area", "area");
+
+ // Test image maps. Their children are not in the layout tree.
+ var theLetterA = getAccessible("imgmap").firstChild;
+ hitTest("imgmap", theLetterA, theLetterA);
+ hitTest("container", "imgmap", theLetterA);
+
+ // hit testing for element contained by zero-width element
+ hitTest("container2", "container2_input", "container2_input");
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doPreTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=491657"
+ title="nsIAccessible::childAtPoint() tests">Mozilla Bug 491657</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div role="list" id="list">
+ <div role="listitem" id="listitem"><span role="image" id="image">img</span>item</div>
+ </div>
+
+ <span role="button">button1</span><span role="button" id="btn">button2</span>
+
+ <span role="textbox">textbox1</span><span role="textbox" id="txt">textbox2</span>
+
+ <div id="outofflow" style="width: 10px; height: 10px; position: absolute; left: 0px; top: 0px; background-color: yellow;">
+ </div>
+ <div id="area" style="width: 100px; height: 100px; background-color: blue;"></div>
+
+ <map name="atoz_map">
+ <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a"
+ coords="0,0,15,15" alt="thelettera" shape="rect"/>
+ </map>
+
+ <div id="container">
+ <img id="imgmap" width="447" height="15" usemap="#atoz_map" src="../letters.gif"/>
+ </div>
+
+ <div id="container2" style="width: 0px">
+ <input id="container2_input">
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/hittest/test_menu.xhtml b/accessible/tests/mochitest/hittest/test_menu.xhtml
new file mode 100644
index 0000000000..d80b31305d
--- /dev/null
+++ b/accessible/tests/mochitest/hittest/test_menu.xhtml
@@ -0,0 +1,133 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Hit testing for XUL menus">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../layout.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function openMenu(aMenuID, aMenuPopupID, aMenuItemID)
+ {
+ this.menuNode = getNode(aMenuID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, this.menuNode)
+ ];
+
+ this.invoke = function openMenu_invoke()
+ {
+ this.menuNode.open = true;
+ }
+
+ this.finalCheck = function openMenu_finalCheck()
+ {
+ hitTest(aMenuPopupID, aMenuItemID, aMenuItemID);
+ }
+
+ this.getID = function openMenu_invoke()
+ {
+ return "open menu '" + aMenuID + "' and do hit testing";
+ }
+ }
+
+ function closeMenu(aID, aSubID, aSub2ID)
+ {
+ this.menuNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, document)
+ ];
+
+ this.invoke = function openMenu_invoke()
+ {
+ this.menuNode.open = false;
+ }
+
+ this.finalCheck = function openMenu_finalCheck()
+ {
+ testStates(aID, 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN);
+ testStates(aSubID, STATE_INVISIBLE, 0, STATE_OFFSCREEN);
+ testStates(aSub2ID, STATE_INVISIBLE, 0, STATE_OFFSCREEN);
+ }
+
+ this.getID = function openMenu_invoke()
+ {
+ return "open menu and test states";
+ }
+ }
+
+ var gQueue = null;
+ function doTest()
+ {
+ if (LINUX) {
+ ok(true, "No tests is running on Linux");
+ SimpleTest.finish();
+ return;
+ }
+
+ getNode("mi_file1").scrollIntoView(true);
+
+ gQueue = new eventQueue();
+ gQueue.push(new openMenu("mi_file1", "mp_file1", "mi_file1.1"));
+ gQueue.push(new openMenu("mi_file1.2", "mp_file1.2", "mi_file1.2.1"));
+ gQueue.push(new closeMenu("mi_file1", "mi_file1.1", "mi_file1.2.1"));
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=670087"
+ title="AccessibleObjectFromPoint returns incorrect accessible for popup menus">
+ Bug 670087
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+
+ <menubar>
+ <menu label="File" id="mi_file1">
+ <menupopup id="mp_file1">
+ <menuitem label="SubFile" id="mi_file1.1"/>
+ <menu label="SubFile2" id="mi_file1.2">
+ <menupopup style="max-height: 5em;" id="mp_file1.2">
+ <menuitem label="SubSubFile" id="mi_file1.2.1"/>
+ <menuitem label="SubSubFile2" id="mi_file1.2.2"/>
+ <menuitem label="SubSubFile3" id="mi_file1.2.3"/>
+ <menuitem label="SubSubFile4" id="mi_file1.2.4"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ </menubar>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/hittest/test_shadowroot.html b/accessible/tests/mochitest/hittest/test_shadowroot.html
new file mode 100644
index 0000000000..6acdc47987
--- /dev/null
+++ b/accessible/tests/mochitest/hittest/test_shadowroot.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>ShadowRoot hit tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+</head>
+<body>
+
+ <a target="_blank"
+ title="Test getChildAtPoint works for shadow DOM content"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1027315">
+ Mozilla Bug 1027315
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <script>
+ SimpleTest.waitForExplicitFinish();
+
+ window.onload = () => {
+ var iframe = document.createElement("iframe");
+ iframe.src = "test_shadowroot_subframe.html";
+ document.body.appendChild(iframe);
+ };
+
+ </script>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/hittest/test_shadowroot_subframe.html b/accessible/tests/mochitest/hittest/test_shadowroot_subframe.html
new file mode 100644
index 0000000000..25c41341cd
--- /dev/null
+++ b/accessible/tests/mochitest/hittest/test_shadowroot_subframe.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>ShadowRoot hit tests</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript" src="../common.js"></script>
+ <script type="application/javascript" src="../layout.js"></script>
+
+ <script type="application/javascript">
+ let SimpleTest = window.parent.SimpleTest;
+ let ok = window.parent.ok;
+ let is = window.parent.is;
+
+ function doTest() {
+ var componentAcc = getAccessible("component1");
+ testChildAtPoint(componentAcc, 1, 1, componentAcc.firstChild,
+ componentAcc.firstChild);
+
+ componentAcc = getAccessible("component2");
+ testChildAtPoint(componentAcc, 1, 1, componentAcc.firstChild,
+ componentAcc.firstChild);
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+<body>
+ <div role="group" class="components" id="component1" style="display: inline-block;">
+ <!--
+ <div role="button" id="component-child"
+ style="width: 100px; height: 100px; background-color: pink;">
+ </div>
+ -->
+ </div>
+ <div role="group" class="components" id="component2" style="display: inline-block;">
+ <!--
+ <button>Hello world</button>
+ -->
+ </div>
+ <script>
+ // This routine adds the comment children of each 'component' to its
+ // shadow root.
+ var components = document.querySelectorAll(".components");
+ for (var i = 0; i < components.length; i++) {
+ var component = components[i];
+ var shadow = component.attachShadow({mode: "open"});
+ for (var child = component.firstChild; child; child = child.nextSibling) {
+ if (child.nodeType === 8)
+ shadow.innerHTML = child.data;
+ }
+ }
+ </script>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/hittest/test_zoom.html b/accessible/tests/mochitest/hittest/test_zoom.html
new file mode 100644
index 0000000000..70e71f7a6d
--- /dev/null
+++ b/accessible/tests/mochitest/hittest/test_zoom.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>childAtPoint when page is zoomed</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../layout.js"></script>
+ <script type="application/javascript"
+ src="../browser.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+if (!MAC) {
+ var tabDocument = currentTabDocument();
+ var p1 = tabDocument.body.firstElementChild;
+ var p2 = tabDocument.body.lastElementChild;
+
+ hitTest(tabDocument, p1, p1.firstChild);
+ hitTest(tabDocument, p2, p2.firstChild);
+
+ zoomDocument(tabDocument, 2.0);
+
+ hitTest(tabDocument, p1, p1.firstChild);
+ hitTest(tabDocument, p2, p2.firstChild);
+
+ closeBrowserWindow();
+} else {
+ todo(false, "Bug 746974 - deepest child must be correct on all platforms, disabling on Mac!");
+}
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ openBrowserWindow(doTest,
+ "data:text/html,<html><body><p>para 1</p><p>para 2</p></body></html>",
+ { left: 100, top: 100 });
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=727942"
+ title="childAtPoint may return incorrect accessibles when page zoomed">
+ Mozilla Bug 727942
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/hittest/test_zoom_text.html b/accessible/tests/mochitest/hittest/test_zoom_text.html
new file mode 100644
index 0000000000..4dc92b9639
--- /dev/null
+++ b/accessible/tests/mochitest/hittest/test_zoom_text.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>getOffsetAtPoint when page is zoomed</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../layout.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var hyperText = getNode("paragraph");
+ var textNode = hyperText.firstChild;
+ let [x, y, width, height] = getBounds(textNode);
+ testOffsetAtPoint(hyperText, x + width / 2, y + height / 2,
+ COORDTYPE_SCREEN_RELATIVE,
+ hyperText.textContent.length / 2);
+
+ zoomDocument(document, 2.0);
+
+ document.body.offsetTop; // getBounds doesn't flush layout on its own, looks like.
+
+ [x, y, width, height] = getBounds(textNode);
+ testOffsetAtPoint(hyperText, x + width / 2, y + height / 2,
+ COORDTYPE_SCREEN_RELATIVE,
+ hyperText.textContent.length / 2);
+
+ zoomDocument(document, 1.0);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=727942"
+ title="getOffsetAtPoint returns incorrect value when page is zoomed">
+ Mozilla Bug 727942
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <p id="paragraph" style="font-family: monospace;">Болтали две сороки</p>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/hittest/test_zoom_tree.xhtml b/accessible/tests/mochitest/hittest/test_zoom_tree.xhtml
new file mode 100644
index 0000000000..54cb37c871
--- /dev/null
+++ b/accessible/tests/mochitest/hittest/test_zoom_tree.xhtml
@@ -0,0 +1,97 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="nsIAccessible::getChildAtPoint and getDeepestChildAtPoint">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/chrome-harness.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../layout.js" />
+ <script type="application/javascript"
+ src="../browser.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function doTest()
+ {
+ var tabDocument = currentTabDocument();
+ var tabWindow = currentTabWindow();
+
+ var tree = tabDocument.getElementById("tree");
+ var treecols = tabDocument.getElementById("treecols");
+ var treecol1 = tabDocument.getElementById("treecol1");
+
+ // tree columns
+ hitTest(tree, treecols, treecol1);
+
+ // tree rows and cells
+ var treeRect = tree.treeBody.getBoundingClientRect();
+ var rect = tree.getCoordsForCellItem(1, tree.columns[0], "cell");
+
+ var treeAcc = getAccessible(tree, [nsIAccessibleTable]);
+ var cellAcc = treeAcc.getCellAt(1, 0);
+ var rowAcc = cellAcc.parent;
+
+ var cssX = rect.x + treeRect.x;
+ var cssY = rect.y + treeRect.y;
+ var [x, y] = CSSToDevicePixels(tabWindow, cssX, cssY);
+
+ testChildAtPoint(treeAcc, x, y, rowAcc, cellAcc);
+ testChildAtPoint(rowAcc, x, y, cellAcc, cellAcc);
+
+ // do zoom
+ zoomDocument(tabDocument, 1.5);
+
+ // tree columns
+ hitTest(tree, treecols, treecol1);
+
+ // tree rows and cells
+ [x, y] = CSSToDevicePixels(tabWindow, cssX, cssY);
+ testChildAtPoint(treeAcc, x, y, rowAcc, cellAcc);
+ testChildAtPoint(rowAcc, x, y, cellAcc, cellAcc);
+
+ closeBrowserWindow();
+ SimpleTest.finish();
+ }
+
+ function prepareTest()
+ {
+ var tabDocument = currentTabDocument();
+ var tree = tabDocument.getElementById("tree");
+ loadXULTreeAndDoTest(doTest, tree, new nsTableTreeView(5));
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ openBrowserWindow(prepareTest,
+ getRootDirectory(window.location.href) + "zoom_tree.xhtml",
+ { left: 100, top: 100 });
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=471493"
+ title=" crash [@ nsPropertyTable::GetPropertyInternal]">
+ Mozilla Bug 471493
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/hittest/zoom_tree.xhtml b/accessible/tests/mochitest/hittest/zoom_tree.xhtml
new file mode 100644
index 0000000000..52ec0932ab
--- /dev/null
+++ b/accessible/tests/mochitest/hittest/zoom_tree.xhtml
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="nsIAccessible::getChildAtPoint and getDeepestChildAtPoint for XUL trees">
+
+ <tree id="tree" flex="1">
+ <treecols id="treecols">
+ <treecol id="treecol1" flex="1" primary="true" label="column"/>
+ <treecol id="treecol2" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren id="treechildren"/>
+ </tree>
+
+</window>
+
diff --git a/accessible/tests/mochitest/hyperlink/a11y.toml b/accessible/tests/mochitest/hyperlink/a11y.toml
new file mode 100644
index 0000000000..77f551ee5d
--- /dev/null
+++ b/accessible/tests/mochitest/hyperlink/a11y.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+support-files = [ "hyperlink.js",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/letters.gif"]
+
+["test_general.html"]
+
+["test_general.xhtml"]
diff --git a/accessible/tests/mochitest/hyperlink/hyperlink.js b/accessible/tests/mochitest/hyperlink/hyperlink.js
new file mode 100644
index 0000000000..93caa9090c
--- /dev/null
+++ b/accessible/tests/mochitest/hyperlink/hyperlink.js
@@ -0,0 +1,46 @@
+/* import-globals-from ../common.js */
+/* import-globals-from ../events.js */
+/* import-globals-from ../states.js */
+
+/**
+ * Focus hyperlink invoker.
+ *
+ * @param aID [in] hyperlink identifier
+ * @param aSelectedAfter [in] specifies if hyperlink is selected/focused after
+ * the focus
+ */
+function focusLink(aID, aSelectedAfter) {
+ this.node = getNode(aID);
+ this.accessible = getAccessible(this.node);
+
+ this.eventSeq = [];
+ this.unexpectedEventSeq = [];
+
+ var checker = new invokerChecker(EVENT_FOCUS, this.accessible);
+ if (aSelectedAfter) {
+ this.eventSeq.push(checker);
+ } else {
+ this.unexpectedEventSeq.push(checker);
+ }
+
+ this.invoke = function focusLink_invoke() {
+ var expectedStates = aSelectedAfter ? STATE_FOCUSABLE : 0;
+ var unexpectedStates =
+ (!aSelectedAfter ? STATE_FOCUSABLE : 0) | STATE_FOCUSED;
+ testStates(aID, expectedStates, 0, unexpectedStates, 0);
+
+ this.node.focus();
+ };
+
+ this.finalCheck = function focusLink_finalCheck() {
+ var expectedStates = aSelectedAfter ? STATE_FOCUSABLE | STATE_FOCUSED : 0;
+ var unexpectedStates = !aSelectedAfter
+ ? STATE_FOCUSABLE | STATE_FOCUSED
+ : 0;
+ testStates(aID, expectedStates, 0, unexpectedStates, 0);
+ };
+
+ this.getID = function focusLink_getID() {
+ return "focus hyperlink " + prettyName(aID);
+ };
+}
diff --git a/accessible/tests/mochitest/hyperlink/test_general.html b/accessible/tests/mochitest/hyperlink/test_general.html
new file mode 100644
index 0000000000..8b0eb39b01
--- /dev/null
+++ b/accessible/tests/mochitest/hyperlink/test_general.html
@@ -0,0 +1,278 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=418368
+-->
+<head>
+ <title>nsIHyperLinkAccessible chrome tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript"
+ src="hyperlink.js"></script>
+
+ <script type="application/javascript">
+ function testThis(aID, aAcc, aRole, aAnchors, aName, aValid, aStartIndex,
+ aEndIndex) {
+ testRole(aAcc, aRole);
+ is(aAcc.anchorCount, aAnchors, "Wrong number of anchors for ID "
+ + aID + "!");
+ is(aAcc.getAnchor(0).name, aName, "Wrong name for ID "
+ + aID + "!");
+ is(aAcc.valid, aValid, "No correct valid state for ID "
+ + aID + "!");
+ is(aAcc.startIndex, aStartIndex, "Wrong startIndex value for ID "
+ + aID + "!");
+ is(aAcc.endIndex, aEndIndex, "Wrong endIndex value for ID "
+ + aID + "!");
+ }
+
+ function testAction(aId, aAcc, aActionName) {
+ var actionCount = aActionName ? 1 : 0;
+ is(aAcc.actionCount, actionCount,
+ "Wrong actions number for ID " + aId);
+ try {
+ is(aAcc.getActionName(0), aActionName,
+ "Wrong action name for ID " + aId);
+ } catch (e) {
+ if (actionCount)
+ ok(false, "Exception on action name getting for ID " + aId);
+ else
+ ok(true, "Correct action name for ID " + aId);
+ }
+ }
+
+ // gA11yEventDumpToConsole = true; // debug stuff
+ function doPreTest() {
+ waitForImageMap("imgmap", doTest);
+ }
+
+ var gQueue = null;
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // normal hyperlink
+ var normalHyperlinkAcc = getAccessible("NormalHyperlink",
+ [nsIAccessibleHyperLink]);
+ testThis("NormalHyperlink", normalHyperlinkAcc, ROLE_LINK, 1,
+ "Mozilla Foundation", true, 17, 18);
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ is(normalHyperlinkAcc.getURI(0).spec, "http://www.mozilla.org/",
+ "URI wrong for normalHyperlinkElement!");
+ testStates(normalHyperlinkAcc, STATE_LINKED, 0);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA hyperlink
+ var ariaHyperlinkAcc = getAccessible("AriaHyperlink",
+ [nsIAccessibleHyperLink]);
+ testThis("AriaHyperlink", ariaHyperlinkAcc, ROLE_LINK, 1,
+ "Mozilla Foundation Home", true, 30, 31);
+ testStates(ariaHyperlinkAcc, STATE_LINKED, 0);
+ testAction("AriaHyperlink", ariaHyperlinkAcc, "click");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA hyperlink with status invalid
+ var invalidAriaHyperlinkAcc = getAccessible("InvalidAriaHyperlink",
+ [nsIAccessibleHyperLink]);
+ is(invalidAriaHyperlinkAcc.valid, false, "Should not be valid!");
+ testStates(invalidAriaHyperlinkAcc, STATE_LINKED, 0);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // image map and its link children
+
+ var imageMapHyperlinkAcc = getAccessible("imgmap",
+ [nsIAccessibleHyperLink]);
+ testThis("imgmap", imageMapHyperlinkAcc, ROLE_IMAGE_MAP, 2, "b", true,
+ 79, 80);
+ is(imageMapHyperlinkAcc.getURI(0).spec,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.bbc.co.uk/radio4/atoz/index.shtml#b", "URI wrong!");
+ is(imageMapHyperlinkAcc.getURI(1).spec,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.bbc.co.uk/radio4/atoz/index.shtml#a", "URI wrong!");
+ testStates(imageMapHyperlinkAcc, 0, 0);
+
+ var area1 = getAccessible(imageMapHyperlinkAcc.firstChild,
+ [nsIAccessibleHyperLink]);
+ testThis("Area1", area1, ROLE_LINK, 1, "b", true, 0, 1);
+ is(area1.getURI(0).spec,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.bbc.co.uk/radio4/atoz/index.shtml#b", "URI wrong!");
+ testStates(area1, (STATE_LINKED));
+
+ var area2 = getAccessible(area1.nextSibling,
+ [nsIAccessibleHyperLink]);
+ testThis("Area2", area2, ROLE_LINK, 1, "a", true, 1, 2);
+ is(area2.getURI(0).spec,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "http://www.bbc.co.uk/radio4/atoz/index.shtml#a", "URI wrong!");
+ testStates(area2, (STATE_LINKED));
+
+ // ////////////////////////////////////////////////////////////////////////
+ // empty hyperlink
+ var EmptyHLAcc = getAccessible("emptyLink",
+ [nsIAccessibleHyperLink]);
+ testThis("emptyLink", EmptyHLAcc, ROLE_LINK, 1, null, true, 93, 94);
+ testStates(EmptyHLAcc, (STATE_FOCUSABLE | STATE_LINKED), 0);
+ testAction("emptyLink", EmptyHLAcc, "jump");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // normal hyperlink with embedded span
+ var hyperlinkWithSpanAcc = getAccessible("LinkWithSpan",
+ [nsIAccessibleHyperLink]);
+ testThis("LinkWithSpan", hyperlinkWithSpanAcc, ROLE_LINK, 1,
+ "Heise Online", true, 119, 120);
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ is(hyperlinkWithSpanAcc.getURI(0).spec, "http://www.heise.de/",
+ "URI wrong for hyperlinkElementWithSpan!");
+ testStates(hyperlinkWithSpanAcc, STATE_LINKED, 0);
+ testAction("LinkWithSpan", hyperlinkWithSpanAcc, "jump");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Named anchor, should never have state_linked
+ var namedAnchorAcc = getAccessible("namedAnchor",
+ [nsIAccessibleHyperLink]);
+ testThis("namedAnchor", namedAnchorAcc, ROLE_TEXT, 1,
+ null, true, 196, 197);
+ testStates(namedAnchorAcc, 0, 0, (STATE_FOCUSABLE | STATE_LINKED));
+ testAction("namedAnchor", namedAnchorAcc, "");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // No link (hasn't any attribute), should never have state_linked
+ var noLinkAcc = getAccessible("noLink",
+ [nsIAccessibleHyperLink]);
+ testThis("noLink", noLinkAcc, ROLE_TEXT, 1,
+ null, true, 254, 255);
+ testStates(noLinkAcc, 0, 0, (STATE_FOCUSABLE | STATE_LINKED));
+ testAction("noLink", noLinkAcc, "");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Link with registered 'click' event, should have state_linked
+ var linkWithClickAcc = getAccessible("linkWithClick",
+ [nsIAccessibleHyperLink]);
+ testThis("linkWithClick", linkWithClickAcc, ROLE_LINK, 1,
+ "This should have state_linked", true, 292, 293);
+ testStates(linkWithClickAcc, STATE_LINKED, 0);
+ testAction("linkWithClick", linkWithClickAcc, "click");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Maps to group links (bug 431615).
+ // var linksMapAcc = getAccessible("linksmap");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Link with title attribute, no name from the subtree (bug 438325).
+ var id = "linkWithTitleNoNameFromSubtree";
+ var linkAcc = getAccessible(id, [nsIAccessibleHyperLink]);
+ testThis(id, linkAcc, ROLE_LINK, 1, "Link with title", true, 344, 345);
+ testStates(linkAcc, STATE_LINKED, 0);
+ testAction(id, linkAcc, "jump");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Link with title attribute, name from the subtree - onscreen name
+ // (bug 438325).
+ id = "linkWithTitleNameFromSubtree";
+ linkAcc = getAccessible(id, [nsIAccessibleHyperLink]);
+ testThis(id, linkAcc, ROLE_LINK, 1, "the name from subtree", true, 393,
+ 394);
+ testStates(linkAcc, STATE_LINKED, 0);
+ testAction(id, linkAcc, "jump");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Link with title attribute, name from the nested html:img (bug 438325).
+ id = "linkWithTitleNameFromImg";
+ linkAcc = getAccessible(id, [nsIAccessibleHyperLink]);
+ testThis(id, linkAcc, ROLE_LINK, 1, "The title for link", true, 447,
+ 448);
+ testStates(linkAcc, STATE_LINKED, 0);
+ testAction(id, linkAcc, "jump");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Text accessible shouldn't implement nsIAccessibleHyperLink
+ var res = isAccessible(getNode("namedAnchor").firstChild,
+ [nsIAccessibleHyperLink]);
+ ok(!res, "Text accessible shouldn't implement nsIAccessibleHyperLink");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Test focus
+ gQueue = new eventQueue();
+
+ gQueue.push(new focusLink("NormalHyperlink", true));
+ gQueue.push(new focusLink("AriaHyperlink", true));
+ gQueue.push(new focusLink("InvalidAriaHyperlink", false));
+ gQueue.push(new focusLink("LinkWithSpan", true));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doPreTest);
+ </script>
+
+</head>
+<body><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=418368">Mozilla Bug 418368</a
+ ><p id="display"></p
+ ><div id="content" style="display: none"></div
+ ><pre id="test">
+ </pre
+ ><br
+ >Simple link:<br
+ ><a id="NormalHyperlink" href="http://www.mozilla.org">Mozilla Foundation</a
+ ><br>ARIA link:<br
+ ><span id="AriaHyperlink" role="link"
+ onclick="window.open('http://www.mozilla.org/');"
+ tabindex="0">Mozilla Foundation Home</span
+ ><br
+ >Invalid, non-focusable hyperlink:<br
+ ><span id="InvalidAriaHyperlink" role="link" aria-invalid="true"
+ onclick="window.open('http:/www.mozilla.org/');">Invalid link</span
+ ><br>Image map:<br
+ ><map name="atoz_map"
+ ><area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b"
+ coords="17,0,30,14"
+ alt="b"
+ shape="rect"></area
+ ><area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a"
+ coords="0,0,13,14"
+ alt="a"
+ shape="rect"></area
+ ></map
+ ><img width="447" id="imgmap"
+ height="15"
+ usemap="#atoz_map"
+ src="../letters.gif"><br>Empty link:<br
+ ><a id="emptyLink" href=""><img src=""></a
+ ><br>Link with embedded span<br
+ ><a id="LinkWithSpan" href="http://www.heise.de/"><span lang="de">Heise Online</span></a
+ ><br>Named anchor, must not have "linked" state for it to be exposed correctly:<br
+ ><a id="namedAnchor" name="named_anchor">This should never be of state_linked</a
+ ><br>Link having no attributes, must not have "linked" state:<a id="noLink"
+ >This should never be of state_linked</a
+ ><br>Link with registered 'click' event: <a id="linkWithClick" onclick="var clicked = true;"
+ >This should have state_linked</a
+ ><br>Link with title attribute (no name from subtree): <a
+ id="linkWithTitleNoNameFromSubtree" href="http://www.heise.de/"
+ title="Link with title"><img src=""/></a
+ ><br>Link with title attribute (name from subtree): <a
+ id="linkWithTitleNameFromSubtree" href="http://www.heise.de/"
+ title="Link with title">the name from subtree</a
+ ><br>Link with title attribute (name from nested image): <a
+ id="linkWithTitleNameFromImg" href="http://www.heise.de/"
+ title="Link with title"><img src="" alt="The title for link"/></a
+ ><br><br>Map that is used to group links (www.w3.org/TR/WCAG10-HTML-TECHS/#group-bypass), also see the bug 431615:<br
+ ><map id="linksmap" title="Site navigation"><ul
+ ><li><a href="http://mozilla.org">About the project</a></li
+ ><li><a href="http://mozilla.org">Sites and sounds</a></li
+ ></ul
+ ></map
+></body>
+</html>
diff --git a/accessible/tests/mochitest/hyperlink/test_general.xhtml b/accessible/tests/mochitest/hyperlink/test_general.xhtml
new file mode 100644
index 0000000000..b006e58ef4
--- /dev/null
+++ b/accessible/tests/mochitest/hyperlink/test_general.xhtml
@@ -0,0 +1,98 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="test for nsIAccessibleHyperLink interface on XUL:label elements">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript"
+ src="hyperlink.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function testThis(aID, aAcc, aRole, aAnchorCount, aAnchorName, aURI,
+ aStartIndex, aEndIndex, aValid)
+ {
+ testRole(aID, aRole);
+ is(aAcc.anchorCount, aAnchorCount, "Wrong number of anchors for ID "
+ + aID + "!");
+ is(aAcc.getAnchor(0).name, aAnchorName, "Wrong name for ID " + aID + "!");
+ is(aAcc.getURI(0).spec, aURI, "URI wrong for ID " + aID + "!");
+ is(aAcc.startIndex, aStartIndex, "Wrong startIndex value for ID " + aID
+ + "!");
+ is(aAcc.endIndex, aEndIndex, "Wrong endIndex value for ID " + aID + "!");
+ is(aAcc.valid, aValid, "Wrong valid state for ID " + aID + "!");
+ }
+
+ var gQueue = null;
+ function doTest()
+ {
+ var linkedLabelAcc = getAccessible("linkedLabel",
+ [nsIAccessibleHyperLink]);
+ testThis("linkedLabel", linkedLabelAcc, ROLE_LINK, 1,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "Mozilla Foundation home", "http://www.mozilla.org/", 1, 2,
+ true);
+ testStates(linkedLabelAcc, STATE_LINKED, 0);
+
+ var labelWithValueAcc = getAccessible("linkLabelWithValue",
+ [nsIAccessibleHyperLink]);
+ testThis("linkLabelWithValue", labelWithValueAcc, ROLE_LINK, 1,
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "Mozilla Foundation", "http://www.mozilla.org/", 2, 3, true,
+ false, true);
+ testStates(labelWithValueAcc, STATE_LINKED, 0);
+
+ var normalLabelAcc = getAccessible("normalLabel");
+ testRole(normalLabelAcc, ROLE_LABEL);
+ is(normalLabelAcc.name, "This label should not be a link",
+ "Wrong name for normal label!");
+ testStates(normalLabelAcc, 0, 0, (STATE_FOCUSABLE | STATE_LINKED));
+
+ //////////////////////////////////////////////////////////////////////////
+ // Test focus
+
+ gQueue = new eventQueue();
+
+ gQueue.push(new focusLink("linkedLabel", true));
+ gQueue.push(new focusLink("linkLabelWithValue", true));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=421066"
+ title="Implement Mochitests for the nsIAccessibleHyperLink interface on XUL:label elements">
+ Mozilla Bug 421066
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <label id="linkedLabel" href="http://www.mozilla.org/" is="text-link">
+ Mozilla Foundation home</label>
+ <label id="linkLabelWithValue" value="Mozilla Foundation" is="text-link"
+ href="http://www.mozilla.org/" />
+ <label id="normalLabel" value="This label should not be a link" />
+</window>
diff --git a/accessible/tests/mochitest/hypertext/a11y.toml b/accessible/tests/mochitest/hypertext/a11y.toml
new file mode 100644
index 0000000000..04370669ae
--- /dev/null
+++ b/accessible/tests/mochitest/hypertext/a11y.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+support-files = [
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/letters.gif"]
+
+["test_general.html"]
+
+["test_update.html"]
diff --git a/accessible/tests/mochitest/hypertext/test_general.html b/accessible/tests/mochitest/hypertext/test_general.html
new file mode 100644
index 0000000000..80ac293ee1
--- /dev/null
+++ b/accessible/tests/mochitest/hypertext/test_general.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=428248
+-->
+<head>
+ <title>nsIHyper>TextAccessible chrome tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ var gParagraphAcc;
+
+ function testLinkIndexAtOffset(aID, aOffset, aIndex) {
+ var htAcc = getAccessible(aID, [nsIAccessibleHyperText]);
+ is(htAcc.getLinkIndexAtOffset(aOffset), aIndex,
+ "Wrong link index at offset " + aOffset + " for ID " + aID + "!");
+ }
+
+ function testThis(aID, aCharIndex, aExpectedLinkIndex, aName) {
+ testLinkIndexAtOffset(gParagraphAcc, aCharIndex, aExpectedLinkIndex);
+
+ var linkAcc = gParagraphAcc.getLinkAt(aExpectedLinkIndex);
+ ok(linkAcc, "No accessible for link " + aID + "!");
+
+ var linkIndex = gParagraphAcc.getLinkIndex(linkAcc);
+ is(linkIndex, aExpectedLinkIndex, "Wrong link index for " + aID + "!");
+
+ // Just test the link's name to make sure we get the right one.
+ is(linkAcc.getAnchor(0).name, aName, "Wrong name for " + aID + "!");
+ }
+
+ // gA11yEventDumpToConsole = true;
+ function doPreTest() {
+ waitForImageMap("imgmap", doTest);
+ }
+
+ function doTest() {
+ // Test link count
+ gParagraphAcc = getAccessible("testParagraph", [nsIAccessibleHyperText]);
+ is(gParagraphAcc.linkCount, 7, "Wrong link count for paragraph!");
+
+ // normal hyperlink
+ testThis("NormalHyperlink", 14, 0, "Mozilla Foundation");
+
+ // ARIA hyperlink
+ testThis("AriaHyperlink", 27, 1, "Mozilla Foundation Home");
+
+ // ARIA hyperlink with status invalid
+ testThis("InvalidAriaHyperlink", 63, 2, "Invalid link");
+
+ // image map, but not its link children. They are not part of hypertext.
+ testThis("imgmap", 76, 3, "b");
+
+ // empty hyperlink
+ testThis("emptyLink", 90, 4, null);
+
+ // normal hyperlink with embedded span
+ testThis("LinkWithSpan", 116, 5, "Heise Online");
+
+ // Named anchor
+ testThis("namedAnchor", 193, 6, null);
+
+ // Paragraph with link
+ var p2 = getAccessible("p2", [nsIAccessibleHyperText]);
+ var link = p2.getLinkAt(0);
+ is(link, p2.getChildAt(0), "Wrong link for p2");
+ is(p2.linkCount, 1, "Wrong link count for p2");
+
+ // getLinkIndexAtOffset, causes the offsets to be cached;
+ testLinkIndexAtOffset("p4", 0, 0); // 1st 'mozilla' link
+ testLinkIndexAtOffset("p4", 1, 1); // 2nd 'mozilla' link
+ testLinkIndexAtOffset("p4", 2, -1); // ' ' of ' te' text node
+ testLinkIndexAtOffset("p4", 3, -1); // 't' of ' te' text node
+ testLinkIndexAtOffset("p4", 5, -1); // 'x' of 'xt ' text node
+ testLinkIndexAtOffset("p4", 7, -1); // ' ' of 'xt ' text node
+ testLinkIndexAtOffset("p4", 8, 2); // 3d 'mozilla' link
+ testLinkIndexAtOffset("p4", 9, 2); // the end, latest link
+
+ // the second pass to make sure link indexes are calculated propertly from
+ // cached offsets.
+ testLinkIndexAtOffset("p4", 0, 0); // 1st 'mozilla' link
+ testLinkIndexAtOffset("p4", 1, 1); // 2nd 'mozilla' link
+ testLinkIndexAtOffset("p4", 2, -1); // ' ' of ' te' text node
+ testLinkIndexAtOffset("p4", 3, -1); // 't' of ' te' text node
+ testLinkIndexAtOffset("p4", 5, -1); // 'x' of 'xt ' text node
+ testLinkIndexAtOffset("p4", 7, -1); // ' ' of 'xt ' text node
+ testLinkIndexAtOffset("p4", 8, 2); // 3d 'mozilla' link
+ testLinkIndexAtOffset("p4", 9, 2); // the end, latest link
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doPreTest);
+ </script>
+
+</head>
+<body>
+
+ <a target="_blank"
+ title="Create tests for NSIAccessibleHyperlink interface"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=418368">
+ Mozilla Bug 418368
+ </a><br>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <p id="testParagraph"><br
+ >Simple link:<br
+ ><a id="NormalHyperlink" href="http://www.mozilla.org">Mozilla Foundation</a><br
+ >ARIA link:<br
+ ><span id="AriaHyperlink" role="link"
+ onclick="window.open('http://www.mozilla.org/');"
+ tabindex="0">Mozilla Foundation Home</span><br
+ >Invalid, non-focusable hyperlink:<br
+ ><span id="InvalidAriaHyperlink" role="link" aria-invalid="true"
+ onclick="window.open('http:/www.mozilla.org/');">Invalid link</span><br
+ >Image map:<br
+ ><map name="atoz_map"><area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b"
+ coords="17,0,30,14"
+ alt="b"
+ shape="rect"></area
+ ><area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a"
+ coords="0,0,13,14"
+ alt="a"
+ shape="rect"></area></map
+ ><img width="447" id="imgmap"
+ height="15"
+ usemap="#atoz_map"
+ src="../letters.gif"></img><br
+ >Empty link:<br
+ ><a id="emptyLink" href=""><img src=""></img></a><br
+ >Link with embedded span<br
+ ><a id="LinkWithSpan" href="http://www.heise.de/"><span lang="de">Heise Online</span></a><br
+ >Named anchor, must not have "linked" state for it to be exposed correctly:<br
+ ><a id="namedAnchor" name="named_anchor">This should never be of state_linked</a>
+ </p>
+ <p id="p2"><a href="http://mozilla.org">mozilla.org</a></p>
+ <p id="p4"><a href="www">mozilla</a><a href="www">mozilla</a><span> te</span><span>xt </span><a href="www">mozilla</a></p>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/hypertext/test_update.html b/accessible/tests/mochitest/hypertext/test_update.html
new file mode 100644
index 0000000000..f3407bea64
--- /dev/null
+++ b/accessible/tests/mochitest/hypertext/test_update.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>nsIHyper>TextAccessible in dynamic tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ const kLinksCount = 128;
+ function addLinks(aContainerID) {
+ this.containerNode = getNode(aContainerID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.invoke = function addLinks_invoke() {
+ for (var jdx = 0; jdx < kLinksCount; jdx++) {
+ var a = document.createElement("a");
+ a.setAttribute("href", "mozilla.org");
+ a.textContent = "mozilla";
+ this.containerNode.appendChild(a);
+
+ var span = document.createElement("span");
+ span.textContent = " text ";
+ this.containerNode.appendChild(span);
+ }
+ };
+
+ this.finalCheck = function addLinks_finalCheck() {
+ // getLinkAt and getLinkIndex.
+ var htAcc = getAccessible(this.containerNode, [nsIAccessibleHyperText]);
+ for (var jdx = 0; jdx < kLinksCount; jdx++) {
+ var link = htAcc.getLinkAt(jdx);
+ ok(link, "No link at index " + jdx + " for '" + aContainerID + "'");
+
+ var linkIdx = htAcc.getLinkIndex(link);
+ is(linkIdx, jdx, "Wrong link index for '" + aContainerID + "'!");
+ }
+ };
+
+ this.getID = function addLinks_getID() {
+ return "Add links for '" + aContainerID + "'";
+ };
+ }
+
+ function updateText(aContainerID) {
+ this.containerNode = getNode(aContainerID);
+ this.container = getAccessible(this.containerNode, nsIAccessibleHyperText);
+ this.text = this.container.firstChild;
+ this.textNode = this.text.DOMNode;
+ this.textLen = this.textNode.data.length;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_TEXT_INSERTED, this.containerNode),
+ ];
+
+ this.invoke = function updateText_invoke() {
+ is(this.container.getLinkIndexAtOffset(this.textLen), 0,
+ "Wrong intial text offsets!");
+
+ this.text.DOMNode.appendData(" my");
+ };
+
+ this.finalCheck = function updateText_finalCheck() {
+ is(this.container.getLinkIndexAtOffset(this.textLen), -1,
+ "Text offsets weren't updated!");
+ };
+
+ this.getID = function updateText_getID() {
+ return "update text for '" + aContainerID + "'";
+ };
+ }
+
+ /**
+ * Text offsets must be updated when hypertext child is removed.
+ */
+ function removeChild(aContainerID, aChildID, aInitialText, aFinalText) {
+ this.containerNode = getNode(aContainerID);
+ this.container = getAccessible(this.containerNode, nsIAccessibleText);
+ this.childNode = getNode(aChildID);
+
+ // Call first to getText so offsets are cached
+ is(this.container.getText(0, -1), aInitialText,
+ "Wrong text before child removal");
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.invoke = function removeChild_invoke() {
+ this.containerNode.removeChild(this.childNode);
+ };
+
+ this.finalCheck = function removeChild_finalCheck() {
+ is(this.container.getText(0, -1), aFinalText,
+ "Wrong text after child removal");
+ is(this.container.characterCount, aFinalText.length,
+ "Wrong text after child removal");
+ };
+
+ this.getID = function removeChild_getID() {
+ return "check text after removing child from '" + aContainerID + "'";
+ };
+ }
+
+ function removeFirstChild(aContainer) {
+ this.ht = getAccessible(aContainer, [ nsIAccessibleHyperText ]);
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, aContainer),
+ ];
+
+ this.invoke = function removeFirstChild_invoke() {
+ is(this.ht.linkCount, 2, "Wrong embedded objects count before removal");
+
+ getNode(aContainer).removeChild(getNode(aContainer).firstElementChild);
+ };
+
+ this.finalCheck = function removeFirstChild_finalCheck() {
+ // check list index before link count
+ is(this.ht.getLinkIndex(this.ht.firstChild), 0, "Wrong child index");
+ is(this.ht.linkCount, 1, "Wrong embedded objects count after removal");
+ };
+
+ this.getID = function removeFirstChild_getID() {
+ return "Remove first child and check embedded object indeces";
+ };
+ }
+
+ function removeLastChild(aContainer) {
+ this.ht = getAccessible(aContainer, [ nsIAccessibleHyperText ]);
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, aContainer),
+ ];
+
+ this.invoke = function removeLastChild_invoke() {
+ is(this.ht.linkCount, 1, "Wrong embedded objects count before removal");
+
+ getNode(aContainer).removeChild(getNode(aContainer).lastElementChild);
+ };
+
+ this.finalCheck = function removeLastChild_finalCheck() {
+ is(this.ht.linkCount, 0, "Wrong embedded objects count after removal");
+
+ var link = null;
+ try {
+ link = this.ht.getLinkAt(0);
+ } catch (e) { }
+ ok(!link, "No embedded object is expected");
+ };
+
+ this.getID = function removeLastChild_getID() {
+ return "Remove last child and try its embedded object";
+ };
+ }
+
+ // gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTest() {
+ gQueue = new eventQueue();
+ gQueue.push(new addLinks("p1"));
+ gQueue.push(new updateText("p2"));
+ gQueue.push(new removeChild("div1", "div2",
+ "hello my good friend", "hello friend"));
+ gQueue.push(new removeFirstChild("c4"));
+ gQueue.push(new removeLastChild("c5"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Cache links within hypertext accessible"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=572394">
+ Mozilla Bug 572394
+ </a>
+ <a target="_blank"
+ title="Text offsets don't get updated when text of first child text accessible is changed"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=625009">
+ Mozilla Bug 625009
+ </a>
+ <a target="_blank"
+ title="Crash in nsHyperTextAccessible::GetText()"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=630841">
+ Mozilla Bug 630841
+ </a><br>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <p id="p1"></p>
+ <p id="p2"><b>hello</b><a>friend</a></p>
+ <div id="div1">hello<span id="div2"> my<span id="div3"> good</span></span> friend</span></div>
+ <form id="c4">
+ <label for="c4_input">label</label>
+ <input id="c4_input">
+ </form>
+ <div id="c5">TextLeaf<input id="c5_input"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/layout.js b/accessible/tests/mochitest/layout.js
new file mode 100644
index 0000000000..1467d5fe7f
--- /dev/null
+++ b/accessible/tests/mochitest/layout.js
@@ -0,0 +1,390 @@
+/* import-globals-from common.js */
+
+/**
+ * Tests if the given child and grand child accessibles at the given point are
+ * expected.
+ *
+ * @param aID [in] accessible identifier
+ * @param aX [in] x coordinate of the point relative accessible
+ * @param aY [in] y coordinate of the point relative accessible
+ * @param aChildID [in] expected child accessible
+ * @param aGrandChildID [in] expected child accessible
+ */
+function testChildAtPoint(aID, aX, aY, aChildID, aGrandChildID) {
+ var child = getChildAtPoint(aID, aX, aY, false);
+ var expectedChild = getAccessible(aChildID);
+
+ var msg =
+ "Wrong direct child accessible at the point (" +
+ aX +
+ ", " +
+ aY +
+ ") of " +
+ prettyName(aID);
+ isObject(child, expectedChild, msg);
+
+ var grandChild = getChildAtPoint(aID, aX, aY, true);
+ var expectedGrandChild = getAccessible(aGrandChildID);
+
+ msg =
+ "Wrong deepest child accessible at the point (" +
+ aX +
+ ", " +
+ aY +
+ ") of " +
+ prettyName(aID);
+ isObject(grandChild, expectedGrandChild, msg);
+}
+
+/**
+ * Test if getChildAtPoint returns the given child and grand child accessibles
+ * at coordinates of child accessible (direct and deep hit test).
+ */
+function hitTest(aContainerID, aChildID, aGrandChildID) {
+ var container = getAccessible(aContainerID);
+ var child = getAccessible(aChildID);
+ var grandChild = getAccessible(aGrandChildID);
+
+ var [x, y] = getBoundsForDOMElm(child);
+
+ var actualChild = container.getChildAtPoint(x + 1, y + 1);
+ isObject(
+ actualChild,
+ child,
+ "Wrong direct child of " + prettyName(aContainerID)
+ );
+
+ var actualGrandChild = container.getDeepestChildAtPoint(x + 1, y + 1);
+ isObject(
+ actualGrandChild,
+ grandChild,
+ "Wrong deepest child of " + prettyName(aContainerID)
+ );
+}
+
+/**
+ * Test if getOffsetAtPoint returns the given text offset at given coordinates.
+ */
+function testOffsetAtPoint(aHyperTextID, aX, aY, aCoordType, aExpectedOffset) {
+ var hyperText = getAccessible(aHyperTextID, [nsIAccessibleText]);
+ var offset = hyperText.getOffsetAtPoint(aX, aY, aCoordType);
+ is(
+ offset,
+ aExpectedOffset,
+ "Wrong offset at given point (" +
+ aX +
+ ", " +
+ aY +
+ ") for " +
+ prettyName(aHyperTextID)
+ );
+}
+
+/**
+ * Zoom the given document.
+ */
+function zoomDocument(aDocument, aZoom) {
+ SpecialPowers.setFullZoom(aDocument.defaultView, aZoom);
+}
+
+/**
+ * Set the relative resolution of this document. This is what apz does.
+ * On non-mobile platforms you won't see a visible change.
+ */
+function setResolution(aDocument, aZoom) {
+ var windowUtils = aDocument.defaultView.windowUtils;
+
+ windowUtils.setResolutionAndScaleTo(aZoom);
+}
+
+/**
+ * Return child accessible at the given point.
+ *
+ * @param aIdentifier [in] accessible identifier
+ * @param aX [in] x coordinate of the point relative accessible
+ * @param aY [in] y coordinate of the point relative accessible
+ * @param aFindDeepestChild [in] points whether deepest or nearest child should
+ * be returned
+ * @return the child accessible at the given point
+ */
+function getChildAtPoint(aIdentifier, aX, aY, aFindDeepestChild) {
+ var acc = getAccessible(aIdentifier);
+ if (!acc) {
+ return null;
+ }
+
+ var [screenX, screenY] = getBoundsForDOMElm(acc.DOMNode);
+
+ var x = screenX + aX;
+ var y = screenY + aY;
+
+ try {
+ if (aFindDeepestChild) {
+ return acc.getDeepestChildAtPoint(x, y);
+ }
+ return acc.getChildAtPoint(x, y);
+ } catch (e) {}
+
+ return null;
+}
+
+/**
+ * Test the accessible position.
+ */
+function testPos(aID, aPoint) {
+ var [expectedX, expectedY] =
+ aPoint != undefined ? aPoint : getBoundsForDOMElm(aID);
+
+ var [x, y] = getBounds(aID);
+ is(x, expectedX, "Wrong x coordinate of " + prettyName(aID));
+ is(y, expectedY, "Wrong y coordinate of " + prettyName(aID));
+}
+
+/**
+ * Test the accessible boundaries.
+ */
+function testBounds(aID, aRect) {
+ var [expectedX, expectedY, expectedWidth, expectedHeight] =
+ aRect != undefined ? aRect : getBoundsForDOMElm(aID);
+
+ var [x, y, width, height] = getBounds(aID);
+ is(x, expectedX, "Wrong x coordinate of " + prettyName(aID));
+ is(y, expectedY, "Wrong y coordinate of " + prettyName(aID));
+ is(width, expectedWidth, "Wrong width of " + prettyName(aID));
+ is(height, expectedHeight, "Wrong height of " + prettyName(aID));
+}
+
+/**
+ * Test text position at the given offset.
+ */
+function testTextPos(aID, aOffset, aPoint, aCoordOrigin) {
+ var [expectedX, expectedY] = aPoint;
+
+ var xObj = {},
+ yObj = {};
+ var hyperText = getAccessible(aID, [nsIAccessibleText]);
+ hyperText.getCharacterExtents(aOffset, xObj, yObj, {}, {}, aCoordOrigin);
+ is(
+ xObj.value,
+ expectedX,
+ "Wrong x coordinate at offset " + aOffset + " for " + prettyName(aID)
+ );
+ ok(
+ yObj.value - expectedY <= 2 && expectedY - yObj.value <= 2,
+ "Wrong y coordinate at offset " +
+ aOffset +
+ " for " +
+ prettyName(aID) +
+ " - got " +
+ yObj.value +
+ ", expected " +
+ expectedY +
+ "The difference doesn't exceed 1."
+ );
+}
+
+/**
+ * Test text bounds that is enclosed betwene the given offsets.
+ */
+function testTextBounds(aID, aStartOffset, aEndOffset, aRect, aCoordOrigin) {
+ var [expectedX, expectedY, expectedWidth, expectedHeight] = aRect;
+
+ var xObj = {},
+ yObj = {},
+ widthObj = {},
+ heightObj = {};
+ var hyperText = getAccessible(aID, [nsIAccessibleText]);
+ hyperText.getRangeExtents(
+ aStartOffset,
+ aEndOffset,
+ xObj,
+ yObj,
+ widthObj,
+ heightObj,
+ aCoordOrigin
+ );
+
+ // x
+ isWithin(
+ expectedX,
+ xObj.value,
+ 1,
+ "Wrong x coordinate of text between offsets (" +
+ aStartOffset +
+ ", " +
+ aEndOffset +
+ ") for " +
+ prettyName(aID)
+ );
+
+ // y
+ isWithin(
+ expectedY,
+ yObj.value,
+ 1,
+ `y coord of text between offsets (${aStartOffset}, ${aEndOffset}) ` +
+ `for ${prettyName(aID)}`
+ );
+
+ // Width
+ var msg =
+ "Wrong width of text between offsets (" +
+ aStartOffset +
+ ", " +
+ aEndOffset +
+ ") for " +
+ prettyName(aID) +
+ " - Got " +
+ widthObj.value +
+ " Expected " +
+ expectedWidth;
+ if (!WIN) {
+ isWithin(expectedWidth, widthObj.value, 1, msg);
+ } else {
+ // fails on some windows machines
+ todo(false, msg);
+ }
+
+ // Height
+ isWithin(
+ expectedHeight,
+ heightObj.value,
+ 1,
+ `height of text between offsets (${aStartOffset}, ${aEndOffset}) ` +
+ `for ${prettyName(aID)}`
+ );
+}
+
+/**
+ * Return the accessible coordinates relative to the screen in device pixels.
+ */
+function getPos(aID) {
+ var accessible = getAccessible(aID);
+ var x = {},
+ y = {};
+ accessible.getBounds(x, y, {}, {});
+ return [x.value, y.value];
+}
+
+/**
+ * Return the accessible coordinates and size relative to the screen in device
+ * pixels. This methods also retrieves coordinates in CSS pixels and ensures that they
+ * match Dev pixels with a given device pixel ratio.
+ */
+function getBounds(aID, aDPR = window.devicePixelRatio) {
+ const accessible = getAccessible(aID);
+ let x = {},
+ y = {},
+ width = {},
+ height = {};
+ let xInCSS = {},
+ yInCSS = {},
+ widthInCSS = {},
+ heightInCSS = {};
+ accessible.getBounds(x, y, width, height);
+ accessible.getBoundsInCSSPixels(xInCSS, yInCSS, widthInCSS, heightInCSS);
+
+ info(`DPR is: ${aDPR}`);
+ isWithin(
+ xInCSS.value,
+ x.value / aDPR,
+ 1,
+ "X in CSS pixels is calculated correctly"
+ );
+ isWithin(
+ yInCSS.value,
+ y.value / aDPR,
+ 1,
+ "Y in CSS pixels is calculated correctly"
+ );
+ isWithin(
+ widthInCSS.value,
+ width.value / aDPR,
+ 1,
+ "Width in CSS pixels is calculated correctly"
+ );
+ isWithin(
+ heightInCSS.value,
+ height.value / aDPR,
+ 1,
+ "Height in CSS pixels is calculated correctly"
+ );
+
+ return [x.value, y.value, width.value, height.value];
+}
+
+function getRangeExtents(aID, aStartOffset, aEndOffset, aCoordOrigin) {
+ var hyperText = getAccessible(aID, [nsIAccessibleText]);
+ var x = {},
+ y = {},
+ width = {},
+ height = {};
+ hyperText.getRangeExtents(
+ aStartOffset,
+ aEndOffset,
+ x,
+ y,
+ width,
+ height,
+ aCoordOrigin
+ );
+ return [x.value, y.value, width.value, height.value];
+}
+
+/**
+ * Return DOM node coordinates relative the screen and its size in device
+ * pixels.
+ */
+function getBoundsForDOMElm(aID) {
+ var x = 0,
+ y = 0,
+ width = 0,
+ height = 0;
+
+ var elm = getNode(aID);
+ if (elm.localName == "area") {
+ var mapName = elm.parentNode.getAttribute("name");
+ var selector = "[usemap='#" + mapName + "']";
+ var img = elm.ownerDocument.querySelector(selector);
+
+ var areaCoords = elm.coords.split(",");
+ var areaX = parseInt(areaCoords[0]);
+ var areaY = parseInt(areaCoords[1]);
+ var areaWidth = parseInt(areaCoords[2]) - areaX;
+ var areaHeight = parseInt(areaCoords[3]) - areaY;
+
+ let rect = img.getBoundingClientRect();
+ x = rect.left + areaX;
+ y = rect.top + areaY;
+ width = areaWidth;
+ height = areaHeight;
+ } else {
+ let rect = elm.getBoundingClientRect();
+ x = rect.left;
+ y = rect.top;
+ width = rect.width;
+ height = rect.height;
+ }
+
+ var elmWindow = elm.ownerGlobal;
+ return CSSToDevicePixels(
+ elmWindow,
+ x + elmWindow.mozInnerScreenX,
+ y + elmWindow.mozInnerScreenY,
+ width,
+ height
+ );
+}
+
+function CSSToDevicePixels(aWindow, aX, aY, aWidth, aHeight) {
+ var ratio = aWindow.devicePixelRatio;
+
+ // CSS pixels and ratio can be not integer. Device pixels are always integer.
+ // Do our best and hope it works.
+ return [
+ Math.round(aX * ratio),
+ Math.round(aY * ratio),
+ Math.round(aWidth * ratio),
+ Math.round(aHeight * ratio),
+ ];
+}
diff --git a/accessible/tests/mochitest/letters.gif b/accessible/tests/mochitest/letters.gif
new file mode 100644
index 0000000000..299b91784a
--- /dev/null
+++ b/accessible/tests/mochitest/letters.gif
Binary files differ
diff --git a/accessible/tests/mochitest/longdesc_src.html b/accessible/tests/mochitest/longdesc_src.html
new file mode 100644
index 0000000000..37248795dd
--- /dev/null
+++ b/accessible/tests/mochitest/longdesc_src.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>Mozilla logo</title>
+</head>
+<body>
+<p>This file would contain a longer description of the Mozilla logo, if I knew what it looked like.</p>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/moz.build b/accessible/tests/mochitest/moz.build
new file mode 100644
index 0000000000..fa8e1b3cdd
--- /dev/null
+++ b/accessible/tests/mochitest/moz.build
@@ -0,0 +1,35 @@
+# -*- 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/.
+
+A11Y_MANIFESTS += [
+ "a11y.toml",
+ "actions/a11y.toml",
+ "aom/a11y.toml",
+ "attributes/a11y.toml",
+ "bounds/a11y.toml",
+ "elm/a11y.toml",
+ "events/a11y.toml",
+ "events/docload/a11y.toml",
+ "focus/a11y.toml",
+ "hittest/a11y.toml",
+ "hyperlink/a11y.toml",
+ "hypertext/a11y.toml",
+ "name/a11y.toml",
+ "relations/a11y.toml",
+ "role/a11y.toml",
+ "scroll/a11y.toml",
+ "selectable/a11y.toml",
+ "states/a11y.toml",
+ "table/a11y.toml",
+ "text/a11y.toml",
+ "textattrs/a11y.toml",
+ "textcaret/a11y.toml",
+ "textrange/a11y.toml",
+ "textselection/a11y.toml",
+ "tree/a11y.toml",
+ "treeupdate/a11y.toml",
+ "value/a11y.toml",
+]
diff --git a/accessible/tests/mochitest/moz.png b/accessible/tests/mochitest/moz.png
new file mode 100644
index 0000000000..743292dc6f
--- /dev/null
+++ b/accessible/tests/mochitest/moz.png
Binary files differ
diff --git a/accessible/tests/mochitest/name.js b/accessible/tests/mochitest/name.js
new file mode 100644
index 0000000000..48bf2d7038
--- /dev/null
+++ b/accessible/tests/mochitest/name.js
@@ -0,0 +1,38 @@
+/* import-globals-from common.js */
+
+/**
+ * Test accessible name for the given accessible identifier.
+ */
+function testName(aAccOrElmOrID, aName, aMsg, aTodo) {
+ var msg = aMsg ? aMsg : "";
+
+ var acc = getAccessible(aAccOrElmOrID);
+ if (!acc) {
+ return "";
+ }
+
+ var func = aTodo ? todo_is : is;
+ var txtID = prettyName(aAccOrElmOrID);
+ try {
+ func(acc.name, aName, msg + "Wrong name of the accessible for " + txtID);
+ } catch (e) {
+ ok(false, msg + "Can't get name of the accessible for " + txtID);
+ }
+ return acc;
+}
+
+/**
+ * Test accessible description for the given accessible.
+ */
+function testDescr(aAccOrElmOrID, aDescr) {
+ var acc = getAccessible(aAccOrElmOrID);
+ if (!acc) {
+ return;
+ }
+
+ is(
+ acc.description,
+ aDescr,
+ "Wrong description for " + prettyName(aAccOrElmOrID)
+ );
+}
diff --git a/accessible/tests/mochitest/name/a11y.toml b/accessible/tests/mochitest/name/a11y.toml
new file mode 100644
index 0000000000..8faf1b1c45
--- /dev/null
+++ b/accessible/tests/mochitest/name/a11y.toml
@@ -0,0 +1,29 @@
+[DEFAULT]
+support-files = [
+ "markup.js",
+ "markuprules.xml",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/moz.png"]
+
+["test_ARIACore_examples.html"]
+
+["test_browserui.xhtml"]
+
+["test_counterstyle.html"]
+
+["test_general.html"]
+
+["test_general.xhtml"]
+
+["test_link.html"]
+
+["test_list.html"]
+
+["test_markup.html"]
+skip-if = [
+ "win11_2009 && debug", # Bug 1296784
+]
+
+["test_svg.html"]
+
+["test_tree.xhtml"]
diff --git a/accessible/tests/mochitest/name/markup.js b/accessible/tests/mochitest/name/markup.js
new file mode 100644
index 0000000000..a267bd4f7b
--- /dev/null
+++ b/accessible/tests/mochitest/name/markup.js
@@ -0,0 +1,425 @@
+/* import-globals-from ../attributes.js */
+/* import-globals-from ../common.js */
+/* import-globals-from ../events.js */
+/* import-globals-from ../name.js */
+
+// //////////////////////////////////////////////////////////////////////////////
+// Name tests described by "markuprules.xml" file.
+
+var gNameRulesFileURL = "markuprules.xml";
+
+var gRuleDoc = null;
+
+// Debuggin stuff.
+var gDumpToConsole = false;
+
+/**
+ * Start name tests. Run through markup elements and test names for test
+ * element (see namerules.xml for details).
+ */
+function testNames() {
+ // enableLogging("tree,stack"); // debugging
+
+ var request = new XMLHttpRequest();
+ request.open("get", gNameRulesFileURL, false);
+ request.send();
+
+ gRuleDoc = request.responseXML;
+
+ var markupElms = evaluateXPath(gRuleDoc, "//rules/rulesample/markup");
+ gTestIterator.iterateMarkups(markupElms);
+}
+
+// //////////////////////////////////////////////////////////////////////////////
+// Private section.
+
+/**
+ * Helper class to interate through name tests.
+ */
+var gTestIterator = {
+ iterateMarkups: function gTestIterator_iterateMarkups(aMarkupElms) {
+ this.markupElms = aMarkupElms;
+
+ this.iterateNext();
+ },
+
+ iterateRules: function gTestIterator_iterateRules(
+ aElm,
+ aContainer,
+ aRuleSetElm,
+ aRuleElms,
+ aTestID
+ ) {
+ this.ruleSetElm = aRuleSetElm;
+ this.ruleElms = aRuleElms;
+ this.elm = aElm;
+ this.container = aContainer;
+ this.testID = aTestID;
+
+ this.iterateNext();
+ },
+
+ iterateNext: function gTestIterator_iterateNext() {
+ if (this.markupIdx == -1) {
+ this.markupIdx++;
+ testNamesForMarkup(this.markupElms[this.markupIdx]);
+ return;
+ }
+
+ this.ruleIdx++;
+ if (this.ruleIdx == this.ruleElms.length) {
+ // When test is finished then name is empty and no explict-name.
+ var defaultName = this.ruleSetElm.hasAttribute("defaultName")
+ ? this.ruleSetElm.getAttribute("defaultName")
+ : null;
+ testName(
+ this.elm,
+ defaultName,
+ "Default name test (" + gTestIterator.testID + "). "
+ );
+ testAbsentAttrs(this.elm, { "explicit-name": "true" });
+
+ this.markupIdx++;
+ if (this.markupIdx == this.markupElms.length) {
+ // disableLogging("tree"); // debugging
+ SimpleTest.finish();
+ return;
+ }
+
+ this.ruleIdx = -1;
+
+ if (gDumpToConsole) {
+ dump(
+ "\nPend next markup processing. Wait for reorder event on " +
+ prettyName(document) +
+ "'\n"
+ );
+ }
+ waitForEvent(
+ EVENT_REORDER,
+ document,
+ testNamesForMarkup,
+ null,
+ this.markupElms[this.markupIdx]
+ );
+
+ document.body.removeChild(this.container);
+ return;
+ }
+
+ testNameForRule(this.elm, this.ruleElms[this.ruleIdx]);
+ },
+
+ markupElms: null,
+ markupIdx: -1,
+ rulesetElm: null,
+ ruleElms: null,
+ ruleIdx: -1,
+ elm: null,
+ container: null,
+ testID: "",
+};
+
+/**
+ * Process every 'markup' element and test names for it. Used by testNames
+ * function.
+ */
+function testNamesForMarkup(aMarkupElm) {
+ if (gDumpToConsole) {
+ dump("\nProcessing markup '" + aMarkupElm.getAttribute("id") + "'\n");
+ }
+
+ var div = document.createElement("div");
+ div.setAttribute("id", "test");
+
+ var child = aMarkupElm.firstChild;
+ while (child) {
+ var newChild = document.importNode(child, true);
+ div.appendChild(newChild);
+ child = child.nextSibling;
+ }
+
+ if (gDumpToConsole) {
+ dump(
+ "\nProcessing markup. Wait for reorder event on " +
+ prettyName(document) +
+ "'\n"
+ );
+ }
+ waitForEvent(
+ EVENT_REORDER,
+ document,
+ testNamesForMarkupRules,
+ null,
+ aMarkupElm,
+ div
+ );
+
+ document.body.appendChild(div);
+}
+
+function testNamesForMarkupRules(aMarkupElm, aContainer) {
+ var testID = aMarkupElm.getAttribute("id");
+ if (gDumpToConsole) {
+ dump("\nProcessing markup rules '" + testID + "'\n");
+ }
+
+ var expr = "//html/body/div[@id='test']/" + aMarkupElm.getAttribute("ref");
+ var elm = evaluateXPath(document, expr, htmlDocResolver)[0];
+
+ var ruleId = aMarkupElm.getAttribute("ruleset");
+ var ruleElm = gRuleDoc.querySelector("[id='" + ruleId + "']");
+ var ruleElms = getRuleElmsByRulesetId(ruleId);
+
+ var processMarkupRules = gTestIterator.iterateRules.bind(
+ gTestIterator,
+ elm,
+ aContainer,
+ ruleElm,
+ ruleElms,
+ testID
+ );
+
+ // Images may be recreated after we append them into subtree. We need to wait
+ // in this case. If we are on profiling enabled build then stack tracing
+ // works and thus let's log instead. Note, that works if you enabled logging
+ // (refer to testNames() function).
+ if (isAccessible(elm) || isLogged("stack")) {
+ processMarkupRules();
+ } else {
+ waitForEvent(EVENT_SHOW, elm, processMarkupRules);
+ }
+}
+
+/**
+ * Test name for current rule and current 'markup' element. Used by
+ * testNamesForMarkup function.
+ */
+function testNameForRule(aElm, aRuleElm) {
+ if (aRuleElm.hasAttribute("attr")) {
+ if (gDumpToConsole) {
+ dump(
+ "\nProcessing rule { attr: " + aRuleElm.getAttribute("attr") + " }\n"
+ );
+ }
+
+ testNameForAttrRule(aElm, aRuleElm);
+ } else if (aRuleElm.hasAttribute("elm")) {
+ if (gDumpToConsole) {
+ dump(
+ "\nProcessing rule { elm: " +
+ aRuleElm.getAttribute("elm") +
+ ", elmattr: " +
+ aRuleElm.getAttribute("elmattr") +
+ " }\n"
+ );
+ }
+
+ testNameForElmRule(aElm, aRuleElm);
+ } else if (aRuleElm.getAttribute("fromsubtree") == "true") {
+ if (gDumpToConsole) {
+ dump(
+ "\nProcessing rule { fromsubtree: " +
+ aRuleElm.getAttribute("fromsubtree") +
+ " }\n"
+ );
+ }
+
+ testNameForSubtreeRule(aElm, aRuleElm);
+ }
+}
+
+function testNameForAttrRule(aElm, aRule) {
+ var name = "";
+
+ var attr = aRule.getAttribute("attr");
+ var attrValue = aElm.getAttribute(attr);
+
+ var type = aRule.getAttribute("type");
+ if (type == "string") {
+ name = attrValue;
+ } else if (type == "ref" && attrValue) {
+ var ids = attrValue.split(/\s+/);
+ for (var idx = 0; idx < ids.length; idx++) {
+ var labelElm = getNode(ids[idx]);
+ if (name != "") {
+ name += " ";
+ }
+
+ name += labelElm.getAttribute("textequiv");
+ }
+ }
+
+ var msg = "Attribute '" + attr + "' test (" + gTestIterator.testID + "). ";
+ testName(aElm, name, msg);
+
+ if (aRule.getAttribute("explict-name") != "false") {
+ testAttrs(aElm, { "explicit-name": "true" }, true);
+ } else {
+ testAbsentAttrs(aElm, { "explicit-name": "true" });
+ }
+
+ waitForEvent(
+ EVENT_NAME_CHANGE,
+ aElm,
+ gTestIterator.iterateNext,
+ gTestIterator
+ );
+
+ aElm.removeAttribute(attr);
+}
+
+function testNameForElmRule(aElm, aRule) {
+ var labelElm;
+
+ var tagname = aRule.getAttribute("elm");
+ var attrname = aRule.getAttribute("elmattr");
+ if (attrname) {
+ var filter = {
+ acceptNode: function filter_acceptNode(aNode) {
+ if (
+ aNode.localName == this.mLocalName &&
+ aNode.getAttribute(this.mAttrName) == this.mAttrValue
+ ) {
+ return NodeFilter.FILTER_ACCEPT;
+ }
+
+ return NodeFilter.FILTER_SKIP;
+ },
+
+ mLocalName: tagname,
+ mAttrName: attrname,
+ mAttrValue: aElm.getAttribute("id"),
+ };
+
+ var treeWalker = document.createTreeWalker(
+ document.body,
+ NodeFilter.SHOW_ELEMENT,
+ filter
+ );
+ labelElm = treeWalker.nextNode();
+ } else {
+ // if attrname is empty then look for the element in subtree.
+ labelElm = aElm.getElementsByTagName(tagname)[0];
+ if (!labelElm) {
+ labelElm = aElm.getElementsByTagName("html:" + tagname)[0];
+ }
+ }
+
+ if (!labelElm) {
+ ok(false, msg + " Failed to find '" + tagname + "' element.");
+ gTestIterator.iterateNext();
+ return;
+ }
+
+ var msg = "Element '" + tagname + "' test (" + gTestIterator.testID + ").";
+ testName(aElm, labelElm.getAttribute("textequiv"), msg);
+ testAttrs(aElm, { "explicit-name": "true" }, true);
+
+ var parentNode = labelElm.parentNode;
+
+ if (gDumpToConsole) {
+ dump(
+ "\nProcessed elm rule. Wait for name change event on " +
+ prettyName(aElm) +
+ "\n"
+ );
+ }
+ waitForEvent(
+ EVENT_NAME_CHANGE,
+ aElm,
+ gTestIterator.iterateNext,
+ gTestIterator
+ );
+
+ parentNode.removeChild(labelElm);
+}
+
+function testNameForSubtreeRule(aElm, aRule) {
+ var msg = "From subtree test (" + gTestIterator.testID + ").";
+ testName(aElm, aElm.getAttribute("textequiv"), msg);
+ testAbsentAttrs(aElm, { "explicit-name": "true" });
+
+ if (gDumpToConsole) {
+ dump(
+ "\nProcessed from subtree rule. Wait for reorder event on " +
+ prettyName(aElm) +
+ "\n"
+ );
+ }
+ waitForEvent(
+ EVENT_NAME_CHANGE,
+ aElm,
+ gTestIterator.iterateNext,
+ gTestIterator
+ );
+
+ while (aElm.firstChild) {
+ aElm.firstChild.remove();
+ }
+}
+
+/**
+ * Return array of 'rule' elements. Used in conjunction with
+ * getRuleElmsFromRulesetElm() function.
+ */
+function getRuleElmsByRulesetId(aRulesetId) {
+ var expr = "//rules/ruledfn/ruleset[@id='" + aRulesetId + "']";
+ var rulesetElm = evaluateXPath(gRuleDoc, expr);
+ return getRuleElmsFromRulesetElm(rulesetElm[0]);
+}
+
+function getRuleElmsFromRulesetElm(aRulesetElm) {
+ var rulesetId = aRulesetElm.getAttribute("ref");
+ if (rulesetId) {
+ return getRuleElmsByRulesetId(rulesetId);
+ }
+
+ var ruleElms = [];
+
+ var child = aRulesetElm.firstChild;
+ while (child) {
+ if (child.localName == "ruleset") {
+ ruleElms = ruleElms.concat(getRuleElmsFromRulesetElm(child));
+ }
+ if (child.localName == "rule") {
+ ruleElms.push(child);
+ }
+
+ child = child.nextSibling;
+ }
+
+ return ruleElms;
+}
+
+/**
+ * Helper method to evaluate xpath expression.
+ */
+function evaluateXPath(aNode, aExpr, aResolver) {
+ var xpe = new XPathEvaluator();
+
+ var resolver = aResolver;
+ if (!resolver) {
+ var node =
+ aNode.ownerDocument == null
+ ? aNode.documentElement
+ : aNode.ownerDocument.documentElement;
+ resolver = xpe.createNSResolver(node);
+ }
+
+ var result = xpe.evaluate(aExpr, aNode, resolver, 0, null);
+ var found = [];
+ var res;
+ while ((res = result.iterateNext())) {
+ found.push(res);
+ }
+
+ return found;
+}
+
+function htmlDocResolver(aPrefix) {
+ var ns = {
+ html: "http://www.w3.org/1999/xhtml",
+ };
+ return ns[aPrefix] || null;
+}
diff --git a/accessible/tests/mochitest/name/markuprules.xml b/accessible/tests/mochitest/name/markuprules.xml
new file mode 100644
index 0000000000..045a25b437
--- /dev/null
+++ b/accessible/tests/mochitest/name/markuprules.xml
@@ -0,0 +1,367 @@
+<?xml version="1.0"?>
+
+<!--
+ This XML file is used to create sequence of accessible name tests. It consist
+ of two sections. The first section 'ruledfn' declares name computation rules.
+ The second section 'rulesample' defines markup samples we need to check name
+ computation rules for.
+
+ <ruledfn>
+ <ruleset>
+ <rule>
+
+ Section 'ruledfn' contains 'ruleset' elements. Every 'ruleset' element is
+ presented by 'rule' elements so that sequence of 'rule' elements gives the
+ sequence of name computations rules. Every 'rule' element can be one of four
+ types.
+
+ * <rule attr='' type='string'/> used when name is equal to the value of
+ attribute presented on the element.
+
+ Example, 'aria-label' attribute. In this case 'rule' element has 'attr'
+ attribute pointing to attribute name and 'type' attribute with 'string'
+ value. For example, <rule attr="aria-label" type="string"/>.
+
+ * <rule attr='' type='ref'/> used when name is calculated from elements that
+ are pointed to by attribute value on the element.
+
+ Example is 'aria-labelledby'. In this case 'rule' element has 'attr'
+ attribute holding the sequence of IDs of elements used to compute the name,
+ in addition the 'rule' element has 'type' attribute with 'ref' value.
+ For example, <rule attr="aria-labelledby" type="ref"/>.
+
+ * <rule elm='' elmattr=''/> used when name is calculated from another
+ element. These attributes are used to find an element by tagname and
+ attribute with value equaled to ID of the element. If 'elmattr' is missed
+ then element from subtree with the given tagname is used.
+
+ Example, html:label@for element, <rule elm="label" elmattr="for"/>.
+ Example, html:caption element, <rule elm="caption"/>
+
+ * <rule fromsubtree='true'/> used when name is computed from subtree.
+
+ Example, html:button. In this case 'rule' element has 'fromsubtree'
+ attribute with 'true' value.
+
+ <rulesample>
+ <markup ruleset=''>
+
+ Section 'rulesample' provides set of markup samples ('markup' elements). Every
+ 'markup' element contains an element that accessible name will be computed for
+ (let's call it test element). In addition the 'markup' element contains some
+ other elements from native markup used in name calculation process for test
+ element. Test element is pointed to by 'ref' attribute on 'markup' element.
+ Also 'markup' element has 'ruleset' attribute to indicate ruleset for the test
+ element.
+
+ How does it work? Let's consider simple example:
+ <ruledfn>
+ <ruleset id="aria">
+ <rule attr="aria-label" type="string"/>
+ <rule attr="aria-labelledby" type="ref"/>
+ </ruleset>
+ </ruledfn>
+ <rulesample>
+ <markup ref="html:div" ruleset="aria">
+ <html:span id="label" textequiv="test2">test2</html:span>
+ <html:div aria-label="test1"
+ aria-labelledby="label">it's a div</html:div>
+ </markup>
+ </rulesample>
+
+ Initially 'markup' element holds markup for all rules specified by 'ruleset'
+ attribute. This allows us to check if the sequence of name computation rules
+ is correct. Here 'ruleset' element defines two rules. We get the first rule
+ which means accessible name is computed from value of 'aria-label' attribute.
+ Then we check accessible name for the test element and remove 'aria-label'
+ attribute. After we get the second rule which means we should get IDs from
+ 'aria-labelledby' attribute and compose accessible name from values of
+ 'textequiv' attributes (that are supposed to give the desired name for each
+ element that is being pointed to by aria-labelledby). Check accessible name
+ and finish test.
+-->
+
+<rules xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <ruledfn>
+
+ <!-- bricks -->
+ <ruleset id="ARIA">
+ <rule attr="aria-labelledby" type="ref"/>
+ <rule attr="aria-label" type="string"/>
+ </ruleset>
+
+ <ruleset id="HTMLControl:Head">
+ <ruleset ref="ARIA"/>
+ <rule elm="label" elmattr="for"/>
+ </ruleset>
+
+ <!-- general -->
+ <ruleset id="HTMLControl">
+ <ruleset ref="HTMLControl:Head"/>
+ <rule fromsubtree="true"/>
+ <rule attr="title" type="string"/>
+ </ruleset>
+
+ <ruleset id="HTMLElm">
+ <ruleset ref="ARIA"/>
+ <rule attr="title" type="string"/>
+ </ruleset>
+
+ <!-- specific -->
+ <ruleset id="HTMLARIAGridCell">
+ <ruleset ref="ARIA"/>
+ <rule fromsubtree="true"/>
+ <rule attr="title" type="string"/>
+ </ruleset>
+
+ <ruleset id="HTMLInputButton">
+ <ruleset ref="HTMLControl:Head"/>
+ <rule attr="value" type="string" explict-name="false" reordered="true"/>
+ <rule attr="title" type="string"/>
+ </ruleset>
+
+ <ruleset id="HTMLInputSubmit" defaultName="Submit Query">
+ <ruleset ref="HTMLControl:Head"/>
+ <rule attr="value" type="string" explict-name="false"/>
+ </ruleset>
+
+ <ruleset id="HTMLInputReset" defaultName="Reset">
+ <ruleset ref="HTMLControl:Head"/>
+ <rule attr="value" type="string" explict-name="false"/>
+ </ruleset>
+
+ <ruleset id="HTMLInputImage">
+ <ruleset ref="HTMLControl:Head"/>
+ <rule attr="alt" type="string"/>
+ <rule attr="value" type="string"/>
+ <rule attr="title" type="string"/>
+ </ruleset>
+
+ <ruleset id="HTMLInputImageNoValidSrc" defaultName="Submit Query">
+ <ruleset ref="HTMLControl:Head"/>
+ <rule attr="alt" type="string" explict-name="false"/>
+ <rule attr="value" type="string" explict-name="false"/>
+ </ruleset>
+
+ <ruleset id="HTMLOption">
+ <ruleset ref="ARIA"/>
+ <rule attr="label" type="string"/>
+ <rule fromsubtree="true"/>
+ <rule attr="title" type="string"/>
+ </ruleset>
+
+ <ruleset id="HTMLImg">
+ <ruleset ref="ARIA"/>
+ <rule attr="alt" type="string"/>
+ <rule attr="title" type="string"/>
+ </ruleset>
+
+ <ruleset id="HTMLTable">
+ <ruleset ref="ARIA"/>
+ <rule elm="caption"/>
+ <rule attr="summary" type="string"/>
+ <rule attr="title" type="string"/>
+ </ruleset>
+ </ruledfn>
+
+ <rulesample>
+
+ <markup id="HTMLButtonTest"
+ ref="html:button" ruleset="HTMLControl">
+ <html:span id="l1" textequiv="test2">test2</html:span>
+ <html:span id="l2" textequiv="test3">test3</html:span>
+ <html:label for="btn" textequiv="test4">test4</html:label>
+ <html:button id="btn"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ title="test5"
+ textequiv="press me">press me</html:button>
+ </markup>
+
+ <markup id="HTMLInputButtonTest"
+ ref="html:input" ruleset="HTMLInputButton">
+ <html:span id="l1" textequiv="test2">test2</html:span>
+ <html:span id="l2" textequiv="test3">test3</html:span>
+ <html:label for="btn" textequiv="test4">test4</html:label>
+ <html:input id="btn"
+ type="button"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ value="name from value"
+ alt="no name from al"
+ src="no name from src"
+ data="no name from data"
+ title="name from title"/>
+ </markup>
+
+ <markup id="HTMLInputSubmitTest"
+ ref="html:input" ruleset="HTMLInputSubmit">
+ <html:span id="l1" textequiv="test2">test2</html:span>
+ <html:span id="l2" textequiv="test3">test3</html:span>
+ <html:label for="btn-submit" textequiv="test4">test4</html:label>
+ <html:input id="btn-submit"
+ type="submit"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ value="name from value"
+ alt="no name from atl"
+ src="no name from src"
+ data="no name from data"
+ title="no name from title"/>
+ </markup>
+
+ <markup id="HTMLInputResetTest"
+ ref="html:input" ruleset="HTMLInputReset">
+ <html:span id="l1" textequiv="test2">test2</html:span>
+ <html:span id="l2" textequiv="test3">test3</html:span>
+ <html:label for="btn-reset" textequiv="test4">test4</html:label>
+ <html:input id="btn-reset"
+ type="reset"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ value="name from value"
+ alt="no name from alt"
+ src="no name from src"
+ data="no name from data"
+ title="no name from title"/>
+ </markup>
+
+ <!--
+ Disabled due to intermittent failures (bug 1436323) which became more
+ frequent due to the landing of bug 1383682. The latter bug made loading
+ of images from cache much more consistent, which appears to have impacted
+ the timing for this test case. If the image is switched to a unique
+ image (e.g. always decoding since there is no cache), the failure rate
+ increases, presumably because the test is dependent on a specific ordering
+ of events, and implicitly assumes the image is loaded immediately.
+ -->
+
+ <!--
+ <markup id="HTMLInputImageTest"
+ ref="html:input" ruleset="HTMLInputImage">
+ <html:span id="l1" textequiv="test2">test2</html:span>
+ <html:span id="l2" textequiv="test3">test3</html:span>
+ <html:label for="btn-image" textequiv="test4">test4</html:label>
+ <html:input id="btn-image"
+ type="image"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ alt="name from alt"
+ value="name from value"
+ src="../moz.png"
+ data="no name from data"
+ title="name from title"/>
+ </markup>
+ -->
+
+ <markup id="HTMLInputImageNoValidSrcTest"
+ ref="html:input" ruleset="HTMLInputImageNoValidSrc">
+ <html:span id="l1" textequiv="test2">test2</html:span>
+ <html:span id="l2" textequiv="test3">test3</html:span>
+ <html:label for="btn-image" textequiv="test4">test4</html:label>
+ <html:input id="btn-image"
+ type="image"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ alt="name from alt"
+ value="name from value"
+ data="no name from data"
+ title="no name from title"/>
+ </markup>
+
+ <markup id="HTMLOptionTest"
+ ref="html:select/html:option[1]" ruleset="HTMLOption">
+ <html:span id="l1" textequiv="test2">test2</html:span>
+ <html:span id="l2" textequiv="test3">test3</html:span>
+ <html:select>
+ <html:option id="opt"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ label="test4"
+ title="test5"
+ textequiv="option1">option1</html:option>
+ <html:option>option2</html:option>
+ </html:select>
+ </markup>
+
+ <markup id="HTMLImageTest"
+ ref="html:img" ruleset="HTMLImg">
+ <html:span id="l1" textequiv="test2">test2</html:span>
+ <html:span id="l2" textequiv="test3">test3</html:span>
+ <html:img id="img"
+ aria-label="Logo of Mozilla"
+ aria-labelledby="l1 l2"
+ alt="Mozilla logo"
+ title="This is a logo"
+ src="../moz.png"/>
+ </markup>
+
+ <markup id="HTMLTdTest"
+ ref="html:table/html:tr/html:td" ruleset="HTMLElm">
+ <html:span id="l1" textequiv="test2">test2</html:span>
+ <html:span id="l2" textequiv="test3">test3</html:span>
+ <html:label for="tc" textequiv="test4">test4</html:label>
+ <html:table>
+ <html:tr>
+ <html:td id="tc"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ title="test5">
+ <html:p>This is a paragraph</html:p>
+ <html:a href="#">This is a link</html:a>
+ <html:ul>
+ <html:li>This is a list</html:li>
+ </html:ul>
+ </html:td>
+ </html:tr>
+ </html:table>
+ </markup>
+
+ <markup id="HTMLTdARIAGridCellTest"
+ ref="html:table/html:tr/html:td" ruleset="HTMLARIAGridCell">
+ <html:span id="l1" textequiv="test2">test2</html:span>
+ <html:span id="l2" textequiv="test3">test3</html:span>
+ <html:label for="gc" textequiv="test4">test4</html:label>
+ <html:table>
+ <html:tr>
+ <html:td id="gc"
+ role="gridcell"
+ aria-label="test1"
+ aria-labelledby="l1 l2"
+ textequiv="This is a paragraph This is a link • Listitem1 • Listitem2"
+ title="This is a paragraph This is a link This is a list">
+ <html:p>This is a paragraph</html:p>
+ <html:a href="#">This is a link</html:a>
+ <html:ul>
+ <html:li>Listitem1</html:li>
+ <html:li>Listitem2</html:li>
+ </html:ul>
+ </html:td>
+ </html:tr>
+ </html:table>
+ </markup>
+
+ <markup id="HTMLTableTest"
+ ref="html:table" ruleset="HTMLTable">
+ <html:span id="l1" textequiv="lby_tst6_1">lby_tst6_1</html:span>
+ <html:span id="l2" textequiv="lby_tst6_2">lby_tst6_2</html:span>
+ <html:label for="t" textequiv="label_tst6">label_tst6</html:label>
+ <!-- layout frame are recreated due to varous reasons, here's text frame
+ placed after caption frame triggres table frame recreation when
+ caption element is removed from DOM; get rid text node after caption
+ node to make the test working -->
+ <html:table id="t" aria-label="arialabel_tst6"
+ aria-labelledby="l1 l2"
+ summary="summary_tst6"
+ title="title_tst6">
+ <html:caption textequiv="caption_tst6">caption_tst6</html:caption><html:tr>
+ <html:td>cell1</html:td>
+ <html:td>cell2</html:td>
+ </html:tr>
+ </html:table>
+ </markup>
+
+ </rulesample>
+</rules>
diff --git a/accessible/tests/mochitest/name/test_ARIACore_examples.html b/accessible/tests/mochitest/name/test_ARIACore_examples.html
new file mode 100644
index 0000000000..a15fee78f8
--- /dev/null
+++ b/accessible/tests/mochitest/name/test_ARIACore_examples.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Testing a11y name ccomputation testcases</title>
+
+ <link rel="stylesheet"
+ type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // All test cases taken from https://www.w3.org/TR/accname-1.1/
+ // These were especially called out to demonstrate edge cases.
+
+ // Example 1 from section 4.3.1 under 2.B.
+ // Element1 should get its name from the text in element3.
+ // Element2 should not gets name from element1 because that already
+ // gets its name from another element.
+ testName("el1", "hello");
+ testName("el2", null);
+
+ // Example 2 from section 4.3.1 under 2.C.
+ // The buttons should get their name from their labels and the links.
+ testName("del_row1", "Delete Documentation.pdf");
+ testName("del_row2", "Delete HolidayLetter.pdf");
+
+ // Example 3 from section 4.3.1 under 2.F.
+ // Name should be own content text plus the value of the input plus
+ // more own inner text, separated by 1 space.
+ testName("chkbx", "Flash the screen 5 times");
+
+ // Example 4 from section 4.3.1 under 2.F.
+ // Name from content should include all the child nodes, including
+ // table cells.
+ testName("input_with_html_label", "foo bar baz");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- el1 should be labeled, el2 should not. -->
+ <div id="el1" aria-labelledby="el3"></div>
+ <div id="el2" aria-labelledby="el1"></div>
+ <div id="el3"> hello </div>
+
+ <!-- The buttons should be labeled by themselves and the referenced link -->
+ <ul>
+ <li>
+ <a id="file_row1" href="./files/Documentation.pdf">Documentation.pdf</a>
+ <span role="button" tabindex="0" id="del_row1" aria-label="Delete"
+ aria-labelledby="del_row1 file_row1"></span>
+ </li>
+ <li>
+ <a id="file_row2" href="./files/HolidayLetter.pdf">HolidayLetter.pdf</a>
+ <span role="button" tabindex="0" id="del_row2" aria-label="Delete"
+ aria-labelledby="del_row2 file_row2"></span>
+ </li>
+ </ul>
+
+ <!-- Label from combined text and subtree -->
+ <div id="chkbx" role="checkbox" aria-checked="false">Flash the screen
+ <span role="textbox" aria-multiline="false"> 5 </span> times</div>
+
+ <!-- Label with name from content should include table -->
+ <input id="input_with_html_label" />
+ <label for="input_with_html_label" id="label">
+ <div>foo</div>
+ <table><tr><td>bar</td></tr></table>
+ <div>baz</div>
+ </label>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/name/test_browserui.xhtml b/accessible/tests/mochitest/name/test_browserui.xhtml
new file mode 100644
index 0000000000..d7834eb3c0
--- /dev/null
+++ b/accessible/tests/mochitest/name/test_browserui.xhtml
@@ -0,0 +1,85 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessibility Name Calculating Test.">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+ const { BrowserTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/BrowserTestUtils.sys.mjs");
+ const ABOUT_MOZILLA_URL = "about:mozilla";
+ const ABOUT_LICENSE_URL = "about:license";
+
+ SimpleTest.waitForExplicitFinish();
+
+ (async () => {
+ info("Opening a new browser window.");
+ const win = await BrowserTestUtils.openNewBrowserWindow({
+ remote: false,
+ fission: false,
+ });
+ const winFocused = SimpleTest.promiseFocus(win);
+ const loaded = BrowserTestUtils.browserLoaded(
+ win.gBrowser.selectedBrowser);
+ let docLoaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, event =>
+ event.accessible.QueryInterface(nsIAccessibleDocument).URL === ABOUT_LICENSE_URL,
+ `Loaded tab: ${ABOUT_LICENSE_URL}`);
+ BrowserTestUtils.startLoadingURIString(win.gBrowser.selectedBrowser,
+ "about:license");
+ await loaded;
+ await docLoaded;
+ await winFocused;
+
+ info(`Loading a new tab: ${ABOUT_MOZILLA_URL}.`);
+ docLoaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, event =>
+ event.accessible.QueryInterface(nsIAccessibleDocument).URL === ABOUT_MOZILLA_URL,
+ `Added tab: ${ABOUT_MOZILLA_URL}`);
+ const tab = win.gBrowser.addTrustedTab(ABOUT_MOZILLA_URL);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await docLoaded;
+
+ info("Focusing on the newly opened tab.");
+ const focused = waitForEvent(EVENT_FOCUS, event =>
+ event.DOMNode === win.gBrowser.getBrowserAtIndex(1).contentDocument);
+ await BrowserTestUtils.synthesizeKey("VK_TAB", { ctrlKey: true },
+ win.gBrowser.selectedBrowser);
+ const focusEvent = await focused;
+
+ const title = getAccessible(win.document).name;
+ const accName = focusEvent.accessible.name;
+ isnot(title.indexOf(accName), -1,
+ `Window title contains the name of active tab document (Is "${accName}" in "${title}"?)`);
+
+ await BrowserTestUtils.closeWindow(win);
+ SimpleTest.finish();
+ })();
+ ]]>
+ </script>
+
+ <vbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=507382"
+ title="focus is fired earlier than root accessible name is changed when switching between tabs">
+ Mozilla Bug
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="eventdump"></vbox>
+ </vbox>
+</window>
diff --git a/accessible/tests/mochitest/name/test_counterstyle.html b/accessible/tests/mochitest/name/test_counterstyle.html
new file mode 100644
index 0000000000..a20aca23f1
--- /dev/null
+++ b/accessible/tests/mochitest/name/test_counterstyle.html
@@ -0,0 +1,150 @@
+<html>
+
+<head>
+ <title>nsIAccessible::name calculation for @counter-style</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+
+ <style id="counterstyles" type="text/css">
+ @counter-style system-alphabetic {
+ system: alphabetic;
+ symbols: x y z;
+ }
+ @counter-style system-cyclic {
+ system: cyclic;
+ symbols: x y z;
+ }
+ @counter-style system-numeric {
+ system: numeric;
+ symbols: x y z;
+ }
+ @counter-style speak-as-bullets {
+ system: extends decimal;
+ speak-as: bullets;
+ }
+ @counter-style speak-as-numbers {
+ system: extends system-alphabetic;
+ speak-as: numbers;
+ }
+ @counter-style speak-as-words {
+ system: additive;
+ additive-symbols: 20 "twenty ", 9 "nine", 7 "seven", 1 "one";
+ speak-as: words;
+ }
+ @counter-style speak-as-spell-out {
+ system: extends system-alphabetic;
+ speak-as: spell-out;
+ }
+ @counter-style speak-as-other {
+ system: extends decimal;
+ speak-as: speak-as-words;
+ }
+ @counter-style speak-as-loop {
+ system: extends upper-latin;
+ speak-as: speak-as-loop0;
+ }
+ @counter-style speak-as-loop0 {
+ system: extends disc;
+ speak-as: speak-as-loop1;
+ }
+ @counter-style speak-as-loop1 {
+ system: extends decimal;
+ speak-as: speak-as-loop0;
+ }
+ @counter-style speak-as-extended0 {
+ system: extends decimal;
+ speak-as: speak-as-extended1;
+ }
+ @counter-style speak-as-extended1 {
+ system: extends speak-as-extended0;
+ speak-as: disc;
+ }
+ @counter-style speak-as-extended2 {
+ system: extends decimal;
+ speak-as: speak-as-extended3;
+ }
+ @counter-style speak-as-extended3 {
+ system: extends speak-as-extended2;
+ }
+ </style>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ function testRule(aRule, aNames, aTodo) {
+ testName(aRule + "-1", aNames[0], null, aTodo);
+ testName(aRule + "-7", aNames[1], null, aTodo);
+ testName(aRule + "-29", aNames[2], null, aTodo);
+ }
+
+ var spellOutNames = ["X. 1", "Y X. 7", "Y Z Y. 29"];
+ var bulletsNames = [kDiscBulletText + "1",
+ kDiscBulletText + "7",
+ kDiscBulletText + "29"];
+ var numbersNames = ["1. 1", "7. 7", "29. 29"];
+ var wordsNames = ["one. 1", "seven. 7", "twenty nine. 29"];
+
+ testRule("system-alphabetic", spellOutNames, true); // bug 1024178
+ testRule("system-cyclic", bulletsNames);
+ testRule("system-numeric", numbersNames);
+
+ testRule("speak-as-bullets", bulletsNames);
+ testRule("speak-as-numbers", numbersNames);
+ testRule("speak-as-words", wordsNames);
+ testRule("speak-as-spell-out", spellOutNames, true); // bug 1024178
+ testRule("speak-as-other", wordsNames);
+
+ testRule("speak-as-loop", bulletsNames);
+ testRule("speak-as-loop0", bulletsNames);
+ testRule("speak-as-loop1", numbersNames);
+
+ testRule("speak-as-extended0", bulletsNames);
+ testRule("speak-as-extended1", bulletsNames);
+ testRule("speak-as-extended2", numbersNames);
+ testRule("speak-as-extended3", numbersNames);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=966166"
+ title="Bug 966166 - Implement @counter-style rule">
+ Bug 966166
+ </a>
+
+ <ol id="list"></ol>
+
+ <script type="application/javascript">
+ var list = getNode("list");
+ var rules = getNode("counterstyles").sheet.cssRules;
+ var values = [1, 7, 29];
+ for (var i = 0; i < rules.length; i++) {
+ var rule = rules[i];
+ for (var j = 0; j < values.length; j++) {
+ var item = document.createElement("li");
+ item.id = rule.name + "-" + values[j];
+ item.value = values[j];
+ item.textContent = values[j];
+ item.setAttribute("style", "list-style-type: " + rule.name);
+ list.appendChild(item);
+ }
+ }
+ </script>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/name/test_general.html b/accessible/tests/mochitest/name/test_general.html
new file mode 100644
index 0000000000..09723e6222
--- /dev/null
+++ b/accessible/tests/mochitest/name/test_general.html
@@ -0,0 +1,780 @@
+<html>
+
+<head>
+ <title>nsIAccessible::name calculation</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ // aria-label
+
+ // Simple label provided via ARIA
+ testName("btn_simple_aria_label", "I am a button");
+
+ // aria-label and aria-labelledby, expect aria-labelledby
+ testName("btn_both_aria_labels", "text I am a button, two");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // aria-labelledby
+
+ // Single relation. The value of 'aria-labelledby' contains the ID of
+ // an element. Gets the name from text node of that element.
+ testName("btn_labelledby_text", "text");
+
+ // Multiple relations. The value of 'aria-labelledby' contains the IDs
+ // of elements. Gets the name from text nodes of those elements.
+ testName("btn_labelledby_texts", "text1 text2");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Name from named accessible
+
+ testName("input_labelledby_namedacc", "Data");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Name from subtree (single relation labelled_by).
+
+ // Gets the name from text nodes contained by nested elements
+ testName("btn_labelledby_mixed", "nomore text");
+
+ // Gets the name from text nodes contained by nested elements, ignores
+ // hidden elements (bug 443081).
+ testName("btn_labelledby_mixed_hidden_child", "nomore text2");
+
+ // Gets the name from hidden text nodes contained by nested elements,
+ // (label element is hidden entirely), (bug 443081).
+ testName("btn_labelledby_mixed_hidden", "lala more hidden text");
+
+ // Gets the name from text nodes contained by nested elements having block
+ // representation (every text node value in the name should be devided by
+ // spaces)
+ testName("btn_labelledby_mixed_block", "text more text");
+
+ // Gets the name from text nodes contained by html:td. The text nodes
+ // should not be divided by spaces.
+ testName("btn_labelledby_mixed_table", "textTEXTtext");
+
+ // Gets the name from image accessible.
+ testName("btn_labelledby_mixed_img", "text image");
+
+ // Gets the name from input accessibles
+ // Note: if input have label elements then the name isn't calculated
+ // from them.
+ testName("btn_labelledby_mixed_input",
+ "input button Submit Query Reset Submit Query");
+
+ // Gets the name from the title of object element.
+ testName("btn_labelledby_mixed_object", "object");
+
+ // Gets the name from text nodes. Element br adds space between them.
+ testName("btn_labelledby_mixed_br", "text text");
+
+ // Gets the name from label content which allows name from subtree,
+ // ignore @title attribute on label
+ testName("from_label_ignoretitle", "Country:");
+
+ // Gets the name from html:p content, which doesn't allow name from
+ // subtree, ignore @title attribute on label
+ testName("from_p_ignoretitle", "Choose country from.");
+
+ // Gets the name from html:input value, ignore @title attribute on input
+ testName("from_input_ignoretitle", "Custom country");
+
+ // Insert spaces around the control's value to not jamm sibling text nodes
+ testName("insert_spaces_around_control", "start value end");
+
+ // Gets the name from @title, ignore whitespace content
+ testName("from_label_ignore_ws_subtree", "about");
+
+ // role="alert" doesn't get name from subtree...
+ testName("alert", null);
+ // but the subtree is used if referenced by aria-labelledby.
+ testName("inputLabelledByAlert", "Error");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // label element
+
+ // The label element contains the button. The name is calculated from
+ // this button.
+ // Note: the name contains the content of the button.
+ testName("btn_label_inside", "text10text");
+
+ // The label element and the button are placed in the same form. Gets
+ // the name from the label subtree.
+ testName("btn_label_inform", "in form");
+
+ // The label element is placed outside of form where the button is.
+ // Take into account the label.
+ testName("btn_label_outform", "out form");
+
+ // The label element and the button are in the same document. Gets the
+ // name from the label subtree.
+ testName("btn_label_indocument", "in document");
+
+ // Multiple label elements for single button
+ testName("btn_label_multi", "label1label2");
+
+ // Multiple controls inside a label element
+ testName("ctrl_in_label_1", "Enable a button control");
+ testName("ctrl_in_label_2", "button");
+
+
+ // ////////////////////////////////////////////////////////////////////////
+ // name from children
+
+ // ARIA role button is presented allowing the name calculation from
+ // children.
+ testName("btn_children", "14");
+
+ // html:button, no name from content
+ testName("btn_nonamefromcontent", null);
+
+ // ARIA role option is presented allowing the name calculation from
+ // visible children (bug 443081).
+ testName("lb_opt1_children_hidden", "i am visible");
+
+ // Get the name from subtree of menuitem crossing role nothing to get
+ // the name from its children.
+ testName("tablemenuitem", "menuitem 1");
+
+ // Get the name from child acronym title attribute rather than from
+ // acronym content.
+ testName("label_with_acronym", "O A T F World Wide Web");
+
+ testName("testArticle", "Test article");
+
+ testName("h1", "heading");
+ testName("aria_heading", "aria_heading");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // title attribute
+
+ // If nothing is left. Let's try title attribute.
+ testName("btn_title", "title");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // textarea name
+
+ // textarea's name should have the value, which initially is specified by
+ // a text child.
+ testName("textareawithchild", "Story Foo is ended.");
+
+ // new textarea name should reflect the value change.
+ var elem = document.getElementById("textareawithchild");
+ elem.value = "Bar";
+
+ testName("textareawithchild", "Story Bar is ended.");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // controls having a value used as a part of computed name
+
+ testName("ctrlvalue_progressbar:input", "foo 5 baz");
+ testName("ctrlvalue_scrollbar:input", "foo 5 baz");
+ testName("ctrlvalue_slider:input", "foo 5 baz");
+ testName("ctrlvalue_spinbutton:input", "foo 5 baz");
+ testName("ctrlvalue_combobox:input", "foo 5 baz");
+ testName("ctrlvalue_meter:input", "foo 5 baz");
+
+
+ // ///////////////////////////////////////////////////////////////////////
+ // label with nested combobox (test for 'f' item of name computation guide)
+
+ testName("comboinstart", "One day(s).");
+ testName("combo3", "day(s).");
+
+ testName("textboxinstart", "Two days.");
+ testName("textbox1", "days.");
+
+ testName("comboinmiddle", "Subscribe to ATOM feed.");
+ testName("combo4", "Subscribe to ATOM feed.");
+
+ testName("comboinmiddle2", "Play the Haliluya sound when new mail arrives");
+ testName("combo5", null); // label isn't used as a name for control
+ testName("checkbox", "Play the Haliluya sound when new mail arrives");
+ testName("comboinmiddle3", "Play the Haliluya sound when new mail arrives");
+ testName("combo6", "Play the Haliluya sound when new mail arrives");
+
+ testName("comboinend", "This day was sunny");
+ testName("combo7", "This day was");
+
+ testName("textboxinend", "This day was sunny");
+ testName("textbox2", "This day was");
+
+ // placeholder
+ testName("ph_password", "a placeholder");
+ testName("ph_text", "a placeholder");
+ testName("ph_textarea", "a placeholder");
+ testName("ph_text2", "a label");
+ testName("ph_textarea2", "a label");
+ testName("ph_text3", "a label");
+
+ // Test equation image
+ testName("img_eq", "x^2 + y^2 + z^2");
+ testName("input_img_eq", "x^2 + y^2 + z^2");
+ testName("txt_eq", "x^2 + y^2 + z^2");
+
+ // //////////////////////////////////////////////////////////////////////
+ // tests for duplicate announcement of content
+
+ testName("test_note", null);
+
+ // //////////////////////////////////////////////////////////////////////
+ // Tests for name from sub tree of tr element.
+
+ // By default, we want no name.
+ testName("NoNameForTR", null);
+ testName("NoNameForNonStandardTR", null);
+
+ // But we want it if the tr has an ARIA role.
+ testName("NameForTRBecauseStrongARIA", "a b");
+
+ // But not if it is a weak (landmark) ARIA role
+ testName("NoNameForTRBecauseWeakARIA", null);
+
+ // Name from sub tree of grouping if requested by other accessible.
+ testName("grouping", null);
+ testName("requested_name_from_grouping", "label");
+ testName("listitem_containing_block_tbody", "label");
+ // Groupings shouldn't be included when calculating from the subtree of
+ // a treeitem.
+ testName("treeitem_containing_grouping", "root");
+
+ // Name from subtree of grouping labelled by an ancestor.
+ testName("grouping_labelledby_ancestor", "label");
+
+ // Name from subtree of a container containing text nodes and inline
+ // elements. There should be no spaces because everyhing is inline.
+ testName("container_text_inline", "abc");
+ // Name from subtree of a container containing text nodes and block
+ // elements. There should be a space on both sides of the block.
+ testName("container_text_block", "a b c");
+ // Name from subtree of a container containing text nodes and empty
+ // block elements. There should be space on either side of the blocks, but
+ // not a double space.
+ testName("container_text_emptyblock", "a b");
+
+ const shadowRoot = getNode("shadowHost").shadowRoot;
+ const shadowButtonVisibleText = shadowRoot.getElementById("shadowButtonVisibleText");
+ testName(shadowButtonVisibleText, "shadowButtonVisibleText");
+ const shadowButtonHiddenText = shadowRoot.getElementById("shadowButtonHiddenText");
+ testName(shadowButtonHiddenText, "shadowButtonHiddenText");
+
+ // Label from a hidden element containing script and style elements.
+ testName("buttonScriptStyle", "content");
+
+ // Name from subtree on a link containing <code>, etc.
+ testName("linkWithCodeSupSubInsDel", "before code sup sub ins del after");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=428479"
+ title="Bug 428479 - Support ARIA role=math">
+ Bug 428479
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=429666"
+ title="Expose ROLE_DOCUMENT for ARIA landmarks that inherit from document">
+ Bug 429666
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=444279"
+ title="mochitest for accessible name calculating">
+ Bug 444279
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=459635"
+ title="nsIAccessible::name calculation for HTML buttons">
+ Bug 459635
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=530081"
+ title="Clean up our tree walker">
+ Bug 530081
+ </a><br>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=604391"
+ title="Use placeholder as name if name is otherwise empty">
+ Bug 604391
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=669312"
+ title="Accessible name is duplicated when input has a label associated uisng for/id and is wrapped around the input">
+ Bug 669312
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=704416"
+ title="HTML acronym and abbr names should be provided by @title">
+ Bug 704416
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=812041"
+ title="ARIA slider and spinbutton don't provide a value for name computation">
+ Bug 812041
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=823927"
+ title="Text is jammed with control's text in name computation">
+ Bug 823927
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=835666"
+ title="ARIA combobox selected value is not a part of name computation">
+ Bug 835666
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=833256"
+ title="role note shouldn't pick up the name from subtree">
+ Mozilla Bug 833256
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- aria-label, simple label -->
+ <span id="btn_simple_aria_label" role="button" aria-label="I am a button"/>
+ <br/>
+ <!-- aria-label plus aria-labelledby -->
+ <span id="btn_both_aria_labels" role="button" aria-label="I am a button, two"
+ aria-labelledby="labelledby_text btn_both_aria_labels"/>
+ <br/>
+
+ <!-- aria-labelledby, single relation -->
+ <span id="labelledby_text">text</span>
+ <button id="btn_labelledby_text"
+ aria-labelledby="labelledby_text">1</button>
+ <br/>
+
+ <!-- aria-labelledby, multiple relations -->
+ <span id="labelledby_text1">text1</span>
+ <span id="labelledby_text2">text2</span>
+ <button id="btn_labelledby_texts"
+ aria-labelledby="labelledby_text1 labelledby_text2">2</button>
+ <br/>
+
+ <!-- name from named accessible -->
+ <input id="labelledby_namedacc" type="checkbox"
+ aria-label="Data" />
+ <input id="input_labelledby_namedacc"
+ aria-labelledby="labelledby_namedacc" />
+
+ <!-- the name from subtree, mixed content -->
+ <span id="labelledby_mixed">no<span>more text</span></span>
+ <button id="btn_labelledby_mixed"
+ aria-labelledby="labelledby_mixed">3</button>
+ <br/>
+
+ <!-- the name from subtree, mixed/hidden content -->
+ <span id="labelledby_mixed_hidden_child">
+ no<span>more
+ <span style="display: none;">hidden</span>
+ text2
+ <span style="visibility: hidden">hidden2</span>
+ </span>
+ </span>
+ <button id="btn_labelledby_mixed_hidden_child"
+ aria-labelledby="labelledby_mixed_hidden_child">3.1</button>
+ <br/>
+
+ <!-- the name from subtree, mixed/completely hidden content -->
+ <span id="labelledby_mixed_hidden"
+ style="display: none;">lala <span>more hidden </span>text</span></span>
+ <button id="btn_labelledby_mixed_hidden"
+ aria-labelledby="labelledby_mixed_hidden">3.2</button>
+ <br/>
+
+ <!-- the name from subtree, mixed content, block structure -->
+ <div id="labelledby_mixed_block"><div>text</div>more text</div></div>
+ <button id="btn_labelledby_mixed_block"
+ aria-labelledby="labelledby_mixed_block">4</button>
+ <br/>
+
+ <!-- the name from subtree, mixed content, table structure -->
+ <table><tr>
+ <td id="labelledby_mixed_table">text<span>TEXT</span>text</td>
+ </tr></table>
+ <button id="btn_labelledby_mixed_table"
+ aria-labelledby="labelledby_mixed_table">5</button>
+ <br/>
+
+ <!-- the name from subtree, child img -->
+ <span id="labelledby_mixed_img">text<img alt="image"/></span>
+ <button id="btn_labelledby_mixed_img"
+ aria-labelledby="labelledby_mixed_img">6</button>
+ <br/>
+
+ <!-- the name from subtree, child inputs -->
+ <span id="labelledby_mixed_input">
+ <input type="button" id="input_button" title="input button"/>
+ <input type="submit" id="input_submit"/>
+ <input type="reset" id="input_reset"/>
+ <input type="image" id="input_image" title="input image"/>
+ </span>
+ <button id="btn_labelledby_mixed_input"
+ aria-labelledby="labelledby_mixed_input">7</button>
+ <br/>
+
+ <!-- the name from subtree, child object -->
+ <span id="labelledby_mixed_object">
+ <object data="about:blank" title="object"></object>
+ </span>
+ <button id="btn_labelledby_mixed_object"
+ aria-labelledby="labelledby_mixed_object">8</button>
+ <br/>
+
+ <!-- the name from subtree, child br -->
+ <span id="labelledby_mixed_br">text<br/>text</span>
+ <button id="btn_labelledby_mixed_br"
+ aria-labelledby="labelledby_mixed_br">9</button>
+ <br/>
+
+ <!-- the name from subtree, name from label content rather than from its title
+ attribute -->
+ <label for="from_label_ignoretitle"
+ title="Select your country of origin">Country:</label>
+ <select id="from_label_ignoretitle">
+ <option>Germany</option>
+ <option>Russia</option>
+ </select>
+
+ <!-- the name from subtree, name from html:p content rather than from its
+ title attribute -->
+ <p id="p_ignoretitle"
+ title="Select your country of origin">Choose country from.</p>
+ <select id="from_p_ignoretitle" aria-labelledby="p_ignoretitle">
+ <option>Germany</option>
+ <option>Russia</option>
+ </select>
+
+ <!-- the name from subtree, name from html:input value rather than from its
+ title attribute -->
+ <p id="from_input_ignoretitle" aria-labelledby="input_ignoretitle">Country</p>
+ <input id="input_ignoretitle"
+ value="Custom country"
+ title="Input your country of origin"/ >
+
+ <!-- name from subtree, surround control by spaces to not jamm the text -->
+ <label id="insert_spaces_around_control">
+ start<input value="value">end
+ </label>
+
+ <!-- no name from subtree because it holds whitespaces only -->
+ <a id="from_label_ignore_ws_subtree" href="about:mozilla" title="about">&nbsp;&nbsp; </a>
+
+ <!-- Don't use subtree unless referenced by aria-labelledby. -->
+ <div id="alert" role="alert">Error</div>
+ <input type="text" id="inputLabelledByAlert" aria-labelledby="alert">
+
+ <!-- label element, label contains control -->
+ <label>text<button id="btn_label_inside">10</button>text</label>
+ <br/>
+
+ <!-- label element, label and the button are in the same form -->
+ <form>
+ <label for="btn_label_inform">in form</label>
+ <button id="btn_label_inform">11</button>
+ </form>
+
+ <!-- label element, label is outside of the form of the button -->
+ <label for="btn_label_outform">out form</label>
+ <form>
+ <button id="btn_label_outform">12</button>
+ </form>
+
+ <!-- label element, label and the button are in the same document -->
+ <label for="btn_label_indocument">in document</label>
+ <button id="btn_label_indocument">13</button>
+
+ <!-- multiple label elements for single button -->
+ <label for="btn_label_multi">label1</label>
+ <label for="btn_label_multi">label2</label>
+ <button id="btn_label_multi">button</button>
+
+ <!-- a label containing more than one controls -->
+ <label>
+ Enable <input id="ctrl_in_label_1" type="checkbox"> a
+ <input id="ctrl_in_label_2" type="button" value="button"> control
+ </label>
+
+ <!-- name from children -->
+ <span id="btn_children" role="button">14</span>
+
+ <!-- no name from content, ARIA role overrides this rule -->
+ <button id="btn_nonamefromcontent" role="img">1</button>
+
+ <!-- name from children, hidden children -->
+ <div role="listbox" tabindex="0">
+ <div id="lb_opt1_children_hidden" role="option" tabindex="0">
+ <span>i am visible</span>
+ <span style="display:none">i am hidden</span>
+ </div>
+ </div>
+
+ <table role="menu">
+ <tr role="menuitem" id="tablemenuitem">
+ <td>menuitem 1</td>
+ </tr>
+ <tr role="menuitem">
+ <td>menuitem 2</td>
+ </tr>
+ </table>
+
+ <label id="label_with_acronym">
+ <acronym title="O A T F">OATF</acronym>
+ <abbr title="World Wide Web">WWW</abbr>
+ </label>
+
+ <div id="testArticle" role="article" title="Test article">
+ <p>This is a paragraph inside the article.</p>
+ </div>
+
+ <h1 id="h1" title="oops">heading</h1>
+ <div role="heading" id="aria_heading">aria_heading</div>
+
+ <!-- name from title attribute -->
+ <span id="btn_title" role="group" title="title">15</span>
+
+ <!-- A textarea nested in a label with a text child (bug #453371). -->
+ <form>
+ <label>Story
+ <textarea id="textareawithchild" name="name">Foo</textarea>
+ is ended.
+ </label>
+ </form>
+
+ <!-- controls having a value used as part of computed name -->
+ <input type="checkbox" id="ctrlvalue_progressbar:input">
+ <label for="ctrlvalue_progressbar:input">
+ foo <span role="progressbar"
+ aria-valuenow="5" aria-valuemin="1"
+ aria-valuemax="10">5</span> baz
+ </label>
+
+ <input type="checkbox" id="ctrlvalue_scrollbar:input" />
+ <label for="ctrlvalue_scrollbar:input">
+ foo <span role="scrollbar"
+ aria-valuenow="5" aria-valuemin="1"
+ aria-valuemax="10">5</span> baz
+ </label>
+
+ <input type="checkbox" id="ctrlvalue_slider:input">
+ <label for="ctrlvalue_slider:input">
+ foo <input role="slider" type="range"
+ value="5" min="1" max="10"
+ aria-valuenow="5" aria-valuemin="1"
+ aria-valuemax="10"> baz
+ </label>
+
+ <input type="checkbox" id="ctrlvalue_spinbutton:input">
+ <label for="ctrlvalue_spinbutton:input">
+ foo <input role="spinbutton" type="number"
+ value="5" min="1" max="10"
+ aria-valuenow="5" aria-valuemin="1"
+ aria-valuemax="10">
+ baz
+ </label>
+
+ <input type="checkbox" id="ctrlvalue_combobox:input">
+ <label for="ctrlvalue_combobox:input">
+ foo
+ <div role="combobox">
+ <div role="textbox"></div>
+ <ul role="listbox" style="list-style-type: none;">
+ <li role="option">1</li>
+ <li role="option" aria-selected="true">5</li>
+ <li role="option">3</li>
+ </ul>
+ </div>
+ baz
+ </label>
+
+ <input type="checkbox" id="ctrlvalue_meter:input">
+ <label for="ctrlvalue_meter:input">
+ foo
+ <meter>5</meter>
+ </div>
+ baz
+ </label>
+
+ <!-- a label with a nested control in the start, middle and end -->
+ <form>
+ <!-- at the start (without and with whitespaces) -->
+ <label id="comboinstart"><select id="combo3">
+ <option>One</option>
+ <option>Two</option>
+ </select>
+ day(s).
+ </label>
+
+ <label id="textboxinstart">
+ <input id="textbox1" value="Two">
+ days.
+ </label>
+
+ <!-- in the middle -->
+ <label id="comboinmiddle">
+ Subscribe to
+ <select id="combo4" name="occupation">
+ <option>ATOM</option>
+ <option>RSS</option>
+ </select>
+ feed.
+ </label>
+
+ <label id="comboinmiddle2" for="checkbox">Play the
+ <select id="combo5">
+ <option>Haliluya</option>
+ <option>Hurra</option>
+ </select>
+ sound when new mail arrives
+ </label>
+ <input id="checkbox" type="checkbox" />
+
+ <label id="comboinmiddle3" for="combo6">Play the
+ <select id="combo6">
+ <option>Haliluya</option>
+ <option>Hurra</option>
+ </select>
+ sound when new mail arrives
+ </label>
+
+ <!-- at the end (without and with whitespaces) -->
+ <label id="comboinend">
+ This day was
+ <select id="combo7" name="occupation">
+ <option>sunny</option>
+ <option>rainy</option>
+ </select></label>
+
+ <label id="textboxinend">
+ This day was
+ <input id="textbox2" value="sunny">
+ </label>
+ </form>
+
+ <!-- placeholder -->
+ <input id="ph_password" type="password" value="" placeholder="a placeholder" />
+ <input id="ph_text" type="text" placeholder="a placeholder" />
+ <textarea id="ph_textarea" cols="5" placeholder="a placeholder"></textarea>
+
+ <!-- placeholder does not win -->
+ <input id="ph_text2" type="text" aria-label="a label" placeholder="meh" />
+ <textarea id="ph_textarea2" cols="5" aria-labelledby="ph_text2"
+ placeholder="meh"></textarea>
+
+ <label for="ph_text3">a label</label>
+ <input id="ph_text3" placeholder="meh" />
+
+ <p>Image:
+ <img id="img_eq" role="math" src="foo" alt="x^2 + y^2 + z^2">
+ <input type="image" id="input_img_eq" src="foo" alt="x^2 + y^2 + z^2">
+ </p>
+
+ <p>Text:
+ <span id="txt_eq" role="math" title="x^2 + y^2 + z^2">x<sup>2</sup> +
+ y<sup>2</sup> + z<sup>2</sup></span>
+
+ <!-- duplicate announcement -->
+ <div id="test_note" role="note">subtree</div>
+
+ <!-- No name for tr from its sub tree -->
+ <table><tr id="NoNameForTR"><th>a</th><td>b</td></tr></table>
+ <table style="display: block;">
+ <tr id="NoNameForNonStandardTR" style="display:block;">
+ <th>a</th><td>b</td>
+ </tr>
+ </table>
+
+ <!-- Name from sub tree of tr, because it has a strong ARIA role -->
+ <table><tr id="NameForTRBecauseStrongARIA" role="row"><th>a</th><td>b</td></tr></table>
+
+ <!-- No name for tr because of weak (landmark) role -->
+ <table><tr id="NoNameForTRBecauseWeakARIA" role="main"><th>a</th><td>b</td></tr></table>
+
+ <!-- Name from subtree of grouping if requested by other object -->
+ <div id="grouping" role="group">label</div>
+ <button id="requested_name_from_grouping"aria-labelledby="grouping"></button>
+ <!-- Name from sub tree of tbody marked as display:block;, which is also a grouping -->
+ <div id="listitem_containing_block_tbody" role="listitem">
+ <table>
+ <tbody style="display: block;">
+ <tr><td>label</td></tr>
+ </tbody>
+ </table>
+ </div>
+ <!-- Name from subtree of treeitem containing grouping -->
+ <div id="treeitem_containing_grouping" role="treeitem" aria-level="1" aria-expanded="true">root
+ <div role="group">
+ <div role="treeitem" aria-level="2">sub</div>
+ </div>
+ </div>
+
+ <!-- Name from subtree of grouping labelled by an ancestor. -->
+ <div id="grouping_ancestor_label">label
+ <div id="grouping_labelledby_ancestor" role="group" aria-labelledby="grouping_ancestor_label">
+ This content should not be included in the grouping's label.
+ </div>
+ </div>
+
+ <!-- Text nodes and inline elements. -->
+ <div id="container_text_inline" role="option">a<strong>b</strong>c</div>
+ <!-- Text nodes and block elements. -->
+ <div id="container_text_block" role="option">a<p>b</p>c</div>
+ <!-- Text nodes and empty block elements. -->
+ <div id="container_text_emptyblock" role="option">a<p></p><p></p>b</div>
+
+ <!-- aria-labelledby referring to a slot -->
+ <div id="shadowHost">
+ shadowButtonVisibleText
+ <span slot="hiddenSlot">shadowButtonHiddenText</span>
+ </div>
+ <template id="shadowTemplate">
+ <input type="button" id="shadowButtonVisibleText" aria-labelledby="visibleSlot">
+ <slot id="visibleSlot"></slot>
+ <input type="button" id="shadowButtonHiddenText" aria-labelledby="hiddenSlot">
+ <slot id="hiddenSlot" name="hiddenSlot" hidden></slot>
+ </template>
+ <script>
+ const shadowHost = document.getElementById("shadowHost");
+ const shadowRoot = shadowHost.attachShadow({ mode: "open" });
+ shadowRoot.append(document.getElementById("shadowTemplate").content.cloneNode(true));
+ </script>
+
+ <!-- aria-labelledby referring to a hidden container with script/style -->
+ <button id="buttonScriptStyle" aria-labelledby="hiddenScriptStyle"></button>
+ <div id="hiddenScriptStyle" hidden>
+ <script> 42; </script>
+ <style> .noOp {} </style>
+ <span>content</span>
+ </div>
+
+ <!-- Name from subtree on link including <code>, etc. -->
+ <a id="linkWithCodeSupSubInsDel" href="#">
+ before
+ <code>code</code>
+ <sup>sup</sup>
+ <sub>sub</sub>
+ <ins>ins</ins>
+ <del>del</del>
+ after
+ </a>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/name/test_general.xhtml b/accessible/tests/mochitest/name/test_general.xhtml
new file mode 100644
index 0000000000..3c5bb23309
--- /dev/null
+++ b/accessible/tests/mochitest/name/test_general.xhtml
@@ -0,0 +1,345 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Accessibility Name Calculating Test.">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function doTest()
+ {
+ // aria-label
+
+ // Simple label provided via ARIA
+ testName("btn_simple_aria_label", "I am a button");
+
+ // aria-label and aria-labelledby, expect aria-labelledby
+ testName("btn_both_aria_labels", "text I am a button, two");
+
+ //////////////////////////////////////////////////////////////////////////
+ // aria-labelledby
+
+ // Single relation. The value of 'aria-labelledby' contains the ID of
+ // an element. Gets the name from text node of that element.
+ testName("btn_labelledby_text", "text");
+
+ // Multiple relations. The value of 'aria-labelledby' contains the IDs
+ // of elements. Gets the name from text nodes of those elements.
+ testName("btn_labelledby_texts", "text1 text2");
+
+ // Trick cases. Self and recursive referencing.
+ testName("rememberHistoryDays", "Remember 3 days");
+ testName("historyDays", "Remember 3 days");
+ testName("rememberAfter", "days");
+
+ //////////////////////////////////////////////////////////////////////////
+ // Name from subtree (single relation labelled_by).
+
+ // Gets the name from text nodes contained by nested elements.
+ testName("btn_labelledby_mixed", "no more text");
+
+ // Gets the name from text nodes and selected item of menulist
+ // (other items are ignored).
+ testName("btn_labelledby_mixed_menulist",
+ "no more text selected item more text");
+
+ // Gets the name from text nodes contained by nested elements, ignores
+ // hidden elements (bug 443081).
+ testName("btn_labelledby_mixed_hidden_child", "no more text2");
+
+ // Gets the name from hidden text nodes contained by nested elements,
+ // (label element is hidden entirely), (bug 443081)
+ testName("btn_labelledby_mixed_hidden", "lala more hidden text");
+
+
+ //////////////////////////////////////////////////////////////////////////
+ // Name from @label attribute.
+
+ // Gets the name from @label attribute.
+ testName("btn_labelattr", "labeled element");
+
+
+ //////////////////////////////////////////////////////////////////////////
+ // Name for nsIDOMXULSelectControlItemElement.
+
+ // Gets the name from @label attribute.
+ testName("li_nsIDOMXULSelectControlItemElement", "select control item");
+
+
+ //////////////////////////////////////////////////////////////////////////
+ // Name if the XUL element doesn't implement nsIDOMXULSelectControlElement
+ // and has @label attribute.
+
+ testName("box_not_nsIDOMXULSelectControlElement", "box");
+
+
+ //////////////////////////////////////////////////////////////////////////
+ // Name from the label element.
+
+ // The label and button are placed on 2nd level relative common parent.
+ testName("btn_label_1", "label1");
+
+ // The label is on 1st, the button is on 5th level relative common parent.
+ testName("btn_label_2", "label2");
+
+ // The label and button are siblings.
+ testName("btn_label_3", "label3");
+
+ // Multiple labels for single button: XUL button takes the last one.
+ testName("btn_label_4", "label5");
+
+ // Label associated with HTML element.
+ testName("input_label", "input label");
+
+
+ //////////////////////////////////////////////////////////////////////////
+ // tooltiptext (if nothing above isn't presented then tooltiptext is used)
+ testName("box_tooltiptext", "tooltiptext label");
+
+
+ //////////////////////////////////////////////////////////////////////////
+ // Name from the @title attribute of <toolbaritem/> (original bug 237249).
+
+ // Direct child of toolbaritem.
+ testName("toolbaritem_child", null);
+
+ // Child from subtree of toolbaritem.
+ testName("toolbaritem_hboxbutton", "button");
+
+
+ //////////////////////////////////////////////////////////////////////////
+ // name from label inside toolbar button
+ testName("toolbarbuttonwithlabel", "I am the button");
+
+
+ //////////////////////////////////////////////////////////////////////////
+ // Name from children
+
+ // ARIA role button is presented allowing the name calculation from
+ // children.
+ testName("box_children", "14");
+
+ // Button labelled by a text child.
+ testName("button_text", "Text");
+ testName("toolbarbutton_text", "Text");
+
+ // ARIA role option is presented allowing the name calculation from
+ // the visible children (bug 443081)
+ testName("lb_opt1_children_hidden", "i am visible");
+
+
+ //////////////////////////////////////////////////////////////////////////
+ // Name from aria-labelledby: menuitem label+ listitem label
+ testName("li_labelledby", "Show an Alert The moment the event starts");
+
+ //////////////////////////////////////////////////////////////////////////
+ // groupbox labeling from first label
+ testName("groupbox", "Some caption");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=444279"
+ title="mochitest for accessible name calculating">
+ Mozilla Bug 444279
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=441991"
+ title="nsXULListitemAccessible::GetName prefers label \
+ attribute over aria-labelledby and doesn't allow recursion">
+ Mozilla Bug 441991
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+
+ <!-- aria-label, simple label -->
+ <button id="btn_simple_aria_label" aria-label="I am a button"/>
+
+ <!-- aria-label plus aria-labelledby -->
+ <button id="btn_both_aria_labels" aria-label="I am a button, two"
+ aria-labelledby="labelledby_text btn_both_aria_labels"/>
+
+ <!-- aria-labelledby, single relation -->
+ <description id="labelledby_text">text</description>
+ <button id="btn_labelledby_text"
+ aria-labelledby="labelledby_text"/>
+
+ <!-- aria-labelledby, multiple relations -->
+ <description id="labelledby_text1">text1</description>
+ <description id="labelledby_text2">text2</description>
+ <button id="btn_labelledby_texts"
+ aria-labelledby="labelledby_text1 labelledby_text2"/>
+
+ <!-- trick aria-labelledby -->
+ <checkbox id="rememberHistoryDays"
+ label="Remember "
+ aria-labelledby="rememberHistoryDays historyDays rememberAfter"/>
+ <html:input id="historyDays" value="3"
+ aria-labelledby="rememberHistoryDays historyDays rememberAfter"/>
+ <label id="rememberAfter">days</label>
+
+ <!-- the name from subtree, mixed content -->
+ <description id="labelledby_mixed">
+ no<description>more text</description>
+ </description>
+ <button id="btn_labelledby_mixed"
+ aria-labelledby="labelledby_mixed"/>
+
+ <!-- the name from subtree, mixed/hidden content -->
+ <description id="labelledby_mixed_hidden_child">no<description>more <description hidden="true">hidden</description>text2</description></description>
+ <button id="btn_labelledby_mixed_hidden_child"
+ aria-labelledby="labelledby_mixed_hidden_child"/>
+
+ <!-- the name from subtree, mixed/completely hidden content -->
+ <description id="labelledby_mixed_hidden"
+ hidden="true">lala <description>more hidden </description>text</description>
+ <button id="btn_labelledby_mixed_hidden"
+ aria-labelledby="labelledby_mixed_hidden"/>
+ <br/>
+
+ <!-- the name from subtree, mixed content, ignore items of menulist -->
+ <description id="labelledby_mixed_menulist">
+ no<description>more text</description>
+ <menulist>
+ <menupopup>
+ <menuitem label="selected item"/>
+ <menuitem label="item"/>
+ </menupopup>
+ </menulist>
+ more text
+ </description>
+ <button id="btn_labelledby_mixed_menulist"
+ aria-labelledby="labelledby_mixed_menulist"/>
+
+ <!-- @label -->
+ <button id="btn_labelattr"
+ label="labeled element"/>
+
+ <!-- nsIDOMXULSelectControlItemElement -->
+ <richlistbox>
+ <richlistitem id="li_nsIDOMXULSelectControlItemElement">
+ <label value="select control item"/>
+ </richlistitem>
+ </richlistbox>
+
+ <!-- not nsIDOMXULSelectControlElement -->
+ <box id="box_not_nsIDOMXULSelectControlElement" role="group" label="box"/>
+
+ <!-- label element -->
+ <hbox>
+ <box>
+ <label control="btn_label_1">label1</label>
+ </box>
+ <label control="btn_label_2">label2</label>
+ <box>
+ <button id="btn_label_1"/>
+ <box>
+ <box>
+ <box>
+ <button id="btn_label_2"/>
+ </box>
+ </box>
+ </box>
+ </box>
+ <label control="btn_label_3">label3</label>
+ <button id="btn_label_3"/>
+
+ <label control="btn_label_4">label4</label>
+ <label control="btn_label_4">label5</label>
+ <button id="btn_label_4"/>
+
+ <label control="input_label">input label</label>
+ <html:input id="input_label"/>
+ </hbox>
+
+ <!-- tooltiptext -->
+ <box id="box_tooltiptext"
+ role="group"
+ tooltiptext="tooltiptext label"/>
+
+ <!-- the name from @title of toolbaritem -->
+ <!-- and the name from label of a toolbarbutton -->
+ <toolbar>
+ <toolbaritem title="ooospspss">
+ <box id="toolbaritem_child"
+ role="group"
+ flex="1">
+ <hbox role="button" id="toolbaritem_hboxbutton">
+ <description value="button"/>
+ </hbox>
+ </box>
+ </toolbaritem>
+ <toolbarbutton id="toolbarbuttonwithlabel">
+ <label flex="1">I am the button</label>
+ </toolbarbutton>
+ </toolbar>
+
+ <!-- name from children -->
+ <box id="box_children" role="button">14</box>
+ <button id="button_text">Text</button>
+ <toolbarbutton id="toolbarbutton_text">Text</toolbarbutton>
+
+ <!-- name from children, hidden children -->
+ <vbox role="listbox" tabindex="0">
+ <hbox id="lb_opt1_children_hidden" role="option" tabindex="0">
+ <description>i am visible</description>
+ <description style="display:none">i am hidden</description>
+ </hbox>
+
+ <!-- Name from caption sub tree -->
+ <groupbox id="groupbox">
+ <label>Some caption</label>
+ <checkbox label="some checkbox label" />
+ </groupbox>
+ </vbox>
+
+ <!-- bug 441991; create name from other menuitem label listitem's own label -->
+ <hbox>
+ <richlistbox>
+ <richlistitem id="li_labelledby"
+ aria-labelledby="menuitem-DISPLAY li_labelledby">
+ <label value="The moment the event starts"/>
+ </richlistitem>
+ </richlistbox>
+ <menulist>
+ <menupopup>
+ <menuitem id="menuitem-DISPLAY"
+ value="DISPLAY"
+ label="Show an Alert"/>
+ <menuitem id="menuitem-EMAIL"
+ value="EMAIL"
+ label="Send an E-mail"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ </vbox> <!-- close tests area -->
+ </hbox> <!-- close main area -->
+</window>
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 @@
+<html>
+
+<head>
+ <title>nsIAccessible::name calculation for HTML links (html:a)</title>
+
+ <link rel="stylesheet"
+ type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // aria-label
+ testName("aria_label", "anchor label");
+
+ // aria-labelledby
+ testName("aria_labelledby", "text");
+
+ // name from content
+ testName("namefromcontent", "1");
+
+ // name from content
+ testName("namefromimg", "img title");
+
+ // no name from content
+ testName("nonamefromcontent", null);
+
+ // title
+ testName("title", "title");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=459782"
+ title="nsIAccessible::name calculation for HTML links (html:a)">
+ Mozilla Bug 459782
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- aria-label -->
+ <a id="aria_label" href="mozilla.org"
+ aria-label="anchor label">1</a>
+ <br/>
+
+ <!-- aria-labelledby, preferred to html:label -->
+ <span id="text">text</span>
+ <label for="aria_labelledby">label</label>
+ <a id="aria_labelledby" href="mozilla.org"
+ aria-labelledby="text">1</a>
+ <br/>
+
+ <!-- name from content, preferred to @title -->
+ <a id="namefromcontent" href="mozilla.org"
+ title="title">1</a>
+ <br/>
+
+ <!-- name from content, preferred to @title -->
+ <a id="namefromimg" href="mozilla.org"
+ title="title"><img alt="img title" /></a>
+
+ <!-- no name from content, ARIA role overrides this rule -->
+ <a id="nonamefromcontent" href="mozilla.org" role="img">1</a>
+ <br/>
+
+ <!-- no content, name from @title -->
+ <a id="title" href="mozilla.org"
+ title="title"></a>
+
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>nsIAccessible::name calculation for HTML li</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ /**
+ * Alter list item numbering and change list style type.
+ */
+ function bulletUpdate() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, getNode("list")),
+ ];
+
+ this.invoke = function bulletUpdate_invoke() {
+ testName("li_end", "1. list end");
+
+ var li = document.createElement("li");
+ li.setAttribute("id", "li_start");
+ li.textContent = "list start";
+ getNode("list").insertBefore(li, getNode("li_end"));
+ };
+
+ this.finalCheck = function bulletUpdate_finalCheck() {
+ testName("li_start", "1. list start");
+ testName("li_end", "2. list end");
+ };
+
+ this.getID = function bulletUpdate_getID() {
+ return "insertBefore new list item";
+ };
+ }
+ function bulletUpdate2() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, getNode("li_end")),
+ ];
+
+ this.invoke = function bulletUpdate2_invoke() {
+ // change list style type
+ var list = getNode("list");
+ list.setAttribute("style", "list-style-type: disc;");
+
+ // Flush both the style change and the resulting layout change.
+ // Flushing style on its own is not sufficient, because that can
+ // leave frames marked with NS_FRAME_IS_DIRTY, which will cause
+ // nsTextFrame::GetRenderedText to report the text of a text
+ // frame is empty.
+ list.offsetWidth; // flush layout (which also flushes style)
+ };
+
+ this.finalCheck = function bulletUpdate2_finalCheck() {
+ testName("li_start", kDiscBulletText + "list start");
+ testName("li_end", kDiscBulletText + "list end");
+ };
+
+ this.getID = function bulletUpdate2_getID() {
+ return "Update list item style";
+ };
+ }
+
+ var gQueue = null;
+ function doTest() {
+ gQueue = new eventQueue();
+ gQueue.push(new bulletUpdate());
+ gQueue.push(new bulletUpdate2());
+ gQueue.invoke(); // SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=634200"
+ title="crash [@ nsIFrame::StyleVisibility() ]">
+ Mozilla Bug 634200
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <ol id="list">
+ <li id="li_end">list end</li>
+ </ol>
+
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>nsIAccessible::name calculation for elements</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript"
+ src="markup.js"></script>
+
+ <script type="application/javascript">
+ // gA11yEventDumpID = "eventdump";
+ // gDumpToConsole = true;
+ // gA11yEventDumpToConsole = true;
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(testNames);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=459635"
+ title="nsIAccessible::name calculation for elements">
+ Bug 459635
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=666212"
+ title="summary attribute content mapped to accessible name in MSAA">
+ Bug 666212
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=786163"
+ title=" Sort out name calculation for HTML input buttons">
+ Bug 786163
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="eventdump"></div>
+</body>
+</html>
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 @@
+<html>
+
+<head>
+ <title>Accessible name and description for SVG elements</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ testName("svg1", "A name");
+ testDescr("svg1", "A description");
+ testName("svg2", "A tooltip");
+ testDescr("svg2", "");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=459357"
+ title="Support accessible name computation for SVG">
+ Mozilla Bug 459357
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg1">
+ <title>A name</title>
+ <desc>A description</title>
+ </svg>
+
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2">
+ <desc>A tooltip</desc>
+ </svg>
+</body>
+</html>
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 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessibility Name Calculating Test.">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function treeTester(aID)
+ {
+ this.DOMNode = getNode(aID);
+
+ this.invoke = function treeTester_invoke()
+ {
+ this.DOMNode.view = new nsTreeTreeView();
+ }
+
+ this.check = function treeTester_check(aEvent)
+ {
+ var tree = {
+ role: ROLE_OUTLINE,
+ children: [
+ {
+ role: ROLE_LIST
+ },
+ {
+ role: ROLE_OUTLINEITEM,
+ children: [],
+ name: "row1col"
+ },
+ {
+ role: ROLE_OUTLINEITEM,
+ children: [],
+ name: "row2_col"
+ },
+ {
+ role: ROLE_OUTLINEITEM,
+ children: [],
+ name: "row2.1_col"
+ },
+ {
+ role: ROLE_OUTLINEITEM,
+ children: [],
+ name: "row2.2_col"
+ },
+ {
+ role: ROLE_OUTLINEITEM,
+ children: [],
+ name: "row3_col"
+ },
+ {
+ role: ROLE_OUTLINEITEM,
+ children: [],
+ name: "row4col"
+ }
+ ]
+ };
+ testAccessibleTree(this.DOMNode, tree);
+ }
+
+ this.getID = function treeTester_getID()
+ {
+ return "Tree name testing for " + aID;
+ }
+ }
+
+ function tableTester(aID, aIsTable, aCol1ID, aCol2ID)
+ {
+ this.DOMNode = getNode(aID);
+
+ this.invoke = function tableTester_invoke()
+ {
+ this.DOMNode.view = new nsTableTreeView(2);
+ }
+
+ this.check = function tableTester_check(aEvent)
+ {
+ var tree = {
+ role: aIsTable ? ROLE_TABLE : ROLE_TREE_TABLE,
+ children: [
+ {
+ role: ROLE_LIST
+ },
+ {
+ role: ROLE_ROW,
+ children: [
+ {
+ role: ROLE_GRID_CELL,
+ children: [],
+ name: "row0_" + aCol1ID
+ },
+ {
+ role: ROLE_GRID_CELL,
+ children: [],
+ name: "row0_" + aCol2ID
+ }
+ ],
+ name: "row0_" + aCol1ID + " row0_" + aCol2ID
+ },
+ {
+ role: ROLE_ROW,
+ children: [
+ {
+ role: ROLE_GRID_CELL,
+ children: [],
+ name: "row1_" + aCol1ID
+ },
+ {
+ role: ROLE_GRID_CELL,
+ children: [],
+ name: "row1_" + aCol2ID
+ }
+ ],
+ name: "row1_" + aCol1ID + " row1_" + aCol2ID
+ }
+ ]
+ };
+ testAccessibleTree(this.DOMNode, tree);
+ }
+
+ this.getID = function tableTester_getID()
+ {
+ return "Tree name testing for " + aID;
+ }
+ }
+
+ var gQueue = null;
+ function doTest()
+ {
+ gQueue = new eventQueue(EVENT_REORDER);
+
+ gQueue.push(new treeTester("tree"));
+ gQueue.push(new tableTester("table", true, "t_col1", "t_col2"));
+ gQueue.push(new tableTester("treetable", false, "tt_col1", "tt_col2"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=546812"
+ title="Treegrid row accessible shouldn't inherit name from tree accessible">
+ Mozilla Bug 546812
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=664376"
+ title="Table rows of XUL trees no longer containing cell content as accessible name">
+ Mozilla Bug 664376
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col" flex="1" primary="true" label="column"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <tree id="table" flex="1">
+ <treecols>
+ <treecol id="t_col1" flex="1" label="column"/>
+ <treecol id="t_col2" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <tree id="treetable" flex="1">
+ <treecols>
+ <treecol id="tt_col1" flex="1" label="column" primary="true"/>
+ <treecol id="tt_col2" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ </vbox> <!-- close tests area -->
+ </hbox> <!-- close main area -->
+</window>
diff --git a/accessible/tests/mochitest/promisified-events.js b/accessible/tests/mochitest/promisified-events.js
new file mode 100644
index 0000000000..c58466dcf1
--- /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 + ": " : ""}Received ${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 received 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.toml b/accessible/tests/mochitest/relations/a11y.toml
new file mode 100644
index 0000000000..ae41bf91a5
--- /dev/null
+++ b/accessible/tests/mochitest/relations/a11y.toml
@@ -0,0 +1,23 @@
+[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_shadowdom.html"]
+
+["test_tabbrowser.xhtml"]
+
+["test_tree.xhtml"]
+
+["test_ui_modalprompt.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 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Embeds relation tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../relations.js"></script>
+ <script type="application/javascript"
+ src="../browser.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function loadURI(aURI)
+ {
+ this.invoke = function loadURI_invoke()
+ {
+ tabBrowser().loadURI(Services.io.newURI(aURI), {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, currentTabDocument)
+ ];
+
+ this.finalCheck = function loadURI_finalCheck()
+ {
+ testRelation(browserDocument(), RELATION_EMBEDS,
+ getAccessible(currentTabDocument()));
+ }
+
+ this.getID = function loadURI_getID()
+ {
+ return "load uri " + aURI;
+ }
+ }
+
+ function addTab(aURI)
+ {
+ this.invoke = function addTab_invoke()
+ {
+ tabBrowser().addTab(aURI, {
+ referrerURI: null,
+ charset: null,
+ postData: null,
+ inBackground: false,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, currentTabDocument)
+ ];
+
+ this.finalCheck = function loadURI_finalCheck()
+ {
+ testRelation(browserDocument(), RELATION_EMBEDS,
+ getAccessible(currentTabDocument()));
+ }
+
+ this.getID = function addTab_getID()
+ {
+ return "load uri '" + aURI + "' in new tab";
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Testing
+
+ //gA11yEventDumpToConsole = true; // debug
+
+ var gQueue = null;
+ function doTests()
+ {
+ testRelation(browserDocument(), RELATION_EMBEDS,
+ getAccessible(currentTabDocument()));
+
+ enableLogging("docload");
+ gQueue = new eventQueue();
+
+ gQueue.push(new loadURI("about:robots"));
+ gQueue.push(new addTab("about:mozilla"));
+
+ gQueue.onFinish = function()
+ {
+ disableLogging();
+ closeBrowserWindow();
+ }
+ gQueue.invoke();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ openBrowserWindow(doTests, "about:license");
+ ]]>
+ </script>
+
+ <vbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=707654"
+ title="Embeds relation on root accessible can return not content document">
+ Mozilla Bug 707654
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+ </vbox>
+</window>
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 @@
+<html>
+
+<head>
+ <title>nsIAccessible::getAccessibleRelated() tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../relations.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // html:label@for
+ testRelation("label1_1", RELATION_LABEL_FOR, "control1_1");
+ testRelation("control1_1", RELATION_LABELLED_BY, "label1_1");
+
+ // html:label@for, multiple
+ testRelation("label1_2", RELATION_LABEL_FOR, "control1_2");
+ testRelation("label1_3", RELATION_LABEL_FOR, "control1_2");
+ testRelation("control1_2", RELATION_LABELLED_BY,
+ [ "label1_2", "label1_3" ]);
+
+ // ancestor html:label (implicit association)
+ testRelation("label1_4", RELATION_LABEL_FOR, "control1_4");
+ testRelation("control1_4", RELATION_LABELLED_BY, "label1_4");
+ testRelation("control1_4_option1", RELATION_LABELLED_BY, null);
+ testRelation("label1_5", RELATION_LABEL_FOR, "control1_5");
+ testRelation("control1_5", RELATION_LABELLED_BY, "label1_5");
+ testRelation("label1_6", RELATION_LABEL_FOR, "control1_6");
+ testRelation("control1_6", RELATION_LABELLED_BY, "label1_6");
+ testRelation("label1_7", RELATION_LABEL_FOR, "control1_7");
+ testRelation("control1_7", RELATION_LABELLED_BY, "label1_7");
+ testRelation("label1_8", RELATION_LABEL_FOR, "control1_8");
+ testRelation("control1_8", RELATION_LABELLED_BY, "label1_8");
+ testRelation("label1_9", RELATION_LABEL_FOR, "control1_9");
+ testRelation("control1_9", RELATION_LABELLED_BY, "label1_9");
+ testRelation("label1_10", RELATION_LABEL_FOR, "control1_10");
+ testRelation("control1_10", RELATION_LABELLED_BY, "label1_10");
+ testRelation("label1_11", RELATION_LABEL_FOR, "control1_11");
+ testRelation("control1_11", RELATION_LABELLED_BY, "label1_11");
+ testRelation("label1_12", RELATION_LABEL_FOR, "control1_12");
+ testRelation("control1_12", RELATION_LABELLED_BY, "label1_12");
+
+ testRelation("label1_13", RELATION_LABEL_FOR, null);
+ testRelation("control1_13", RELATION_LABELLED_BY, null);
+ testRelation("control1_14", RELATION_LABELLED_BY, "label1_14");
+
+ // aria-labelledby
+ testRelation("label2", RELATION_LABEL_FOR, "checkbox2");
+ testRelation("checkbox2", RELATION_LABELLED_BY, "label2");
+
+ // aria-labelledby, multiple relations
+ testRelation("label3", RELATION_LABEL_FOR, "checkbox3");
+ testRelation("label4", RELATION_LABEL_FOR, "checkbox3");
+ testRelation("checkbox3", RELATION_LABELLED_BY, ["label3", "label4"]);
+
+ // aria-describedby
+ testRelation("descr1", RELATION_DESCRIPTION_FOR, "checkbox4");
+ testRelation("checkbox4", RELATION_DESCRIBED_BY, "descr1");
+
+ // aria-describedby, multiple relations
+ testRelation("descr2", RELATION_DESCRIPTION_FOR, "checkbox5");
+ testRelation("descr3", RELATION_DESCRIPTION_FOR, "checkbox5");
+ testRelation("checkbox5", RELATION_DESCRIBED_BY, ["descr2", "descr3"]);
+
+ // aria_owns, multiple relations
+ testRelation("treeitem1", RELATION_NODE_CHILD_OF, "tree");
+ testRelation("treeitem2", RELATION_NODE_CHILD_OF, "tree");
+
+ // 'node child of' relation for outlineitem role
+ testRelation("treeitem3", RELATION_NODE_CHILD_OF, "tree");
+ testRelation("treeitem4", RELATION_NODE_CHILD_OF, "tree");
+ testRelation("treeitem5", RELATION_NODE_CHILD_OF, "treeitem4");
+ testRelation("treeitem6", RELATION_NODE_CHILD_OF, "tree");
+ testRelation("treeitem7", RELATION_NODE_CHILD_OF, "treeitem6");
+ testRelation("tree2_ti1", RELATION_NODE_CHILD_OF, "tree2");
+ testRelation("tree2_ti1a", RELATION_NODE_CHILD_OF, "tree2_ti1");
+ testRelation("tree2_ti1b", RELATION_NODE_CHILD_OF, "tree2_ti1");
+
+ // 'node child of' relation for row role in grid.
+ // Relation for row associated using aria-level should exist.
+ testRelation("simplegrid-row3", RELATION_NODE_CHILD_OF,
+ "simplegrid-row2");
+ // Relations for hierarchical children elements shouldn't exist.
+ testAbsentRelation("simplegrid-row1", RELATION_NODE_CHILD_OF,
+ "simplegrid");
+ testAbsentRelation("simplegrid-row2", RELATION_NODE_CHILD_OF,
+ "simplegrid");
+
+ // 'node child of' relation for row role of treegrid
+ testRelation("treegridrow1", RELATION_NODE_CHILD_OF, "treegrid");
+ testRelation("treegridrow2", RELATION_NODE_CHILD_OF, "treegrid");
+ testRelation("treegridrow3", RELATION_NODE_CHILD_OF, "treegridrow2");
+
+ // 'node child of' relation for lists organized by groups
+ testRelation("listitem1", RELATION_NODE_CHILD_OF, "list");
+ testRelation("listitem1.1", RELATION_NODE_CHILD_OF, "listitem1");
+ testRelation("listitem1.2", RELATION_NODE_CHILD_OF, "listitem1");
+
+ // 'node child of' relation for lists and trees organized by groups, with
+ // intervening generic accessibles between widget components.
+ testRelation("list2_listitem1.1", RELATION_NODE_CHILD_OF, "list2_listitem1");
+ testRelation("list2_listitem1.2", RELATION_NODE_CHILD_OF, "list2_listitem1");
+ testRelation("tree4_treeitem1.1", RELATION_NODE_CHILD_OF, "tree4_treeitem1");
+ testRelation("tree4_treeitem1.2", RELATION_NODE_CHILD_OF, "tree4_treeitem1");
+
+ // 'node child of' relation for a treeitem sibling group special case,
+ // with intervening generic accessibles. In this case, if a treeitem's
+ // parent is a group and that group has a previous treeitem sibling, the
+ // treeitem is a child of that previous treeitem sibling.
+ testRelation("tree3_treeitem1.1", RELATION_NODE_CHILD_OF, "tree3_treeitem1");
+ testRelation("tree3_treeitem1.2", RELATION_NODE_CHILD_OF, "tree3_treeitem1");
+
+ // 'node child of' relation for the document having window, returns
+ // direct accessible parent (fixed in bug 419770).
+ var iframeElmObj = {};
+ var iframeAcc = getAccessible("iframe", null, iframeElmObj);
+ var iframeDoc = iframeElmObj.value.contentDocument;
+ var iframeDocAcc = getAccessible(iframeDoc);
+ testRelation(iframeDocAcc, RELATION_NODE_CHILD_OF, iframeAcc);
+
+ // 'node parent of' relation on ARIA tree and treegrid.
+ testRelation("tree", RELATION_NODE_PARENT_OF,
+ ["treeitem1", "treeitem2", // aria-owns
+ "treeitem3", "treeitem4", "treeitem6"]); // children
+ testRelation("treeitem4", RELATION_NODE_PARENT_OF,
+ "treeitem5"); // aria-level
+ testRelation("treeitem6", RELATION_NODE_PARENT_OF,
+ "treeitem7"); // // group role
+ testRelation("tree2", RELATION_NODE_PARENT_OF, "tree2_ti1"); // group role
+ testRelation("tree2_ti1", RELATION_NODE_PARENT_OF,
+ ["tree2_ti1a", "tree2_ti1b"]); // group role
+
+ testRelation("treegridrow2", RELATION_NODE_PARENT_OF, "treegridrow3");
+ testRelation("treegrid", RELATION_NODE_PARENT_OF,
+ ["treegridrow1", "treegridrow2"]);
+
+ // 'node parent of' relation on ARIA grid.
+ // 'node parent of' relation on ARIA grid's row.
+ // Should only have relation to child through aria-level.
+ testRelation("simplegrid-row2", RELATION_NODE_PARENT_OF,
+ "simplegrid-row3");
+
+ // 'node parent of' relation on ARIA list structured by groups
+ testRelation("list", RELATION_NODE_PARENT_OF,
+ "listitem1");
+ testRelation("listitem1", RELATION_NODE_PARENT_OF,
+ [ "listitem1.1", "listitem1.2" ]);
+
+ // aria-atomic
+ testRelation(getNode("atomic").firstChild, RELATION_MEMBER_OF, "atomic");
+
+ // aria-controls
+ getAccessible("tab");
+ todo(false,
+ "Getting an accessible tab, otherwise relations for tabpanel aren't cached. Bug 606924 will fix that.");
+ testRelation("tabpanel", RELATION_CONTROLLED_BY, "tab");
+ testRelation("tab", RELATION_CONTROLLER_FOR, "tabpanel");
+
+ // aria-controls, multiple relations
+ testRelation("lr1", RELATION_CONTROLLED_BY, "button");
+ testRelation("lr2", RELATION_CONTROLLED_BY, "button");
+ testRelation("button", RELATION_CONTROLLER_FOR, ["lr1", "lr2"]);
+
+ // aria-flowto
+ testRelation("flowto", RELATION_FLOWS_TO, "flowfrom");
+ testRelation("flowfrom", RELATION_FLOWS_FROM, "flowto");
+
+ // aria-flowto, multiple relations
+ testRelation("flowto1", RELATION_FLOWS_TO, ["flowfrom1", "flowfrom2"]);
+ testRelation("flowfrom1", RELATION_FLOWS_FROM, "flowto1");
+ testRelation("flowfrom2", RELATION_FLOWS_FROM, "flowto1");
+
+ // 'default button' relation
+ testRelation("input", RELATION_DEFAULT_BUTTON, "submit");
+
+ // output 'for' relations
+ testRelation("output", RELATION_CONTROLLED_BY, ["input", "input2"]);
+ testRelation("output2", RELATION_CONTROLLED_BY, ["input", "input2"]);
+ testRelation("input", RELATION_CONTROLLER_FOR, ["output", "output2"]);
+ testRelation("input2", RELATION_CONTROLLER_FOR, ["output", "output2"]);
+
+ // 'described by'/'description for' relation for html:table and
+ // html:caption
+ testRelation("caption", RELATION_LABEL_FOR, "table");
+ testRelation("table", RELATION_LABELLED_BY, "caption");
+
+ // 'labelled by'/'label for' relation for html:fieldset and
+ // html:legend
+ testRelation("legend", RELATION_LABEL_FOR, "fieldset");
+ testRelation("fieldset", RELATION_LABELLED_BY, "legend");
+
+ // containing relations
+ testRelation("control1_1", RELATION_CONTAINING_DOCUMENT, document);
+ testRelation("control1_1", RELATION_CONTAINING_TAB_PANE, getTabDocAccessible("control1_1"));
+ testRelation("control1_1", RELATION_CONTAINING_APPLICATION, getApplicationAccessible());
+
+ // details
+ testRelation("has_details", RELATION_DETAILS, "details");
+ testRelation("details", RELATION_DETAILS_FOR, "has_details");
+ testRelation("has_multiple_details", RELATION_DETAILS, ["details2", "details3"]);
+ testRelation("details2", RELATION_DETAILS_FOR, "has_multiple_details");
+ testRelation("details3", RELATION_DETAILS_FOR, "has_multiple_details");
+
+ // error
+ testRelation("has_error", RELATION_ERRORMSG, "error");
+ testRelation("error", RELATION_ERRORMSG_FOR, "has_error");
+
+ // finish test
+ SimpleTest.finish();
+ }
+
+ disableLogging(); // from test_embeds.xhtml
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=475298"
+ title="mochitests for accessible relations">
+ Bug 475298
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=527461"
+ title="Implement RELATION_NODE_PARENT_OF">
+ Bug 527461
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=558036"
+ title="make HTML <output> accessible">
+ Bug 558036
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=682790"
+ title="Ignore implicit label association when it's associated explicitly">
+ Bug 682790
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=687393"
+ title="HTML select options gets relation from containing label">
+ Bug 687393
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=864224"
+ title="Support nested ARIA listitems structured by role='group'">
+ Bug 864224
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <label id="label1_1" for="control1_1">label</label>
+ <input id="control1_1">
+
+ <label id="label1_2" for="control1_2">label</label>
+ <label id="label1_3" for="control1_2">label</label>
+ <input id="control1_2">
+
+ <label id="label1_4">Label
+ <select id="control1_4">
+ <option id="control1_4_option1">option</option>
+ </select>
+ </label>
+ <label id="label1_5">Label
+ <button id="control1_5">button</button>
+ </label>
+ <label id="label1_6">Label
+ <input id="control1_6">
+ </label>
+ <label id="label1_7">Label
+ <input id="control1_7" type="checkbox">
+ </label>
+ <label id="label1_8">Label
+ <input id="control1_8" type="radio">
+ </label>
+ <label id="label1_9">Label
+ <input id="control1_9" type="button" value="button">
+ </label>
+ <label id="label1_10">Label
+ <input id="control1_10" type="submit">
+ </label>
+ <label id="label1_11">Label
+ <input id="control1_11" type="image">
+ </label>
+ <label id="label1_12">Label
+ <progress id="control1_12"></progress>
+ </label>
+
+ <label id="label1_13" for="">Label
+ <input id="control1_13">
+ </label>
+ <label id="label1_14" for="control1_14">Label
+ <input id="control1_14">
+ </label>
+
+ <span id="label2">label</span>
+ <span role="checkbox" id="checkbox2" aria-labelledby="label2"></span>
+
+ <span id="label3">label1</span>
+ <span id="label4">label2</span>
+ <span role="checkbox" id="checkbox3" aria-labelledby="label3 label4"></span>
+
+ <span id="descr1">description</span>
+ <span role="checkbox" id="checkbox4" aria-describedby="descr1"></span>
+
+ <span id="descr2">description1</span>
+ <span id="descr3">description2</span>
+ <span role="checkbox" id="checkbox5" aria-describedby="descr2 descr3"></span>
+
+ <div role="treeitem" id="treeitem1">Yellow</div>
+ <div role="treeitem" id="treeitem2">Orange</div>
+ <div id="tree" role="tree" aria-owns="treeitem1 treeitem2">
+ <div role="treeitem" id="treeitem3">Blue</div>
+ <div role="treeitem" id="treeitem4" aria-level="1">Green</div>
+ <div role="treeitem" id="treeitem5" aria-level="2">Light green</div>
+ <div role="treeitem" id="treeitem6" aria-level="1">Green2</div>
+ <div role="group">
+ <div role="treeitem" id="treeitem7">Super light green</div>
+ </div>
+ </div>
+
+ <div role="grid" id="simplegrid">
+ <div role="row" id="simplegrid-row1" aria-level="1">
+ <div role="gridcell">cell 1,1</div>
+ <div role="gridcell">cell 1,2</div>
+ </div>
+ <div role="row" id="simplegrid-row2" aria-level="1">
+ <div role="gridcell">cell 2,1</div>
+ <div role="gridcell">cell 2,2</div>
+ </div>
+ <div role="row" id="simplegrid-row3" aria-level="2">
+ <div role="gridcell">cell 3,1</div>
+ <div role="gridcell">cell 3,2</div>
+ </div>
+ </div>
+
+ <ul role="tree" id="tree2">
+ <li role="treeitem" id="tree2_ti1">Item 1
+ <ul role="group">
+ <li role="treeitem" id="tree2_ti1a">Item 1A</li>
+ <li role="treeitem" id="tree2_ti1b">Item 1B</li>
+ </ul>
+ </li>
+ </ul>
+
+ <div role="tree" id="tree3">
+ <div tabindex="0">
+ <div role="treeitem" id="tree3_treeitem1">1</div>
+ </div>
+ <div tabindex="0">
+ <div role="group">
+ <div role="treeitem" id="tree3_treeitem1.1">1.1</div>
+ <div role="treeitem" id="tree3_treeitem1.2">1.2</div>
+ </div>
+ </div>
+ </div>
+
+ <div role="treegrid" id="treegrid">
+ <div role="row" id="treegridrow1">
+ <span role="gridcell">cell1</span><span role="gridcell">cell2</span>
+ </div>
+ <div role="row" id="treegridrow2" aria-level="1">
+ <span role="gridcell">cell3</span><span role="gridcell">cell4</span>
+ </div>
+ <div role="row" id="treegridrow3" aria-level="2">
+ <span role="gridcell">cell5</span><span role="gridcell">cell6</span>
+ </div>
+ </div>
+
+ <div role="list" id="list">
+ <div role="listitem" id="listitem1">Item 1
+ <div role="group">
+ <div role="listitem" id="listitem1.1">Item 1A</div>
+ <div role="listitem" id="listitem1.2">Item 1B</div>
+ </div>
+ </div>
+ </div>
+
+ <div role="tree" id="tree4">
+ <div role="treeitem" id="tree4_treeitem1">1
+ <div tabindex="0">
+ <div role="group">
+ <div role="treeitem" id="tree4_treeitem1.1">1.1</div>
+ <div role="treeitem" id="tree4_treeitem1.2">1.2</div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div role="list" id="list2">
+ <div role="listitem" id="list2_listitem1">1
+ <div tabindex="0">
+ <div role="group">
+ <div role="listitem" id="list2_listitem1.1">1.1</div>
+ <div role="listitem" id="list2_listitem1.2">1.2</div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <iframe id="iframe"></iframe>
+
+ <div id="tablist" role="tablist">
+ <div id="tab" role="tab" aria-controls="tabpanel">tab</div>
+ </div>
+ <div id="tabpanel" role="tabpanel">tabpanel</div>
+
+ <div id="lr1" aria-live="assertive">1</div>
+ <div id="lr2" aria-live="assertive">a</div>
+ <input type="button" id="button" aria-controls="lr1 lr2"
+ onclick="getNode('lr1').textContent += '1'; getNode('lr2').textContent += 'a';"/>
+
+ <div id="atomic" aria-atomic="true">live region</div>
+
+ <span id="flowto" aria-flowto="flowfrom">flow to</span>
+ <span id="flowfrom">flow from</span>
+
+ <span id="flowto1" aria-flowto="flowfrom1 flowfrom2">flow to</span>
+ <span id="flowfrom1">flow from</span>
+ <span id="flowfrom2">flow from</span>
+
+ <form id="form">
+ <input id="input" />
+ <input id="input2" />
+ <input type="submit" id="submit" />
+ <output id="output" style="display:block" for="input input2"></output>
+ <output id="output2" for="input input2"></output>
+ </form>
+
+ <table id="table">
+ <caption id="caption">tabple caption</caption>
+ <tr>
+ <td>cell1</td><td>cell2</td>
+ </tr>
+ </table>
+
+ <fieldset id="fieldset">
+ <legend id="legend">legend</legend>
+ <input />
+ </fieldset>
+
+ <input id="has_details" aria-details="details"><section id="details"></section>
+ <input id="has_multiple_details" aria-details="details2 details3"><section id="details2"></section><section id="details3"></section>
+ <input id="has_error" aria-errormessage="error"><section id="error"></section>
+</body>
+</html>
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 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="nsIAccessible::getAccessibleRelated() tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../relations.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function doTest()
+ {
+ // xul:label@control
+ testRelation("label1", RELATION_LABEL_FOR, "checkbox1");
+ testRelation("checkbox1", RELATION_LABELLED_BY, "label1");
+
+ // xul:label@control, multiple
+ testRelation("label1_1", RELATION_LABEL_FOR, "checkbox1_1");
+ testRelation("label1_2", RELATION_LABEL_FOR, "checkbox1_1");
+ testRelation("checkbox1_1", RELATION_LABELLED_BY,
+ [ "label1_1", "label1_2" ]);
+
+ // aria-labelledby
+ testRelation("label2", RELATION_LABEL_FOR, "checkbox2");
+ testRelation("checkbox2", RELATION_LABELLED_BY, "label2");
+
+ // aria-labelledby, multiple relations
+ testRelation("label3", RELATION_LABEL_FOR, "checkbox3");
+ testRelation("label4", RELATION_LABEL_FOR, "checkbox3");
+ testRelation("checkbox3", RELATION_LABELLED_BY, ["label3", "label4"]);
+
+ // xul:label@control referring to HTML element
+ testRelation("label_input", RELATION_LABEL_FOR, "input");
+ testRelation("input", RELATION_LABELLED_BY, "label_input");
+
+ // aria-describedby
+ testRelation("descr1", RELATION_DESCRIPTION_FOR, "checkbox4");
+ testRelation("checkbox4", RELATION_DESCRIBED_BY, "descr1");
+
+ // aria-describedby, multiple relations
+ testRelation("descr2", RELATION_DESCRIPTION_FOR, "checkbox5");
+ testRelation("descr3", RELATION_DESCRIPTION_FOR, "checkbox5");
+ testRelation("checkbox5", RELATION_DESCRIBED_BY, ["descr2", "descr3"]);
+
+ // xul:description@control
+ testRelation("descr4", RELATION_DESCRIPTION_FOR, "checkbox6");
+ testRelation("checkbox6", RELATION_DESCRIBED_BY, "descr4");
+
+ // xul:description@control, multiple
+ testRelation("descr5", RELATION_DESCRIPTION_FOR, "checkbox7");
+ testRelation("descr6", RELATION_DESCRIPTION_FOR, "checkbox7");
+ testRelation("checkbox7", RELATION_DESCRIBED_BY,
+ [ "descr5", "descr6" ]);
+
+ // aria_owns, multiple relations
+ testRelation("treeitem1", RELATION_NODE_CHILD_OF, "tree");
+ testRelation("treeitem2", RELATION_NODE_CHILD_OF, "tree");
+
+ // 'node child of' relation for outlineitem role
+ testRelation("treeitem3", RELATION_NODE_CHILD_OF, "tree");
+ testRelation("treeitem4", RELATION_NODE_CHILD_OF, "tree");
+ testRelation("treeitem5", RELATION_NODE_CHILD_OF, "treeitem4");
+
+ // no relation node_child_of for accessible contained in an unexpected
+ // parent
+ testRelation("treeitem6", RELATION_NODE_CHILD_OF, null);
+
+ // 'node child of' relation for the document having window, returns
+ // direct accessible parent (fixed in bug 419770).
+ var iframeElmObj = {};
+ var iframeAcc = getAccessible("iframe", null, iframeElmObj);
+ var iframeDoc = iframeElmObj.value.contentDocument;
+ var iframeDocAcc = getAccessible(iframeDoc);
+ testRelation(iframeDocAcc, RELATION_NODE_CHILD_OF, iframeAcc);
+
+ // aria-controls
+ getAccessible("tab");
+ todo(false,
+ "Getting an accessible tab, otherwise relations for tabpanel aren't cached. Bug 606924 will fix that.");
+ testRelation("tabpanel", RELATION_CONTROLLED_BY, "tab");
+ testRelation("tab", RELATION_CONTROLLER_FOR, "tabpanel");
+
+ // aria-controls, multiple relations
+ testRelation("lr1", RELATION_CONTROLLED_BY, "button");
+ testRelation("lr2", RELATION_CONTROLLED_BY, "button");
+ testRelation("button", RELATION_CONTROLLER_FOR, ["lr1", "lr2"]);
+
+ // aria-flowto
+ testRelation("flowto", RELATION_FLOWS_TO, "flowfrom");
+ testRelation("flowfrom", RELATION_FLOWS_FROM, "flowto");
+
+ // aria-flowto, multiple relations
+ testRelation("flowto1", RELATION_FLOWS_TO, ["flowfrom1", "flowfrom2"]);
+ testRelation("flowfrom1", RELATION_FLOWS_FROM, "flowto1");
+ testRelation("flowfrom2", RELATION_FLOWS_FROM, "flowto1");
+
+ // 'labelled by'/'label for' relation for xul:groupbox and xul:label
+ var groupboxAcc = getAccessible("groupbox");
+ var labelAcc = groupboxAcc.firstChild;
+ testRelation(labelAcc, RELATION_LABEL_FOR, groupboxAcc);
+ testRelation(groupboxAcc, RELATION_LABELLED_BY, labelAcc);
+
+ // 'labelled by'/'label for' relations for xul:tab and xul:tabpanel
+ // (fixed in bug 366527)
+ testRelation("tabpanel1", RELATION_LABELLED_BY, "tab1");
+ testRelation("tab1", RELATION_LABEL_FOR, "tabpanel1");
+ testRelation("tabpanel2", RELATION_LABELLED_BY, "tab2");
+ testRelation("tab2", RELATION_LABEL_FOR, "tabpanel2");
+ testRelation("tabpanel3", RELATION_LABELLED_BY, "tab3");
+ testRelation("tab3", RELATION_LABEL_FOR, "tabpanel3");
+
+ // finish test
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <vbox style="overflow: auto;" flex="1">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=475298"
+ title="mochitests for accessible relations">
+ Mozilla Bug 475298
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673389"
+ title="node_child_of on an item not in a proper container">
+ Mozilla Bug 67389
+ </a><br/>
+
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <label id="label1" control="checkbox1">label</label>
+ <checkbox id="checkbox1"/>
+
+ <label id="label1_1" control="checkbox1_1">label</label>
+ <label id="label1_2" control="checkbox1_1">label</label>
+ <checkbox id="checkbox1_1"/>
+
+ <description id="label2">label</description>
+ <description role="checkbox" id="checkbox2" aria-labelledby="label2"/>
+
+ <description id="label3">label</description>
+ <description id="label4">label</description>
+ <description role="checkbox" id="checkbox3"
+ aria-labelledby="label3 label4"/>
+
+ <label id="label_input" control="input">label</label>
+ <html:input id="input"/>
+
+ <description id="descr1">description</description>
+ <description role="checkbox" id="checkbox4" aria-describedby="descr1"/>
+
+ <description id="descr2">label</description>
+ <description id="descr3">label</description>
+ <description role="checkbox" id="checkbox5"
+ aria-describedby="descr2 descr3"/>
+
+ <description id="descr4" control="checkbox6">description</description>
+ <checkbox id="checkbox6"/>
+
+ <description id="descr5" control="checkbox7">description</description>
+ <description id="descr6" control="checkbox7">description</description>
+ <checkbox id="checkbox7"/>
+
+ <description role="treeitem" id="treeitem1">Yellow</description>
+ <description role="treeitem" id="treeitem2">Orange</description>
+ <vbox id="tree" role="tree" aria-owns="treeitem1 treeitem2">
+ <description role="treeitem" id="treeitem3">Blue</description>
+ <description role="treeitem" id="treeitem4" aria-level="1">Green</description>
+ <description role="treeitem" id="treeitem5" aria-level="2">Light green</description>
+ </vbox>
+
+ <description role="treeitem" id="treeitem6">Dark green</description>
+
+ <iframe id="iframe"/>
+
+ <hbox id="tablist" role="tablist">
+ <description id="tab" role="tab" aria-controls="tabpanel">tab</description>
+ </hbox>
+ <description id="tabpanel" role="tabpanel">tabpanel</description>
+
+ <description id="lr1" aria-live="assertive">1</description>
+ <description id="lr2" aria-live="assertive">a</description>
+ <button id="button" aria-controls="lr1 lr2" label="button"
+ oncommand="getNode('lr1').textContent += '1'; getNode('lr2').textContent += 'a';"/>
+
+ <description id="flowto1" aria-flowto="flowfrom1 flowfrom2">flow to</description>
+ <description id="flowfrom1">flow from</description>
+ <description id="flowfrom2">flow from</description>
+
+ <description id="flowto" aria-flowto="flowfrom">flow to</description>
+ <description id="flowfrom">flow from</description>
+
+ <groupbox id="groupbox">
+ <label value="caption"/>
+ </groupbox>
+
+ <tabbox>
+ <tabs>
+ <tab label="tab1" id="tab1"/>
+ <tab label="tab2" id="tab2" linkedpanel="tabpanel2"/>
+ <tab label="tab3" id="tab3" linkedpanel="tabpanel3"/>
+ </tabs>
+ <tabpanels>
+ <tabpanel id="tabpanel1">
+ <description>tabpanel1</description>
+ </tabpanel>
+ <tabpanel id="tabpanel3">
+ <description>tabpanel3</description>
+ </tabpanel>
+ <tabpanel id="tabpanel2">
+ <description>tabpanel2</description>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+
+ </vbox>
+</window>
diff --git a/accessible/tests/mochitest/relations/test_groupInfoUpdate.html b/accessible/tests/mochitest/relations/test_groupInfoUpdate.html
new file mode 100644
index 0000000000..efca27617c
--- /dev/null
+++ b/accessible/tests/mochitest/relations/test_groupInfoUpdate.html
@@ -0,0 +1,57 @@
+<html>
+<head>
+ <title>Test accessible relations when AccGroupInfo updated</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../relations.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ async function doTests() {
+ info("Testing NODE_CHILD_OF update after DOM removal");
+ testRelation("l1i2", RELATION_NODE_CHILD_OF, "l1i1");
+ let reorder = waitForEvent(EVENT_REORDER, "l1");
+ getNode("l1i1").remove();
+ await reorder;
+ testRelation("l1i2", RELATION_NODE_CHILD_OF, "l1");
+
+ info("Testing NODE_CHILD_OF update after aria-owns removal");
+ testRelation("l2i2", RELATION_NODE_CHILD_OF, "l2i1");
+ reorder = waitForEvent(EVENT_REORDER, "l2");
+ // Move l2i1 out of l2 using aria-owns.
+ getNode("l2trash").setAttribute("aria-owns", "l2i1");
+ await reorder;
+ testRelation("l2i2", RELATION_NODE_CHILD_OF, "l2");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body id="body">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="l1" role="list">
+ <div id="l1i1" role="listitem" aria-level="1">a</div>
+ <div id="l1i2" role="listitem" aria-level="2">b</div>
+ </div>
+
+ <div id="l2" role="list">
+ <div id="l2i1" role="listitem" aria-level="1">a</div>
+ <div id="l2i2" role="listitem" aria-level="2">b</div>
+ </div>
+ <div id="l2trash"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/relations/test_shadowdom.html b/accessible/tests/mochitest/relations/test_shadowdom.html
new file mode 100644
index 0000000000..adb9490d99
--- /dev/null
+++ b/accessible/tests/mochitest/relations/test_shadowdom.html
@@ -0,0 +1,58 @@
+<html>
+
+<head>
+ <title>Explicit content and shadow DOM content relations tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../relations.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // explicit content
+ let label = document.getElementById("label");
+ let element = document.getElementById("element");
+ testRelation(label, RELATION_LABEL_FOR, element);
+ testRelation(element, RELATION_LABELLED_BY, label);
+
+ // shadow DOM content
+ let shadowRoot = document.getElementById("shadowcontainer").shadowRoot;
+ let shadowLabel = shadowRoot.getElementById("label");
+ let shadowElement = shadowRoot.getElementById("element");
+
+ testRelation(shadowLabel, RELATION_LABEL_FOR, shadowElement);
+ testRelation(shadowElement, RELATION_LABELLED_BY, shadowLabel);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ addA11yLoadEvent(doTest, window);
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content">
+ <div id="label"></div>
+ <div id="element" aria-labelledby="label"></div>
+ <div id="shadowcontainer"></div>
+ <script>
+ let shadowRoot = document.getElementById("shadowcontainer").
+ attachShadow({mode: "open"});
+ shadowRoot.innerHTML =
+ `<div id="label"></div><div id="element" aria-labelledby="label"></div>`;
+ </script>
+ </div>
+ <pre id="test">
+ </pre>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/relations/test_tabbrowser.xhtml b/accessible/tests/mochitest/relations/test_tabbrowser.xhtml
new file mode 100644
index 0000000000..3356bc6140
--- /dev/null
+++ b/accessible/tests/mochitest/relations/test_tabbrowser.xhtml
@@ -0,0 +1,109 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL tabbrowser relation tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../relations.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../browser.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Invoker
+ function testTabRelations()
+ {
+ this.eventSeq = [
+ new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, tabDocumentAt, 0),
+ new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, tabDocumentAt, 1)
+ ];
+
+ this.invoke = function testTabRelations_invoke()
+ {
+ var docURIs = ["about:license", "about:mozilla"];
+ tabBrowser().loadTabs(docURIs, {
+ inBackground: false,
+ replace: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ // Flush layout, so as to guarantee that the a11y tree is constructed.
+ browserDocument().documentElement.getBoundingClientRect();
+ }
+
+ this.finalCheck = function testTabRelations_finalCheck(aEvent)
+ {
+ ////////////////////////////////////////////////////////////////////////
+ // 'labelled by'/'label for' relations for xul:tab and xul:tabpanel
+
+ var tabs = Array.from(tabBrowser().tabContainer.allTabs);
+ // For preloaded tabs, there might be items in this array where this relation
+ // doesn't hold, so just deal with that:
+ var panels = tabs.map(t => t.linkedBrowser.closest("tabpanels > *"));
+
+ testRelation(panels[0], RELATION_LABELLED_BY, tabs[0]);
+ testRelation(tabs[0], RELATION_LABEL_FOR, panels[0]);
+ testRelation(panels[1], RELATION_LABELLED_BY, tabs[1]);
+ testRelation(tabs[1], RELATION_LABEL_FOR, panels[1]);
+ }
+
+ this.getID = function testTabRelations_getID()
+ {
+ return "relations of tabs";
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTest()
+ {
+ // Load documents into tabs and wait for DocLoadComplete events caused by
+ // these documents load before we start the test.
+
+ gQueue = new eventQueue();
+
+ gQueue.push(new testTabRelations());
+ gQueue.onFinish = function() { closeBrowserWindow(); }
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ openBrowserWindow(doTest);
+ ]]>
+ </script>
+
+ <vbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=552944"
+ title="No relationship between tabs and associated property page in new tabbrowser construct">
+ Mozilla Bug 552944
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="eventdump"></vbox>
+ </vbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/relations/test_tree.xhtml b/accessible/tests/mochitest/relations/test_tree.xhtml
new file mode 100644
index 0000000000..7c309f0956
--- /dev/null
+++ b/accessible/tests/mochitest/relations/test_tree.xhtml
@@ -0,0 +1,105 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL tree relations tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../relations.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ function doTest()
+ {
+ var treeNode = getNode("tree");
+
+ var tree = getAccessible(treeNode);
+ var treeitem1 = tree.firstChild.nextSibling;
+ testRelation(treeitem1, RELATION_NODE_CHILD_OF, [tree]);
+
+ var treeitem2 = treeitem1.nextSibling;
+ testRelation(treeitem2, RELATION_NODE_CHILD_OF, [tree]);
+
+ var treeitem3 = treeitem2.nextSibling;
+ testRelation(treeitem3, RELATION_NODE_CHILD_OF, [treeitem2]);
+
+ var treeitem4 = treeitem3.nextSibling;
+ testRelation(treeitem4, RELATION_NODE_CHILD_OF, [treeitem2]);
+
+ var treeitem5 = treeitem4.nextSibling;
+ testRelation(treeitem5, RELATION_NODE_CHILD_OF, [tree]);
+
+ var treeitem6 = treeitem5.nextSibling;
+ testRelation(treeitem6, RELATION_NODE_CHILD_OF, [tree]);
+
+ testRelation(tree, RELATION_NODE_PARENT_OF,
+ [treeitem1, treeitem2, treeitem5, treeitem6]);
+ testRelation(treeitem2, RELATION_NODE_PARENT_OF,
+ [treeitem3, treeitem4]);
+
+ // treeitems and treecells shouldn't pick up relations from tree
+ testRelation(treeitem1, RELATION_LABELLED_BY, null);
+ testRelation(treeitem1.firstChild, RELATION_LABELLED_BY, null);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yXULTreeLoadEvent(doTest, "tree", new nsTreeTreeView());
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727"
+ title="Reorganize implementation of XUL tree accessibility">
+ Bug 503727
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=527461"
+ title="Implement RELATION_NODE_PARENT_OF">
+ Bug 527461
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=691248"
+ title="XUL tree items shouldn't pick up relations from XUL tree">
+ Bug 691248
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <label control="tree" value="It's a tree"/>
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col" flex="1" primary="true" label="column"/>
+ <treecol id="col2" flex="1" label="column2"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <vbox id="debug"/>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/relations/test_ui_modalprompt.html b/accessible/tests/mochitest/relations/test_ui_modalprompt.html
new file mode 100644
index 0000000000..a05b273d86
--- /dev/null
+++ b/accessible/tests/mochitest/relations/test_ui_modalprompt.html
@@ -0,0 +1,111 @@
+<html>
+
+<head>
+ <title>Modal prompts</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../relations.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../browser.js"></script>
+
+ <script type="application/javascript">
+ SpecialPowers.pushPrefEnv({
+ set: [["prompts.contentPromptSubDialog", false]],
+ });
+ function showAlert() {
+ this.eventSeq = [
+ {
+ type: EVENT_SHOW,
+ match(aEvent) {
+ return aEvent.accessible.role == ROLE_DIALOG;
+ },
+ },
+ ];
+
+ this.invoke = function showAlert_invoke() {
+ window.setTimeout(
+ function() {
+ currentTabDocument().defaultView.alert("hello");
+ }, 0);
+ };
+
+ this.check = function showAlert_finalCheck(aEvent) {
+ if(aEvent.type === EVENT_HIDE) {
+ return;
+ }
+ var dialog = aEvent.accessible.DOMNode;
+ var info = dialog.querySelector(".tabmodalprompt-infoBody");
+ testRelation(info, RELATION_DESCRIPTION_FOR, dialog);
+ testRelation(dialog, RELATION_DESCRIBED_BY, info);
+ };
+
+ this.getID = function showAlert_getID() {
+ return "show alert";
+ };
+ }
+
+ function closeAlert() {
+ this.eventSeq = [
+ {
+ type: EVENT_HIDE,
+ match(aEvent) {
+ return aEvent.accessible.role == ROLE_DIALOG;
+ },
+ },
+ ];
+
+ this.invoke = function showAlert_invoke() {
+ synthesizeKey("VK_RETURN", {}, browserWindow());
+ };
+
+ this.getID = function showAlert_getID() {
+ return "cleanup alert";
+ };
+ }
+
+
+ // gA11yEventDumpToConsole = true; // debug
+
+ var gQueue = null;
+ function doTests() {
+ gQueue = new eventQueue();
+ gQueue.push(new showAlert());
+ gQueue.push(new closeAlert());
+ gQueue.onFinish = function() {
+ closeBrowserWindow();
+ };
+ gQueue.invoke(); // will call SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ openBrowserWindow(doTests);
+ </script>
+
+</head>
+
+<body id="body">
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=661293"
+ title="The tabmodalprompt dialog's prompt label doesn't get the text properly associated for accessibility">
+ Mozilla Bug 661293
+ </a>
+ <br>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/relations/test_update.html b/accessible/tests/mochitest/relations/test_update.html
new file mode 100644
index 0000000000..581d592bec
--- /dev/null
+++ b/accessible/tests/mochitest/relations/test_update.html
@@ -0,0 +1,213 @@
+<html>
+
+<head>
+ <title>Test updating of accessible relations</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../relations.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function testRelated(aRelAttr, aHostRelation, aDependentRelation,
+ aHostID, aHostNodeID, aDependent1ID, aDependent2ID) {
+ // no attribute
+ testRelation(aDependent1ID, aDependentRelation, null);
+ testRelation(aDependent2ID, aDependentRelation, null);
+ if (aHostRelation)
+ testRelation(aHostID, aHostRelation, null);
+
+ // set attribute
+ getNode(aHostNodeID).setAttribute(aRelAttr, aDependent1ID);
+ testRelation(aDependent1ID, aDependentRelation, aHostID);
+ testRelation(aDependent2ID, aDependentRelation, null);
+ if (aHostRelation)
+ testRelation(aHostID, aHostRelation, aDependent1ID);
+
+ // change attribute
+ getNode(aHostNodeID).setAttribute(aRelAttr, aDependent2ID);
+ testRelation(aDependent1ID, aDependentRelation, null);
+ testRelation(aDependent2ID, aDependentRelation, aHostID);
+ if (aHostRelation)
+ testRelation(aHostID, aHostRelation, aDependent2ID);
+
+ // remove attribute
+ getNode(aHostNodeID).removeAttribute(aRelAttr);
+ testRelation(aDependent1ID, aDependentRelation, null);
+ testRelation(aDependent2ID, aDependentRelation, null);
+ if (aHostRelation)
+ testRelation(aHostID, aHostRelation, null);
+ }
+
+ function insertRelated(aHostRelAttr, aDependentID, aInsertHostFirst,
+ aHostRelation, aDependentRelation) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, document),
+ ];
+
+ this.invoke = function insertRelated_invoke() {
+ this.hostNode = document.createElement("div");
+ this.hostNode.setAttribute(aHostRelAttr, aDependentID);
+
+ this.dependentNode = document.createElement("div");
+ this.dependentNode.setAttribute("id", aDependentID);
+
+ if (aInsertHostFirst) {
+ document.body.appendChild(this.hostNode);
+ document.body.appendChild(this.dependentNode);
+ } else {
+ document.body.appendChild(this.dependentNode);
+ document.body.appendChild(this.hostNode);
+ }
+ };
+
+ this.finalCheck = function insertRelated_finalCheck() {
+ testRelation(this.dependentNode, aDependentRelation, this.hostNode);
+ if (aHostRelation)
+ testRelation(this.hostNode, aHostRelation, this.dependentNode);
+ };
+
+ this.getID = function insertRelated_getID() {
+ return "Insert " + aHostRelAttr + "='" + aDependentID + "' node" +
+ (aInsertHostFirst ? " before" : "after") + " dependent node";
+ };
+ }
+
+ /**
+ * Relative accessible recreation shouldn't break accessible relations.
+ * Note: modify this case if the invoke function doesn't change accessible
+ * tree due to changes in layout module. It can be changed on any case
+ * when accessibles are recreated.
+ */
+ function recreateRelatives(aContainerID, aLabelID, aElmID) {
+ this.containerNode = getNode(aContainerID);
+ this.container = getNode(this.containerNode);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.container),
+ new invokerChecker(EVENT_SHOW, this.containerNode),
+ ];
+
+ this.invoke = function recreateRelatives_invoke() {
+ testRelation(aLabelID, RELATION_LABEL_FOR, aElmID);
+ testRelation(aElmID, RELATION_LABELLED_BY, aLabelID);
+
+ this.containerNode.setAttribute('role', 'group');
+ };
+
+ this.finalCheck = function recreateRelatives_finalCheck() {
+ testRelation(aLabelID, RELATION_LABEL_FOR, aElmID);
+ testRelation(aElmID, RELATION_LABELLED_BY, aLabelID);
+ };
+
+ this.getID = function recreateRelatives_getID() {
+ return "recreate relatives ";
+ };
+ }
+
+ // gA11yEventDumpToConsole = true; // debug
+
+ var gQueue = null;
+
+ function doTest() {
+ // Relation updates on ARIA attribute changes.
+ testRelated("aria-labelledby",
+ RELATION_LABELLED_BY, RELATION_LABEL_FOR,
+ "host", "host", "dependent1", "dependent2");
+
+ testRelated("aria-describedby",
+ RELATION_DESCRIBED_BY, RELATION_DESCRIPTION_FOR,
+ "host", "host", "dependent1", "dependent2");
+
+ testRelated("aria-controls",
+ RELATION_CONTROLLER_FOR, RELATION_CONTROLLED_BY,
+ "host", "host", "dependent1", "dependent2");
+
+ testRelated("aria-flowto",
+ RELATION_FLOWS_TO, RELATION_FLOWS_FROM,
+ "host", "host", "dependent1", "dependent2");
+
+ // Document relation updates on ARIA attribute change.
+ testRelated("aria-labelledby",
+ RELATION_LABELLED_BY, RELATION_LABEL_FOR,
+ document, "body", "dependent1", "dependent2");
+
+ // Insert related accessibles into tree.
+ gQueue = new eventQueue();
+ gQueue.push(new insertRelated("aria-labelledby", "dependent3", true,
+ RELATION_LABELLED_BY, RELATION_LABEL_FOR));
+ gQueue.push(new insertRelated("aria-labelledby", "dependent4", false,
+ RELATION_LABELLED_BY, RELATION_LABEL_FOR));
+
+ gQueue.push(new insertRelated("aria-describedby", "dependent5", true,
+ RELATION_DESCRIBED_BY,
+ RELATION_DESCRIPTION_FOR));
+ gQueue.push(new insertRelated("aria-describedby", "dependent6", false,
+ RELATION_DESCRIBED_BY,
+ RELATION_DESCRIPTION_FOR));
+
+ gQueue.push(new insertRelated("aria-controls", "dependent9", true,
+ RELATION_CONTROLLER_FOR,
+ RELATION_CONTROLLED_BY));
+ gQueue.push(new insertRelated("aria-controls", "dependent10", false,
+ RELATION_CONTROLLER_FOR,
+ RELATION_CONTROLLED_BY));
+
+ gQueue.push(new insertRelated("aria-flowto", "dependent11", true,
+ RELATION_FLOWS_TO, RELATION_FLOWS_FROM));
+ gQueue.push(new insertRelated("aria-flowto", "dependent12", false,
+ RELATION_FLOWS_TO, RELATION_FLOWS_FROM));
+
+ // Update relations when accessibles are recreated
+ gQueue.push(new recreateRelatives("container", "label", "input"));
+
+ gQueue.invoke(); // will call SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body id="body">
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=573469"
+ title="Cache relations defined by ARIA attributes">
+ Mozilla Bug 573469
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=631068"
+ title="Accessible recreation breaks relations">
+ Mozilla Bug 631068
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=635346"
+ title="Allow relations for document defined on document content">
+ Mozilla Bug 635346
+ </a>
+ <br>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="dependent1">label</div>
+ <div id="dependent2">label2</div>
+ <div role="checkbox" id="host"></div>
+
+ <form id="container" style="overflow: hidden;">
+ <label for="input" id="label">label</label>
+ <input id="input">
+ </form>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/role.js b/accessible/tests/mochitest/role.js
new file mode 100644
index 0000000000..07b820c2ac
--- /dev/null
+++ b/accessible/tests/mochitest/role.js
@@ -0,0 +1,198 @@
+/* import-globals-from common.js */
+
+// //////////////////////////////////////////////////////////////////////////////
+// Role constants
+
+const ROLE_ALERT = nsIAccessibleRole.ROLE_ALERT;
+const ROLE_ARTICLE = nsIAccessibleRole.ROLE_ARTICLE;
+const ROLE_ANIMATION = nsIAccessibleRole.ROLE_ANIMATION;
+const ROLE_APPLICATION = nsIAccessibleRole.ROLE_APPLICATION;
+const ROLE_APP_ROOT = nsIAccessibleRole.ROLE_APP_ROOT;
+const ROLE_BLOCKQUOTE = nsIAccessibleRole.ROLE_BLOCKQUOTE;
+const ROLE_BUTTONMENU = nsIAccessibleRole.ROLE_BUTTONMENU;
+const ROLE_CANVAS = nsIAccessibleRole.ROLE_CANVAS;
+const ROLE_CAPTION = nsIAccessibleRole.ROLE_CAPTION;
+const ROLE_CELL = nsIAccessibleRole.ROLE_CELL;
+const ROLE_CHECKBUTTON = nsIAccessibleRole.ROLE_CHECKBUTTON;
+const ROLE_CHECK_MENU_ITEM = nsIAccessibleRole.ROLE_CHECK_MENU_ITEM;
+const ROLE_CHROME_WINDOW = nsIAccessibleRole.ROLE_CHROME_WINDOW;
+const ROLE_CODE = nsIAccessibleRole.ROLE_CODE;
+const ROLE_COLUMNHEADER = nsIAccessibleRole.ROLE_COLUMNHEADER;
+const ROLE_COMBOBOX = nsIAccessibleRole.ROLE_COMBOBOX;
+const ROLE_COMBOBOX_LIST = nsIAccessibleRole.ROLE_COMBOBOX_LIST;
+const ROLE_COMBOBOX_OPTION = nsIAccessibleRole.ROLE_COMBOBOX_OPTION;
+const ROLE_COMMENT = nsIAccessibleRole.ROLE_COMMENT;
+const ROLE_CONTENT_DELETION = nsIAccessibleRole.ROLE_CONTENT_DELETION;
+const ROLE_CONTENT_INSERTION = nsIAccessibleRole.ROLE_CONTENT_INSERTION;
+const ROLE_DATE_EDITOR = nsIAccessibleRole.ROLE_DATE_EDITOR;
+const ROLE_DEFINITION = nsIAccessibleRole.ROLE_DEFINITION;
+const ROLE_DEFINITION_LIST = nsIAccessibleRole.ROLE_DEFINITION_LIST;
+const ROLE_DETAILS = nsIAccessibleRole.ROLE_DETAILS;
+const ROLE_DIAGRAM = nsIAccessibleRole.ROLE_DIAGRAM;
+const ROLE_DIALOG = nsIAccessibleRole.ROLE_DIALOG;
+const ROLE_DOCUMENT = nsIAccessibleRole.ROLE_DOCUMENT;
+const ROLE_EDITCOMBOBOX = nsIAccessibleRole.ROLE_EDITCOMBOBOX;
+const ROLE_EMPHASIS = nsIAccessibleRole.ROLE_EMPHASIS;
+const ROLE_ENTRY = nsIAccessibleRole.ROLE_ENTRY;
+const ROLE_FIGURE = nsIAccessibleRole.ROLE_FIGURE;
+const ROLE_FOOTNOTE = nsIAccessibleRole.ROLE_FOOTNOTE;
+const ROLE_FLAT_EQUATION = nsIAccessibleRole.ROLE_FLAT_EQUATION;
+const ROLE_FORM = nsIAccessibleRole.ROLE_FORM;
+const ROLE_FORM_LANDMARK = nsIAccessibleRole.ROLE_FORM_LANDMARK;
+const ROLE_GRAPHIC = nsIAccessibleRole.ROLE_GRAPHIC;
+const ROLE_GRID_CELL = nsIAccessibleRole.ROLE_GRID_CELL;
+const ROLE_GROUPING = nsIAccessibleRole.ROLE_GROUPING;
+const ROLE_HEADING = nsIAccessibleRole.ROLE_HEADING;
+const ROLE_IMAGE_MAP = nsIAccessibleRole.ROLE_IMAGE_MAP;
+const ROLE_INTERNAL_FRAME = nsIAccessibleRole.ROLE_INTERNAL_FRAME;
+const ROLE_LABEL = nsIAccessibleRole.ROLE_LABEL;
+const ROLE_LANDMARK = nsIAccessibleRole.ROLE_LANDMARK;
+const ROLE_LINK = nsIAccessibleRole.ROLE_LINK;
+const ROLE_LIST = nsIAccessibleRole.ROLE_LIST;
+const ROLE_LISTBOX = nsIAccessibleRole.ROLE_LISTBOX;
+const ROLE_LISTITEM = nsIAccessibleRole.ROLE_LISTITEM;
+const ROLE_LISTITEM_MARKER = nsIAccessibleRole.ROLE_LISTITEM_MARKER;
+const ROLE_MARK = nsIAccessibleRole.ROLE_MARK;
+const ROLE_MATHML_MATH = nsIAccessibleRole.ROLE_MATHML_MATH;
+const ROLE_MATHML_IDENTIFIER = nsIAccessibleRole.ROLE_MATHML_IDENTIFIER;
+const ROLE_MATHML_NUMBER = nsIAccessibleRole.ROLE_MATHML_NUMBER;
+const ROLE_MATHML_OPERATOR = nsIAccessibleRole.ROLE_MATHML_OPERATOR;
+const ROLE_MATHML_TEXT = nsIAccessibleRole.ROLE_MATHML_TEXT;
+const ROLE_MATHML_STRING_LITERAL = nsIAccessibleRole.ROLE_MATHML_STRING_LITERAL;
+const ROLE_MATHML_GLYPH = nsIAccessibleRole.ROLE_MATHML_GLYPH;
+const ROLE_MATHML_ROW = nsIAccessibleRole.ROLE_MATHML_ROW;
+const ROLE_MATHML_FRACTION = nsIAccessibleRole.ROLE_MATHML_FRACTION;
+const ROLE_MATHML_SQUARE_ROOT = nsIAccessibleRole.ROLE_MATHML_SQUARE_ROOT;
+const ROLE_MATHML_ROOT = nsIAccessibleRole.ROLE_MATHML_ROOT;
+const ROLE_MATHML_FENCED = nsIAccessibleRole.ROLE_MATHML_FENCED;
+const ROLE_MATHML_ENCLOSED = nsIAccessibleRole.ROLE_MATHML_ENCLOSED;
+const ROLE_MATHML_STYLE = nsIAccessibleRole.ROLE_MATHML_STYLE;
+const ROLE_MATHML_SUB = nsIAccessibleRole.ROLE_MATHML_SUB;
+const ROLE_MATHML_SUP = nsIAccessibleRole.ROLE_MATHML_SUP;
+const ROLE_MATHML_SUB_SUP = nsIAccessibleRole.ROLE_MATHML_SUB_SUP;
+const ROLE_MATHML_UNDER = nsIAccessibleRole.ROLE_MATHML_UNDER;
+const ROLE_MATHML_OVER = nsIAccessibleRole.ROLE_MATHML_OVER;
+const ROLE_MATHML_UNDER_OVER = nsIAccessibleRole.ROLE_MATHML_UNDER_OVER;
+const ROLE_MATHML_MULTISCRIPTS = nsIAccessibleRole.ROLE_MATHML_MULTISCRIPTS;
+const ROLE_MATHML_TABLE = nsIAccessibleRole.ROLE_MATHML_TABLE;
+const ROLE_MATHML_LABELED_ROW = nsIAccessibleRole.ROLE_MATHML_LABELED_ROW;
+const ROLE_MATHML_TABLE_ROW = nsIAccessibleRole.ROLE_MATHML_TABLE_ROW;
+const ROLE_MATHML_CELL = nsIAccessibleRole.ROLE_MATHML_CELL;
+const ROLE_MATHML_ACTION = nsIAccessibleRole.ROLE_MATHML_ACTION;
+const ROLE_MATHML_ERROR = nsIAccessibleRole.ROLE_MATHML_ERROR;
+const ROLE_MATHML_STACK = nsIAccessibleRole.ROLE_MATHML_STACK;
+const ROLE_MATHML_LONG_DIVISION = nsIAccessibleRole.ROLE_MATHML_LONG_DIVISION;
+const ROLE_MATHML_STACK_GROUP = nsIAccessibleRole.ROLE_MATHML_STACK_GROUP;
+const ROLE_MATHML_STACK_ROW = nsIAccessibleRole.ROLE_MATHML_STACK_ROW;
+const ROLE_MATHML_STACK_CARRIES = nsIAccessibleRole.ROLE_MATHML_STACK_CARRIES;
+const ROLE_MATHML_STACK_CARRY = nsIAccessibleRole.ROLE_MATHML_STACK_CARRY;
+const ROLE_MATHML_STACK_LINE = nsIAccessibleRole.ROLE_MATHML_STACK_LINE;
+const ROLE_MENUBAR = nsIAccessibleRole.ROLE_MENUBAR;
+const ROLE_MENUITEM = nsIAccessibleRole.ROLE_MENUITEM;
+const ROLE_MENUPOPUP = nsIAccessibleRole.ROLE_MENUPOPUP;
+const ROLE_METER = nsIAccessibleRole.ROLE_METER;
+const ROLE_NAVIGATION = nsIAccessibleRole.ROLE_NAVIGATION;
+const ROLE_NON_NATIVE_DOCUMENT = nsIAccessibleRole.ROLE_NON_NATIVE_DOCUMENT;
+const ROLE_NOTHING = nsIAccessibleRole.ROLE_NOTHING;
+const ROLE_NOTE = nsIAccessibleRole.ROLE_NOTE;
+const ROLE_OPTION = nsIAccessibleRole.ROLE_OPTION;
+const ROLE_OUTLINE = nsIAccessibleRole.ROLE_OUTLINE;
+const ROLE_OUTLINEITEM = nsIAccessibleRole.ROLE_OUTLINEITEM;
+const ROLE_PAGETAB = nsIAccessibleRole.ROLE_PAGETAB;
+const ROLE_PAGETABLIST = nsIAccessibleRole.ROLE_PAGETABLIST;
+const ROLE_PANE = nsIAccessibleRole.ROLE_PANE;
+const ROLE_PARAGRAPH = nsIAccessibleRole.ROLE_PARAGRAPH;
+const ROLE_PARENT_MENUITEM = nsIAccessibleRole.ROLE_PARENT_MENUITEM;
+const ROLE_PASSWORD_TEXT = nsIAccessibleRole.ROLE_PASSWORD_TEXT;
+const ROLE_PROGRESSBAR = nsIAccessibleRole.ROLE_PROGRESSBAR;
+const ROLE_PROPERTYPAGE = nsIAccessibleRole.ROLE_PROPERTYPAGE;
+const ROLE_PUSHBUTTON = nsIAccessibleRole.ROLE_PUSHBUTTON;
+const ROLE_RADIOBUTTON = nsIAccessibleRole.ROLE_RADIOBUTTON;
+const ROLE_RADIO_GROUP = nsIAccessibleRole.ROLE_RADIO_GROUP;
+const ROLE_RADIO_MENU_ITEM = nsIAccessibleRole.ROLE_RADIO_MENU_ITEM;
+const ROLE_REGION = nsIAccessibleRole.ROLE_REGION;
+const ROLE_RICH_OPTION = nsIAccessibleRole.ROLE_RICH_OPTION;
+const ROLE_ROW = nsIAccessibleRole.ROLE_ROW;
+const ROLE_ROWHEADER = nsIAccessibleRole.ROLE_ROWHEADER;
+const ROLE_SCROLLBAR = nsIAccessibleRole.ROLE_SCROLLBAR;
+const ROLE_SECTION = nsIAccessibleRole.ROLE_SECTION;
+const ROLE_SEPARATOR = nsIAccessibleRole.ROLE_SEPARATOR;
+const ROLE_SLIDER = nsIAccessibleRole.ROLE_SLIDER;
+const ROLE_SPINBUTTON = nsIAccessibleRole.ROLE_SPINBUTTON;
+const ROLE_STATICTEXT = nsIAccessibleRole.ROLE_STATICTEXT;
+const ROLE_STATUSBAR = nsIAccessibleRole.ROLE_STATUSBAR;
+const ROLE_STRONG = nsIAccessibleRole.ROLE_STRONG;
+const ROLE_SUBSCRIPT = nsIAccessibleRole.ROLE_SUBSCRIPT;
+const ROLE_SUGGESTION = nsIAccessibleRole.ROLE_SUGGESTION;
+const ROLE_SUPERSCRIPT = nsIAccessibleRole.ROLE_SUPERSCRIPT;
+const ROLE_SUMMARY = nsIAccessibleRole.ROLE_SUMMARY;
+const ROLE_SWITCH = nsIAccessibleRole.ROLE_SWITCH;
+const ROLE_TABLE = nsIAccessibleRole.ROLE_TABLE;
+const ROLE_TERM = nsIAccessibleRole.ROLE_TERM;
+const ROLE_TEXT = nsIAccessibleRole.ROLE_TEXT;
+const ROLE_TEXT_CONTAINER = nsIAccessibleRole.ROLE_TEXT_CONTAINER;
+const ROLE_TEXT_LEAF = nsIAccessibleRole.ROLE_TEXT_LEAF;
+const ROLE_TIME = nsIAccessibleRole.ROLE_TIME;
+const ROLE_TIME_EDITOR = nsIAccessibleRole.ROLE_TIME_EDITOR;
+const ROLE_TOGGLE_BUTTON = nsIAccessibleRole.ROLE_TOGGLE_BUTTON;
+const ROLE_TOOLBAR = nsIAccessibleRole.ROLE_TOOLBAR;
+const ROLE_TOOLTIP = nsIAccessibleRole.ROLE_TOOLTIP;
+const ROLE_TREE_TABLE = nsIAccessibleRole.ROLE_TREE_TABLE;
+const ROLE_WHITESPACE = nsIAccessibleRole.ROLE_WHITESPACE;
+
+// //////////////////////////////////////////////////////////////////////////////
+// Public methods
+
+/**
+ * Test that the role of the given accessible is the role passed in.
+ *
+ * @param aAccOrElmOrID the accessible, DOM element or ID to be tested.
+ * @param aRole The role that is to be expected.
+ */
+function testRole(aAccOrElmOrID, aRole) {
+ var role = getRole(aAccOrElmOrID);
+ is(role, aRole, "Wrong role for " + prettyName(aAccOrElmOrID) + "!");
+}
+
+/**
+ * Return the role of the given accessible. Return -1 if accessible could not
+ * be retrieved.
+ *
+ * @param aAccOrElmOrID [in] The accessible, DOM element or element ID the
+ * accessible role is being requested for.
+ */
+function getRole(aAccOrElmOrID) {
+ var acc = getAccessible(aAccOrElmOrID);
+ if (!acc) {
+ return -1;
+ }
+
+ var role = -1;
+ try {
+ role = acc.role;
+ } catch (e) {
+ ok(false, "Role for " + aAccOrElmOrID + " could not be retrieved!");
+ }
+
+ return role;
+}
+
+/**
+ * Analogy of SimpleTest.is function used to check the role.
+ */
+function isRole(aIdentifier, aRole, aMsg) {
+ var role = getRole(aIdentifier);
+ if (role == -1) {
+ return;
+ }
+
+ if (role == aRole) {
+ ok(true, aMsg);
+ return;
+ }
+
+ var got = roleToString(role);
+ var expected = roleToString(aRole);
+
+ ok(false, aMsg + "got '" + got + "', expected '" + expected + "'");
+}
diff --git a/accessible/tests/mochitest/role/a11y.toml b/accessible/tests/mochitest/role/a11y.toml
new file mode 100644
index 0000000000..8febfc3bd7
--- /dev/null
+++ b/accessible/tests/mochitest/role/a11y.toml
@@ -0,0 +1,19 @@
+[DEFAULT]
+support-files = [
+ "chrome_body_role_alert.xhtml",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/moz.png"]
+
+["test_aria.html"]
+
+["test_aria.xhtml"]
+
+["test_dpub_aria.html"]
+
+["test_general.html"]
+
+["test_general.xhtml"]
+
+["test_graphics_aria.html"]
+
+["test_svg.html"]
diff --git a/accessible/tests/mochitest/role/chrome_body_role_alert.xhtml b/accessible/tests/mochitest/role/chrome_body_role_alert.xhtml
new file mode 100644
index 0000000000..29ff10fb01
--- /dev/null
+++ b/accessible/tests/mochitest/role/chrome_body_role_alert.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <body role="alert">
+ </body>
+</html>
diff --git a/accessible/tests/mochitest/role/test_aria.html b/accessible/tests/mochitest/role/test_aria.html
new file mode 100644
index 0000000000..bd7ffd27fb
--- /dev/null
+++ b/accessible/tests/mochitest/role/test_aria.html
@@ -0,0 +1,729 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test weak ARIA roles</title>
+
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+
+ // To test initial roles on body elements, we need to use an iframe.
+ function testBodyRole(iframeId, role) {
+ let iframe = getNode(iframeId);
+ let doc = iframe.contentDocument;
+ let docAcc = getAccessible(doc);
+ testRole(docAcc, role);
+ }
+
+ async function doTest() {
+ // ARIA role map.
+ testRole("aria_alert", ROLE_ALERT);
+ testRole("aria_alert_mixed", ROLE_ALERT);
+ testRole("aria_alertdialog", ROLE_DIALOG);
+ testRole("aria_alertdialog_mixed", ROLE_DIALOG);
+ testRole("aria_application", ROLE_APPLICATION);
+ testRole("aria_application_mixed", ROLE_APPLICATION);
+ testRole("aria_article", ROLE_ARTICLE);
+ testRole("aria_article_mixed", ROLE_ARTICLE);
+ testRole("aria_blockquote", ROLE_BLOCKQUOTE);
+ testRole("aria_blockquote_mixed", ROLE_BLOCKQUOTE);
+ testRole("aria_button", ROLE_PUSHBUTTON);
+ testRole("aria_button_mixed", ROLE_PUSHBUTTON);
+ testRole("aria_caption", ROLE_CAPTION);
+ testRole("aria_caption_mixed", ROLE_CAPTION);
+ testRole("aria_checkbox", ROLE_CHECKBUTTON);
+ testRole("aria_checkbox_mixed", ROLE_CHECKBUTTON);
+ testRole("aria_code", ROLE_CODE);
+ testRole("aria_code_mixed", ROLE_CODE);
+ testRole("aria_columnheader", ROLE_COLUMNHEADER);
+ testRole("aria_columnheader_mixed", ROLE_COLUMNHEADER);
+ testRole("aria_combobox", ROLE_EDITCOMBOBOX);
+ testRole("aria_combobox_mixed", ROLE_EDITCOMBOBOX);
+ testRole("aria_comment", ROLE_COMMENT);
+ testRole("aria_comment_mixed", ROLE_COMMENT);
+ testRole("aria_deletion", ROLE_CONTENT_DELETION);
+ testRole("aria_deletion_mixed", ROLE_CONTENT_DELETION);
+ testRole("aria_dialog", ROLE_DIALOG);
+ testRole("aria_dialog_mixed", ROLE_DIALOG);
+ testRole("aria_directory", ROLE_LIST);
+ testRole("aria_directory_mixed", ROLE_LIST);
+ testRole("aria_document", ROLE_NON_NATIVE_DOCUMENT);
+ testRole("aria_document_mixed", ROLE_NON_NATIVE_DOCUMENT);
+ testRole("aria_form", ROLE_FORM);
+ testRole("aria_form_mixed", ROLE_FORM);
+ testRole("aria_form_with_label", ROLE_FORM);
+ testRole("aria_form_with_label_mixed", ROLE_FORM);
+ testRole("aria_feed", ROLE_GROUPING);
+ testRole("aria_feed_mixed", ROLE_GROUPING);
+ testRole("aria_figure", ROLE_FIGURE);
+ testRole("aria_figure_mixed", ROLE_FIGURE);
+ testRole("aria_grid", ROLE_TABLE);
+ testRole("aria_grid_mixed", ROLE_TABLE);
+ testRole("aria_gridcell", ROLE_GRID_CELL);
+ testRole("aria_gridcell_mixed", ROLE_GRID_CELL);
+ testRole("aria_group", ROLE_GROUPING);
+ testRole("aria_group_mixed", ROLE_GROUPING);
+ testRole("aria_heading", ROLE_HEADING);
+ testRole("aria_heading_mixed", ROLE_HEADING);
+ testRole("aria_img", ROLE_GRAPHIC);
+ testRole("aria_img_mixed", ROLE_GRAPHIC);
+ testRole("aria_insertion", ROLE_CONTENT_INSERTION);
+ testRole("aria_insertion_mixed", ROLE_CONTENT_INSERTION);
+ testRole("aria_link", ROLE_LINK);
+ testRole("aria_link_mixed", ROLE_LINK);
+ testRole("aria_list", ROLE_LIST);
+ testRole("aria_list_mixed", ROLE_LIST);
+ testRole("aria_listbox", ROLE_LISTBOX);
+ testRole("aria_listbox_mixed", ROLE_LISTBOX);
+ testRole("aria_listitem", ROLE_LISTITEM);
+ testRole("aria_listitem_mixed", ROLE_LISTITEM);
+ testRole("aria_log", ROLE_TEXT); // weak role
+ testRole("aria_log_mixed", ROLE_TEXT); // weak role
+ testRole("aria_mark", ROLE_MARK);
+ testRole("aria_mark_mixed", ROLE_MARK);
+ testRole("aria_marquee", ROLE_ANIMATION);
+ testRole("aria_marquee_mixed", ROLE_ANIMATION);
+ testRole("aria_math", ROLE_FLAT_EQUATION);
+ testRole("aria_math_mixed", ROLE_FLAT_EQUATION);
+ testRole("aria_menu", ROLE_MENUPOPUP);
+ testRole("aria_menu_mixed", ROLE_MENUPOPUP);
+ testRole("aria_menubar", ROLE_MENUBAR);
+ testRole("aria_menubar_mixed", ROLE_MENUBAR);
+ testRole("aria_menuitem", ROLE_MENUITEM);
+ testRole("aria_menuitem_mixed", ROLE_MENUITEM);
+ testRole("aria_menuitemcheckbox", ROLE_CHECK_MENU_ITEM);
+ testRole("aria_menuitemcheckbox_mixed", ROLE_CHECK_MENU_ITEM);
+ testRole("aria_menuitemradio", ROLE_RADIO_MENU_ITEM);
+ testRole("aria_menuitemradio_mixed", ROLE_RADIO_MENU_ITEM);
+ testRole("aria_meter", ROLE_METER);
+ testRole("aria_meter_mixed", ROLE_METER);
+ testRole("aria_note", ROLE_NOTE);
+ testRole("aria_note_mixed", ROLE_NOTE);
+ testRole("aria_paragraph", ROLE_PARAGRAPH);
+ testRole("aria_paragraph_mixed", ROLE_PARAGRAPH);
+ testRole("aria_presentation", ROLE_TEXT); // weak role
+ testRole("aria_presentation_mixed", ROLE_TEXT); // weak role
+ testRole("aria_progressbar", ROLE_PROGRESSBAR);
+ testRole("aria_progressbar_mixed", ROLE_PROGRESSBAR);
+ testRole("aria_radio", ROLE_RADIOBUTTON);
+ testRole("aria_radio_mixed", ROLE_RADIOBUTTON);
+ testRole("aria_radiogroup", ROLE_RADIO_GROUP);
+ testRole("aria_radiogroup_mixed", ROLE_RADIO_GROUP);
+ testRole("aria_region_no_name", ROLE_TEXT);
+ testRole("aria_region_no_name_mixed", ROLE_TEXT);
+ testRole("aria_region_has_label", ROLE_REGION);
+ testRole("aria_region_has_label_mixed", ROLE_REGION);
+ testRole("aria_region_has_labelledby", ROLE_REGION);
+ testRole("aria_region_has_labelledby_mixed", ROLE_REGION);
+ testRole("aria_region_has_title", ROLE_REGION);
+ testRole("aria_region_has_title_mixed", ROLE_REGION);
+ testRole("aria_region_empty_name", ROLE_TEXT);
+ testRole("aria_region_empty_name_mixed", ROLE_TEXT);
+ testRole("aria_region_as_table_with_caption", ROLE_REGION);
+ testRole("aria_region_as_table_with_caption_mixed", ROLE_REGION);
+ testRole("aria_region_as_table_with_miscaption", ROLE_TABLE);
+ testRole("aria_region_as_table_with_miscaption_mixed", ROLE_TABLE);
+ testRole("aria_row", ROLE_ROW);
+ testRole("aria_row_mixed", ROLE_ROW);
+ testRole("aria_rowheader", ROLE_ROWHEADER);
+ testRole("aria_rowheader_mixed", ROLE_ROWHEADER);
+ testRole("aria_scrollbar", ROLE_SCROLLBAR);
+ testRole("aria_scrollbar_mixed", ROLE_SCROLLBAR);
+ testRole("aria_searchbox", ROLE_ENTRY);
+ testRole("aria_searchbox_mixed", ROLE_ENTRY);
+ testRole("aria_separator", ROLE_SEPARATOR);
+ testRole("aria_separator_mixed", ROLE_SEPARATOR);
+ testRole("aria_slider", ROLE_SLIDER);
+ testRole("aria_slider_mixed", ROLE_SLIDER);
+ testRole("aria_spinbutton", ROLE_SPINBUTTON);
+ testRole("aria_spinbutton_mixed", ROLE_SPINBUTTON);
+ testRole("aria_status", ROLE_STATUSBAR);
+ testRole("aria_status_mixed", ROLE_STATUSBAR);
+ testRole("aria_subscript", ROLE_SUBSCRIPT);
+ testRole("aria_subscript_mixed", ROLE_SUBSCRIPT);
+ testRole("aria_suggestion", ROLE_SUGGESTION);
+ testRole("aria_suggestion_mixed", ROLE_SUGGESTION);
+ testRole("aria_superscript", ROLE_SUPERSCRIPT);
+ testRole("aria_superscript_mixed", ROLE_SUPERSCRIPT);
+ testRole("aria_switch", ROLE_SWITCH);
+ testRole("aria_switch_mixed", ROLE_SWITCH);
+ testRole("aria_tab", ROLE_PAGETAB);
+ testRole("aria_tab_mixed", ROLE_PAGETAB);
+ testRole("aria_tablist", ROLE_PAGETABLIST);
+ testRole("aria_tablist_mixed", ROLE_PAGETABLIST);
+ testRole("aria_tabpanel", ROLE_PROPERTYPAGE);
+ testRole("aria_tabpanel_mixed", ROLE_PROPERTYPAGE);
+ testRole("aria_term", ROLE_TERM);
+ testRole("aria_term_mixed", ROLE_TERM);
+ testRole("aria_textbox", ROLE_ENTRY);
+ testRole("aria_textbox_mixed", ROLE_ENTRY);
+ testRole("aria_timer", ROLE_TEXT); // weak role
+ testRole("aria_timer_mixed", ROLE_TEXT); // weak role
+ testRole("aria_toolbar", ROLE_TOOLBAR);
+ testRole("aria_toolbar_mixed", ROLE_TOOLBAR);
+ testRole("aria_tooltip", ROLE_TOOLTIP);
+ testRole("aria_tooltip_mixed", ROLE_TOOLTIP);
+ testRole("aria_tree", ROLE_OUTLINE);
+ testRole("aria_tree_mixed", ROLE_OUTLINE);
+ testRole("aria_treegrid", ROLE_TREE_TABLE);
+ testRole("aria_treegrid_mixed", ROLE_TREE_TABLE);
+ testRole("aria_treeitem", ROLE_OUTLINEITEM);
+ testRole("aria_treeitem_mixed", ROLE_OUTLINEITEM);
+
+ // Note:
+ // The phrase "weak foo" here means that there is no good foo-to-platform
+ // role mapping. Similarly "strong foo" means there is a good foo-to-
+ // platform role mapping.
+
+ testRole("articlemain", ROLE_LANDMARK);
+ testRole("articlemain_mixed", ROLE_LANDMARK);
+ testRole("articleform", ROLE_FORM);
+ testRole("articleform_mixed", ROLE_FORM);
+
+ // Test article exposed as article
+ testRole("testArticle", ROLE_ARTICLE);
+ testRole("testArticle_mixed", ROLE_ARTICLE);
+
+ // weak roles that are forms of "live regions"
+ testRole("log_table", ROLE_TABLE);
+ testRole("log_table_mixed", ROLE_TABLE);
+ testRole("timer_div", ROLE_SECTION);
+ testRole("timer_div_mixed", ROLE_SECTION);
+
+ // other roles that are forms of "live regions"
+ testRole("marquee_h1", ROLE_ANIMATION);
+ testRole("marquee_h1_mixed", ROLE_ANIMATION);
+
+ // strong landmark
+ testRole("application", ROLE_APPLICATION);
+ testRole("application_mixed", ROLE_APPLICATION);
+ testRole("form", ROLE_FORM);
+ testRole("form_mixed", ROLE_FORM);
+ testRole("application_table", ROLE_APPLICATION);
+ testRole("application_table_mixed", ROLE_APPLICATION);
+
+ // landmarks
+ let landmarks = ["banner", "complementary", "contentinfo",
+ "main", "navigation", "search"];
+ for (const l in landmarks) {
+ testRole(landmarks[l], ROLE_LANDMARK);
+ testRole(landmarks[l] + "_mixed", ROLE_LANDMARK);
+ }
+
+ for (const l in landmarks) {
+ let id = landmarks[l] + "_table";
+ testRole(id, ROLE_LANDMARK);
+ testRole(id + "_mixed", ROLE_LANDMARK);
+
+ let accessibleTable = getAccessible(id, [nsIAccessibleTable], null,
+ DONOTFAIL_IF_NO_INTERFACE);
+ ok(!!accessibleTable, "landmarked table should have nsIAccessibleTable");
+
+ accessibleTable = getAccessible(id+"_mixed", [nsIAccessibleTable], null,
+ DONOTFAIL_IF_NO_INTERFACE);
+ ok(!!accessibleTable, "Uppercase landmarked table should have nsIAccessibleTable");
+
+ if (accessibleTable)
+ is(accessibleTable.getCellAt(0, 0).firstChild.name, "hi", "no cell");
+ }
+
+ // ////////////////////////////////////////////////////////////////////////
+ // test gEmptyRoleMap
+ testRole("buttontable_row", ROLE_TEXT_CONTAINER);
+ testRole("buttontable_row_mixed", ROLE_TEXT_CONTAINER);
+ testRole("buttontable_cell", ROLE_TEXT_CONTAINER);
+ testRole("buttontable_cell_mixed", ROLE_TEXT_CONTAINER);
+
+ // abstract roles
+ var abstract_roles = ["composite", "landmark", "structure", "widget",
+ "window", "input", "range", "select", "section",
+ "sectionhead"];
+ for (const a in abstract_roles) {
+ testRole(abstract_roles[a], ROLE_SECTION);
+ testRole(abstract_roles[a]+ "_mixed", ROLE_SECTION);
+ }
+
+ // ////////////////////////////////////////////////////////////////////////
+ // roles transformed by ARIA state attributes
+ testRole("togglebutton", ROLE_TOGGLE_BUTTON);
+ testRole("togglebutton_mixed", ROLE_TOGGLE_BUTTON);
+ testRole("implicit_gridcell", ROLE_GRID_CELL);
+ testRole("implicit_gridcell_mixed", ROLE_GRID_CELL);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ignore unknown roles, take first known
+ testRole("unknown_roles", ROLE_PUSHBUTTON);
+ testRole("unknown_roles_mixed", ROLE_PUSHBUTTON);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // misc roles
+ testRole("note", ROLE_NOTE);
+ testRole("note_mixed", ROLE_NOTE);
+ testRole("scrollbar", ROLE_SCROLLBAR);
+ testRole("scrollbar_mixed", ROLE_SCROLLBAR);
+ testRole("dir", ROLE_LIST);
+ testRole("dir_mixed", ROLE_LIST);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // test document role map update
+ var testDoc = getAccessible(document, [nsIAccessibleDocument]);
+ testRole(testDoc, ROLE_DOCUMENT);
+ document.body.setAttribute("role", "application");
+ testRole(testDoc, ROLE_APPLICATION);
+ document.body.setAttribute("role", "APPLICATION");
+ testRole(testDoc, ROLE_APPLICATION);
+ document.body.setAttribute("role", "dialog");
+ testRole(testDoc, ROLE_DIALOG);
+ document.body.setAttribute("role", "DIALOG");
+ testRole(testDoc, ROLE_DIALOG);
+ // Other roles aren't valid on body elements.
+ document.body.setAttribute("role", "document");
+ testRole(testDoc, ROLE_DOCUMENT);
+ document.body.setAttribute("role", "DOCUMENT");
+ testRole(testDoc, ROLE_DOCUMENT);
+ document.body.setAttribute("role", "button");
+ testRole(testDoc, ROLE_DOCUMENT);
+ document.body.setAttribute("role", "BUTTON");
+ testRole(testDoc, ROLE_DOCUMENT);
+ document.body.setAttribute("role", "main");
+ testRole(testDoc, ROLE_DOCUMENT);
+ document.body.setAttribute("role", "MAIN");
+ testRole(testDoc, ROLE_DOCUMENT);
+
+ // Test equation image
+ testRole("img_eq", ROLE_FLAT_EQUATION);
+ testRole("img_eq_mixed", ROLE_FLAT_EQUATION);
+
+ // Test textual equation
+ testRole("txt_eq", ROLE_FLAT_EQUATION);
+ testRole("txt_eq_mixed", ROLE_FLAT_EQUATION);
+
+ // Test initial ARIA roles on the body element.
+ testBodyRole("iframe_aria_application", ROLE_APPLICATION);
+ testBodyRole("iframe_aria_application_mixed", ROLE_APPLICATION);
+ testBodyRole("iframe_aria_dialog", ROLE_DIALOG);
+ testBodyRole("iframe_aria_dialog_mixed", ROLE_DIALOG);
+ // role="alert" is valid on the body of chrome documents.
+ let win = Services.ww.openWindow(
+ null,
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/role/chrome_body_role_alert.xhtml",
+ "_blank",
+ "chrome",
+ []
+ );
+ await new Promise(resolve => addA11yLoadEvent(resolve, win));
+ testRole(win.document, ROLE_ALERT);
+ win.close();
+ // Other roles aren't valid on body elements.
+ testBodyRole("iframe_aria_document", ROLE_DOCUMENT);
+ testBodyRole("iframe_aria_document_mixed", ROLE_DOCUMENT);
+ testBodyRole("iframe_aria_button", ROLE_DOCUMENT);
+ testBodyRole("iframe_aria_button_mixed", ROLE_DOCUMENT);
+ testBodyRole("iframe_aria_main", ROLE_DOCUMENT);
+ testBodyRole("iframe_aria_main_mixed", ROLE_DOCUMENT);
+ // role="alert" is not valid on the body of content documents.
+ testBodyRole("iframe_aria_alert", ROLE_DOCUMENT);
+ testBodyRole("iframe_aria_alert_mixed", ROLE_DOCUMENT);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=428479">Mozilla Bug 428479</a>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=429666">Mozilla Bug 429666</a>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=481114">Mozilla Bug 481114</a>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=469688">Mozilla Bug 469688</a>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=469688">Mozilla Bug 520188</a>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=529289">Mozilla Bug 529289</a>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=529289">Mozilla Bug 607219</a>
+ <a target="_blank"
+ title="HTML buttons with aria-pressed not exposing IA2 TOGGLE_BUTTON role"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=725432">
+ Bug 725432
+ </a>
+ <a target="_blank"
+ title="Map ARIA role FORM"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=735645">
+ Bug 735645
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1136563"
+ title="Support ARIA 1.1 switch role">
+ Bug 1136563
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1121518"
+ title="Support ARIA 1.1 searchbox role">
+ Bug 1121518
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1356049"
+ title="Map ARIA figure role">
+ Bug 1356049
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <span id="aria_alert" role="alert"></span>
+ <span id="aria_alert_mixed" role="aLERt"></span>
+ <span id="aria_alertdialog" role="alertdialog"></span>
+ <span id="aria_alertdialog_mixed" role="aLERTDIALOg"></span>
+ <span id="aria_application" role="application"></span>
+ <span id="aria_application_mixed" role="aPPLICATIOn"></span>
+ <span id="aria_article" role="article"></span>
+ <span id="aria_article_mixed" role="aRTICLe"></span>
+ <span id="aria_blockquote" role="blockquote"></span>
+ <span id="aria_blockquote_mixed" role="bLOCKQUOTe"></span>
+ <span id="aria_button" role="button"></span>
+ <span id="aria_button_mixed" role="bUTTOn"></span>
+ <span id="aria_caption" role="caption"></span>
+ <span id="aria_caption_mixed" role="cAPTIOn"></span>
+ <span id="aria_checkbox" role="checkbox"></span>
+ <span id="aria_checkbox_mixed" role="cHECKBOx"></span>
+ <span id="aria_code" role="code"></span>
+ <span id="aria_code_mixed" role="cODe"></span>
+ <span id="aria_columnheader" role="columnheader"></span>
+ <span id="aria_columnheader_mixed" role="cOLUMNHEADEr"></span>
+ <span id="aria_combobox" role="combobox"></span>
+ <span id="aria_combobox_mixed" role="cOMBOBOx"></span>
+ <span id="aria_comment" role="comment"></span>
+ <span id="aria_comment_mixed" role="cOMMENt"></span>
+ <span id="aria_deletion" role="deletion"></span>
+ <span id="aria_deletion_mixed" role="dELETIOn"></span>
+ <span id="aria_dialog" role="dialog"></span>
+ <span id="aria_dialog_mixed" role="dIALOg"></span>
+ <span id="aria_directory" role="directory"></span>
+ <span id="aria_directory_mixed" role="dIRECTORy"></span>
+ <span id="aria_document" role="document"></span>
+ <span id="aria_document_mixed" role="dOCUMENt"></span>
+ <span id="aria_form" role="form"></span>
+ <span id="aria_form_mixed" role="fORm"></span>
+ <span id="aria_form_with_label" role="form" aria-label="Label"></span>
+ <span id="aria_form_with_label_mixed" role="fORm" aria-label="Label"></span>
+ <span id="aria_feed" role="feed"></span>
+ <span id="aria_feed_mixed" role="fEEd"></span>
+ <span id="aria_figure" role="figure"></span>
+ <span id="aria_figure_mixed" role="fIGURe"></span>
+ <span id="aria_grid" role="grid"></span>
+ <span id="aria_grid_mixed" role="gRId"></span>
+ <span id="aria_gridcell" role="gridcell"></span>
+ <span id="aria_gridcell_mixed" role="gRIDCELl"></span>
+ <span id="aria_group" role="group"></span>
+ <span id="aria_group_mixed" role="gROUp"></span>
+ <span id="aria_heading" role="heading"></span>
+ <span id="aria_heading_mixed" role="hEADINg"></span>
+ <span id="aria_img" role="img"></span>
+ <span id="aria_img_mixed" role="iMg"></span>
+ <span id="aria_insertion" role="insertion"></span>
+ <span id="aria_insertion_mixed" role="iNSERTIOn"></span>
+ <span id="aria_link" role="link"></span>
+ <span id="aria_link_mixed" role="lINk"></span>
+ <span id="aria_list" role="list"></span>
+ <span id="aria_list_mixed" role="lISt"></span>
+ <span id="aria_listbox" role="listbox"></span>
+ <span id="aria_listbox_mixed" role="lISTBOx"></span>
+ <span id="aria_listitem" role="listitem"></span>
+ <span id="aria_listitem_mixed" role="lISTITEm"></span>
+ <span id="aria_log" role="log"></span>
+ <span id="aria_log_mixed" role="lOg"></span>
+ <span id="aria_mark" role="mark"></span>
+ <span id="aria_mark_mixed" role="mARk"></span>
+ <span id="aria_marquee" role="marquee"></span>
+ <span id="aria_marquee_mixed" role="mARQUEe"></span>
+ <span id="aria_math" role="math"></span>
+ <span id="aria_math_mixed" role="mATh"></span>
+ <span id="aria_menu" role="menu"></span>
+ <span id="aria_menu_mixed" role="mENu"></span>
+ <span id="aria_menubar" role="menubar"></span>
+ <span id="aria_menubar_mixed" role="mENUBAr"></span>
+ <span id="aria_menuitem" role="menuitem"></span>
+ <span id="aria_menuitem_mixed" role="mENUITEm"></span>
+ <span id="aria_menuitemcheckbox" role="menuitemcheckbox"></span>
+ <span id="aria_menuitemcheckbox_mixed" role="mENUITEMCHECKBOx"></span>
+ <span id="aria_menuitemradio" role="menuitemradio"></span>
+ <span id="aria_menuitemradio_mixed" role="mENUITEMRADIo"></span>
+ <span id="aria_meter" role="meter"></span>
+ <span id="aria_meter_mixed" role="meTer"></span>
+ <span id="aria_note" role="note"></span>
+ <span id="aria_note_mixed" role="nOTe"></span>
+ <span id="aria_paragraph" role="paragraph"></span>
+ <span id="aria_paragraph_mixed" role="pARAGRAPh"></span>
+ <span id="aria_presentation" role="presentation" tabindex="0"></span>
+ <span id="aria_presentation_mixed" role="pRESENTATIOn" tabindex="0"></span>
+ <span id="aria_progressbar" role="progressbar"></span>
+ <span id="aria_progressbar_mixed" role="pROGRESSBAr"></span>
+ <span id="aria_radio" role="radio"></span>
+ <span id="aria_radio_mixed" role="rADIo"></span>
+ <span id="aria_radiogroup" role="radiogroup"></span>
+ <span id="aria_radiogroup_mixed" role="rADIOGROUp"></span>
+ <span id="aria_region_no_name" role="region"></span>
+ <span id="aria_region_no_name_mixed" role="rEGIOn"></span>
+ <span id="aria_region_has_label" role="region" aria-label="label"></span>
+ <span id="aria_region_has_label_mixed" role="rEGIOn" aria-label="label"></span>
+ <span id="aria_region_has_labelledby" role="region" aria-labelledby="label"><span id="label" aria-label="label"></span>
+ <span id="aria_region_has_labelledby_mixed" role="rEGIOn" aria-labelledby="label"><span id="label" aria-label="label"></span>
+ <span id="aria_region_has_title" role="region" title="title"></span>
+ <span id="aria_region_has_title_mixed" role="rEGIOn" title="title"></span>
+ <span id="aria_region_empty_name" role="region" aria-label="" title="" aria-labelledby="empty"></span><span id="empty"></span>
+ <span id="aria_region_empty_name_mixed" role="rEGIOn" aria-label="" title="" aria-labelledby="empty"></span><span id="empty"></span>
+ <table id="aria_region_as_table_with_caption" role="region"><caption>hello</caption></table>
+ <table id="aria_region_as_table_with_caption_mixed" role="rEGIOn"><caption>hello</caption></table>
+ <table id="aria_region_as_table_with_miscaption" role="region"><caption role="option">hello</caption></table>
+ <table id="aria_region_as_table_with_miscaption_mixed" role="rEGIOn"><caption role="option">hello</caption></table>
+ <span id="aria_row" role="row"></span>
+ <span id="aria_row_mixed" role="rOw"></span>
+ <span id="aria_rowheader" role="rowheader"></span>
+ <span id="aria_rowheader_mixed" role="rOWHEADEr"></span>
+ <span id="aria_scrollbar" role="scrollbar"></span>
+ <span id="aria_scrollbar_mixed" role="sCROLLBAr"></span>
+ <span id="aria_searchbox" role="textbox"></span>
+ <span id="aria_searchbox_mixed" role="tEXTBOx"></span>
+ <span id="aria_separator" role="separator"></span>
+ <span id="aria_separator_mixed" role="sEPARATOr"></span>
+ <span id="aria_slider" role="slider"></span>
+ <span id="aria_slider_mixed" role="sLIDEr"></span>
+ <span id="aria_spinbutton" role="spinbutton"></span>
+ <span id="aria_spinbutton_mixed" role="sPINBUTTOn"></span>
+ <span id="aria_status" role="status"></span>
+ <span id="aria_status_mixed" role="sTATUs"></span>
+ <span id="aria_subscript" role="subscript"></span>
+ <span id="aria_subscript_mixed" role="sUBSCRIPt"></span>
+ <span id="aria_suggestion" role="suggestion"></span>
+ <span id="aria_suggestion_mixed" role="sUGGESTIOn"></span>
+ <span id="aria_superscript" role="superscript"></span>
+ <span id="aria_superscript_mixed" role="sUPERSCRIPt"></span>
+ <span id="aria_switch" role="switch"></span>
+ <span id="aria_switch_mixed" role="sWITCh"></span>
+ <span id="aria_tab" role="tab"></span>
+ <span id="aria_tab_mixed" role="tAb"></span>
+ <span id="aria_tablist" role="tablist"></span>
+ <span id="aria_tablist_mixed" role="tABLISt"></span>
+ <span id="aria_tabpanel" role="tabpanel"></span>
+ <span id="aria_tabpanel_mixed" role="tABPANEl"></span>
+ <span id="aria_term" role="term"></span>
+ <span id="aria_term_mixed" role="tERm"></span>
+ <span id="aria_textbox" role="textbox"></span>
+ <span id="aria_textbox_mixed" role="tEXTBOx"></span>
+ <span id="aria_timer" role="timer"></span>
+ <span id="aria_timer_mixed" role="tIMEr"></span>
+ <span id="aria_toolbar" role="toolbar"></span>
+ <span id="aria_toolbar_mixed" role="tOOLBAr"></span>
+ <span id="aria_tooltip" role="tooltip"></span>
+ <span id="aria_tooltip_mixed" role="tOOLTIp"></span>
+ <span id="aria_tree" role="tree"></span>
+ <span id="aria_tree_mixed" role="tREe"></span>
+ <span id="aria_treegrid" role="treegrid"></span>
+ <span id="aria_treegrid_mixed" role="tREEGRId"></span>
+ <span id="aria_treeitem" role="treeitem"></span>
+ <span id="aria_treeitem_mixed" role="tREEITEm"></span>
+
+ <article id="articlemain" role="main">a main area</article>
+ <article id="articlemain_mixed" role="mAIn">a main area</article>
+ <article id="articleform" role="form">a form area</article>
+ <article id="articleform_mixed" role="fORm">a form area</article>
+
+ <div id="testArticle" role="article" title="Test article">
+ <p>This is a paragraph inside the article.</p>
+ </div>
+
+ <div id="testArticle_mixed" role="aRTICLe" title="Test article">
+ <p>This is a paragraph inside the article.</p>
+ </div>
+
+ <!-- "live" roles -->
+ <table role="log" id="log_table">
+ <tr><td>Table based log</td></tr>
+ </table>
+ <table role="LOG" id="log_table_mixed">
+ <tr><td>Table based log</td></tr>
+ </table>
+ <h1 role="marquee" id="marquee_h1">marquee</h1>
+ <h1 role="MARQUEE" id="marquee_h1_mixed">marquee</h1>
+ <div role="timer" id="timer_div">timer</div>
+ <div role="TIMER" id="timer_div_mixed">timer</div>
+
+ <!-- landmarks -->
+ <div role="application" id="application">application</div>
+ <div role="aPPLICATIOn" id="application_mixed">application</div>
+ <div role="form" id="form">form</div>
+ <div role="fORm" id="form_mixed">form</div>
+
+ <!-- weak landmarks -->
+ <div role="banner" id="banner">banner</div>
+ <div role="bANNEr" id="banner_mixed">banner</div>
+ <div role="complementary" id="complementary">complementary</div>
+ <div role="cOMPLEMENTARy" id="complementary_mixed">complementary</div>
+ <div role="contentinfo" id="contentinfo">contentinfo</div>
+ <div role="cONTENTINFo" id="contentinfo_mixed">contentinfo</div>
+ <div role="main" id="main">main</div>
+ <div role="mAIN" id="main_mixed">main</div>
+ <div role="navigation" id="navigation">navigation</div>
+ <div role="nAVIGATIOn" id="navigation_mixed">navigation</div>
+ <div role="search" id="search">search</div>
+ <div role="sEARCh" id="search_mixed">search</div>
+
+ <!-- landmarks are tables -->
+ <table role="application" id="application_table">application table
+ <tr><td>hi<td></tr></table>
+ <table role="aPPLICATIOn" id="application_table_mixed">application table
+ <tr><td>hi<td></tr></table>
+ <table role="banner" id="banner_table">banner table
+ <tr><td>hi<td></tr></table>
+ <table role="bANNEr" id="banner_table_mixed">banner table
+ <tr><td>hi<td></tr></table>
+ <table role="complementary" id="complementary_table">complementary table
+ <tr><td>hi<td></tr></table>
+ <table role="cOMPLEMENTARy" id="complementary_table_mixed">complementary table
+ <tr><td>hi<td></tr></table>
+ <table role="contentinfo" id="contentinfo_table">contentinfo table
+ <tr><td>hi<td></tr></table>
+ <table role="cONTENTINFo" id="contentinfo_table_mixed">contentinfo table
+ <tr><td>hi<td></tr></table>
+ <table role="main" id="main_table">main table
+ <tr><td>hi<td></tr></table>
+ <table role="mAIn" id="main_table_mixed">main table
+ <tr><td>hi<td></tr></table>
+ <table role="navigation" id="navigation_table">navigation table
+ <tr><td>hi<td></tr></table>
+ <table role="nAVIGATIOn" id="navigation_table_mixed">navigation table
+ <tr><td>hi<td></tr></table>
+ <table role="search" id="search_table">search table
+ <tr><td>hi<td></tr></table>
+ <table role="sEARCh" id="search_table_mixed">search table
+ <tr><td>hi<td></tr></table>
+
+ <!-- test gEmptyRoleMap -->
+ <table role="button">
+ <tr id="buttontable_row">
+ <td id="buttontable_cell">cell</td>
+ </tr>
+ </table>
+ <table role="bUTTOn">
+ <tr id="buttontable_row_mixed">
+ <td id="buttontable_cell_mixed">cell</td>
+ </tr>
+ </table>
+
+ <!-- user agents must not map abstract roles to platform API -->
+ <!-- test abstract base type roles -->
+ <div role="composite" id="composite">composite</div>
+ <div role="cOMPOSITe" id="composite_mixed">composite</div>
+ <div role="landmark" id="landmark">landmark</div>
+ <div role="lANDMARk" id="landmark_mixed">landmark</div>
+ <div role="roletype" id="roletype">roletype</div>
+ <div role="rOLETYPe" id="roletype_mixed">roletype</div>
+ <div role="structure" id="structure">structure</div>
+ <div role="sTRUCTURe" id="structure_mixed">structure</div>
+ <div role="widget" id="widget">widget</div>
+ <div role="wIDGEt" id="widget_mixed">widget</div>
+ <div role="window" id="window">window</div>
+ <div role="wINDOw" id="window_mixed">window</div>
+ <!-- test abstract input roles -->
+ <div role="input" id="input">input</div>
+ <div role="iNPUt" id="input_mixed">input</div>
+ <div role="range" id="range">range</div>
+ <div role="rANGe" id="range_mixed">range</div>
+ <div role="select" id="select">select</div>
+ <div role="sELECt" id="select_mixed">select</div>
+ <!-- test abstract structure roles -->
+ <div role="section" id="section">section</div>
+ <div role="sECTIOn" id="section_mixed">section</div>
+ <div role="sectionhead" id="sectionhead">sectionhead</div>
+ <div role="sECTIONHEAd" id="sectionhead_mixed">sectionhead</div>
+
+ <!-- roles transformed by ARIA roles of ancestors -->
+ <table role="grid">
+ <tr>
+ <td id="implicit_gridcell">foo</td>
+ </tr>
+ </table>
+ <table role="gRId">
+ <tr>
+ <td id="implicit_gridcell_mixed">foo</td>
+ </tr>
+ </table>
+
+ <!-- roles transformed by ARIA state attributes -->
+ <button aria-pressed="true" id="togglebutton"></button>
+ <button aria-pressed="tRUe" id="togglebutton_mixed"></button>
+
+ <!-- take the first known mappable role -->
+ <div role="wiggly:worm abc123 button" id="unknown_roles">worm button</div>
+ <div role="wiggly:worm abc123 bUTTOn" id="unknown_roles_mixed">worm button</div>
+
+ <!-- misc roles -->
+ <div role="note" id="note">note</div>
+ <div role="nOTe" id="note_mixed">note</div>
+ <div role="scrollbar" id="scrollbar">scrollbar</div>
+ <div role="sCROLLBAr" id="scrollbar_mixed">scrollbar</div>
+
+ <div id="dir" role="directory">
+ <div role="listitem">A</div>
+ <div role="listitem">B</div>
+ <div role="listitem">C</div>
+ </div>
+
+ <div id="dir_mixed" role="dIRECTORy">
+ <div role="listitem">A</div>
+ <div role="listitem">B</div>
+ <div role="listitem">C</div>
+ </div>
+
+ <p>Image:
+ <img id="img_eq" role="math" src="foo" alt="x^2 + y^2 + z^2">
+ </p>
+
+ <p>Image:
+ <img id="img_eq_mixed" role="mATh" src="foo" alt="x^2 + y^2 + z^2">
+ </p>
+
+ <p>Text:
+ <span id="txt_eq" role="math" title="x^2 + y^2 + z^2">x<sup>2</sup> +
+ y<sup>2</sup> + z<sup>2</sup></span>
+ </p>
+ <p>Text:
+ <span id="txt_eq_mixed" role="mATh" title="x^2 + y^2 + z^2">x<sup>2</sup> +
+ y<sup>2</sup> + z<sup>2</sup></span>
+ </p>
+
+ <iframe id="iframe_aria_application"
+ src="data:text/html,<body role='application'>"></iframe>
+ <iframe id="iframe_aria_application_mixed"
+ src="data:text/html,<body role='aPPLICATIOn'>"></iframe>
+ <iframe id="iframe_aria_dialog"
+ src="data:text/html,<body role='dialog'>"></iframe>
+ <iframe id="iframe_aria_dialog_mixed"
+ src="data:text/html,<body role='dIALOg'>"></iframe>
+ <iframe id="iframe_aria_document"
+ src="data:text/html,<body role='document'>"></iframe>
+ <iframe id="iframe_aria_document_mixed"
+ src="data:text/html,<body role='dOCUMENt'>"></iframe>
+ <iframe id="iframe_aria_button"
+ src="data:text/html,<body role='button'>"></iframe>
+ <iframe id="iframe_aria_button_mixed"
+ src="data:text/html,<body role='bUTTOn'>"></iframe>
+ <iframe id="iframe_aria_main"
+ src="data:text/html,<body role='main'>"></iframe>
+ <iframe id="iframe_aria_main_mixed"
+ src="data:text/html,<body role='mAIn'>"></iframe>
+ <iframe id="iframe_aria_alert"
+ src="data:text/html,<body role='alert'>"></iframe>
+ <iframe id="iframe_aria_alert_mixed"
+ src="data:text/html,<body role='aLERt'>"></iframe>
+
+</body>
+</html>
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 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessibility Name Calculating Test.">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function doTest()
+ {
+ ok(!isAccessible("presentation_label"),
+ "Presentation label shouldn't be accessible.");
+ ok(!isAccessible("presentation_descr"),
+ "Presentation description shouldn't be accessible.");
+
+ // aria-pressed
+ testRole("pressed_button", ROLE_TOGGLE_BUTTON);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=494345"
+ title="Do not create accessibles for XUL label or description having a role of 'presentation'">
+ Mozilla Bug 494345
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1033283"
+ title="Expose pressed state on XUL menu toggle buttons">
+ Mozilla Bug 1033283
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <label id="presentation_label" role="presentation" value="label"/>
+ <description id="presentation_descr" role="presentation" value="description"/>
+ <button id="pressed_button" aria-pressed="true" label="I am pressed" />
+ </vbox>
+
+
+ </hbox>
+</window>
+
diff --git a/accessible/tests/mochitest/role/test_dpub_aria.html b/accessible/tests/mochitest/role/test_dpub_aria.html
new file mode 100644
index 0000000000..621c86a59b
--- /dev/null
+++ b/accessible/tests/mochitest/role/test_dpub_aria.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test DPub ARIA roles</title>
+
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ // DPub ARIA role map.
+ testRole("doc-abstract", ROLE_SECTION);
+ testRole("doc-acknowledgments", ROLE_LANDMARK);
+ testRole("doc-afterword", ROLE_LANDMARK);
+ testRole("doc-appendix", ROLE_LANDMARK);
+ testRole("doc-backlink", ROLE_LINK);
+ testRole("doc-biblioentry", ROLE_LISTITEM);
+ testRole("doc-bibliography", ROLE_LANDMARK);
+ testRole("doc-biblioref", ROLE_LINK);
+ testRole("doc-chapter", ROLE_LANDMARK);
+ testRole("doc-colophon", ROLE_SECTION);
+ testRole("doc-conclusion", ROLE_LANDMARK);
+ testRole("doc-cover", ROLE_GRAPHIC);
+ testRole("doc-credit", ROLE_SECTION);
+ testRole("doc-credits", ROLE_LANDMARK);
+ testRole("doc-dedication", ROLE_SECTION);
+ testRole("doc-endnote", ROLE_LISTITEM);
+ testRole("doc-endnotes", ROLE_LANDMARK);
+ testRole("doc-epigraph", ROLE_SECTION);
+ testRole("doc-epilogue", ROLE_LANDMARK);
+ testRole("doc-errata", ROLE_LANDMARK);
+ testRole("doc-example", ROLE_SECTION);
+ testRole("doc-footnote", ROLE_FOOTNOTE);
+ testRole("doc-foreword", ROLE_LANDMARK);
+ testRole("doc-glossary", ROLE_LANDMARK);
+ testRole("doc-glossref", ROLE_LINK);
+ testRole("doc-index", ROLE_NAVIGATION);
+ testRole("doc-introduction", ROLE_LANDMARK);
+ testRole("doc-noteref", ROLE_LINK);
+ testRole("doc-notice", ROLE_NOTE);
+ testRole("doc-pagebreak", ROLE_SEPARATOR);
+ testRole("doc-pagelist", ROLE_NAVIGATION);
+ testRole("doc-part", ROLE_LANDMARK);
+ testRole("doc-preface", ROLE_LANDMARK);
+ testRole("doc-prologue", ROLE_LANDMARK);
+ testRole("doc-pullquote", ROLE_SECTION);
+ testRole("doc-qna", ROLE_SECTION);
+ testRole("doc-subtitle", ROLE_HEADING);
+ testRole("doc-tip", ROLE_NOTE);
+ testRole("doc-toc", ROLE_NAVIGATION);
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1343537"
+ title="implement ARIA DPUB extension">
+ Bug 1343537
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ <div id="doc-abstract" role="doc-abstract">abstract</div>
+ <div id="doc-acknowledgments" role="doc-acknowledgments">acknowledgments</div>
+ <div id="doc-afterword" role="doc-afterword">afterword</div>
+ <div id="doc-appendix" role="doc-appendix">appendix</div>
+ <div id="doc-backlink" role="doc-backlink">backlink</div>
+ <div id="doc-biblioentry" role="doc-biblioentry">biblioentry</div>
+ <div id="doc-bibliography" role="doc-bibliography">bibliography</div>
+ <div id="doc-biblioref" role="doc-biblioref">biblioref</div>
+ <div id="doc-chapter" role="doc-chapter">chapter</div>
+ <div id="doc-colophon" role="doc-colophon">colophon</div>
+ <div id="doc-conclusion" role="doc-conclusion">conclusion</div>
+ <div id="doc-cover" role="doc-cover">cover</div>
+ <div id="doc-credit" role="doc-credit">credit</div>
+ <div id="doc-credits" role="doc-credits">credits</div>
+ <div id="doc-dedication" role="doc-dedication">dedication</div>
+ <div id="doc-endnote" role="doc-endnote">endnote</div>
+ <div id="doc-endnotes" role="doc-endnotes">endnotes</div>
+ <div id="doc-epigraph" role="doc-epigraph">epigraph</div>
+ <div id="doc-epilogue" role="doc-epilogue">epilogue</div>
+ <div id="doc-errata" role="doc-errata">errata</div>
+ <div id="doc-example" role="doc-example">example</div>
+ <div id="doc-footnote" role="doc-footnote">footnote</div>
+ <div id="doc-foreword" role="doc-foreword">foreword</div>
+ <div id="doc-glossary" role="doc-glossary">glossary</div>
+ <div id="doc-glossref" role="doc-glossref">glossref</div>
+ <div id="doc-index" role="doc-index">index</div>
+ <div id="doc-introduction" role="doc-introduction">introduction</div>
+ <div id="doc-noteref" role="doc-noteref">noteref</div>
+ <div id="doc-notice" role="doc-notice">notice</div>
+ <div id="doc-pagebreak" role="doc-pagebreak">pagebreak</div>
+ <div id="doc-pagelist" role="doc-pagelist">pagelist</div>
+ <div id="doc-part" role="doc-part">part</div>
+ <div id="doc-preface" role="doc-preface">preface</div>
+ <div id="doc-prologue" role="doc-prologue">prologue</div>
+ <div id="doc-pullquote" role="doc-pullquote">pullquote</div>
+ <div id="doc-qna" role="doc-qna">qna</div>
+ <div id="doc-subtitle" role="doc-subtitle">subtitle</div>
+ <div id="doc-tip" role="doc-tip">tip</div>
+ <div id="doc-toc" role="doc-toc">toc</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/role/test_general.html b/accessible/tests/mochitest/role/test_general.html
new file mode 100644
index 0000000000..38dde3325a
--- /dev/null
+++ b/accessible/tests/mochitest/role/test_general.html
@@ -0,0 +1,201 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>test nsHyperTextAccessible accesible objects creation and their roles</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+ function doTests() {
+ // landmark tests section
+ testRole("frm", ROLE_FORM);
+
+ // nsHyperTextAcc tests section
+ // Test html:form.
+ testRole("nav", ROLE_LANDMARK);
+ testRole("header", ROLE_LANDMARK);
+ testRole("footer", ROLE_LANDMARK);
+ testRole("article", ROLE_ARTICLE);
+ testRole("aside", ROLE_LANDMARK);
+ testRole("section", ROLE_SECTION);
+
+ // Bug 996821
+ // Check that landmark elements get accessibles with styled overflow.
+ testRole("section_overflow", ROLE_SECTION);
+ testRole("nav_overflow", ROLE_LANDMARK);
+ testRole("header_overflow", ROLE_SECTION);
+ testRole("aside_overflow", ROLE_LANDMARK);
+ testRole("footer_overflow", ROLE_SECTION);
+ testRole("article_overflow", ROLE_ARTICLE);
+
+ // test html:div
+ testRole("sec", ROLE_SECTION);
+
+ // Test html:blockquote
+ testRole("quote", ROLE_BLOCKQUOTE);
+
+ // Test html:h, all levels
+ testRole("head1", ROLE_HEADING);
+ testRole("head2", ROLE_HEADING);
+ testRole("head3", ROLE_HEADING);
+ testRole("head4", ROLE_HEADING);
+ testRole("head5", ROLE_HEADING);
+ testRole("head6", ROLE_HEADING);
+
+ // Test that an html:input @type="file" is exposed as ROLE_PUSHBUTTON.
+ testRole("data", ROLE_PUSHBUTTON);
+
+ // Test that input type="checkbox" and type="radio" are
+ // exposed as such regardless of appearance style.
+ testRole("checkbox_regular", ROLE_CHECKBUTTON);
+ testRole("checkbox_appearance_none", ROLE_CHECKBUTTON);
+ testRole("radio_regular", ROLE_RADIOBUTTON);
+ testRole("radio_appearance_none", ROLE_RADIOBUTTON);
+
+ // Test regular paragraph by comparison to make sure exposure does not
+ // get broken.
+ testRole("p", ROLE_PARAGRAPH);
+
+ // Test dl, dt, dd
+ testRole("definitionlist", ROLE_DEFINITION_LIST);
+ testRole("definitionterm", ROLE_TERM);
+ testRole("definitiondescription", ROLE_DEFINITION);
+
+ // Has click, mousedown or mouseup listeners.
+ testRole("span1", ROLE_TEXT);
+ testRole("span2", ROLE_TEXT);
+ testRole("span3", ROLE_TEXT);
+
+ // Test role of listbox inside combobox
+ testRole("listbox1", ROLE_COMBOBOX_LIST);
+ testRole("listbox2", ROLE_COMBOBOX_LIST);
+
+ // Test role of menu and li items in menu
+ testRole("menu", ROLE_LIST);
+ testRole("menuItem", ROLE_LISTITEM);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+<body>
+
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=472326"
+ title="html:input of type "file" no longer rendered to screen readers">
+ Mozilla Bug 472326
+ </a><br>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=474261"
+ title="Test remaining implementations in nsHyperTextAccessible::GetRole">
+ bug 474261
+ </a><br>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=423409"
+ title="Expose click action if mouseup and mousedown are registered">
+ Mozilla Bug 423409
+ </a>
+ <a target="_blank"
+ title="Provide mappings for html5 <nav> <header> <footer> <article>"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=593368">
+ Bug 593368
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=613502"
+ title="Map <article> like we do aria role article">
+ Bug 613502
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=610650"
+ title="Change implementation of HTML5 landmark elements to conform">
+ Bug 610650
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=614310"
+ title="Map section to pane (like role=region)">
+ Mozilla Bug 614310
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=734982"
+ title="Map ARIA role FORM">
+ Bug 734982
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1044431"
+ title="Listbox owned by combobox has the wrong role">
+ Mozilla Bug 1044431
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <form id="frm" action="submit.php" method="post">
+ <label for="data">File</label>:
+ <input type="file" id="data" name="data" size="50"/>
+ <input type="checkbox" id="checkbox_regular" value="Check me"/>
+ <input type="checkbox" style="-moz-appearance: none;" id="checkbox_appearance_none" value="Check me"/>
+ <input type="radio" id="radio_regular" value="Check me"/>
+ <input type="radio" style="-moz-appearance: none;" id="radio_appearance_none" value="Check me"/>
+ </form>
+
+ <nav id="nav">a nav</nav>
+ <header id="header">a header</header>
+ <footer id="footer">a footer</footer>
+ <article id="article">an article</article>
+ <aside id="aside">by the way I am an aside</aside>
+ <section id="section">a section</section>
+
+ <section style="overflow: hidden;" id="section_overflow">
+ <nav style="overflow: hidden;"
+ id="nav_overflow">overflow nav</nav>
+ <header style="overflow: hidden;"
+ id="header_overflow">overflow header</header>
+ <aside style="overflow: hidden;"
+ id="aside_overflow">overflow aside</aside>
+ <footer style="overflow: hidden;"
+ id="footer_overflow">overflow footer</footer>
+ </section>
+ <article style="overflow: hidden;"
+ id="article_overflow">overflow article</article>
+
+ <p id="p">A paragraph for comparison.</p>
+ <div id="sec">A normal div</div>
+ <blockquote id="quote">A citation</blockquote>
+ <h1 id="head1">A heading level 1</h1>
+ <h2 id="head2">A heading level 2</h2>
+ <h3 id="head3">A heading level 3</h3>
+ <h4 id="head4">A heading level 4</h4>
+ <h5 id="head5">A heading level 5</h5>
+ <h6 id="head6">A heading level 6</h6>
+
+ <dl id="definitionlist">
+ <dt id="definitionterm">gecko</dt>
+ <dd id="definitiondescription">geckos have sticky toes</dd>
+ </dl>
+
+ <span id="span1" onclick="">clickable span</span>
+ <span id="span2" onmousedown="">clickable span</span>
+ <span id="span3" onmouseup="">clickable span</span>
+
+ <div id="combobox1" role="combobox">
+ <div id="listbox1" role="listbox"></div>
+ </div>
+ <div id="combobox2" role="combobox" aria-owns="listbox2"></div>
+ <div id="listbox2" role="listbox"></div>
+
+ <menu id="menu">
+ <li id="menuItem">menu item!</li>
+ </menu>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/role/test_general.xhtml b/accessible/tests/mochitest/role/test_general.xhtml
new file mode 100644
index 0000000000..95de7775ea
--- /dev/null
+++ b/accessible/tests/mochitest/role/test_general.xhtml
@@ -0,0 +1,59 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessibility Role XUL Test.">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+ function doTest()
+ {
+ ok(!isAccessible("image"),
+ "image without tooltiptext shouldn't be accessible.");
+ testRole("image-tooltiptext", ROLE_GRAPHIC);
+
+ ok(!isAccessible("statusbarpanel"),
+ "statusbarpanel shouldn't be accessible.");
+ testRole("statusbar", ROLE_STATUSBAR);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=900097"
+ title="statusbarpanel shouldn't be a button accessible">
+ Mozilla Bug 900097
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <image id="image" src="../moz.png"/>
+ <image id="image-tooltiptext" src="../moz.png" tooltiptext="hello"/>
+
+ <statusbarpanel id="statusbarpanel"></statusbarpanel>
+ <statusbar id="statusbar"></statusbar>
+
+ </hbox>
+</window>
+
diff --git a/accessible/tests/mochitest/role/test_graphics_aria.html b/accessible/tests/mochitest/role/test_graphics_aria.html
new file mode 100644
index 0000000000..8ddfe9224d
--- /dev/null
+++ b/accessible/tests/mochitest/role/test_graphics_aria.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test Graphics ARIA roles</title>
+
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ // Graphics ARIA role map.
+ testRole("graphics-document", ROLE_NON_NATIVE_DOCUMENT);
+ testRole("graphics-object", ROLE_GROUPING);
+ testRole("graphics-symbol", ROLE_GRAPHIC);
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432513"
+ title="implement ARIA Graphics roles">
+ Bug 1432513
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ <div id="graphics-document" role="graphics-document">document</div>
+ <div id="graphics-object" role="graphics-object">object</div>
+ <div id="graphics-symbol" role="graphics-symbol">symbol</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/role/test_svg.html b/accessible/tests/mochitest/role/test_svg.html
new file mode 100644
index 0000000000..716dae7ee2
--- /dev/null
+++ b/accessible/tests/mochitest/role/test_svg.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>SVG elements accessible roles</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+ function doTests() {
+ testRole("svg", ROLE_DIAGRAM);
+ testRole("g", ROLE_GROUPING);
+ testRole("rect", ROLE_GRAPHIC);
+ testRole("circle", ROLE_GRAPHIC);
+ testRole("ellipse", ROLE_GRAPHIC);
+ testRole("line", ROLE_GRAPHIC);
+ testRole("polygon", ROLE_GRAPHIC);
+ testRole("polyline", ROLE_GRAPHIC);
+ testRole("path", ROLE_GRAPHIC);
+ testRole("image", ROLE_GRAPHIC);
+ testRole("image2", ROLE_GRAPHIC);
+ testRole("a", ROLE_LINK);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=822983"
+ title="Map SVG graphic elements to accessibility API">
+ Bug 822983
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g id="g">
+ <title>g</title>
+ </g>
+ <rect width="300" height="100" id="rect"
+ style="fill:rgb(0,0,255);stroke-width:1;stroke:rgb(0,0,0)">
+ <title>rect</title>
+ </rect>
+ <circle cx="100" cy="50" r="40" stroke="black" id="circle"
+ stroke-width="2" fill="red">
+ <title>circle</title>
+ </circle>
+ <ellipse cx="300" cy="80" rx="100" ry="50" id="ellipse"
+ style="fill:yellow;stroke:purple;stroke-width:2">
+ <title>ellipse</title>
+ </ellipse>
+ <line x1="0" y1="0" x2="200" y2="200" id="line"
+ style="stroke:rgb(255,0,0);stroke-width:2">
+ <title>line</title>
+ </line>
+ <polygon points="200,10 250,190 160,210" id="polygon"
+ style="fill:lime;stroke:purple;stroke-width:1">
+ <title>polygon</title>
+ </polygon>
+ <polyline points="20,20 40,25 60,40 80,120 120,140 200,180" id="polyline"
+ style="fill:none;stroke:black;stroke-width:3" >
+ <title>polyline</title>
+ </polyline>
+ <path d="M150 0 L75 200 L225 200 Z" id="path">
+ <title>path</title>
+ </path>
+ <image x1="25" y1="80" width="50" height="20" id="image"
+ xlink:href="../moz.png">
+ <title>image</title>
+ </image>
+ <image x1="25" y1="110" width="50" height="20" id="image2"
+ xlink:href="../moz.png" aria-label="hello"/>
+ <a href="#" id="a"><text>a</text></a>
+ </svg>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/scroll/a11y.toml b/accessible/tests/mochitest/scroll/a11y.toml
new file mode 100644
index 0000000000..66a0aadf28
--- /dev/null
+++ b/accessible/tests/mochitest/scroll/a11y.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+support-files = "!/accessible/tests/mochitest/*.js"
+
+["test_zoom.html"]
diff --git a/accessible/tests/mochitest/scroll/test_zoom.html b/accessible/tests/mochitest/scroll/test_zoom.html
new file mode 100644
index 0000000000..b0ece28a91
--- /dev/null
+++ b/accessible/tests/mochitest/scroll/test_zoom.html
@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test scrollToPoint when page is zoomed</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../layout.js"></script>
+
+ <script type="application/javascript">
+ function testScrollToPoint() {
+ // scrollToPoint relative screen
+ var anchor = getAccessible("bottom1");
+ let [x /* y */] = getPos(anchor);
+ var [docX, docY] = getPos(document);
+
+ anchor.scrollToPoint(COORDTYPE_SCREEN_RELATIVE, docX, docY);
+ testPos(anchor, [x, docY]);
+
+ // scrollToPoint relative window
+ anchor = getAccessible("bottom2");
+ [x /* y */] = getPos(anchor);
+ var wnd = getRootAccessible().DOMDocument.defaultView;
+ var [screenX, screenY] = CSSToDevicePixels(wnd, wnd.screenX, wnd.screenY);
+ let scrollToX = docX - screenX, scrollToY = docY - screenY;
+
+ anchor.scrollToPoint(COORDTYPE_WINDOW_RELATIVE, scrollToX, scrollToY);
+ testPos(anchor, [x, docY]);
+
+ // scrollToPoint relative parent
+ anchor = getAccessible("bottom3");
+ [x /* y */] = getPos(anchor);
+ var [parentX, parentY] = getPos(anchor.parent);
+ scrollToX = parentX - docX;
+ scrollToY = parentY - docY;
+
+ anchor.scrollToPoint(COORDTYPE_PARENT_RELATIVE, scrollToX, scrollToY);
+ testPos(anchor, [x, docY]);
+ }
+
+ function doTest() {
+ testScrollToPoint();
+ zoomDocument(document, 2.0);
+ testScrollToPoint(); // zoom and test again
+
+ zoomDocument(document, 1.0);
+ SimpleTest.finish();
+ }
+
+ addA11yLoadEvent(doTest);
+ SimpleTest.waitForExplicitFinish();
+ </script>
+
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=727942"
+ title="scrollToPoint is broken when page is zoomed">
+ Mozilla Bug 727942
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <h1>Below there is a bunch of named anchors</h1>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ This is in the middle anchor #1<a id="bottom1"></a>
+ <br><br><br><br><br><br><br><br><br><br>
+ This is in the middle anchor #2<a id="bottom2"></a>
+ <br><br><br><br><br><br><br><br><br><br>
+ This is in the middle anchor #3<a id="bottom3"></a>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/selectable.js b/accessible/tests/mochitest/selectable.js
new file mode 100644
index 0000000000..43c7260b0a
--- /dev/null
+++ b/accessible/tests/mochitest/selectable.js
@@ -0,0 +1,138 @@
+/* import-globals-from common.js */
+/* import-globals-from states.js */
+
+/**
+ * Test selection getter methods of nsIAccessibleSelectable.
+ *
+ * @param aIdentifier [in] selectable container accessible
+ * @param aSelectedChildren [in] array of selected children
+ */
+function testSelectableSelection(aIdentifier, aSelectedChildren, aMsg) {
+ var acc = getAccessible(aIdentifier, [nsIAccessibleSelectable]);
+ if (!acc) {
+ return;
+ }
+
+ var msg = aMsg ? aMsg : "";
+ var len = aSelectedChildren.length;
+
+ // getSelectedChildren
+ var selectedChildren = acc.selectedItems;
+ is(
+ selectedChildren ? selectedChildren.length : 0,
+ len,
+ msg +
+ "getSelectedChildren: wrong selected children count for " +
+ prettyName(aIdentifier)
+ );
+
+ for (let idx = 0; idx < len; idx++) {
+ let expectedAcc = aSelectedChildren[idx];
+ let expectedAccId =
+ expectedAcc instanceof nsIAccessible
+ ? getAccessibleDOMNodeID(expectedAcc)
+ : expectedAcc;
+ let actualAcc = selectedChildren.queryElementAt(idx, nsIAccessible);
+ let actualAccId = getAccessibleDOMNodeID(actualAcc);
+ is(
+ actualAccId,
+ expectedAccId,
+ msg +
+ "getSelectedChildren: wrong selected child at index " +
+ idx +
+ " for " +
+ prettyName(aIdentifier) +
+ " { actual : " +
+ prettyName(actualAcc) +
+ ", expected: " +
+ prettyName(expectedAcc) +
+ "}"
+ );
+ }
+
+ // selectedItemCount
+ is(
+ acc.selectedItemCount,
+ aSelectedChildren.length,
+ "selectedItemCount: wrong selected children count for " +
+ prettyName(aIdentifier)
+ );
+
+ // getSelectedItemAt
+ for (let idx = 0; idx < len; idx++) {
+ let expectedAcc = aSelectedChildren[idx];
+ let expectedAccId =
+ expectedAcc instanceof nsIAccessible
+ ? getAccessibleDOMNodeID(expectedAcc)
+ : expectedAcc;
+ is(
+ getAccessibleDOMNodeID(acc.getSelectedItemAt(idx)),
+ expectedAccId,
+ msg +
+ "getSelectedItemAt: wrong selected child at index " +
+ idx +
+ " for " +
+ prettyName(aIdentifier)
+ );
+ }
+
+ // isItemSelected
+ testIsItemSelected(acc, acc, { value: 0 }, aSelectedChildren, msg);
+}
+
+/**
+ * Test isItemSelected method, helper for testSelectableSelection
+ */
+function testIsItemSelected(
+ aSelectAcc,
+ aTraversedAcc,
+ aIndexObj,
+ aSelectedChildren,
+ aMsg
+) {
+ var childCount = aTraversedAcc.childCount;
+ for (var idx = 0; idx < childCount; idx++) {
+ var child = aTraversedAcc.getChildAt(idx);
+ var [state /* extraState */] = getStates(child);
+ if (state & STATE_SELECTABLE) {
+ var isSelected = false;
+ var len = aSelectedChildren.length;
+ for (var jdx = 0; jdx < len; jdx++) {
+ let expectedAcc = aSelectedChildren[jdx];
+ let matches =
+ expectedAcc instanceof nsIAccessible
+ ? child == expectedAcc
+ : getAccessibleDOMNodeID(child) == expectedAcc;
+
+ if (matches) {
+ isSelected = true;
+ break;
+ }
+ }
+
+ // isItemSelected
+ is(
+ aSelectAcc.isItemSelected(aIndexObj.value++),
+ isSelected,
+ aMsg +
+ "isItemSelected: wrong selected child " +
+ prettyName(child) +
+ " for " +
+ prettyName(aSelectAcc)
+ );
+
+ // selected state
+ testStates(
+ child,
+ isSelected ? STATE_SELECTED : 0,
+ 0,
+ !isSelected ? STATE_SELECTED : 0,
+ 0
+ );
+
+ continue;
+ }
+
+ testIsItemSelected(aSelectAcc, child, aIndexObj, aSelectedChildren);
+ }
+}
diff --git a/accessible/tests/mochitest/selectable/a11y.toml b/accessible/tests/mochitest/selectable/a11y.toml
new file mode 100644
index 0000000000..4cb700ba73
--- /dev/null
+++ b/accessible/tests/mochitest/selectable/a11y.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+support-files = [
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/treeview.css"]
+
+["test_listbox.xhtml"]
+
+["test_menu.xhtml"]
+
+["test_menulist.xhtml"]
+
+["test_tabs.xhtml"]
+
+["test_tree.xhtml"]
diff --git a/accessible/tests/mochitest/selectable/test_listbox.xhtml b/accessible/tests/mochitest/selectable/test_listbox.xhtml
new file mode 100644
index 0000000000..b915b2790f
--- /dev/null
+++ b/accessible/tests/mochitest/selectable/test_listbox.xhtml
@@ -0,0 +1,144 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<?xml-stylesheet href="../treeview.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="XUL tree selectable tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../selectable.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ //gA11yEventDumpID = "debug";
+
+ var gQueue = null;
+
+ function doTest()
+ {
+ //////////////////////////////////////////////////////////////////////////
+ // single selectable listbox, the first item is selected by default
+
+ var id = "listbox";
+ ok(isAccessible(id, [nsIAccessibleSelectable]),
+ "No selectable accessible for list of " + id);
+
+ var select = getAccessible(id, [nsIAccessibleSelectable]);
+ select.removeItemFromSelection(0);
+ testSelectableSelection(select, [ ]);
+
+ select.addItemToSelection(1);
+ testSelectableSelection(select, [ "lb1_item2" ], "addItemToSelect(1): ");
+
+ select.removeItemFromSelection(1);
+ testSelectableSelection(select, [ ],
+ "removeItemFromSelection(1): ");
+
+ todo(!select.selectAll(),
+ "No way to select all items in listbox '" + id + "'");
+ testSelectableSelection(select, [ "lb1_item1" ], "selectAll: ");
+
+ select.addItemToSelection(1);
+ select.unselectAll();
+ testSelectableSelection(select, [ ], "unselectAll: ");
+
+ //////////////////////////////////////////////////////////////////////////
+ // multiple selectable listbox
+
+ id = "listbox2";
+ ok(isAccessible(id, [nsIAccessibleSelectable]),
+ "No selectable accessible for list of " + id);
+
+ select = getAccessible(id, [nsIAccessibleSelectable]);
+ testSelectableSelection(select, [ ]);
+
+ select.addItemToSelection(1);
+ testSelectableSelection(select, [ "lb2_item2" ], "addItemToSelect(1): ");
+
+ select.removeItemFromSelection(1);
+ testSelectableSelection(select, [ ],
+ "removeItemFromSelection(1): ");
+
+ is(select.selectAll(), true,
+ "All items should be selected in listbox '" + id + "'");
+ testSelectableSelection(select, [ "lb2_item1", "lb2_item2" ],
+ "selectAll: ");
+
+ select.unselectAll();
+ testSelectableSelection(select, [ ], "unselectAll: ");
+
+ //////////////////////////////////////////////////////////////////////////
+ // listbox with headers
+
+ // XXX: addItemToSelection/removeItemFromSelection don't work correctly
+ // on listboxes with headers because header is inserted into hierarchy
+ // and child indexes that are used in these methods are shifted (see bug
+ // 591939).
+ todo(false,
+ "Fix addItemToSelection/removeItemFromSelection on listboxes with headers.");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=590176"
+ title="add pseudo SelectAccessible interface">
+ Mozilla Bug 590176
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <richlistbox id="listbox">
+ <richlistitem id="lb1_item1">
+ <label value="cell0"/>
+ <label value="cell1"/>
+ </richlistitem>
+ <richlistitem id="lb1_item2">
+ <label value="cell3"/>
+ <label value="cell4"/>
+ </richlistitem>
+ </richlistbox>
+
+ <richlistbox id="listbox2" seltype="multiple">
+ <richlistitem id="lb2_item1">
+ <label value="cell0"/>
+ <label value="cell1"/>
+ </richlistitem>
+ <richlistitem id="lb2_item2">
+ <label value="cell3"/>
+ <label value="cell4"/>
+ </richlistitem>
+ </richlistbox>
+
+ <vbox id="debug"/>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/selectable/test_menu.xhtml b/accessible/tests/mochitest/selectable/test_menu.xhtml
new file mode 100644
index 0000000000..bfef622348
--- /dev/null
+++ b/accessible/tests/mochitest/selectable/test_menu.xhtml
@@ -0,0 +1,77 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<?xml-stylesheet href="../treeview.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="XUL tree selectable tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../selectable.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ //gA11yEventDumpID = "debug";
+
+ var gQueue = null;
+
+ function doTest()
+ {
+ //////////////////////////////////////////////////////////////////////////
+ // menu
+
+ var id = "menu";
+ var menu = getAccessible("menu");
+ var menuList = menu.firstChild;
+ todo(isAccessible(menuList, [nsIAccessibleSelectable]),
+ "No selectable accessible for list of menu '" + id + "'");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=590176"
+ title="add pseudo SelectAccessible interface">
+ Mozilla Bug 590176
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <menu label="menu" id="menu">
+ <menupopup>
+ <menuitem label="item1" id="m_item1"/>
+ <menuitem label="item2" id="m_item2"/>
+ </menupopup>
+ </menu>
+
+ <vbox id="debug"/>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/selectable/test_menulist.xhtml b/accessible/tests/mochitest/selectable/test_menulist.xhtml
new file mode 100644
index 0000000000..2e4b5ac08f
--- /dev/null
+++ b/accessible/tests/mochitest/selectable/test_menulist.xhtml
@@ -0,0 +1,95 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<?xml-stylesheet href="../treeview.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="XUL tree selectable tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../selectable.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ //gA11yEventDumpID = "debug";
+
+ var gQueue = null;
+
+ function doTest()
+ {
+ //////////////////////////////////////////////////////////////////////////
+ // menulist aka combobox
+
+ var id = "combobox";
+ var combobox = getAccessible(id);
+ var comboboxList = combobox.firstChild;
+ ok(isAccessible(comboboxList, [nsIAccessibleSelectable]),
+ "No selectable accessible for list of " + id);
+
+ var select = getAccessible(comboboxList, [nsIAccessibleSelectable]);
+ testSelectableSelection(select, [ "cb1_item1" ]);
+
+ select.addItemToSelection(1);
+ testSelectableSelection(select, [ "cb1_item2" ], "addItemToSelection(1): ");
+
+ select.removeItemFromSelection(1);
+ testSelectableSelection(select, [ ],
+ "removeItemFromSelection(1): ");
+
+ is(select.selectAll(), false,
+ "No way to select all items in combobox '" + id + "'");
+ testSelectableSelection(select, [ ], "selectAll: ");
+
+ select.addItemToSelection(1);
+ select.unselectAll();
+ testSelectableSelection(select, [ ], "unselectAll: ");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=590176"
+ title="add pseudo SelectAccessible interface">
+ Mozilla Bug 590176
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <menulist id="combobox">
+ <menupopup>
+ <menuitem id="cb1_item1" label="item1"/>
+ <menuitem id="cb1_item2" label="item2"/>
+ </menupopup>
+ </menulist>
+
+ <vbox id="debug"/>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/selectable/test_tabs.xhtml b/accessible/tests/mochitest/selectable/test_tabs.xhtml
new file mode 100644
index 0000000000..bc16ab0fe7
--- /dev/null
+++ b/accessible/tests/mochitest/selectable/test_tabs.xhtml
@@ -0,0 +1,93 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="XUL tabs selectable tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../selectable.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ function doTest()
+ {
+ var id = "tabs_single";
+ ok(isAccessible(id, [nsIAccessibleSelectable]),
+ "No selectable accessible for tabs_single");
+ var select = getAccessible(id, [nsIAccessibleSelectable]);
+
+ testSelectableSelection(select, ["tab_single1"]);
+
+ select.unselectAll();
+ select.addItemToSelection(1); // tab_single2
+ testSelectableSelection(select, ["tab_single2"], "select tab_single2: ");
+
+ id = "tabs_multi";
+ ok(isAccessible(id, [nsIAccessibleSelectable]),
+ "No selectable accessible for tabs_multi");
+ select = getAccessible(id, [nsIAccessibleSelectable]);
+
+ // Make sure both XUL selection and ARIA selection are included.
+ testSelectableSelection(select, ["tab_multi_xul1", "tab_multi_aria"]);
+
+ select.unselectAll();
+ select.addItemToSelection(2); // tab_multi_xul2
+ // We can only affect XUL selection, so ARIA selection won't change.
+ testSelectableSelection(select, ["tab_multi_aria", "tab_multi_xul2"],
+ "select tab_multi_xul2: ");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1480058"
+ title="XUL tabs don't support ARIA selection">
+ Mozilla Bug 1480058
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <tabbox>
+ <tabs id="tabs_single">
+ <tab id="tab_single1" label="tab1" selected="true"/>
+ <tab id="tab_single2" label="tab2"/>
+ </tabs>
+ </tabbox>
+
+ <tabbox>
+ <tabs id="tabs_multi" aria-multiselectable="true">
+ <tab id="tab_multi_xul1" label="tab1" selected="true"/>
+ <tab id="tab_multi_aria" label="tab2" aria-selected="true"/>
+ <tab id="tab_multi_xul2" label="tab3"/>
+ </tabs>
+ </tabbox>
+ </vbox>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/selectable/test_tree.xhtml b/accessible/tests/mochitest/selectable/test_tree.xhtml
new file mode 100644
index 0000000000..5527625b24
--- /dev/null
+++ b/accessible/tests/mochitest/selectable/test_tree.xhtml
@@ -0,0 +1,171 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<?xml-stylesheet href="../treeview.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="XUL tree selectable tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../selectable.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "debug";
+
+ /**
+ * Event queue invoker object to test accessible states for XUL tree
+ * accessible.
+ */
+ function statesChecker(aTreeID, aView)
+ {
+ this.DOMNode = getNode(aTreeID);
+
+ this.invoke = function invoke()
+ {
+ this.DOMNode.view = aView;
+ }
+ this.check = function check()
+ {
+ var tree = getAccessible(this.DOMNode);
+
+ var isTreeMultiSelectable = false;
+ var seltype = this.DOMNode.getAttribute("seltype");
+ if (seltype != "single")
+ isTreeMultiSelectable = true;
+
+ // selectAll
+ var accSelectable = getAccessible(this.DOMNode,
+ [nsIAccessibleSelectable]);
+ ok(accSelectable, "tree is not selectable!");
+ if (accSelectable) {
+ is(accSelectable.selectAll(), isTreeMultiSelectable,
+ "SelectAll is not correct for seltype: " + seltype);
+ }
+
+ var selectedChildren = [];
+ if (isTreeMultiSelectable) {
+ var rows = tree.children;
+ for (var i = 0; i < rows.length; i++) {
+ var row = rows.queryElementAt(i, nsIAccessible);
+ if (getRole(row) == ROLE_OUTLINEITEM || getRole(row) == ROLE_ROW)
+ selectedChildren.push(row);
+ }
+ }
+ testSelectableSelection(accSelectable, selectedChildren,
+ "selectAll test. ");
+
+ // unselectAll
+ accSelectable.unselectAll();
+ testSelectableSelection(accSelectable, [], "unselectAll test. ");
+
+ // addItemToSelection
+ accSelectable.addItemToSelection(1);
+ accSelectable.addItemToSelection(3);
+
+ selectedChildren = isTreeMultiSelectable ?
+ [ accSelectable.getChildAt(2), accSelectable.getChildAt(4) ] :
+ [ accSelectable.getChildAt(2) ];
+ testSelectableSelection(accSelectable, selectedChildren,
+ "addItemToSelection test. ");
+
+ // removeItemFromSelection
+ accSelectable.removeItemFromSelection(1);
+
+ selectedChildren = isTreeMultiSelectable ?
+ [ accSelectable.getChildAt(4) ] : [ ];
+ testSelectableSelection(accSelectable, selectedChildren,
+ "removeItemFromSelection test. ");
+ }
+
+ this.getID = function getID()
+ {
+ "tree processor for " + prettyName(aTreeID);
+ }
+ }
+
+ var gQueue = null;
+
+ function doTest()
+ {
+ gQueue = new eventQueue(EVENT_REORDER);
+ gQueue.push(new statesChecker("tree", new nsTreeTreeView()));
+ gQueue.push(new statesChecker("treesingle", new nsTreeTreeView()));
+ gQueue.push(new statesChecker("tabletree", new nsTreeTreeView()));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=523118"
+ title="we mistake 'cell' and text' xul tree seltypes for multiselects">
+ Mozilla Bug 523118
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=624977"
+ title="Optimize nsXulTreeAccessible selectedItems()">
+ Mozilla Bug 624977
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col" flex="1" primary="true" label="column"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <tree id="treesingle" flex="1" seltype="single">
+ <treecols>
+ <treecol id="col_single" flex="1" primary="true" label="column"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <tree id="tabletree" flex="1" editable="true">
+ <treecols>
+ <treecol id="tabletree_col1" cycler="true" label="cycler"/>
+ <treecol id="tabletree_col2" flex="1" primary="true" label="column1"/>
+ <treecol id="tabletree_col3" flex="1" label="column2"/>
+ <treecol id="tabletree_col4" flex="1" label="checker"
+ type="checkbox" editable="true"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <vbox id="debug"/>
+ </vbox>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/states.js b/accessible/tests/mochitest/states.js
new file mode 100644
index 0000000000..c8ce9dd79d
--- /dev/null
+++ b/accessible/tests/mochitest/states.js
@@ -0,0 +1,365 @@
+// //////////////////////////////////////////////////////////////////////////////
+// Helper functions for accessible states testing.
+//
+// requires:
+// common.js
+// role.js
+//
+// //////////////////////////////////////////////////////////////////////////////
+/* import-globals-from common.js */
+/* import-globals-from role.js */
+
+// //////////////////////////////////////////////////////////////////////////////
+// State constants
+
+// const STATE_BUSY is defined in common.js
+const STATE_ANIMATED = nsIAccessibleStates.STATE_ANIMATED;
+const STATE_CHECKED = nsIAccessibleStates.STATE_CHECKED;
+const STATE_CHECKABLE = nsIAccessibleStates.STATE_CHECKABLE;
+const STATE_COLLAPSED = nsIAccessibleStates.STATE_COLLAPSED;
+const STATE_DEFAULT = nsIAccessibleStates.STATE_DEFAULT;
+const STATE_EXPANDED = nsIAccessibleStates.STATE_EXPANDED;
+const STATE_EXTSELECTABLE = nsIAccessibleStates.STATE_EXTSELECTABLE;
+const STATE_FLOATING = nsIAccessibleStates.STATE_FLOATING;
+const STATE_FOCUSABLE = nsIAccessibleStates.STATE_FOCUSABLE;
+const STATE_FOCUSED = nsIAccessibleStates.STATE_FOCUSED;
+const STATE_HASPOPUP = nsIAccessibleStates.STATE_HASPOPUP;
+const STATE_INVALID = nsIAccessibleStates.STATE_INVALID;
+const STATE_INVISIBLE = nsIAccessibleStates.STATE_INVISIBLE;
+const STATE_LINKED = nsIAccessibleStates.STATE_LINKED;
+const STATE_MIXED = nsIAccessibleStates.STATE_MIXED;
+const STATE_MULTISELECTABLE = nsIAccessibleStates.STATE_MULTISELECTABLE;
+const STATE_OFFSCREEN = nsIAccessibleStates.STATE_OFFSCREEN;
+const STATE_PRESSED = nsIAccessibleStates.STATE_PRESSED;
+const STATE_PROTECTED = nsIAccessibleStates.STATE_PROTECTED;
+const STATE_READONLY = nsIAccessibleStates.STATE_READONLY;
+const STATE_REQUIRED = nsIAccessibleStates.STATE_REQUIRED;
+const STATE_SELECTABLE = nsIAccessibleStates.STATE_SELECTABLE;
+const STATE_SELECTED = nsIAccessibleStates.STATE_SELECTED;
+const STATE_TRAVERSED = nsIAccessibleStates.STATE_TRAVERSED;
+const STATE_UNAVAILABLE = nsIAccessibleStates.STATE_UNAVAILABLE;
+
+const EXT_STATE_ACTIVE = nsIAccessibleStates.EXT_STATE_ACTIVE;
+const EXT_STATE_CURRENT = nsIAccessibleStates.EXT_STATE_CURRENT;
+const EXT_STATE_DEFUNCT = nsIAccessibleStates.EXT_STATE_DEFUNCT;
+const EXT_STATE_EDITABLE = nsIAccessibleStates.EXT_STATE_EDITABLE;
+const EXT_STATE_ENABLED = nsIAccessibleStates.EXT_STATE_ENABLED;
+const EXT_STATE_EXPANDABLE = nsIAccessibleStates.EXT_STATE_EXPANDABLE;
+const EXT_STATE_HORIZONTAL = nsIAccessibleStates.EXT_STATE_HORIZONTAL;
+const EXT_STATE_MODAL = nsIAccessibleStates.EXT_STATE_MODAL;
+const EXT_STATE_MULTI_LINE = nsIAccessibleStates.EXT_STATE_MULTI_LINE;
+const EXT_STATE_PINNED = nsIAccessibleStates.EXT_STATE_PINNED;
+const EXT_STATE_SENSITIVE = nsIAccessibleStates.EXT_STATE_SENSITIVE;
+const EXT_STATE_SINGLE_LINE = nsIAccessibleStates.EXT_STATE_SINGLE_LINE;
+const EXT_STATE_STALE = nsIAccessibleStates.EXT_STATE_STALE;
+const EXT_STATE_SUPPORTS_AUTOCOMPLETION =
+ nsIAccessibleStates.EXT_STATE_SUPPORTS_AUTOCOMPLETION;
+const EXT_STATE_VERTICAL = nsIAccessibleStates.EXT_STATE_VERTICAL;
+const EXT_STATE_SELECTABLE_TEXT = nsIAccessibleStates.EXT_STATE_SELECTABLE_TEXT;
+const EXT_STATE_OPAQUE = nsIAccessibleStates.EXT_STATE_OPAQUE;
+
+const kOrdinalState = false;
+const kExtraState = 1;
+
+// //////////////////////////////////////////////////////////////////////////////
+// Test functions
+
+/**
+ * Tests the states and extra states of the given accessible.
+ * Also tests for unwanted states and extra states.
+ * In addition, the function performs a few plausibility checks derived from the
+ * sstates and extra states passed in.
+ *
+ * @param aAccOrElmOrID The accessible, DOM element or ID to be tested.
+ * @param aState The state bits that are wanted.
+ * @param aExtraState The extra state bits that are wanted.
+ * @param aAbsentState State bits that are not wanted.
+ * @param aAbsentExtraState Extra state bits that are not wanted.
+ * @param aTestName The test name.
+ */
+function testStates(
+ aAccOrElmOrID,
+ aState,
+ aExtraState,
+ aAbsentState,
+ aAbsentExtraState,
+ aTestName
+) {
+ var [state, extraState] = getStates(aAccOrElmOrID);
+ var role = getRole(aAccOrElmOrID);
+ var id =
+ prettyName(aAccOrElmOrID) + (aTestName ? " [" + aTestName + "]" : "");
+
+ // Primary test.
+ if (aState) {
+ isState(state & aState, aState, false, "wrong state bits for " + id + "!");
+ }
+
+ if (aExtraState) {
+ isState(
+ extraState & aExtraState,
+ aExtraState,
+ true,
+ "wrong extra state bits for " + id + "!"
+ );
+ }
+
+ if (aAbsentState) {
+ isState(
+ state & aAbsentState,
+ 0,
+ false,
+ "state bits should not be present in ID " + id + "!"
+ );
+ }
+
+ if (aAbsentExtraState) {
+ isState(
+ extraState & aAbsentExtraState,
+ 0,
+ true,
+ "extraState bits should not be present in ID " + id + "!"
+ );
+ }
+
+ // Additional test.
+
+ // focused/focusable
+ if (state & STATE_FOCUSED) {
+ isState(
+ state & STATE_FOCUSABLE,
+ STATE_FOCUSABLE,
+ false,
+ "Focused " + id + " must be focusable!"
+ );
+ }
+
+ if (aAbsentState && aAbsentState & STATE_FOCUSABLE) {
+ isState(
+ state & STATE_FOCUSED,
+ 0,
+ false,
+ "Not focusable " + id + " must be not focused!"
+ );
+ }
+
+ // multiline/singleline
+ if (extraState & EXT_STATE_MULTI_LINE) {
+ isState(
+ extraState & EXT_STATE_SINGLE_LINE,
+ 0,
+ true,
+ "Multiline " + id + " cannot be singleline!"
+ );
+ }
+
+ if (extraState & EXT_STATE_SINGLE_LINE) {
+ isState(
+ extraState & EXT_STATE_MULTI_LINE,
+ 0,
+ true,
+ "Singleline " + id + " cannot be multiline!"
+ );
+ }
+
+ // expanded/collapsed/expandable
+ if (state & STATE_COLLAPSED || state & STATE_EXPANDED) {
+ isState(
+ extraState & EXT_STATE_EXPANDABLE,
+ EXT_STATE_EXPANDABLE,
+ true,
+ "Collapsed or expanded " + id + " must be expandable!"
+ );
+ }
+
+ if (state & STATE_COLLAPSED) {
+ isState(
+ state & STATE_EXPANDED,
+ 0,
+ false,
+ "Collapsed " + id + " cannot be expanded!"
+ );
+ }
+
+ if (state & STATE_EXPANDED) {
+ isState(
+ state & STATE_COLLAPSED,
+ 0,
+ false,
+ "Expanded " + id + " cannot be collapsed!"
+ );
+ }
+
+ if (aAbsentState && extraState & EXT_STATE_EXPANDABLE) {
+ if (aAbsentState & STATE_EXPANDED) {
+ isState(
+ state & STATE_COLLAPSED,
+ STATE_COLLAPSED,
+ false,
+ "Not expanded " + id + " must be collapsed!"
+ );
+ } else if (aAbsentState & STATE_COLLAPSED) {
+ isState(
+ state & STATE_EXPANDED,
+ STATE_EXPANDED,
+ false,
+ "Not collapsed " + id + " must be expanded!"
+ );
+ }
+ }
+
+ // checked/mixed/checkable
+ if (
+ state & STATE_CHECKED ||
+ (state & STATE_MIXED &&
+ role != ROLE_TOGGLE_BUTTON &&
+ role != ROLE_PROGRESSBAR)
+ ) {
+ isState(
+ state & STATE_CHECKABLE,
+ STATE_CHECKABLE,
+ false,
+ "Checked or mixed element must be checkable!"
+ );
+ }
+
+ if (state & STATE_CHECKED) {
+ isState(
+ state & STATE_MIXED,
+ 0,
+ false,
+ "Checked element cannot be state mixed!"
+ );
+ }
+
+ if (state & STATE_MIXED) {
+ isState(
+ state & STATE_CHECKED,
+ 0,
+ false,
+ "Mixed element cannot be state checked!"
+ );
+ }
+
+ // selected/selectable
+ if (state & STATE_SELECTED && !(aAbsentState & STATE_SELECTABLE)) {
+ isState(
+ state & STATE_SELECTABLE,
+ STATE_SELECTABLE,
+ false,
+ "Selected element must be selectable!"
+ );
+ }
+}
+
+/**
+ * Tests an accessible and its sub tree for the passed in state bits.
+ * Used to make sure that states are propagated to descendants, for example the
+ * STATE_UNAVAILABLE from a container to its children.
+ *
+ * @param aAccOrElmOrID The accessible, DOM element or ID to be tested.
+ * @param aState The state bits that are wanted.
+ * @param aExtraState The extra state bits that are wanted.
+ * @param aAbsentState State bits that are not wanted.
+ */
+function testStatesInSubtree(aAccOrElmOrID, aState, aExtraState, aAbsentState) {
+ // test accessible and its subtree for propagated states.
+ var acc = getAccessible(aAccOrElmOrID);
+ if (!acc) {
+ return;
+ }
+
+ if (getRole(acc) != ROLE_TEXT_LEAF) {
+ // Right now, text leafs don't get tested because the states are not being
+ // propagated.
+ testStates(acc, aState, aExtraState, aAbsentState);
+ }
+
+ // Iterate over its children to see if the state got propagated.
+ var children = null;
+ try {
+ children = acc.children;
+ } catch (e) {}
+ ok(children, "Could not get children for " + aAccOrElmOrID + "!");
+
+ if (children) {
+ for (var i = 0; i < children.length; i++) {
+ var childAcc = children.queryElementAt(i, nsIAccessible);
+ testStatesInSubtree(childAcc, aState, aExtraState, aAbsentState);
+ }
+ }
+}
+
+/**
+ * Fails if no defunct state on the accessible.
+ */
+function testIsDefunct(aAccessible, aTestName) {
+ var id = prettyName(aAccessible) + (aTestName ? " [" + aTestName + "]" : "");
+ var [, /* state*/ extraState] = getStates(aAccessible);
+ isState(
+ extraState & EXT_STATE_DEFUNCT,
+ EXT_STATE_DEFUNCT,
+ true,
+ "no defuct state for " + id + "!"
+ );
+}
+
+function getStringStates(aAccOrElmOrID) {
+ var [state, extraState] = getStates(aAccOrElmOrID);
+ return statesToString(state, extraState);
+}
+
+function getStates(aAccOrElmOrID) {
+ var acc = getAccessible(aAccOrElmOrID);
+ if (!acc) {
+ return [0, 0];
+ }
+
+ var state = {},
+ extraState = {};
+ acc.getState(state, extraState);
+
+ return [state.value, extraState.value];
+}
+
+/**
+ * Return true if the accessible has given states.
+ */
+function hasState(aAccOrElmOrID, aState, aExtraState) {
+ var [state, exstate] = getStates(aAccOrElmOrID);
+ return (
+ (aState ? state & aState : true) &&
+ (aExtraState ? exstate & aExtraState : true)
+ );
+}
+
+// //////////////////////////////////////////////////////////////////////////////
+// Private implementation details
+
+/**
+ * Analogy of SimpleTest.is function used to compare states.
+ */
+function isState(aState1, aState2, aIsExtraStates, aMsg) {
+ if (aState1 == aState2) {
+ ok(true, aMsg);
+ return;
+ }
+
+ var got = "0";
+ if (aState1) {
+ got = statesToString(
+ aIsExtraStates ? 0 : aState1,
+ aIsExtraStates ? aState1 : 0
+ );
+ }
+
+ var expected = "0";
+ if (aState2) {
+ expected = statesToString(
+ aIsExtraStates ? 0 : aState2,
+ aIsExtraStates ? aState2 : 0
+ );
+ }
+
+ ok(false, aMsg + "got '" + got + "', expected '" + expected + "'");
+}
diff --git a/accessible/tests/mochitest/states/a11y.toml b/accessible/tests/mochitest/states/a11y.toml
new file mode 100644
index 0000000000..f92526226d
--- /dev/null
+++ b/accessible/tests/mochitest/states/a11y.toml
@@ -0,0 +1,60 @@
+[DEFAULT]
+support-files = [
+ "z_frames.html",
+ "z_frames_article.html",
+ "z_frames_checkbox.html",
+ "z_frames_textbox.html",
+ "z_frames_update.html",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/dumbfile.zip",
+ "!/accessible/tests/mochitest/formimage.png",
+ "!/accessible/tests/mochitest/treeview.css"]
+
+["test_aria.html"]
+
+["test_aria.xhtml"]
+
+["test_aria_imgmap.html"]
+
+["test_aria_widgetitems.html"]
+
+["test_buttons.html"]
+
+["test_controls.html"]
+
+["test_controls.xhtml"]
+
+["test_doc.html"]
+
+["test_doc_busy.html"]
+
+["test_docarticle.html"]
+
+["test_editablebody.html"]
+
+["test_expandable.xhtml"]
+
+["test_frames.html"]
+
+["test_inputs.html"]
+
+["test_link.html"]
+
+["test_popup.xhtml"]
+
+["test_selects.html"]
+
+["test_stale.html"]
+
+["test_tabs.xhtml"]
+
+["test_textbox.xhtml"]
+
+["test_tree.xhtml"]
+
+["test_visibility.html"]
+
+["test_visibility.xhtml"]
+skip-if = [
+ "asan", # Bug 1199631
+]
diff --git a/accessible/tests/mochitest/states/test_aria.html b/accessible/tests/mochitest/states/test_aria.html
new file mode 100644
index 0000000000..8db25099c1
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_aria.html
@@ -0,0 +1,652 @@
+<html>
+
+<head>
+ <title>ARIA based nsIAccessible states testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <style type="text/css">
+ .offscreen {
+ position: absolute;
+ left: -5000px;
+ top: -5000px;
+ height: 100px;
+ width: 100px;
+ }
+ </style>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ function testAriaDisabledTree(aAccOrElmOrID) {
+ // test accessible and its subtree for propagated state.
+ var acc = getAccessible(aAccOrElmOrID);
+ if (!acc)
+ return;
+
+ var [state /* extraState */] = getStates(aAccOrElmOrID);
+ if (state & STATE_UNAVAILABLE) {
+ var role = getRole(acc);
+ if (role != ROLE_GROUPING) {
+ testStates(acc, STATE_FOCUSABLE);
+ }
+ }
+
+ // Iterate over its children to see if the state got propagated.
+ var children = null;
+ try {
+ children = acc.children;
+ } catch (e) {}
+ ok(children, "Could not get children for " + aAccOrElmOrID + "!");
+
+ if (children) {
+ for (var i = 0; i < children.length; i++) {
+ var childAcc = children.queryElementAt(i, nsIAccessible);
+ testAriaDisabledTree(childAcc);
+ }
+ }
+ }
+
+ function doTest() {
+ // aria_autocomplete
+ testStates("textbox_autocomplete_inline", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+ testStates("textbox_autocomplete_list", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+ testStates("textbox_autocomplete_both", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+ testStates("combobox_autocomplete_inline", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+ testStates("combobox_autocomplete_list", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+ testStates("combobox_autocomplete_both", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+
+ testStates("htmltext_autocomplete_list", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+ testStates("htmltextarea_autocomplete_list", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+
+ // aria-busy
+ testStates("textbox_busy_false", 0, 0, STATE_BUSY);
+ testStates("textbox_busy_true", STATE_BUSY);
+ testStates("textbox_busy_error", STATE_INVALID);
+
+ // aria-expanded
+ testStates("combobox", STATE_COLLAPSED);
+ testStates("combobox_expanded", STATE_EXPANDED);
+
+ // tri-state checkbox
+ var checkboxElem = getNode("check1");
+ if (checkboxElem) {
+ testStates(checkboxElem, STATE_CHECKED);
+ checkboxElem.checked = false;
+ testStates(checkboxElem, 0, 0, STATE_CHECKED);
+ checkboxElem.indeterminate = true;
+ testStates(checkboxElem, STATE_MIXED, 0);
+ }
+
+ // aria-checked
+ testStates("aria_checked_checkbox", STATE_CHECKED);
+ testStates("aria_mixed_checkbox", STATE_MIXED);
+ testStates("aria_checked_switch", STATE_CHECKED);
+ testStates("aria_mixed_switch", 0, 0, STATE_MIXED); // unsupported
+ testStates("aria_mixed_switch", 0, 0, STATE_CHECKED); // not checked due to being unsupported
+
+ // test disabled group and all its descendants to see if they are
+ // disabled, too. See bug 429285.
+ testAriaDisabledTree("group");
+
+ // aria-modal
+ testStates("aria_modal", 0, EXT_STATE_MODAL);
+ testStates("aria_modal_false", 0, 0, 0, EXT_STATE_MODAL);
+
+ // aria-multiline
+ testStates("aria_multiline_textbox", 0, EXT_STATE_MULTI_LINE);
+
+ // aria-multiselectable
+ testStates("aria_multiselectable_listbox",
+ STATE_MULTISELECTABLE | STATE_EXTSELECTABLE);
+ testStates("aria_multiselectable_tablist",
+ STATE_MULTISELECTABLE | STATE_EXTSELECTABLE);
+
+ // aria-pressed
+ testStates("aria_pressed_button", STATE_PRESSED, 0, STATE_CHECKABLE);
+ testStates("aria_pressed_native_button", STATE_PRESSED, 0, STATE_CHECKABLE);
+
+ // aria-readonly
+ testStates("aria_readonly_textbox", STATE_READONLY);
+
+ // readonly on grid and gridcell
+ testStates("aria_grid_default", 0, 0,
+ STATE_READONLY, 0);
+ testStates("aria_grid_default_colheader_readonly", STATE_READONLY, 0,
+ 0, 0);
+ testStates("aria_grid_default_colheader_inherited", 0, 0,
+ STATE_READONLY, 0);
+ testStates("aria_grid_default_rowheader_readonly", STATE_READONLY, 0,
+ 0, 0);
+ testStates("aria_grid_default_rowheader_inherited", 0, 0,
+ STATE_READONLY, 0);
+ testStates("aria_grid_default_cell_readonly", STATE_READONLY, 0,
+ 0, 0);
+ testStates("aria_grid_default_cell_inherited", 0, 0,
+ STATE_READONLY, 0);
+
+ testStates("aria_grid_readonly", STATE_READONLY, 0,
+ 0, 0);
+ testStates("aria_grid_readonly_colheader_editable", 0, 0,
+ STATE_READONLY, 0);
+ testStates("aria_grid_readonly_colheader_inherited", STATE_READONLY, 0,
+ 0, 0);
+ testStates("aria_grid_readonly_rowheader_editable", 0, 0,
+ STATE_READONLY, 0);
+ testStates("aria_grid_readonly_rowheader_inherited", STATE_READONLY, 0,
+ 0, 0);
+ testStates("aria_grid_readonly_cell_editable", 0, 0,
+ STATE_READONLY, 0);
+ testStates("aria_grid_readonly_cell_inherited", STATE_READONLY, 0,
+ 0, 0);
+
+ // readonly on treegrid and gridcell
+ testStates("aria_treegrid_default", 0, 0,
+ STATE_READONLY, 0);
+ testStates("aria_treegrid_default_colheader_readonly", STATE_READONLY, 0,
+ 0, 0);
+ testStates("aria_treegrid_default_colheader_inherited", 0, 0,
+ STATE_READONLY, 0);
+ testStates("aria_treegrid_default_rowheader_readonly", STATE_READONLY, 0,
+ 0, 0);
+ testStates("aria_treegrid_default_rowheader_inherited", 0, 0,
+ STATE_READONLY, 0);
+ testStates("aria_treegrid_default_cell_readonly", STATE_READONLY, 0,
+ 0, 0);
+ testStates("aria_treegrid_default_cell_inherited", 0, 0,
+ STATE_READONLY, 0);
+
+ testStates("aria_treegrid_readonly", STATE_READONLY, 0,
+ 0, 0);
+ testStates("aria_treegrid_readonly_colheader_editable", 0, 0,
+ STATE_READONLY, 0);
+ testStates("aria_treegrid_readonly_colheader_inherited", STATE_READONLY, 0,
+ 0, 0);
+ testStates("aria_treegrid_readonly_rowheader_editable", 0, 0,
+ STATE_READONLY, 0);
+ testStates("aria_treegrid_readonly_rowheader_inherited", STATE_READONLY, 0,
+ 0, 0);
+ testStates("aria_treegrid_readonly_cell_editable", 0, 0,
+ STATE_READONLY, 0);
+ testStates("aria_treegrid_readonly_cell_inherited", STATE_READONLY, 0,
+ 0, 0);
+
+ // aria-readonly on directory
+ testStates("aria_directory", STATE_READONLY);
+
+ // aria-selectable
+ testStates("aria_selectable_listitem", STATE_SELECTABLE | STATE_SELECTED);
+
+ // active state caused by aria-activedescendant
+ testStates("as_item1", 0, EXT_STATE_ACTIVE);
+ testStates("as_item2", 0, 0, 0, EXT_STATE_ACTIVE);
+
+ // universal ARIA properties inherited from file input control
+ var fileBrowseButton = getAccessible("fileinput");
+ testStates(fileBrowseButton,
+ STATE_BUSY | STATE_UNAVAILABLE | STATE_REQUIRED | STATE_HASPOPUP | STATE_INVALID);
+
+ // offscreen test
+ testStates("aria_offscreen_textbox", STATE_OFFSCREEN);
+
+ //
+ // This section tests aria roles on links/anchors for underlying
+ // HTMLLinkAccessible creation. (see closed bug 494807)
+ //
+
+ // strong roles
+ testStates("aria_menuitem_link", 0, 0, STATE_LINKED);
+ testStates("aria_button_link", 0, 0, STATE_LINKED);
+ testStates("aria_checkbox_link", 0, 0, STATE_LINKED);
+
+ // strong landmark
+ testStates("aria_application_link", 0, 0, STATE_LINKED);
+ testStates("aria_application_anchor", 0, 0, STATE_SELECTABLE);
+
+ // strange cases
+ testStates("aria_link_link", STATE_LINKED);
+
+ // some landmarks that break accessibility for these native elements
+ // Note that these are illegal uses by web authors as per WAI-ARIA in HTML
+ testStates("aria_main_link", 0, 0, STATE_LINKED);
+ testStates("aria_navigation_link", 0, 0, STATE_LINKED);
+ testStates("aria_main_anchor", 0, 0, STATE_SELECTABLE);
+ testStates("aria_navigation_anchor", 0, 0, STATE_SELECTABLE);
+
+ // aria-orientation
+ testStates("aria_combobox", 0, 0, 0, EXT_STATE_HORIZONTAL | EXT_STATE_VERTICAL);
+ testStates("aria_hcombobox", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_vcombobox", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_listbox", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_hlistbox", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_vlistbox", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_menu", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_hmenu", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_vmenu", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_menubar", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_hmenubar", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_vmenubar", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_radiogroup", 0, 0, 0, EXT_STATE_HORIZONTAL | EXT_STATE_VERTICAL);
+ testStates("aria_hradiogroup", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_vradiogroup", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_scrollbar", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_hscrollbar", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_vscrollbar", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_separator", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_hseparator", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_vseparator", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_slider", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_hslider", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_vslider", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_tablist", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_htablist", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_vtablist", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_toolbar", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_htoolbar", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_vtoolbar", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_tree", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_htree", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_vtree", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("aria_treegrid", 0, 0, 0, EXT_STATE_HORIZONTAL | EXT_STATE_VERTICAL);
+ testStates("aria_htreegrid", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+ testStates("aria_vtreegrid", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+
+ // indeterminate ARIA progressbars (no aria-valuenow or aria-valuetext attribute)
+ // should expose mixed state
+ testStates("aria_progressbar", STATE_MIXED);
+ testStates("aria_progressbar_valuenow", 0, 0, STATE_MIXED);
+ testStates("aria_progressbar_valuetext", 0, 0, STATE_MIXED);
+
+ // aria-current
+ testStates("current_page_1", 0, EXT_STATE_CURRENT);
+ testStates("page_2", 0, 0, EXT_STATE_CURRENT);
+ testStates("page_3", 0, 0, EXT_STATE_CURRENT);
+ testStates("page_4", 0, 0, EXT_STATE_CURRENT);
+ testStates("current_foo", 0, EXT_STATE_CURRENT);
+
+ testStates("aria_listbox", STATE_FOCUSABLE);
+ testStates("aria_grid", STATE_FOCUSABLE);
+ testStates("aria_tree", STATE_FOCUSABLE);
+ testStates("aria_treegrid", STATE_FOCUSABLE);
+ testStates("aria_listbox_disabled", 0, 0, STATE_FOCUSABLE);
+ testStates("aria_grid_disabled", 0, 0, STATE_FOCUSABLE);
+ testStates("aria_tree_disabled", 0, 0, STATE_FOCUSABLE);
+ testStates("aria_treegrid_disabled", 0, 0, STATE_FOCUSABLE);
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=457219"
+ title="nsIAccessible states testing">
+ Mozilla Bug 457219
+ </a><br />
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=429285"
+ title="Propagate aria-disabled to descendants">
+ Mozilla Bug 429285
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=457226"
+ title="Mochitests for ARIA states">
+ Mozilla Bug 457226
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=499653"
+ title="Unify ARIA state attributes mapping rules">
+ Mozilla Bug 499653
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=681674"
+ title="aria-autocomplete not supported on standard form text input controls">
+ Mozilla Bug 681674
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=681674"
+ title="aria-orientation should be applied to separator and slider roles">
+ Mozilla Bug 681674
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=689847"
+ title="Expose active state on current item of selectable widgets">
+ Mozilla Bug 689847
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=699017"
+ title="File input control should be propogate states to descendants">
+ Mozilla Bug 699017
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=690199"
+ title="ARIA select widget should expose focusable state regardless the way they manage its children">
+ Mozilla Bug 690199
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=740851"
+ title="ARIA undetermined progressmeters should expose mixed state">
+ Mozilla Bug 740851
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=762876"
+ title="fix default horizontal / vertical state of role=scrollbar and ensure only one of horizontal / vertical states is exposed">
+ Mozilla Bug 762876
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=892091"
+ title="ARIA treegrid should be editable by default">
+ Bug 892091
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=835121"
+ title="ARIA grid should be editable by default">
+ Mozilla Bug 835121
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=989958"
+ title="Pressed state is not exposed on a button element with aria-pressed attribute">
+ Mozilla Bug 989958
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1136563"
+ title="Support ARIA 1.1 switch role">
+ Mozilla Bug 1136563
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1355921"
+ title="Elements with a defined, non-false value for aria-current should expose ATK_STATE_ACTIVE">
+ Mozilla Bug 1355921
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="textbox_autocomplete_inline" role="textbox" aria-autocomplete="inline"></div>
+ <div id="textbox_autocomplete_list" role="textbox" aria-autocomplete="list"></div>
+ <div id="textbox_autocomplete_both" role="textbox" aria-autocomplete="both"></div>
+ <div id="combobox_autocomplete_inline" role="combobox" aria-autocomplete="inline"></div>
+ <div id="combobox_autocomplete_list" role="combobox" aria-autocomplete="list"></div>
+ <div id="combobox_autocomplete_both" role="combobox" aria-autocomplete="both"></div>
+
+ <input id="htmltext_autocomplete_list" type="text" aria-autocomplete="list" />
+ <textarea id="htmltextarea_autocomplete_list" aria-autocomplete="list"></textarea>
+
+ <div id="textbox_busy_false" role="textbox" aria-busy="false"></div>
+ <div id="textbox_busy_true" role="textbox" aria-busy="true"></div>
+ <div id="textbox_busy_error" role="textbox" aria-busy="error"></div>
+
+ <div id="combobox" role="combobox">combobox</div>
+ <div id="combobox_expanded" role="combobox"
+ aria-expanded="true">combobox</div>
+
+ <input type="checkbox" id="check1" value="I agree" checked="true"/>
+
+ <div id="aria_checked_checkbox" role="checkbox" aria-checked="true">
+ I agree
+ </div>
+
+ <div id="aria_mixed_checkbox" role="checkbox" aria-checked="mixed">
+ I might agree
+ </div>
+
+ <div id="aria_checked_switch" role="switch" aria-checked="true">
+ I am switched on
+ </div>
+
+ <div id="aria_mixed_switch" role="switch" aria-checked="mixed">
+ I am unsupported
+ </div>
+
+ <div id="aria_modal" aria-modal="true">modal stuff</div>
+ <div id="aria_modal_false" aria-modal="false">non modal stuff</div>div>
+ <div id="aria_multiline_textbox" role="textbox" aria-multiline="true"></div>
+ <div id="aria_multiselectable_listbox" role="listbox" aria-multiselectable="true"></div>
+ <div id="aria_multiselectable_tablist" role="tablist" aria-multiselectable="true"></div>
+ <div id="aria_pressed_button" role="button" aria-pressed="true">Button</div>
+ <button id="aria_pressed_native_button" aria-pressed="true">Button</button>
+
+ <div id="aria_readonly_textbox"
+ role="textbox" aria-readonly="true">This text should be readonly</div>
+
+ <div id="aria_grid_default" role="grid">
+ <div role="row">
+ <div id="aria_grid_default_colheader_readonly"
+ role="columnheader" aria-readonly="true">colheader1</div>
+ <div id="aria_grid_default_colheader_inherited"
+ role="columnheader">colheader2</div>
+ </div>
+ <div role="row">
+ <div id="aria_grid_default_rowheader_readonly"
+ role="rowheader" aria-readonly="true">rowheader1</div>
+ <div id="aria_grid_default_rowheader_inherited"
+ role="rowheader">rowheader2</div>
+ </div>
+ <div role="row">
+ <div id="aria_grid_default_cell_readonly"
+ role="gridcell" aria-readonly="true">gridcell1</div>
+ <div id="aria_grid_default_cell_inherited"
+ role="gridcell">gridcell2</div>
+ </div>
+ </div>
+
+ <div id="aria_grid_readonly" role="grid" aria-readonly="true">
+ <div role="row">
+ <div id="aria_grid_readonly_colheader_editable"
+ role="columnheader" aria-readonly="false">colheader1</div>
+ <div id="aria_grid_readonly_colheader_inherited"
+ role="columnheader">colheader2</div>
+ </div>
+ <div role="row">
+ <div id="aria_grid_readonly_rowheader_editable"
+ role="rowheader" aria-readonly="false">rowheader1</div>
+ <div id="aria_grid_readonly_rowheader_inherited"
+ role="rowheader">rowheader2</div>
+ </div>
+ <div role="row">
+ <div id="aria_grid_readonly_cell_editable"
+ role="gridcell" aria-readonly="false">gridcell1</div>
+ <div id="aria_grid_readonly_cell_inherited"
+ role="gridcell">gridcell2</div>
+ </div>
+ </div>
+
+ <div id="aria_treegrid_default" role="grid">
+ <div role="row">
+ <div id="aria_treegrid_default_colheader_readonly"
+ role="columnheader" aria-readonly="true">colheader1</div>
+ <div id="aria_treegrid_default_colheader_inherited"
+ role="columnheader">colheader2</div>
+ </div>
+ <div role="row">
+ <div id="aria_treegrid_default_rowheader_readonly"
+ role="rowheader" aria-readonly="true">rowheader1</div>
+ <div id="aria_treegrid_default_rowheader_inherited"
+ role="rowheader">rowheader2</div>
+ </div>
+ <div role="row">
+ <div id="aria_treegrid_default_cell_readonly"
+ role="gridcell" aria-readonly="true">gridcell1</div>
+ <div id="aria_treegrid_default_cell_inherited"
+ role="gridcell">gridcell2</div>
+ </div>
+ </div>
+
+ <div id="aria_treegrid_readonly" role="grid" aria-readonly="true">
+ <div role="row">
+ <div id="aria_treegrid_readonly_colheader_editable"
+ role="columnheader" aria-readonly="false">colheader1</div>
+ <div id="aria_treegrid_readonly_colheader_inherited"
+ role="columnheader">colheader2</div>
+ </div>
+ <div role="row">
+ <div id="aria_treegrid_readonly_rowheader_editable"
+ role="rowheader" aria-readonly="false">rowheader1</div>
+ <div id="aria_treegrid_readonly_rowheader_inherited"
+ role="rowheader">rowheader2</div>
+ </div>
+ <div role="row">
+ <div id="aria_treegrid_readonly_cell_editable"
+ role="gridcell" aria-readonly="false">gridcell1</div>
+ <div id="aria_treegrid_readonly_cell_inherited"
+ role="gridcell">gridcell2</div>
+ </div>
+ </div>
+
+ <div role="listbox">
+ <div id="aria_selectable_listitem" role="option" aria-selected="true">Item1</div>
+ </div>
+
+ <!-- Test that aria-disabled state gets propagated to all descendants -->
+ <div id="group" role="group" aria-disabled="true">
+ <button>hi</button>
+ <div tabindex="0" role="listbox" aria-activedescendant="item1"
+ aria-owns="item5">
+ <div role="option" id="item1">Item 1</div>
+ <div role="option" id="item2">Item 2</div>
+ <div role="option" id="item3">Item 3</div>
+ <div role="option" id="item4">Item 4</div>
+ </div>
+ <div role="slider" tabindex="0">A slider</div>
+ </div>
+ <div role="option" id="item5">Item 5</div>
+
+ <!-- Test active state -->
+ <div id="as_listbox" tabindex="0" role="listbox"
+ aria-activedescendant="as_item1">
+ <div role="option" id="as_item1">Item 1</div>
+ <div role="option" id="as_item2">Item 2</div>
+ </div>
+
+ <!-- universal ARIA properties should be inherited by text field of file input -->
+ <input type="file" id="fileinput"
+ aria-busy="true"
+ aria-disabled="true"
+ aria-required="true"
+ aria-haspopup="true"
+ aria-invalid="true">
+
+ <div id="offscreen_log" role="log" class="offscreen">
+ <div id="aria_offscreen_textbox" role="textbox" aria-readonly="true">This text should be offscreen</div>
+ </div>
+
+ <a id="aria_menuitem_link" role="menuitem" href="foo">menuitem</a>
+ <a id="aria_button_link" role="button" href="foo">button</a>
+ <a id="aria_checkbox_link" role="checkbox" href="foo">checkbox</a>
+
+ <!-- strange edge case: please don't do this in the wild -->
+ <a id="aria_link_link" role="link" href="foo">link</a>
+
+ <!-- landmarks: links -->
+ <a id="aria_application_link" role="application" href="foo">app</a>
+ <a id="aria_main_link" role="main" href="foo">main</a>
+ <a id="aria_navigation_link" role="navigation" href="foo">nav</a>
+
+ <!-- landmarks: anchors -->
+ <a id="aria_application_anchor" role="application" name="app_anchor">app</a>
+ <a id="aria_main_anchor" role="main" name="main_anchor">main</a>
+ <a id="aria_navigation_anchor" role="navigation" name="nav_anchor">nav</a>
+
+ <!-- aria-orientation -->
+ <div id="aria_combobox" role="combobox">combobox</div>
+ <div id="aria_hcombobox" role="combobox" aria-orientation="horizontal">horizontal combobox</div>
+ <div id="aria_vcombobox" role="combobox" aria-orientation="vertical">vertical combobox</div>
+ <div id="aria_listbox" role="listbox">listbox</div>
+ <div id="aria_hlistbox" role="listbox" aria-orientation="horizontal">horizontal listbox</div>
+ <div id="aria_vlistbox" role="listbox" aria-orientation="vertical">vertical listbox</div>
+ <div id="aria_menu" role="menu">menu</div>
+ <div id="aria_hmenu" role="menu" aria-orientation="horizontal">horizontal menu</div>
+ <div id="aria_vmenu" role="menu" aria-orientation="vertical">vertical menu</div>
+ <div id="aria_menubar" role="menubar">menubar</div>
+ <div id="aria_hmenubar" role="menubar" aria-orientation="horizontal">horizontal menubar</div>
+ <div id="aria_vmenubar" role="menubar" aria-orientation="vertical">vertical menubar</div>
+ <div id="aria_radiogroup" role="radiogroup">radiogroup</div>
+ <div id="aria_hradiogroup" role="radiogroup" aria-orientation="horizontal">horizontal radiogroup</div>
+ <div id="aria_vradiogroup" role="radiogroup" aria-orientation="vertical">vertical radiogroup</div>
+ <div id="aria_scrollbar" role="scrollbar">scrollbar</div>
+ <div id="aria_hscrollbar" role="scrollbar" aria-orientation="horizontal">horizontal scrollbar</div>
+ <div id="aria_vscrollbar" role="scrollbar" aria-orientation="vertical">vertical scrollbar</div>
+ <div id="aria_separator" role="separator">separator</div>
+ <div id="aria_hseparator" role="separator" aria-orientation="horizontal">horizontal separator</div>
+ <div id="aria_vseparator" role="separator" aria-orientation="vertical">vertical separator</div>
+ <div id="aria_slider" role="slider">slider</div>
+ <div id="aria_hslider" role="slider" aria-orientation="horizontal">horizontal slider</div>
+ <div id="aria_vslider" role="slider" aria-orientation="vertical">vertical slider</div>
+
+ <div id="aria_tablist" role="tablist">tablist</div>
+ <div id="aria_htablist" role="tablist" aria-orientation="horizontal">horizontal tablist</div>
+ <div id="aria_vtablist" role="tablist" aria-orientation="vertical">vertical tablist</div>
+ <div id="aria_toolbar" role="toolbar">toolbar</div>
+ <div id="aria_htoolbar" role="toolbar" aria-orientation="horizontal">horizontal toolbar</div>
+ <div id="aria_vtoolbar" role="toolbar" aria-orientation="vertical">vertical toolbar</div>
+ <div id="aria_tree" role="tree">tree</div>
+ <div id="aria_htree" role="tree" aria-orientation="horizontal">horizontal tree</div>
+ <div id="aria_vtree" role="tree" aria-orientation="vertical">vertical tree</div>
+ <div id="aria_treegrid" role="treegrid">treegrid</div>
+ <div id="aria_htreegrid" role="treegrid" aria-orientation="horizontal">horizontal treegrid</div>
+ <div id="aria_vtreegrid" role="treegrid" aria-orientation="vertical">vertical treegrid</div>
+
+ <!-- indeterminate ARIA progressbars should expose mixed state -->
+ <div id="aria_progressbar" role="progressbar"></div>
+ <div id="aria_progressbar_valuenow" role="progressbar" aria-valuenow="1"></div>
+ <div id="aria_progressbar_valuetext" role="progressbar" aria-valuetext="value"></div>
+
+ <!-- ARIA select widget should expose focusable state regardless the way they manage its children -->
+ <div id="aria_listbox" role="listbox">
+ <div role="option" tabindex="0">A</div>
+ <div role="option" tabindex="0">a</div>
+ </div>
+ <div id="aria_grid" role="grid">
+ <div role="row"><div role="gridcell" tabindex="0">B</div></div></div>
+ <div role="row"><div role="gridcell" tabindex="0">b</div></div></div>
+ <div id="aria_tree" role="tree">
+ <div role="treeitem" tabindex="0">C</div>
+ <div role="treeitem" tabindex="0">c</div>
+ </div>
+ <div id="aria_treegrid" role="treegrid">
+ <div role="row"><div role="gridcell" tabindex="0">D</div></div>
+ <div role="row"><div role="gridcell" tabindex="0">d</div></div>
+ </div>
+ <div id="aria_listbox_disabled" role="listbox" aria-disabled="true">
+ <div role="option">E</div>
+ <div role="option">e</div>
+ </div>
+ <div id="aria_grid_disabled" role="grid" aria-disabled="true">
+ <div role="row"><div role="gridcell">F</div></div>
+ <div role="row"><div role="gridcell">f</div></div>
+ </div>
+ <div id="aria_tree_disabled" role="tree" aria-disabled="true">
+ <div role="treeitem">G</div>
+ <div role="treeitem">g</div>
+ </div>
+ <div id="aria_treegrid_disabled" role="treegrid" aria-disabled="true">
+ <div role="row"><div role="gridcell">H</div></div>
+ <div role="row"><div role="gridcell">h</div></div>
+ </div>
+
+ <!-- Test that directory is readonly -->
+ <div id="aria_directory" role="directory"></div>
+
+ <!-- aria-current -->
+ <div id="current_page_1" role="link" aria-current="page">1</div>
+ <div id="page_2" role="link" aria-current="false">2</div>
+ <div id="page_3" role="link">3</div>
+ <div id="page_4" role="link" aria-current="">4</div>
+ <div id="current_foo" aria-current="foo">foo</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_aria.xhtml b/accessible/tests/mochitest/states/test_aria.xhtml
new file mode 100644
index 0000000000..e42a0ea96d
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_aria.xhtml
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL ARIA state tests">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function doTest()
+ {
+ // aria-pressed
+ testStates("pressed_button", STATE_PRESSED, 0, STATE_CHECKABLE);
+
+ testStates("tabs", STATE_MULTISELECTABLE);
+ // Make sure XUL selection works, since aria-selected defaults to false.
+ testStates("tab1", STATE_SELECTED);
+ // aria-selected="true".
+ testStates("tab2", STATE_SELECTED);
+ // Neither.
+ testStates("tab3", 0, 0, STATE_SELECTED);
+
+ SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1033283"
+ title="Expose pressed state on XUL menu toggle buttons">
+ Mozilla Bug 1033283
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <button id="pressed_button" aria-pressed="true" label="I am pressed" />
+
+ <tabbox>
+ <tabs id="tabs" aria-multiselectable="true">
+ <tab id="tab1" label="tab1" selected="true"/>
+ <tab id="tab2" label="tab2" aria-selected="true"/>
+ <tab id="tab3" label="tab3"/>
+ </tabs>
+ </tabbox>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/states/test_aria_imgmap.html b/accessible/tests/mochitest/states/test_aria_imgmap.html
new file mode 100644
index 0000000000..387e33259e
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_aria_imgmap.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test usemap elements and ARIA</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // gA11yEventDumpToConsole = true;
+ function doPreTest() {
+ waitForImageMap("imagemap", doTest);
+ }
+
+ function doTest() {
+ var imageMap = getAccessible("imagemap");
+
+ var t1 = imageMap.getChildAt(0);
+ testStates(t1, 0, EXT_STATE_EDITABLE, STATE_LINKED);
+ var t2 = imageMap.getChildAt(1);
+ testStates(t2, 0, EXT_STATE_EDITABLE, STATE_LINKED);
+ var rb1 = imageMap.getChildAt(2);
+ testStates(rb1, (STATE_CHECKABLE | STATE_CHECKED), 0, STATE_LINKED);
+ var rb2 = imageMap.getChildAt(3);
+ testStates(rb2, STATE_CHECKABLE, 0, STATE_CHECKED, STATE_LINKED);
+ var cb1 = imageMap.getChildAt(4);
+ testStates(cb1, (STATE_CHECKABLE | STATE_CHECKED), 0, STATE_LINKED);
+ var cbox = imageMap.getChildAt(5);
+ testStates(cbox, (STATE_HASPOPUP | STATE_COLLAPSED),
+ EXT_STATE_EXPANDABLE, STATE_LINKED);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doPreTest);
+ </script>
+
+</head>
+<body>
+
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=548291"
+ title="ARIA states on image maps">
+Mozilla Bug 548291
+</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test">
+</pre>
+
+<img id="imagemap" src="../formimage.png" width="219" height="229" border="0" usemap="#ariaMap">
+<map id="ariaMap" name="ariaMap">
+ <area id="t1" role="textbox" shape="rect" tabindex="0" alt="" title="first name" coords="4,20,108,48" href="#" />
+ <area id="t2" role="textbox" shape="rect" alt="" title="last name" coords="111,21,215,50" href="#" />
+ <area id="rb1" role="radio" aria-checked="true" shape="circle" alt="" title="male" coords="60,75,11" href="#" />
+ <area id="rb2" role="radio" shape="circle" alt="" title="female" coords="73,94,11" href="#" />
+ <area id="cb1" role="checkbox" aria-checked="true" shape="rect" alt="" title="have bike" coords="95,123,118,145" href="#" />
+ <area id="cbox" role="combobox" shape="rect" alt="" title="bike model" coords="120,124,184,146" href="#" />
+ <area id="cb2" role="checkbox" shape="rect" alt="" title="have car" coords="90,145,114,164" href="#" />
+ <area id="cb3" role="checkbox" shape="rect" alt="" title="have airplane" coords="130,163,152,184" href="#" />
+ <area id="b1" role="button" shape="rect" alt="" title="submit" coords="4,198,67,224" href="#" />
+</map>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_aria_widgetitems.html b/accessible/tests/mochitest/states/test_aria_widgetitems.html
new file mode 100644
index 0000000000..c2d546ba01
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_aria_widgetitems.html
@@ -0,0 +1,152 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Test ARIA tab accessible selected state</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function focusARIAItem(aID, aIsSelected) {
+ this.DOMNode = getNode(aID);
+
+ this.invoke = function focusARIAItem_invoke() {
+ this.DOMNode.focus();
+ };
+
+ this.check = function focusARIAItem_check(aEvent) {
+ testStates(this.DOMNode, aIsSelected ? STATE_SELECTED : 0, 0,
+ aIsSelected ? 0 : STATE_SELECTED);
+ };
+
+ this.getID = function focusARIAItem_getID() {
+ return "Focused ARIA widget item with aria-selected='" +
+ (aIsSelected ? "true', should" : "false', shouldn't") +
+ " have selected state on " + prettyName(aID);
+ };
+ }
+
+ function focusActiveDescendantItem(aItemID, aWidgetID, aIsSelected) {
+ this.DOMNode = getNode(aItemID);
+ this.widgetDOMNode = getNode(aWidgetID);
+
+ this.invoke = function focusActiveDescendantItem_invoke() {
+ this.widgetDOMNode.setAttribute("aria-activedescendant", aItemID);
+ this.widgetDOMNode.focus();
+ };
+
+ this.check = function focusActiveDescendantItem_check(aEvent) {
+ testStates(this.DOMNode, aIsSelected ? STATE_SELECTED : 0, 0,
+ aIsSelected ? 0 : STATE_SELECTED);
+ };
+
+ this.getID = function tabActiveDescendant_getID() {
+ return "ARIA widget item managed by activedescendant " +
+ (aIsSelected ? "should" : "shouldn't") +
+ " have the selected state on " + prettyName(aItemID);
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+
+ function doTest() {
+ // aria-selected
+ testStates("aria_tab1", 0, 0, STATE_SELECTED);
+ testStates("aria_tab2", STATE_SELECTED);
+ testStates("aria_tab3", 0, 0, STATE_SELECTED);
+ testStates("aria_option1", 0, 0, STATE_SELECTED);
+ testStates("aria_option2", STATE_SELECTED);
+ testStates("aria_option3", 0, 0, STATE_SELECTED);
+ testStates("aria_treeitem1", 0, 0, STATE_SELECTED);
+ testStates("aria_treeitem2", STATE_SELECTED);
+ testStates("aria_treeitem3", 0, 0, STATE_SELECTED);
+
+ // selected state when widget item is focused
+ gQueue = new eventQueue(EVENT_FOCUS);
+
+ gQueue.push(new focusARIAItem("aria_tab1", true));
+ gQueue.push(new focusARIAItem("aria_tab2", true));
+ gQueue.push(new focusARIAItem("aria_tab3", false));
+ gQueue.push(new focusARIAItem("aria_option1", true));
+ gQueue.push(new focusARIAItem("aria_option2", true));
+ gQueue.push(new focusARIAItem("aria_option3", false));
+ gQueue.push(new focusARIAItem("aria_treeitem1", true));
+ gQueue.push(new focusARIAItem("aria_treeitem2", true));
+ gQueue.push(new focusARIAItem("aria_treeitem3", false));
+
+ // selected state when widget item is focused (by aria-activedescendant)
+ gQueue.push(new focusActiveDescendantItem("aria_tab5", "aria_tablist2", true));
+ gQueue.push(new focusActiveDescendantItem("aria_tab6", "aria_tablist2", true));
+ gQueue.push(new focusActiveDescendantItem("aria_tab4", "aria_tablist2", false));
+
+ gQueue.invoke(); // SimpleTest.finish() will be called in the end
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=653601"
+ title="aria-selected ignored for ARIA tabs">
+ Mozilla Bug 653601
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=526703"
+ title="Focused widget item should expose selected state by default">
+ Mozilla Bug 526703
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- tab -->
+ <div id="aria_tablist" role="tablist">
+ <div id="aria_tab1" role="tab" tabindex="0">unselected tab</div>
+ <div id="aria_tab2" role="tab" tabindex="0" aria-selected="true">selected tab</div>
+ <div id="aria_tab3" role="tab" tabindex="0" aria-selected="false">focused explicitly unselected tab</div>
+ </div>
+
+ <!-- listbox -->
+ <div id="aria_listbox" role="listbox">
+ <div id="aria_option1" role="option" tabindex="0">unselected option</div>
+ <div id="aria_option2" role="option" tabindex="0" aria-selected="true">selected option</div>
+ <div id="aria_option3" role="option" tabindex="0" aria-selected="false">focused explicitly unselected option</div>
+ </div>
+
+ <!-- tree -->
+ <div id="aria_tree" role="tree">
+ <div id="aria_treeitem1" role="treeitem" tabindex="0">unselected treeitem</div>
+ <div id="aria_treeitem2" role="treeitem" tabindex="0" aria-selected="true">selected treeitem</div>
+ <div id="aria_treeitem3" role="treeitem" tabindex="0" aria-selected="false">focused explicitly unselected treeitem</div>
+ </div>
+
+ <!-- tab managed by active-descendant -->
+ <div id="aria_tablist2" role="tablist" tabindex="0">
+ <div id="aria_tab4" role="tab" aria-selected="false">focused explicitly unselected tab</div>
+ <div id="aria_tab5" role="tab">initially selected tab</div>
+ <div id="aria_tab6" role="tab">later selected tab</div>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_buttons.html b/accessible/tests/mochitest/states/test_buttons.html
new file mode 100644
index 0000000000..ec6e65cf32
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_buttons.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML button accessible states</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // Default state.
+ testStates("f1_image", STATE_DEFAULT | STATE_FOCUSABLE);
+ testStates("f2_submit", STATE_DEFAULT | STATE_FOCUSABLE);
+ testStates("f3_submitbutton", STATE_DEFAULT | STATE_FOCUSABLE);
+ testStates("f3_disabled_reset", STATE_UNAVAILABLE, 0, STATE_FOCUSABLE, 0);
+ testStates("f4_button", STATE_FOCUSABLE, 0, STATE_DEFAULT);
+ testStates("f4_disabled_button", STATE_UNAVAILABLE, 0, STATE_FOCUSABLE, 0);
+ testStates("f4_image1", STATE_DEFAULT | STATE_FOCUSABLE);
+ testStates("f4_image2", STATE_FOCUSABLE, 0, STATE_DEFAULT);
+ testStates("f4_submit", STATE_FOCUSABLE, 0, STATE_DEFAULT);
+ testStates("f4_submitbutton", STATE_FOCUSABLE, 0, STATE_DEFAULT);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=664142"
+ title="DEFAULT state exposed incorrectly for HTML">
+ Mozilla Bug 664142
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <p>A form with an image button</p>
+ <form name="form1" method="get">
+ <input type="text" name="hi">
+
+ <input id="f1_image" type="image" value="image-button">
+ </form>
+
+ <p>A form with a submit button:</p>
+ <form name="form2" method="get">
+ <input type="text" name="hi">
+ <input id="f2_submit" type="submit">
+ </form>
+
+ <p>A form with a HTML4 submit button:</p>
+ <form name="form3" method="get">
+ <input type="text" name="hi">
+ <button id="f3_submitbutton" type="submit">submit</button>
+ <button id="f3_disabled_reset" type="reset" disabled>reset</button>
+ </form>
+
+ <p>A form with normal button, two image buttons, submit button,
+ HTML4 submit button:</p>
+ <form name="form4" method="get">
+ <input type="text" name="hi">
+ <input id="f4_button" type="button" value="normal" name="normal-button">
+ <input id="f4_disabled_button" type="button" value="disabled" name="disabled-button" disabled>
+ <input id="f4_image1" type="image" value="image-button1" name="image-button1">
+ <input id="f4_image2" type="image" value="image-button2" name="image-button2">
+ <input id="f4_submit" type="submit" value="real-submit" name="real-submit">
+ <button id="f4_submitbutton" type="submit">submit</button>
+ </form>
+
+ </body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_controls.html b/accessible/tests/mochitest/states/test_controls.html
new file mode 100644
index 0000000000..27aecf4c52
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_controls.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML control states</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // Undetermined progressbar (no value or aria-value attribute): mixed state
+ testStates("progress", STATE_MIXED);
+ // Determined progressbar (has value): shouldn't have mixed state
+ testStates("progress2", 0, 0, STATE_MIXED);
+ // Determined progressbar (has aria-value): shouldn't have mixed state
+ // testStates("progress3", 0, 0, STATE_MIXED);
+ todo(false, "we should respect ARIA");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=670853"
+ title="Bug 670853 - undetermined progressmeters should expose mixed state">
+ Mozilla Bug 670853
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <progress id="progress"></progress>
+ <progress id="progress2" value="1"></progress>
+ <progress id="progress3" aria-valuenow="1"></progress>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_controls.xhtml b/accessible/tests/mochitest/states/test_controls.xhtml
new file mode 100644
index 0000000000..a1997aef6e
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_controls.xhtml
@@ -0,0 +1,153 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL input control state tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ var gQueue = null;
+ function doTest()
+ {
+ testStates("checkbox", STATE_FOCUSABLE, 0, STATE_UNAVAILABLE);
+ testStates("checkbox2", STATE_UNAVAILABLE, 0 , STATE_FOCUSABLE);
+ testStates("radiogroup", 0, 0, STATE_FOCUSABLE | STATE_UNAVAILABLE);
+ testStates("radio", STATE_FOCUSABLE, 0, STATE_UNAVAILABLE);
+ testStates("radio-disabled", STATE_UNAVAILABLE, 0 , STATE_FOCUSABLE);
+ testStates("radiogroup-disabled", STATE_UNAVAILABLE, 0 , STATE_FOCUSABLE);
+ testStates("radio-disabledradiogroup", STATE_UNAVAILABLE, 0 , STATE_FOCUSABLE);
+ testStates("button", STATE_FOCUSABLE, 0, STATE_UNAVAILABLE);
+ testStates("button-disabled", STATE_UNAVAILABLE, 0 , STATE_FOCUSABLE);
+ testStates("checkbutton",
+ STATE_FOCUSABLE | STATE_PRESSED, 0, STATE_CHECKABLE);
+ testStates("fakecheckbutton", STATE_FOCUSABLE | STATE_PRESSED, 0,
+ STATE_CHECKABLE);
+ testStates("combobox", STATE_FOCUSABLE | STATE_HASPOPUP, 0, STATE_UNAVAILABLE);
+ testStates("combobox-disabled", STATE_UNAVAILABLE | STATE_HASPOPUP, 0, STATE_FOCUSABLE);
+ testStates("listbox", STATE_FOCUSABLE, 0, STATE_UNAVAILABLE);
+ testStates("listitem", STATE_FOCUSABLE | STATE_SELECTABLE | STATE_SELECTED, 0, STATE_UNAVAILABLE);
+ testStates("listbox-disabled", STATE_UNAVAILABLE, 0, STATE_FOCUSABLE | STATE_SELECTABLE);
+ testStates("listitem-disabledlistbox", STATE_UNAVAILABLE | STATE_SELECTED, 0, STATE_FOCUSABLE | STATE_SELECTABLE);
+ testStates("menubar", 0, 0, STATE_FOCUSABLE);
+ testStates("menu", STATE_FOCUSABLE, 0, STATE_UNAVAILABLE);
+ testStates("menu-disabled", STATE_UNAVAILABLE, 0, STATE_FOCUSABLE | STATE_SELECTABLE);
+ testStates("tab", STATE_FOCUSABLE | STATE_SELECTABLE | STATE_SELECTED, 0, STATE_UNAVAILABLE);
+ testStates("tab-disabled", STATE_UNAVAILABLE, 0, STATE_FOCUSABLE | STATE_SELECTABLE | STATE_SELECTED);
+
+ gQueue = new eventQueue();
+ gQueue.invoke(); // Will call SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=599163"
+ title="check disabled state instead of attribute">
+ Mozilla Bug 599163
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=756983"
+ title="Isolate focusable and unavailable states from State()">
+ Mozilla Bug 756983
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+
+ <checkbox id="checkbox" checked="true" label="Steak"/>
+ <checkbox id="checkbox2" checked="true" label="Salad" disabled="true"/>
+
+ <radiogroup id="radiogroup">
+ <radio id="radio" label="Orange"/>
+ <radio id="radio-disabled" selected="true" label="Violet" disabled="true"/>
+ </radiogroup>
+
+ <radiogroup id="radiogroup-disabled" disabled="true">
+ <radio id="radio-disabledradiogroup" label="Orange"/>
+ <radio id="violet2" selected="true" label="Violet"/>
+ </radiogroup>
+
+ <button id="button" value="button"/>
+ <button id="button-disabled" disabled="true" value="button"/>
+ <button id="checkbutton" type="checkbox" checked="true"
+ value="checkbutton" />
+ <button id="fakecheckbutton" checked="true" value="fakecheckbutton" />
+
+ <menulist id="combobox">
+ <menupopup>
+ <menuitem label="item1"/>
+ </menupopup>
+ </menulist>
+
+ <menulist id="combobox-disabled" disabled="true">
+ <menupopup>
+ <menuitem label="item1"/>
+ </menupopup>
+ </menulist>
+
+ <richlistbox id="listbox">
+ <richlistitem id="listitem">
+ <label value="list item"/>
+ </richlistitem>
+ </richlistbox>
+
+ <richlistbox id="listbox-disabled" disabled="true">
+ <richlistitem id="listitem-disabledlistbox">
+ <label value="list item"/>
+ </richlistitem>
+ </richlistbox>
+
+ <toolbox>
+ <menubar id="menubar">
+ <menu id="menu" label="menu1">
+ <menupopup>
+ <menuitem id="menu1-item1" label="menuitem1.1"/>
+ </menupopup>
+ </menu>
+ <menu id="menu-disabled" label="menu2" disabled="true">
+ <menupopup>
+ <menuitem id="menu-disabled-item1" label="menuitem2.1"/>
+ </menupopup>
+ </menu>
+ </menubar>
+ </toolbox>
+
+ <tabbox>
+ <tabs>
+ <tab id="tab" label="tab1" tooltip="tooltip"/>
+ <tab id="tab-disabled" label="tab1" disabled="true"/>
+ </tabs>
+ <tabpanels>
+ <tabpanel/>
+ <tabpanel/>
+ </tabpanels>
+ </tabbox>
+
+ <tooltip id="tooltip"><description>tooltip</description></tooltip>
+ </vbox>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/states/test_doc.html b/accessible/tests/mochitest/states/test_doc.html
new file mode 100644
index 0000000000..a00ca957b9
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_doc.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>states of document</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // Bug 566542: root accesible should expose active state when focused.
+ testStates(getRootAccessible(), 0, EXT_STATE_ACTIVE);
+
+ // Bug 509696, 607219.
+ testStates(document, STATE_READONLY, 0); // role=""
+
+ document.body.setAttribute("role", "application");
+ testStates(document, 0, 0, STATE_READONLY);
+ document.body.setAttribute("role", "foo"); // bogus role
+ testStates(document, STATE_READONLY);
+ document.body.removeAttribute("role");
+ testStates(document, STATE_READONLY);
+
+ // Bugs 454997 and 467387
+ testStates(document, STATE_READONLY);
+ testStates("document", STATE_READONLY);
+ testStates("editable_document", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+
+ testStates("p", 0, EXT_STATE_SELECTABLE_TEXT, 0, EXT_STATE_EDITABLE);
+ testStates("unselectable_p", 0, 0, 0, EXT_STATE_SELECTABLE_TEXT | EXT_STATE_EDITABLE);
+ testStates("unselectable_link", 0, 0, 0, EXT_STATE_SELECTABLE_TEXT | EXT_STATE_EDITABLE);
+
+ document.designMode = "on";
+
+ testStates(document, 0, EXT_STATE_EDITABLE, STATE_READONLY);
+ testStates("p", 0, EXT_STATE_EDITABLE | EXT_STATE_SELECTABLE_TEXT, STATE_READONLY);
+ testStates("document", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+ testStates("editable_document", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+ testStates("unselectable_p", 0, EXT_STATE_SELECTABLE_TEXT | EXT_STATE_EDITABLE);
+ testStates("unselectable_link", 0, EXT_STATE_SELECTABLE_TEXT | EXT_STATE_EDITABLE);
+
+ document.designMode = "off";
+
+ testStates(document, STATE_READONLY);
+ testStates("document", STATE_READONLY);
+ testStates("editable_document", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body role="">
+
+ <a target="_blank"
+ title="<body contenteditable='true'> exposed incorrectly"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=454997">Mozilla Bug 454997</a>
+ <a target="_blank"
+ title="nsIAccessible states tests of editable document"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=467387">Mozilla Bug 467387</a>
+ <a target="_blank"
+ title="Role attribute on body with empty string causes DocAccessible not to have read-only state."
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=509696">Mozilla Bug 509696</a>
+ <a target="_blank"
+ title="Frame for firefox does not implement the state "active" when firefox is the active frame"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=566542">Mozilla Bug 566542</a>
+ <a target="_blank"
+ title="Dynamic role attribute change on body doesn't affect on document role"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=607219">Mozilla Bug 607219</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <p id="p">hello</p>
+
+ <p id="unselectable_p" style="user-select: none;">unselectable <a id="unselectable_link" href="#">link</a></p>
+
+ <div id="document" role="document">document</div>
+ <div id="editable_document" role="document" contentEditable="true">editable document</doc>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_doc_busy.html b/accessible/tests/mochitest/states/test_doc_busy.html
new file mode 100644
index 0000000000..701c749aca
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_doc_busy.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>states of document</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ const { BrowserTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/BrowserTestUtils.sys.mjs");
+ const { Downloads } = ChromeUtils.importESModule(
+ "resource://gre/modules/Downloads.sys.mjs");
+
+ function matchDocBusyChange(isBusy) {
+ return function(event) {
+ const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent);
+ return (
+ event.DOMNode == document &&
+ scEvent.state === STATE_BUSY &&
+ scEvent.isEnabled === isBusy
+ );
+ };
+ }
+
+ function getDownloadStartedPromise() {
+ return Downloads.getList(Downloads.PUBLIC).then(list => {
+ return new Promise(resolve => {
+ list.addView({
+ onDownloadAdded(download) {
+ resolve(download);
+ }
+ })
+ });
+ });
+ }
+
+ async function downloadHandled(downloadResult) {
+ if (Window.isInstance(downloadResult)) {
+ return BrowserTestUtils.closeWindow(downloadResult);
+ }
+ // downloadResult is a download object.
+ await downloadResult.finalize(true);
+ return Downloads.getList(Downloads.PUBLIC).then(list => list.remove(downloadResult));
+ }
+
+ async function doTest() {
+ // Because of variable timing, there are two acceptable possibilities:
+ // 1. We get an event for busy and an event for not busy.
+ // 2. The two events get coalesced, so no events are fired.
+ // However, we fail this test if we get the first event but not the
+ // second.
+ let gotBusy = false;
+ let gotNotBusy = false;
+ const busyEvents = async function() {
+ await waitForEvent(EVENT_STATE_CHANGE, matchDocBusyChange(true));
+ info("Got busy event");
+ gotBusy = true;
+ await waitForEvent(EVENT_STATE_CHANGE, matchDocBusyChange(false));
+ info("Got not-busy event");
+ gotNotBusy = true;
+ }();
+
+ const downloadStarted = getDownloadStartedPromise();
+
+ info("Clicking link to trigger download");
+ synthesizeMouse(getNode("link"), 1, 1, {});
+ info("Waiting for download prompt to open");
+ const downloadResult = await downloadStarted;
+
+ // Once we no longer show a dialog for downloads, the not busy event
+ // might show up a bit later.
+ if (gotBusy && !gotNotBusy) {
+ await busyEvents;
+ }
+
+ // Any busy events should have been fired by the time the download
+ // prompt has opened.
+ if (gotBusy && gotNotBusy) {
+ ok(true, "Got both busy change and not-busy change");
+ } else if (!gotBusy && !gotNotBusy) {
+ ok(true, "No busy events, coalesced");
+ } else {
+ ok(false, "Got busy change but didn't get not-busy change!");
+ }
+ testStates(document, 0, 0, STATE_BUSY, 0, "Document not busy");
+
+ // Clean up.
+ info("Closing download prompt");
+ await downloadHandled(downloadResult);
+ // We might still be waiting on busy events. Remove any pending observers.
+ for (let observer of Services.obs.enumerateObservers(
+ "accessible-event")
+ ) {
+ Services.obs.removeObserver(observer, "accessible-event");
+ }
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ title="Missing busy state change event when downloading files"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=446469">Bug 446469</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <a id="link" href="http://example.com/a11y/accessible/tests/mochitest/dumbfile.zip">a file</a>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_docarticle.html b/accessible/tests/mochitest/states/test_docarticle.html
new file mode 100644
index 0000000000..8e45d5ebd6
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_docarticle.html
@@ -0,0 +1,78 @@
+<html>
+<head>
+ <title>states of document article</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var docAcc = getAccessible(document, [nsIAccessibleDocument]);
+ if (docAcc) {
+ testStates(docAcc, STATE_READONLY);
+ testStates("aria_article", STATE_READONLY);
+ testStates("editable_aria_article", 0, EXT_STATE_EDITABLE,
+ STATE_READONLY);
+ testStates("article", STATE_READONLY);
+ testStates("editable_article", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+
+ document.designMode = "on";
+
+ testStates(docAcc, 0, EXT_STATE_EDITABLE, STATE_READONLY);
+ testStates("aria_article", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+ testStates("editable_aria_article", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+ testStates("article", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+ testStates("editable_article", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+
+ document.designMode = "off";
+
+ testStates(docAcc, STATE_READONLY);
+ testStates("aria_article", STATE_READONLY);
+ testStates("editable_aria_article", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+ testStates("article", STATE_READONLY);
+ testStates("editable_article", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+ }
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body role="article">
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=467387"
+ title="Expose non-editable documents as readonly, regardless of role">
+ Mozilla Bug 467387
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=613502"
+ title="Map <article> like we do aria role article">
+ Mozilla Bug 613502
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="aria_article" role="article">aria article</div>
+ <div id="editable_aria_article" role="article" contentEditable="true">
+ editable aria article</div>
+
+ <article id="article">article</article>
+ <article id="editable_article" contentEditable="true">
+ editable article</article>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_editablebody.html b/accessible/tests/mochitest/states/test_editablebody.html
new file mode 100644
index 0000000000..27b7201a2b
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_editablebody.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=454997
+-->
+<head>
+ <title>nsIAccessible states tests of contenteditable body</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ testStates(document, 0, EXT_STATE_EDITABLE);
+ testStates("p", 0, EXT_STATE_EDITABLE);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body contentEditable="true">
+
+ <a target="_blank"
+ title="nsIAccessible states tests of contenteditable body"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=454997">Mozilla Bug 454997</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <p id="p">hello</p>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_expandable.xhtml b/accessible/tests/mochitest/states/test_expandable.xhtml
new file mode 100644
index 0000000000..0d969bef5b
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_expandable.xhtml
@@ -0,0 +1,112 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<!-- Firefox searchbar -->
+<?xml-stylesheet href="chrome://browser/content/browser.css"
+ type="text/css"?>
+<!-- SeaMonkey searchbar -->
+<?xml-stylesheet href="chrome://navigator/content/navigator.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Expanded state change events tests for comboboxes and autocompletes.">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+
+ <script type="application/javascript"
+ src="../autocomplete.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ //gA11yEventDumpToConsole = true; // debuggin
+
+ var gQueue = null;
+ function doTest()
+ {
+ gQueue = new eventQueue();
+
+ gQueue.push(new openCombobox("menulist"));
+ gQueue.push(new closeCombobox("menulist"));
+
+ todo(false, "Autocompletes don't fire expanded state change events when popup open. See bug 688480!");
+ //gQueue.push(new openCombobox("autocomplete"));
+ //gQueue.push(new closeCombobox("autocomplete"));
+
+ // XXX: searchbar doesn't fire state change events because accessible
+ // parent of combobox_list accessible is pushbutton accessible.
+ //var searchbar = document.getElementById("searchbar");
+ //gQueue.push(new openHideCombobox(searchbar, true));
+ //gQueue.push(new openHideCombobox(searchbar, false));
+ todo(false, "Enable states test for XUL searchbar widget!");
+
+ gQueue.onFinish = function()
+ {
+ // unregister 'test-a11y-search' autocomplete search
+ shutdownAutoComplete();
+ }
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ // This is the hacks needed to use a searchbar without browser.js.
+ var BrowserSearch = {
+ updateOpenSearchBadge() {}
+ };
+
+ SimpleTest.waitForExplicitFinish();
+
+ // Register 'test-a11y-search' autocomplete search.
+ // XPFE AutoComplete needs to register early.
+ initAutoComplete([ "hello", "hi" ],
+ [ "Beep beep'm beep beep yeah", "Baby you can drive my car" ]);
+
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox style="overflow: auto;" flex="1">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=467057"
+ title="xul menulist doesn't fire expand/collapse state change events">
+ Mozilla Bug 467057
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <menulist id="menulist">
+ <menupopup>
+ <menuitem label="item1"/>
+ <menuitem label="item2"/>
+ <menuitem label="item3"/>
+ </menupopup>
+ </menulist>
+
+ <html:input is="autocomplete-input"
+ id="autocomplete"
+ autocompletesearch="test-a11y-search"/>
+
+ <searchbar id="searchbar"/>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/states/test_frames.html b/accessible/tests/mochitest/states/test_frames.html
new file mode 100644
index 0000000000..baac222d83
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_frames.html
@@ -0,0 +1,93 @@
+<html>
+
+<head>
+ <title>frame based document testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ if (navigator.platform.startsWith("Win")) {
+ SimpleTest.expectAssertions(0, 2);
+ }
+
+ function doTest() {
+ const frameDoc = document.getElementById("frame_doc").contentDocument;
+ const frameDocArticle = document.getElementById("frame_doc_article").contentDocument;
+ const frameDocCheckbox = document.getElementById("frame_doc_checkbox").contentDocument;
+ const frameDocTextbox = document.getElementById("frame_doc_textbox").contentDocument;
+
+ testStates(frameDoc, STATE_READONLY, 0, 0, 0,
+ "test1: frameDoc");
+ testStates(frameDocArticle, STATE_READONLY, 0, 0, 0,
+ "test1: frameDocArticle");
+ testStates(frameDocCheckbox, STATE_READONLY, 0, 0, 0,
+ "test1: frameDocCheckbox");
+ testStates(frameDocTextbox, STATE_READONLY, 0, 0, 0,
+ "test1: frameDocTextbox");
+ frameDoc.designMode = "on";
+ testStates(frameDoc, 0, EXT_STATE_EDITABLE, STATE_READONLY, 0,
+ "test2: frameDoc");
+ testStates(frameDocArticle, STATE_READONLY, 0, 0, 0,
+ "test2: frameDocArticle");
+ testStates(frameDocCheckbox, STATE_READONLY, 0, 0, 0,
+ "test2: frameDocCheckbox");
+ testStates(frameDocTextbox, STATE_READONLY, 0, 0, 0,
+ "test2: frameDocTextbox");
+
+ frameDocArticle.designMode = "on";
+ testStates(frameDocArticle, 0, EXT_STATE_EDITABLE, STATE_READONLY, 0,
+ "test3: frameDocArticle");
+
+ frameDocCheckbox.designMode = "on";
+ testStates(frameDocCheckbox, 0, EXT_STATE_EDITABLE, STATE_READONLY, 0,
+ "test4: frameDocCheckbox");
+
+ // Replace iframe document body before the document accessible tree is
+ // created. Check the states are updated for new body.
+ var frameUpdateDoc =
+ document.getElementById("frame_updatedoc").contentDocument;
+ testStates(frameUpdateDoc, 0, EXT_STATE_EDITABLE,
+ STATE_READONLY, EXT_STATE_STALE, "test5: frameUpdateDoc");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=467387"
+ title="Expose non-editable documents as readonly, regardless of role">
+ Mozilla Bug 467387
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=638106"
+ title="CKEditor document should be editable">
+ Mozilla Bug 638106
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <iframe id="frame_doc" src="z_frames.html"></iframe>
+ <iframe id="frame_doc_article" src="z_frames_article.html"></iframe>
+ <iframe id="frame_doc_checkbox" src="z_frames_checkbox.html"></iframe>
+ <iframe id="frame_doc_textbox" src="z_frames_textbox.html"></iframe>
+ <iframe id="frame_updatedoc" src="z_frames_update.html"></iframe>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_inputs.html b/accessible/tests/mochitest/states/test_inputs.html
new file mode 100644
index 0000000000..2b7ac34def
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_inputs.html
@@ -0,0 +1,267 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML input states</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // //////////////////////////////////////////////////////////////////////////
+ // 'editable' and 'multiline' states.
+ testStates("input", 0, EXT_STATE_EDITABLE, 0, EXT_STATE_MULTI_LINE);
+ testStates("textarea", 0, EXT_STATE_EDITABLE | EXT_STATE_MULTI_LINE);
+
+ testStates("input_readonly", 0, EXT_STATE_EDITABLE);
+ testStates("input_disabled", 0, EXT_STATE_EDITABLE);
+ testStates("textarea_readonly", 0, EXT_STATE_EDITABLE);
+ testStates("textarea_disabled", 0, EXT_STATE_EDITABLE);
+
+ // //////////////////////////////////////////////////////////////////////////
+ // 'required', 'readonly' and 'unavailable' states.
+ var maybe_required = ["input", "search", "radio", "checkbox", "textarea"];
+ var never_required = ["submit", "button", "reset", "image"];
+
+ var i;
+ for (i in maybe_required) {
+ testStates(maybe_required[i],
+ STATE_FOCUSABLE, 0,
+ STATE_REQUIRED | STATE_READONLY | STATE_UNAVAILABLE);
+
+ testStates(maybe_required[i] + "_required",
+ STATE_FOCUSABLE | STATE_REQUIRED, 0,
+ STATE_UNAVAILABLE | STATE_READONLY);
+
+ var readonlyID = maybe_required[i] + "_readonly";
+ if (document.getElementById(readonlyID)) {
+ testStates(readonlyID,
+ STATE_FOCUSABLE | STATE_READONLY, 0,
+ STATE_UNAVAILABLE | STATE_REQUIRED);
+ }
+
+ testStates(maybe_required[i] + "_disabled",
+ STATE_UNAVAILABLE, 0,
+ STATE_FOCUSABLE | STATE_READONLY | STATE_REQUIRED);
+ }
+
+ for (i in never_required) {
+ testStates(never_required[i], 0, 0, STATE_REQUIRED | EXT_STATE_EDITABLE);
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // inherited 'unavailable' state
+ testStates("f", STATE_UNAVAILABLE);
+ testStates("f_input", STATE_UNAVAILABLE);
+ testStates("f_input_disabled", STATE_UNAVAILABLE);
+
+ // //////////////////////////////////////////////////////////////////////////
+ // file control
+ var fileBrowseButton = getAccessible("file");
+ testStates(fileBrowseButton, STATE_UNAVAILABLE | STATE_REQUIRED);
+
+ // //////////////////////////////////////////////////////////////////////////
+ // 'invalid' state
+ var invalid = ["pattern", "email", "url"];
+ for (i in invalid) {
+ testStates(invalid[i], STATE_INVALID);
+ testStates(invalid[i] + "2", 0, 0, STATE_INVALID);
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // not 'invalid' state
+ // (per spec, min/maxlength are always valid until interactively edited)
+ var validInput = document.createElement("input");
+ validInput.maxLength = "0";
+ validInput.value = "a";
+ ok(validInput.validity.valid,
+ "input should be valid despite maxlength (no interactive edits)");
+
+ var validInput2 = document.createElement("input");
+ validInput2.minLength = "1";
+ validInput2.value = "";
+ ok(validInput2.validity.valid,
+ "input should be valid despite minlength (no interactive edits)");
+
+ var valid = ["minlength", "maxlength"];
+ for (i in valid) {
+ testStates(valid[i], 0, 0, STATE_INVALID);
+ testStates(valid[i] + "2", 0, 0, STATE_INVALID);
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // 'invalid' state
+ // (per spec, min/maxlength validity is affected by interactive edits)
+ var mininp = document.getElementById("minlength");
+ mininp.focus();
+ mininp.setSelectionRange(mininp.value.length, mininp.value.length);
+ synthesizeKey("KEY_Backspace");
+ ok(!mininp.validity.valid,
+ "input should be invalid after interactive edits");
+ testStates(mininp, STATE_INVALID);
+ // inputs currently cannot be made longer than maxlength interactively,
+ // so we're not testing that case.
+
+ // //////////////////////////////////////////////////////////////////////////
+ // autocomplete states
+ testStates("autocomplete-default", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+ testStates("autocomplete-off", 0, 0, 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+ testStates("autocomplete-formoff", 0, 0, 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+ testStates("autocomplete-list", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+ testStates("autocomplete-list2", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+ testStates("autocomplete-tel", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+ testStates("autocomplete-email", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+ testStates("autocomplete-search", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION);
+
+ // //////////////////////////////////////////////////////////////////////////
+ // haspopup
+ testStates("autocomplete-list", STATE_HASPOPUP);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=559275"
+ title="map attribute required to STATE_REQUIRED">
+ Bug 559275
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=389238"
+ title="Support disabled state on fieldset">
+ Bug 389238
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=599163"
+ title="check disabled state instead of attribute">
+ Bug 599163
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=601205"
+ title="Expose intrinsic invalid state to accessibility API">
+ Bug 601205
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=601205"
+ title="Expose intrinsic invalid state to accessibility API">
+ Bug 601205
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=559766"
+ title="Add accessibility support for @list on HTML input and for HTML datalist">
+ Bug 559766
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=699017"
+ title="File input control should be propogate states to descendants">
+ Bug 699017
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=733382"
+ title="Editable state bit should be present on readonly inputs">
+ Bug 733382
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=878590"
+ title="HTML5 datalist is not conveyed by haspopup property">
+ Bug 878590
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+
+ <form>
+ <input id="input" type="input">
+ <input id="input_required" type="input" required>
+ <input id="input_readonly" type="input" readonly>
+ <input id="input_disabled" type="input" disabled>
+ <input id="search" type="search">
+ <input id="search_required" type="search" required>
+ <input id="search_readonly" type="search" readonly>
+ <input id="search_disabled" type="search" disabled>
+ <input id="radio" type="radio">
+ <input id="radio_required" type="radio" required>
+ <input id="radio_disabled" type="radio" disabled>
+ <input id="checkbox" type="checkbox">
+ <input id="checkbox_required" type="checkbox" required>
+ <input id="checkbox_disabled" type="checkbox" disabled>
+ <textarea id="textarea"></textarea>
+ <textarea id="textarea_required" required></textarea>
+ <textarea id="textarea_readonly" readonly></textarea>
+ <textarea id="textarea_disabled" disabled></textarea>
+ </form>
+
+ <!-- bogus required usage -->
+ <input id="submit" type="submit" required>
+ <input id="button" type="button" required>
+ <input id="reset" type="reset" required>
+ <input id="image" type="image" required>
+
+ <!-- inherited disabled -->
+ <fieldset id="f" disabled>
+ <input id="f_input">
+ <input id="f_input_disabled" disabled>
+ </fieldset>
+
+ <!-- inherited from input@type="file" -->
+ <input id="file" type="file" required disabled>
+
+ <!-- invalid/valid -->
+ <input id="maxlength" maxlength="1" value="f">
+ <input id="maxlength2" maxlength="100" value="foo">
+ <input id="minlength" minlength="2" value="fo">
+ <input id="minlength2" minlength="1" value="foo">
+ <input id="pattern" pattern="bar" value="foo">
+ <input id="pattern2" pattern="bar" value="bar">
+ <input id="email" type="email" value="foo">
+ <input id="email2" type="email" value="foo@bar.com">
+ <input id="url" type="url" value="foo">
+ <input id="url2" type="url" value="http://mozilla.org/">
+
+ <!-- autocomplete -->
+ <input id="autocomplete-default">
+ <input id="autocomplete-off" autocomplete="off">
+ <form autocomplete="off">
+ <input id="autocomplete-formoff">
+ </form>
+ <datalist id="cities">
+ <option>Paris</option>
+ <option>San Francisco</option>
+ </datalist>
+ <input id="autocomplete-list" list="cities">
+ <input id="autocomplete-list2" list="cities" autocomplete="off">
+ <input id="autocomplete-tel" type="tel">
+
+ Email Address:
+ <input id="autocomplete-email" type="email" list="contacts" value="xyzzy">
+ <datalist id="contacts">
+ <option>xyzzy@plughs.com</option>
+ <option>nobody@mozilla.org</option>
+ </datalist>
+
+ </br>Search for:
+ <input id="autocomplete-search" type="search" list="searchhisty" value="Gamma">
+ <datalist id="searchhisty">
+ <option>Gamma Rays</option>
+ <option>Gamma Ray Bursts</option>
+ </datalist>
+
+ </body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_link.html b/accessible/tests/mochitest/states/test_link.html
new file mode 100644
index 0000000000..65632bd12f
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_link.html
@@ -0,0 +1,85 @@
+<html>
+
+<head>
+ <title>HTML link states testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // a@href and its text node
+ testStates("link_href", STATE_LINKED);
+ testStates(getAccessible("link_href").firstChild, STATE_LINKED);
+
+ // a@onclick
+ testStates("link_click", STATE_LINKED);
+
+ // a@onmousedown
+ testStates("link_mousedown", STATE_LINKED);
+
+ // a@onmouseup
+ testStates("link_mouseup", STATE_LINKED);
+
+ // a@role="link"
+ testStates("link_arialink", STATE_LINKED);
+
+ // a@role="button"
+ testStates("link_ariabutton", 0, 0, STATE_LINKED);
+
+ // a (no @href, no click event listener)
+ testStates("link_notlink", 0, 0, STATE_LINKED);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=423409"
+ title="Expose click action if mouseup and mousedown are registered">
+ Mozilla Bug 423409
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=754830"
+ title="Calculate link states separately">
+ Mozilla Bug 754830
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=757774"
+ title="Fire state change event when link is traversed">
+ Mozilla Bug 757774
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <a id="link_href" href="http://mozilla.org">link</a>
+ <a id="link_click" onclick="">link</a>
+ <a id="link_mousedown" onmousedown="">link</a>
+ <a id="link_mouseup" onmouseup="">link</a>
+ <a id="link_arialink" role="link">aria link</a>
+ <a id="link_ariabutton" role="button">aria button</a>
+ <a id="link_notlink">not link</a>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_popup.xhtml b/accessible/tests/mochitest/states/test_popup.xhtml
new file mode 100644
index 0000000000..c51e3ac17c
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_popup.xhtml
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="XUL popup attribute test">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function doTest()
+ {
+ // label with popup
+ testStates("labelWithPopup", STATE_HASPOPUP);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=504252"
+ title="Expose STATE_HASPOPUP on XUL elements that have an @popup attribute">
+ Mozilla Bug 504252
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <!-- label with popup attribute -->
+ <label id="labelWithPopup" value="file name"
+ popup="fileContext"
+ tabindex="0"/>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/states/test_selects.html b/accessible/tests/mochitest/states/test_selects.html
new file mode 100644
index 0000000000..c822fe376f
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_selects.html
@@ -0,0 +1,166 @@
+<html>
+
+<head>
+ <title>HTML selects accessible states tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // gA11yEventDumpToConsole = true;
+
+ function doTest() {
+ // combobox
+ var combobox = getAccessible("combobox");
+ testStates(combobox,
+ STATE_HASPOPUP | STATE_COLLAPSED | STATE_FOCUSABLE, 0,
+ STATE_FOCUSED, 0);
+
+ var comboboxList = combobox.firstChild;
+ testStates(comboboxList, STATE_INVISIBLE, 0, STATE_FOCUSABLE, 0);
+
+ var opt1 = comboboxList.firstChild;
+ testStates(opt1, STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE,
+ 0, STATE_FOCUSED, 0);
+
+ var opt2 = comboboxList.lastChild;
+ testStates(opt2, STATE_SELECTABLE | STATE_FOCUSABLE, 0, STATE_SELECTED, 0,
+ STATE_FOCUSED, 0);
+
+ // collapsed combobox
+ testStates("collapsedcombobox",
+ STATE_COLLAPSED | STATE_FOCUSABLE, 0,
+ STATE_FOCUSED, 0);
+
+ testStates("collapsed-1",
+ STATE_FOCUSABLE | STATE_SELECTABLE, 0,
+ STATE_OFFSCREEN | STATE_INVISIBLE, 0);
+
+ testStates("collapsed-2",
+ STATE_OFFSCREEN, 0,
+ STATE_INVISIBLE, 0);
+
+ // listbox
+ testStates("listbox",
+ STATE_FOCUSABLE, 0,
+ STATE_HASPOPUP | STATE_COLLAPSED | STATE_FOCUSED);
+
+ testStates("listitem-active",
+ STATE_FOCUSABLE | STATE_SELECTABLE, EXT_STATE_ACTIVE,
+ STATE_SELECTED | STATE_FOCUSED);
+
+ testStates("listitem",
+ STATE_FOCUSABLE | STATE_SELECTABLE, 0,
+ STATE_SELECTED | STATE_FOCUSED, EXT_STATE_ACTIVE);
+
+ testStates("listitem-disabled",
+ STATE_UNAVAILABLE, 0,
+ STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE,
+ EXT_STATE_ACTIVE);
+
+ testStates("listgroup",
+ 0, 0,
+ STATE_UNAVAILABLE | STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE,
+ EXT_STATE_ACTIVE);
+
+ testStates("listgroup-disabled",
+ STATE_UNAVAILABLE, 0,
+ STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE,
+ EXT_STATE_ACTIVE);
+
+ todo(false, "no unavailable state on option in disabled group (bug 759666)");
+// testStates("listitem-disabledgroup",
+// STATE_UNAVAILABLE, 0,
+// STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE,
+// EXT_STATE_ACTIVE);
+
+ testStates("listbox-disabled",
+ STATE_UNAVAILABLE, 0,
+ STATE_FOCUSABLE);
+
+ todo(false, "no unavailable state on option in disabled select (bug 759666)");
+// testStates("listitem-disabledlistbox",
+// STATE_UNAVAILABLE, 0,
+// STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE,
+// EXT_STATE_ACTIVE);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=443889"
+ title="mochitest for selects and lists">
+ Mozilla Bug 443889
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=640716"
+ title="mochitest for selects and lists">
+ Mozilla Bug 640716
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=689847"
+ title="Expose active state on current item of selectable widgets">
+ Mozilla Bug 689847
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=756983"
+ title="Isolate focusable and unavailable states from State()">
+ Mozilla Bug 756983
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=907682"
+ title=" HTML:option group position is not correct when select is collapsed">
+ Mozilla Bug 907682
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <select id="combobox">
+ <option>item 1</option>
+ <option>item 2</option>
+ </select>
+
+ <select id="collapsedcombobox">
+ <option id="collapsed-1">item 1</option>
+ <option id="collapsed-2">item 2</option>
+ </select>
+
+ <select id="listbox" name="component" size="3">
+ <option id="listitem-active">Build</option>
+ <option id="listitem">Disability Access APIs</option>
+ <option id="listitem-disabled" disabled>General</option>
+ <optgroup id="listgroup" label="group">
+ <option>option</option>
+ </optgroup>
+ <optgroup id="listgroup-disabled" disabled label="group2">
+ <option id="listitem-disabledgroup">UI</option>
+ </optgroup>
+ </select>
+
+ <select id="listbox-disabled" size="3" disabled>
+ <option id="listitem-disabledlistbox">option</option>
+ </select>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_stale.html b/accessible/tests/mochitest/states/test_stale.html
new file mode 100644
index 0000000000..3218a2125a
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_stale.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Stale state testing</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function addChild(aContainerID) {
+ this.containerNode = getNode(aContainerID);
+ this.childNode = null;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.invoke = function addChild_invoke() {
+ this.childNode = document.createElement("div");
+ // Note after bug 646216, a sole div without text won't be accessible
+ // and would not result in an embedded character.
+ // Therefore, add some text.
+ this.childNode.textContent = "hello";
+ this.containerNode.appendChild(this.childNode);
+ };
+
+ this.finalCheck = function addChild_finalCheck() {
+ // no stale state should be set
+ testStates(this.childNode, 0, 0, 0, EXT_STATE_STALE);
+ };
+
+ this.getID = function addChild_getID() {
+ return "add child for " + prettyName(aContainerID);
+ };
+ }
+
+ function removeChildChecker(aInvoker) {
+ this.type = EVENT_HIDE;
+ this.__defineGetter__("target", function() { return aInvoker.child; });
+
+ this.check = function removeChildChecker_check() {
+ // stale state should be set
+ testStates(aInvoker.child, 0, EXT_STATE_STALE);
+ };
+ }
+
+ function removeChild(aContainerID) {
+ this.containerNode = getNode(aContainerID);
+ this.child = null;
+
+ this.eventSeq = [
+ new removeChildChecker(this),
+ ];
+
+ this.invoke = function removeChild_invoke() {
+ var childNode = this.containerNode.firstChild;
+ this.child = getAccessible(childNode);
+
+ this.containerNode.removeChild(childNode);
+ };
+
+ this.getID = function removeChild_getID() {
+ return "remove child from " + prettyName(aContainerID);
+ };
+ }
+
+ // gA11yEventDumpToConsole = true; //debugging
+
+ var gQueue = null;
+ function doTest() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new addChild("container"));
+ gQueue.push(new removeChild("container"));
+
+ gQueue.invoke(); // will call SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body role="">
+
+ <a target="_blank"
+ title="Expose stale state on accessibles unattached from tree"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=676267">Mozilla Bug 676267</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="container"></div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_tabs.xhtml b/accessible/tests/mochitest/states/test_tabs.xhtml
new file mode 100644
index 0000000000..d51ef6b331
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_tabs.xhtml
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL tabbox hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ function doTest()
+ {
+ testStates("tab1", 0, EXT_STATE_PINNED);
+ testStates("tab2", 0, EXT_STATE_PINNED);
+ testStates("tab3", 0, 0, 0, EXT_STATE_PINNED);
+
+ SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=577727"
+ title="Make pinned tabs distinguishable from other tabs for accessibility">
+ Mozilla Bug 577727
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <tabbox>
+ <tabs id="tabs">
+ <tab id="tab1" label="tab1" pinned=""/>
+ <tab id="tab2" label="tab2" pinned="true"/>
+ <tab id="tab3" label="tab3"/>
+ </tabs>
+ <tabpanels id="tabpanels">
+ <tabpanel/>
+ <tabpanel/>
+ </tabpanels>
+ </tabbox>
+ </vbox>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/states/test_textbox.xhtml b/accessible/tests/mochitest/states/test_textbox.xhtml
new file mode 100644
index 0000000000..eaf455d994
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_textbox.xhtml
@@ -0,0 +1,78 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="nsIAccessible XUL textboxes states tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function getInput(aID)
+ {
+ return getNode(aID).inputField;
+ }
+
+ function doTest()
+ {
+ //////////////////////////////////////////////////////////////////////////
+ // Search textbox without search button, searches as you type and filters
+ // a separate control.
+ testStates(getInput("searchbox"),
+ STATE_FOCUSABLE,
+ EXT_STATE_EDITABLE | EXT_STATE_SUPPORTS_AUTOCOMPLETION,
+ STATE_PROTECTED | STATE_UNAVAILABLE,
+ 0,
+ "searchbox");
+
+ //////////////////////////////////////////////////////////////////////////
+ // Search textbox with search button, does not support autoCompletion.
+ testStates(getInput("searchfield"),
+ STATE_FOCUSABLE,
+ EXT_STATE_EDITABLE,
+ STATE_PROTECTED | STATE_UNAVAILABLE,
+ EXT_STATE_SUPPORTS_AUTOCOMPLETION,
+ "searchfield");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=442648">
+ Mozilla Bug 442648
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=648235"
+ title="XUL textbox can inherit more states from underlying HTML input">
+ Mozilla Bug 648235
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <search-textbox id="searchbox" flex="1" results="historyTree"/>
+ <search-textbox id="searchfield" placeholder="Search all add-ons"
+ searchbutton="true"/>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/states/test_tree.xhtml b/accessible/tests/mochitest/states/test_tree.xhtml
new file mode 100644
index 0000000000..d81be4a54b
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_tree.xhtml
@@ -0,0 +1,146 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+<?xml-stylesheet href="../treeview.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL tree states tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ /**
+ * Event queue invoker object to test accessible states for XUL tree
+ * accessible.
+ */
+ function statesChecker(aTreeID, aView)
+ {
+ this.DOMNode = getNode(aTreeID);
+
+ this.invoke = function statesChecker_invoke()
+ {
+ this.DOMNode.view = aView;
+ }
+
+ this.check = function statesChecker_check()
+ {
+ var tree = getAccessible(this.DOMNode);
+
+ // tree states
+ testStates(tree, STATE_READONLY);
+
+ if (this.DOMNode.getAttribute("seltype") != "single")
+ testStates(tree, STATE_MULTISELECTABLE);
+ else
+ testStates(tree, 0, 0, STATE_MULTISELECTABLE);
+
+ // tree item states
+ var expandedItem = tree.getChildAt(2);
+ testStates(expandedItem,
+ STATE_SELECTABLE | STATE_FOCUSABLE | STATE_EXPANDED);
+
+ var collapsedItem = tree.getChildAt(5);
+ testStates(collapsedItem,
+ STATE_SELECTABLE | STATE_FOCUSABLE | STATE_COLLAPSED);
+
+ // cells states if any
+ var cells = collapsedItem.children;
+ if (cells && cells.length) {
+ for (var idx = 0; idx < cells.length; idx++) {
+ var cell = cells.queryElementAt(idx, nsIAccessible);
+ testStates(cell, STATE_SELECTABLE);
+ }
+
+ var checkboxCell = cells.queryElementAt(3, nsIAccessible);
+ testStates(checkboxCell, STATE_CHECKABLE | STATE_CHECKED);
+ }
+ }
+
+ this.getID = function statesChecker_getID()
+ {
+ return "tree processor for " + prettyName(aTreeID);
+ }
+ }
+
+ gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+
+ function doTest()
+ {
+ gQueue = new eventQueue(EVENT_REORDER);
+ gQueue.push(new statesChecker("tree", new nsTreeTreeView()));
+ gQueue.push(new statesChecker("treesingle", new nsTreeTreeView()));
+ gQueue.push(new statesChecker("tabletree", new nsTreeTreeView()));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727"
+ title="Reorganize implementation of XUL tree accessibility">
+ Mozilla Bug 503727
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col" flex="1" primary="true" label="column"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <tree id="treesingle" flex="1" seltype="single">
+ <treecols>
+ <treecol id="col_single" flex="1" primary="true" label="column"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <tree id="tabletree" flex="1" editable="true">
+ <treecols>
+ <treecol id="tabletree_col1" cycler="true" label="cycler"/>
+ <treecol id="tabletree_col2" flex="1" primary="true" label="column1"/>
+ <treecol id="tabletree_col3" flex="1" label="column2"/>
+ <treecol id="tabletree_col4" flex="1" label="checker"
+ type="checkbox" editable="true"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <vbox id="debug"/>
+ </vbox>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/states/test_visibility.html b/accessible/tests/mochitest/states/test_visibility.html
new file mode 100644
index 0000000000..aa3643673a
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_visibility.html
@@ -0,0 +1,75 @@
+<html>
+<head>
+ <title>visibility state testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // Tests
+
+ function doTests() {
+ testStates("div", 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN);
+ testStates("div_off", STATE_OFFSCREEN, 0, STATE_INVISIBLE);
+ testStates("div_transformed", STATE_OFFSCREEN, 0, STATE_INVISIBLE);
+ testStates("div_abschild", 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN);
+ testStates("ul", STATE_OFFSCREEN, 0, STATE_INVISIBLE);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=591363"
+ title="(in)visible state is not always correct?">
+ Mozilla Bug 591363
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=768786"
+ title="Offscreen state is not exposed under certain circumstances">
+ Mozilla Bug 768786
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="outer_div">
+
+ <!-- trivial cases -->
+ <div id="div">div</div>
+ <div id="div_off" style="position: absolute; left:-999px; top:-999px">
+ offscreen!
+ </div>
+ <div id="div_transformed" style="transform: translate(-999px, -999px);">
+ transformed!
+ </div>
+
+ <!-- edge case: no rect but has out of flow child -->
+ <div id="div_abschild">
+ <p style="position: absolute; left: 120px; top:120px;">absolute</p>
+ </div>
+
+ <ul id="ul" style="display: contents;">
+ <li>Supermarket 1</li>
+ <li>Supermarket 2</li>
+ </ul>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/states/test_visibility.xhtml b/accessible/tests/mochitest/states/test_visibility.xhtml
new file mode 100644
index 0000000000..cc23b6b4bc
--- /dev/null
+++ b/accessible/tests/mochitest/states/test_visibility.xhtml
@@ -0,0 +1,162 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="XUL elements visibility states testing">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function openMenu(aID, aSubID, aOffscreenSubID)
+ {
+ this.menuNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, this.menuNode)
+ ];
+
+ this.invoke = function openMenu_invoke()
+ {
+ this.menuNode.open = true;
+ }
+
+ this.finalCheck = function openMenu_finalCheck()
+ {
+ testStates(aID, 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN);
+ testStates(aSubID, 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN);
+ if (aOffscreenSubID)
+ testStates(aOffscreenSubID, STATE_OFFSCREEN, 0, STATE_INVISIBLE);
+ }
+
+ this.getID = function openMenu_invoke()
+ {
+ return "open menu '" + aID + "' and test states";
+ }
+ }
+
+ function closeMenu(aID, aSubID, aSub2ID)
+ {
+ this.menuNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, document)
+ ];
+
+ this.invoke = function openMenu_invoke()
+ {
+ this.menuNode.open = false;
+ }
+
+ this.finalCheck = function openMenu_finalCheck()
+ {
+ testStates(aID, 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN);
+ testStates(aSubID, STATE_INVISIBLE, 0, STATE_OFFSCREEN);
+ testStates(aSub2ID, STATE_INVISIBLE, 0, STATE_OFFSCREEN);
+ }
+
+ this.getID = function openMenu_invoke()
+ {
+ return "open menu and test states";
+ }
+ }
+
+ var gQueue = null;
+ function doTest()
+ {
+ testStates("deck_pane2", 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN);
+ testStates("tabs_pane1", 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN);
+ testStates("tabs_pane2", STATE_OFFSCREEN, 0, STATE_INVISIBLE);
+
+ gQueue = new eventQueue();
+ gQueue.push(new openMenu("mi_file1", "mi_file1.1"));
+ gQueue.push(new openMenu("mi_file1.2", "mi_file1.2.1", "mi_file1.2.4"));
+ gQueue.push(new closeMenu("mi_file1", "mi_file1.1", "mi_file1.2.1"));
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+ <html:style>
+ <![CDATA[
+ /* We want to control the height of the menu and which elements are visible,
+ and the Windows menu padding interferes with this. */
+ menupopup::part(arrowscrollbox) {
+ margin: 0 !important;
+ padding-block: 0 !important;
+ }
+ ]]>
+ </html:style>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=810260"
+ title="xul:deck hidden pages shouldn't be offscreen">
+ Mozilla Bug 810260
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=865591"
+ title="Visible menu item have offscreen state">
+ Mozilla Bug 865591
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+
+ <deck selectedIndex="1">
+ <description value="This is the first page" id="deck_pane1"/>
+ <button label="This is the second page" id="deck_pane2"/>
+ </deck>
+
+ <tabbox>
+ <tabs>
+ <tab>tab1</tab>
+ <tab>tab2</tab>
+ </tabs>
+ <tabpanels>
+ <description value="This is the first page" id="tabs_pane1"/>
+ <button label="This is the second page" id="tabs_pane2"/>
+ </tabpanels>
+ </tabbox>
+
+ <menubar>
+ <menu label="File" id="mi_file1">
+ <menupopup>
+ <menuitem label="SubFile" id="mi_file1.1"/>
+ <menu label="SubFile2" id="mi_file1.2">
+ <menupopup style="max-height: 3em;">
+ <menuitem style="appearance: none; height: 1em" label="SubSubFile" id="mi_file1.2.1"/>
+ <menuitem style="appearance: none; height: 1em" label="SubSubFile2" id="mi_file1.2.2"/>
+ <menuitem style="appearance: none; height: 1em" label="SubSubFile3" id="mi_file1.2.3"/>
+ <menuitem style="appearance: none; height: 1em" label="SubSubFile4" id="mi_file1.2.4"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ </menubar>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/states/z_frames.html b/accessible/tests/mochitest/states/z_frames.html
new file mode 100644
index 0000000000..819adee63e
--- /dev/null
+++ b/accessible/tests/mochitest/states/z_frames.html
@@ -0,0 +1,11 @@
+<html>
+<!--
+Auxilliary file used as frame source.
+-->
+<head>
+</head>
+<body>
+<p>Frame source body has no role</p>
+</body>
+</html>
+
diff --git a/accessible/tests/mochitest/states/z_frames_article.html b/accessible/tests/mochitest/states/z_frames_article.html
new file mode 100644
index 0000000000..a7a69b4dae
--- /dev/null
+++ b/accessible/tests/mochitest/states/z_frames_article.html
@@ -0,0 +1,11 @@
+<html>
+<!--
+Auxilliary file used as frame source.
+-->
+<head>
+</head>
+<body role="article">
+<p>Article</p>
+</body>
+</html>
+
diff --git a/accessible/tests/mochitest/states/z_frames_checkbox.html b/accessible/tests/mochitest/states/z_frames_checkbox.html
new file mode 100644
index 0000000000..7997644243
--- /dev/null
+++ b/accessible/tests/mochitest/states/z_frames_checkbox.html
@@ -0,0 +1,11 @@
+<html>
+<!--
+Auxilliary file used as frame source.
+-->
+<head>
+</head>
+<body role="checkbox">
+<p>Checkbox</p>
+</body>
+</html>
+
diff --git a/accessible/tests/mochitest/states/z_frames_textbox.html b/accessible/tests/mochitest/states/z_frames_textbox.html
new file mode 100644
index 0000000000..0f4e1b9d66
--- /dev/null
+++ b/accessible/tests/mochitest/states/z_frames_textbox.html
@@ -0,0 +1,11 @@
+<html>
+<!--
+Auxilliary file used as frame source.
+-->
+<head>
+</head>
+<body role="textbox">
+<p>Texbox</p>
+</body>
+</html>
+
diff --git a/accessible/tests/mochitest/states/z_frames_update.html b/accessible/tests/mochitest/states/z_frames_update.html
new file mode 100644
index 0000000000..7e2cc83539
--- /dev/null
+++ b/accessible/tests/mochitest/states/z_frames_update.html
@@ -0,0 +1,21 @@
+<html>
+<head>
+<script>
+function replaceBody() {
+ var accService = Cc["@mozilla.org/accessibilityService;1"].
+ getService(Ci.nsIAccessibilityService);
+ accService.getAccessibleFor(document);
+
+ var newBody = document.createElement("body");
+ newBody.setAttribute("contentEditable", "true");
+ newBody.textContent = "New Hello";
+ document.documentElement.replaceChild(newBody, document.body);
+ getComputedStyle(newBody, "").color;
+}
+</script>
+</head>
+<body onload="replaceBody();">
+OLD hello
+</body>
+</html>
+
diff --git a/accessible/tests/mochitest/table.js b/accessible/tests/mochitest/table.js
new file mode 100644
index 0000000000..9e4989cced
--- /dev/null
+++ b/accessible/tests/mochitest/table.js
@@ -0,0 +1,851 @@
+/**
+ * This file provides set of helper functions to test nsIAccessibleTable
+ * interface.
+ *
+ * Required:
+ * common.js
+ * role.js
+ * states.js
+ */
+/* import-globals-from common.js */
+/* import-globals-from role.js */
+/* import-globals-from states.js */
+
+/**
+ * Constants used to describe cells array.
+ */
+const kDataCell = 1; // Indicates the cell is origin data cell
+const kRowHeaderCell = 2; // Indicates the cell is row header cell
+const kColHeaderCell = 4; // Indicated the cell is column header cell
+const kOrigin = kDataCell | kRowHeaderCell | kColHeaderCell;
+
+const kRowSpanned = 8; // Indicates the cell is not origin and row spanned
+const kColSpanned = 16; // Indicates the cell is not origin and column spanned
+const kSpanned = kRowSpanned | kColSpanned;
+
+/**
+ * Constants to define column header type.
+ */
+const kNoColumnHeader = 0;
+const kListboxColumnHeader = 1;
+const kTreeColumnHeader = 2;
+
+/**
+ * Constants to define table type.
+ */
+const kTable = 0;
+const kTreeTable = 1;
+const kMathTable = 2;
+
+/**
+ * Test table structure and related methods.
+ *
+ * @param aIdentifier [in] table accessible identifier
+ * @param aCellsArray [in] two dimensional array (row X columns) of
+ * cell types (see constants defined above).
+ * @param aColHeaderType [in] specifies wether column header cells are
+ * arranged into the list.
+ * @param aCaption [in] caption text if any
+ * @param aSummary [in] summary text if any
+ * @param aTableType [in] specifies the table type.
+ * @param aRowRoles [in] array of row roles.
+ */
+function testTableStruct(
+ aIdentifier,
+ aCellsArray,
+ aColHeaderType,
+ aCaption,
+ aSummary,
+ aTableType,
+ aRowRoles
+) {
+ var tableNode = getNode(aIdentifier);
+ var isGrid =
+ tableNode.getAttribute("role") == "grid" ||
+ tableNode.getAttribute("role") == "treegrid" ||
+ tableNode.localName == "tree";
+
+ var rowCount = aCellsArray.length;
+ var colsCount = aCellsArray[0] ? aCellsArray[0].length : 0;
+
+ // Test table accessible tree.
+ var tableObj = {
+ children: [],
+ };
+ switch (aTableType) {
+ case kTable:
+ tableObj.role = ROLE_TABLE;
+ break;
+ case kTreeTable:
+ tableObj.role = ROLE_TREE_TABLE;
+ break;
+ case kMathTable:
+ tableObj.role = ROLE_MATHML_TABLE;
+ break;
+ }
+
+ // caption accessible handling
+ if (aCaption) {
+ var captionObj = {
+ role: ROLE_CAPTION,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: aCaption,
+ },
+ ],
+ };
+
+ tableObj.children.push(captionObj);
+ }
+
+ // special types of column headers handling
+ if (aColHeaderType) {
+ var headersObj = {
+ role: ROLE_LIST,
+ children: [],
+ };
+
+ for (let idx = 0; idx < colsCount; idx++) {
+ var headerCellObj = {
+ role: ROLE_COLUMNHEADER,
+ };
+ headersObj.children.push(headerCellObj);
+ }
+
+ if (aColHeaderType == kTreeColumnHeader) {
+ headersObj.children.push({
+ role: ROLE_PUSHBUTTON,
+ });
+ headersObj.children.push({
+ role: ROLE_MENUPOPUP,
+ });
+ }
+
+ tableObj.children.push(headersObj);
+ }
+
+ // rows and cells accessibles
+ for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
+ let rowObj = {
+ role: aRowRoles ? aRowRoles[rowIdx] : ROLE_ROW,
+ children: [],
+ };
+
+ for (let colIdx = 0; colIdx < colsCount; colIdx++) {
+ let celltype = aCellsArray[rowIdx][colIdx];
+
+ var role = ROLE_NOTHING;
+ switch (celltype) {
+ case kDataCell:
+ role =
+ aTableType == kMathTable
+ ? ROLE_MATHML_CELL
+ : isGrid
+ ? ROLE_GRID_CELL
+ : ROLE_CELL;
+ break;
+ case kRowHeaderCell:
+ role = ROLE_ROWHEADER;
+ break;
+ case kColHeaderCell:
+ role = ROLE_COLUMNHEADER;
+ break;
+ }
+
+ if (role != ROLE_NOTHING) {
+ var cellObj = { role };
+ rowObj.children.push(cellObj);
+ }
+ }
+
+ tableObj.children.push(rowObj);
+ }
+
+ testAccessibleTree(aIdentifier, tableObj);
+
+ // Test table table interface.
+ var table = getAccessible(aIdentifier, [nsIAccessibleTable]);
+
+ // summary
+ if (aSummary) {
+ is(
+ table.summary,
+ aSummary,
+ "Wrong summary of the table " + prettyName(aIdentifier)
+ );
+ }
+
+ // rowCount and columnCount
+ is(
+ table.rowCount,
+ rowCount,
+ "Wrong rows count of " + prettyName(aIdentifier)
+ );
+ is(
+ table.columnCount,
+ colsCount,
+ "Wrong columns count of " + prettyName(aIdentifier)
+ );
+
+ // rows and columns extents
+ for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
+ for (let colIdx = 0; colIdx < colsCount; colIdx++) {
+ let celltype = aCellsArray[rowIdx][colIdx];
+ if (celltype & kOrigin) {
+ // table getRowExtentAt
+ var rowExtent = table.getRowExtentAt(rowIdx, colIdx);
+ let idx;
+ /* eslint-disable no-empty */
+ for (
+ idx = rowIdx + 1;
+ idx < rowCount && aCellsArray[idx][colIdx] & kRowSpanned;
+ idx++
+ ) {}
+ /* eslint-enable no-empty */
+
+ var expectedRowExtent = idx - rowIdx;
+ is(
+ rowExtent,
+ expectedRowExtent,
+ "getRowExtentAt: Wrong number of spanned rows at (" +
+ rowIdx +
+ ", " +
+ colIdx +
+ ") for " +
+ prettyName(aIdentifier)
+ );
+
+ // table getColumnExtentAt
+ var colExtent = table.getColumnExtentAt(rowIdx, colIdx);
+ /* eslint-disable no-empty */
+ for (
+ idx = colIdx + 1;
+ idx < colsCount && aCellsArray[rowIdx][idx] & kColSpanned;
+ idx++
+ ) {}
+ /* eslint-enable no-empty */
+
+ var expectedColExtent = idx - colIdx;
+ is(
+ colExtent,
+ expectedColExtent,
+ "getColumnExtentAt: Wrong number of spanned columns at (" +
+ rowIdx +
+ ", " +
+ colIdx +
+ ") for " +
+ prettyName(aIdentifier)
+ );
+
+ // cell rowExtent and columnExtent
+ var cell = getAccessible(table.getCellAt(rowIdx, colIdx), [
+ nsIAccessibleTableCell,
+ ]);
+
+ is(
+ cell.rowExtent,
+ expectedRowExtent,
+ "rowExtent: Wrong number of spanned rows at (" +
+ rowIdx +
+ ", " +
+ colIdx +
+ ") for " +
+ prettyName(aIdentifier)
+ );
+
+ is(
+ cell.columnExtent,
+ expectedColExtent,
+ "columnExtent: Wrong number of spanned column at (" +
+ rowIdx +
+ ", " +
+ colIdx +
+ ") for " +
+ prettyName(aIdentifier)
+ );
+ }
+ }
+ }
+}
+
+/**
+ * Test table indexes.
+ *
+ * @param aIdentifier [in] table accessible identifier
+ * @param aIdxes [in] two dimensional array of cell indexes
+ */
+function testTableIndexes(aIdentifier, aIdxes) {
+ var tableAcc = getAccessible(aIdentifier, [nsIAccessibleTable]);
+ if (!tableAcc) {
+ return;
+ }
+
+ var obtainedRowIdx, obtainedColIdx, obtainedIdx;
+ var cellAcc;
+
+ var id = prettyName(aIdentifier);
+
+ var rowCount = aIdxes.length;
+ for (var rowIdx = 0; rowIdx < rowCount; rowIdx++) {
+ var colCount = aIdxes[rowIdx].length;
+ for (var colIdx = 0; colIdx < colCount; colIdx++) {
+ var idx = aIdxes[rowIdx][colIdx];
+
+ // getCellAt
+ try {
+ cellAcc = null;
+ cellAcc = tableAcc.getCellAt(rowIdx, colIdx);
+ } catch (e) {}
+
+ ok(
+ (idx != -1 && cellAcc) || (idx == -1 && !cellAcc),
+ id +
+ ": Can't get cell accessible at row = " +
+ rowIdx +
+ ", column = " +
+ colIdx
+ );
+
+ if (idx != -1) {
+ // getRowIndexAt
+ var origRowIdx = rowIdx;
+ while (
+ origRowIdx > 0 &&
+ aIdxes[rowIdx][colIdx] == aIdxes[origRowIdx - 1][colIdx]
+ ) {
+ origRowIdx--;
+ }
+
+ try {
+ obtainedRowIdx = tableAcc.getRowIndexAt(idx);
+ } catch (e) {
+ ok(
+ false,
+ id + ": can't get row index for cell index " + idx + "," + e
+ );
+ }
+
+ is(
+ obtainedRowIdx,
+ origRowIdx,
+ id + ": row for index " + idx + " is not correct (getRowIndexAt)"
+ );
+
+ // getColumnIndexAt
+ var origColIdx = colIdx;
+ while (
+ origColIdx > 0 &&
+ aIdxes[rowIdx][colIdx] == aIdxes[rowIdx][origColIdx - 1]
+ ) {
+ origColIdx--;
+ }
+
+ try {
+ obtainedColIdx = tableAcc.getColumnIndexAt(idx);
+ } catch (e) {
+ ok(
+ false,
+ id + ": can't get column index for cell index " + idx + "," + e
+ );
+ }
+
+ is(
+ obtainedColIdx,
+ origColIdx,
+ id +
+ ": column for index " +
+ idx +
+ " is not correct (getColumnIndexAt)"
+ );
+
+ // getRowAndColumnIndicesAt
+ var obtainedRowIdxObj = {},
+ obtainedColIdxObj = {};
+ try {
+ tableAcc.getRowAndColumnIndicesAt(
+ idx,
+ obtainedRowIdxObj,
+ obtainedColIdxObj
+ );
+ } catch (e) {
+ ok(
+ false,
+ id +
+ ": can't get row and column indices for cell index " +
+ idx +
+ "," +
+ e
+ );
+ }
+
+ is(
+ obtainedRowIdxObj.value,
+ origRowIdx,
+ id +
+ ": row for index " +
+ idx +
+ " is not correct (getRowAndColumnIndicesAt)"
+ );
+ is(
+ obtainedColIdxObj.value,
+ origColIdx,
+ id +
+ ": column for index " +
+ idx +
+ " is not correct (getRowAndColumnIndicesAt)"
+ );
+
+ if (cellAcc) {
+ var cellId = prettyName(cellAcc);
+ cellAcc = getAccessible(cellAcc, [nsIAccessibleTableCell]);
+
+ // cell: 'table-cell-index' attribute
+ var attrs = cellAcc.attributes;
+ var strIdx = "";
+ try {
+ strIdx = attrs.getStringProperty("table-cell-index");
+ } catch (e) {
+ ok(
+ false,
+ cellId +
+ ": no cell index from object attributes on the cell accessible at index " +
+ idx +
+ "."
+ );
+ }
+
+ if (strIdx) {
+ is(
+ parseInt(strIdx),
+ idx,
+ cellId +
+ ": cell index from object attributes of cell accessible isn't corrent."
+ );
+ }
+
+ // cell: table
+ try {
+ is(
+ cellAcc.table,
+ tableAcc,
+ cellId + ": wrong table accessible for the cell."
+ );
+ } catch (e) {
+ ok(false, cellId + ": can't get table accessible from the cell.");
+ }
+
+ // cell: getRowIndex
+ try {
+ obtainedRowIdx = cellAcc.rowIndex;
+ } catch (e) {
+ ok(
+ false,
+ cellId +
+ ": can't get row index of the cell at index " +
+ idx +
+ "," +
+ e
+ );
+ }
+
+ is(
+ obtainedRowIdx,
+ origRowIdx,
+ cellId + ": row for the cell at index " + idx + " is not correct"
+ );
+
+ // cell: getColumnIndex
+ try {
+ obtainedColIdx = cellAcc.columnIndex;
+ } catch (e) {
+ ok(
+ false,
+ cellId +
+ ": can't get column index of the cell at index " +
+ idx +
+ "," +
+ e
+ );
+ }
+
+ is(
+ obtainedColIdx,
+ origColIdx,
+ id + ": column for the cell at index " + idx + " is not correct"
+ );
+ }
+ }
+
+ // getCellIndexAt
+ try {
+ obtainedIdx = tableAcc.getCellIndexAt(rowIdx, colIdx);
+ } catch (e) {
+ obtainedIdx = -1;
+ }
+
+ is(
+ obtainedIdx,
+ idx,
+ id +
+ ": row " +
+ rowIdx +
+ " /column " +
+ colIdx +
+ " and index " +
+ obtainedIdx +
+ " aren't inconsistent."
+ );
+ }
+ }
+}
+
+/**
+ * Test table getters selection methods.
+ *
+ * @param aIdentifier [in] table accessible identifier
+ * @param aCellsArray [in] two dimensional array (row X columns) of cells
+ * states (either boolean (selected/unselected) if cell is
+ * origin, otherwise kRowSpanned or kColSpanned constant).
+ * @param aMsg [in] text appended before every message
+ */
+function testTableSelection(aIdentifier, aCellsArray, aMsg) {
+ var msg = aMsg ? aMsg : "";
+ var acc = getAccessible(aIdentifier, [nsIAccessibleTable]);
+ if (!acc) {
+ return;
+ }
+
+ var rowCount = aCellsArray.length;
+ var colsCount = aCellsArray[0].length;
+
+ // Columns selection tests.
+ var selCols = [];
+
+ // isColumnSelected test
+ for (let colIdx = 0; colIdx < colsCount; colIdx++) {
+ var isColSelected = true;
+ for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
+ if (
+ !aCellsArray[rowIdx][colIdx] ||
+ aCellsArray[rowIdx][colIdx] == undefined
+ ) {
+ isColSelected = false;
+ break;
+ }
+ }
+
+ is(
+ acc.isColumnSelected(colIdx),
+ isColSelected,
+ msg +
+ "Wrong selection state of " +
+ colIdx +
+ " column for " +
+ prettyName(aIdentifier)
+ );
+
+ if (isColSelected) {
+ selCols.push(colIdx);
+ }
+ }
+
+ // selectedColsCount test
+ is(
+ acc.selectedColumnCount,
+ selCols.length,
+ msg + "Wrong count of selected columns for " + prettyName(aIdentifier)
+ );
+
+ // getSelectedColumns test
+ var actualSelCols = acc.getSelectedColumnIndices();
+
+ var actualSelColsCount = actualSelCols.length;
+ is(
+ actualSelColsCount,
+ selCols.length,
+ msg +
+ "Wrong count of selected columns for " +
+ prettyName(aIdentifier) +
+ "from getSelectedColumns."
+ );
+
+ for (let i = 0; i < actualSelColsCount; i++) {
+ is(
+ actualSelCols[i],
+ selCols[i],
+ msg + "Column at index " + selCols[i] + " should be selected."
+ );
+ }
+
+ // Rows selection tests.
+ var selRows = [];
+
+ // isRowSelected test
+ for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
+ var isRowSelected = true;
+ for (let colIdx = 0; colIdx < colsCount; colIdx++) {
+ if (
+ !aCellsArray[rowIdx][colIdx] ||
+ aCellsArray[rowIdx][colIdx] == undefined
+ ) {
+ isRowSelected = false;
+ break;
+ }
+ }
+
+ is(
+ acc.isRowSelected(rowIdx),
+ isRowSelected,
+ msg +
+ "Wrong selection state of " +
+ rowIdx +
+ " row for " +
+ prettyName(aIdentifier)
+ );
+
+ if (isRowSelected) {
+ selRows.push(rowIdx);
+ }
+ }
+
+ // selectedRowCount test
+ is(
+ acc.selectedRowCount,
+ selRows.length,
+ msg + "Wrong count of selected rows for " + prettyName(aIdentifier)
+ );
+
+ // getSelectedRows test
+ var actualSelRows = acc.getSelectedRowIndices();
+
+ var actualSelrowCount = actualSelRows.length;
+ is(
+ actualSelrowCount,
+ selRows.length,
+ msg +
+ "Wrong count of selected rows for " +
+ prettyName(aIdentifier) +
+ "from getSelectedRows."
+ );
+
+ for (let i = 0; i < actualSelrowCount; i++) {
+ is(
+ actualSelRows[i],
+ selRows[i],
+ msg + "Row at index " + selRows[i] + " should be selected."
+ );
+ }
+
+ // Cells selection tests.
+ var selCells = [];
+
+ // isCellSelected test
+ for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
+ for (let colIdx = 0; colIdx < colsCount; colIdx++) {
+ if (aCellsArray[rowIdx][colIdx] & kSpanned) {
+ continue;
+ }
+
+ var isSelected = !!aCellsArray[rowIdx][colIdx];
+ is(
+ acc.isCellSelected(rowIdx, colIdx),
+ isSelected,
+ msg +
+ "Wrong selection state of cell at " +
+ rowIdx +
+ " row and " +
+ colIdx +
+ " column for " +
+ prettyName(aIdentifier)
+ );
+
+ if (aCellsArray[rowIdx][colIdx]) {
+ selCells.push(acc.getCellIndexAt(rowIdx, colIdx));
+ }
+ }
+ }
+
+ // selectedCellCount tests
+ is(
+ acc.selectedCellCount,
+ selCells.length,
+ msg + "Wrong count of selected cells for " + prettyName(aIdentifier)
+ );
+
+ // getSelectedCellIndices test
+ var actualSelCells = acc.getSelectedCellIndices();
+
+ var actualSelCellsCount = actualSelCells.length;
+ is(
+ actualSelCellsCount,
+ selCells.length,
+ msg +
+ "Wrong count of selected cells for " +
+ prettyName(aIdentifier) +
+ "from getSelectedCells."
+ );
+
+ for (let i = 0; i < actualSelCellsCount; i++) {
+ is(
+ actualSelCells[i],
+ selCells[i],
+ msg +
+ "getSelectedCellIndices: Cell at index " +
+ selCells[i] +
+ " should be selected."
+ );
+ }
+
+ // selectedCells and isSelected tests
+ var actualSelCellsArray = acc.selectedCells;
+ for (let i = 0; i < actualSelCellsCount; i++) {
+ var actualSelCellAccessible = actualSelCellsArray.queryElementAt(
+ i,
+ nsIAccessibleTableCell
+ );
+
+ let colIdx = acc.getColumnIndexAt(selCells[i]);
+ let rowIdx = acc.getRowIndexAt(selCells[i]);
+ var expectedSelCellAccessible = acc.getCellAt(rowIdx, colIdx);
+
+ is(
+ actualSelCellAccessible,
+ expectedSelCellAccessible,
+ msg +
+ "getSelectedCells: Cell at index " +
+ selCells[i] +
+ " should be selected."
+ );
+
+ ok(
+ actualSelCellAccessible.isSelected(),
+ "isSelected: Cell at index " + selCells[i] + " should be selected."
+ );
+ }
+
+ // selected states tests
+ for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) {
+ for (let colIdx = 0; colIdx < colsCount; colIdx++) {
+ if (aCellsArray[rowIdx][colIdx] & kSpanned) {
+ continue;
+ }
+
+ var cell = acc.getCellAt(rowIdx, colIdx);
+ var isSel = aCellsArray[rowIdx][colIdx];
+ if (isSel == undefined) {
+ testStates(cell, 0, 0, STATE_SELECTABLE | STATE_SELECTED);
+ } else if (isSel) {
+ testStates(cell, STATE_SELECTED);
+ } else {
+ testStates(cell, STATE_SELECTABLE, 0, STATE_SELECTED);
+ }
+ }
+ }
+}
+
+/**
+ * Test columnHeaderCells and rowHeaderCells of accessible table.
+ */
+function testHeaderCells(aHeaderInfoMap) {
+ for (var testIdx = 0; testIdx < aHeaderInfoMap.length; testIdx++) {
+ var dataCellIdentifier = aHeaderInfoMap[testIdx].cell;
+ var dataCell = getAccessible(dataCellIdentifier, [nsIAccessibleTableCell]);
+
+ // row header cells
+ var rowHeaderCells = aHeaderInfoMap[testIdx].rowHeaderCells;
+ var rowHeaderCellsCount = rowHeaderCells.length;
+ var actualRowHeaderCells = dataCell.rowHeaderCells;
+ var actualRowHeaderCellsCount = actualRowHeaderCells.length;
+
+ is(
+ actualRowHeaderCellsCount,
+ rowHeaderCellsCount,
+ "Wrong number of row header cells for the cell " +
+ prettyName(dataCellIdentifier)
+ );
+
+ if (actualRowHeaderCellsCount == rowHeaderCellsCount) {
+ for (let idx = 0; idx < rowHeaderCellsCount; idx++) {
+ var rowHeaderCell = getAccessible(rowHeaderCells[idx]);
+ var actualRowHeaderCell = actualRowHeaderCells.queryElementAt(
+ idx,
+ nsIAccessible
+ );
+ isObject(
+ actualRowHeaderCell,
+ rowHeaderCell,
+ "Wrong row header cell at index " +
+ idx +
+ " for the cell " +
+ dataCellIdentifier
+ );
+ }
+ }
+
+ // column header cells
+ var colHeaderCells = aHeaderInfoMap[testIdx].columnHeaderCells;
+ var colHeaderCellsCount = colHeaderCells.length;
+ var actualColHeaderCells = dataCell.columnHeaderCells;
+ var actualColHeaderCellsCount = actualColHeaderCells.length;
+
+ is(
+ actualColHeaderCellsCount,
+ colHeaderCellsCount,
+ "Wrong number of column header cells for the cell " +
+ prettyName(dataCellIdentifier)
+ );
+
+ if (actualColHeaderCellsCount == colHeaderCellsCount) {
+ for (let idx = 0; idx < colHeaderCellsCount; idx++) {
+ var colHeaderCell = getAccessible(colHeaderCells[idx]);
+ var actualColHeaderCell = actualColHeaderCells.queryElementAt(
+ idx,
+ nsIAccessible
+ );
+ isObject(
+ actualColHeaderCell,
+ colHeaderCell,
+ "Wrong column header cell at index " +
+ idx +
+ " for the cell " +
+ dataCellIdentifier
+ );
+ }
+ }
+ }
+}
+
+// //////////////////////////////////////////////////////////////////////////////
+// private implementation
+
+/**
+ * Return row and column of orig cell for the given spanned cell.
+ */
+function getOrigRowAndColumn(aCellsArray, aRowIdx, aColIdx) {
+ var cellState = aCellsArray[aRowIdx][aColIdx];
+
+ var origRowIdx = aRowIdx,
+ origColIdx = aColIdx;
+ if (cellState & kRowSpanned) {
+ for (var prevRowIdx = aRowIdx - 1; prevRowIdx >= 0; prevRowIdx--) {
+ let prevCellState = aCellsArray[prevRowIdx][aColIdx];
+ if (!(prevCellState & kRowSpanned)) {
+ origRowIdx = prevRowIdx;
+ break;
+ }
+ }
+ }
+
+ if (cellState & kColSpanned) {
+ for (var prevColIdx = aColIdx - 1; prevColIdx >= 0; prevColIdx--) {
+ let prevCellState = aCellsArray[aRowIdx][prevColIdx];
+ if (!(prevCellState & kColSpanned)) {
+ origColIdx = prevColIdx;
+ break;
+ }
+ }
+ }
+
+ return [origRowIdx, origColIdx];
+}
diff --git a/accessible/tests/mochitest/table/a11y.toml b/accessible/tests/mochitest/table/a11y.toml
new file mode 100644
index 0000000000..2250bf85c9
--- /dev/null
+++ b/accessible/tests/mochitest/table/a11y.toml
@@ -0,0 +1,42 @@
+[DEFAULT]
+support-files = "!/accessible/tests/mochitest/*.js"
+
+["test_css_tables.html"]
+
+["test_headers_ariagrid.html"]
+
+["test_headers_ariatable.html"]
+
+["test_headers_table.html"]
+
+["test_headers_tree.xhtml"]
+
+["test_indexes_ariagrid.html"]
+
+["test_indexes_table.html"]
+
+["test_indexes_tree.xhtml"]
+
+["test_layoutguess.html"]
+
+["test_mtable.html"]
+
+["test_sels_ariagrid.html"]
+
+["test_sels_table.html"]
+
+["test_sels_tree.xhtml"]
+
+["test_struct_ariagrid.html"]
+
+["test_struct_ariatreegrid.html"]
+
+["test_struct_table.html"]
+
+["test_struct_tree.xhtml"]
+
+["test_table_1.html"]
+
+["test_table_2.html"]
+
+["test_table_mutation.html"]
diff --git a/accessible/tests/mochitest/table/test_css_tables.html b/accessible/tests/mochitest/table/test_css_tables.html
new file mode 100644
index 0000000000..65877564e4
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_css_tables.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en">
+<html>
+<head>
+ <title>CSS display:table is not a table</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // elements with display:table
+
+ // only display:table
+ var accTree =
+ { SECTION: [
+ { TEXT_LEAF: [ ] },
+ ] };
+ testAccessibleTree("table1", accTree);
+
+ // only display:table and display:table-cell
+ accTree =
+ { SECTION: [
+ { SECTION: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] };
+ testAccessibleTree("table2", accTree);
+
+ // display:table, display:table-row, and display:table-cell
+ accTree =
+ { SECTION: [
+ { SECTION: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] };
+ testAccessibleTree("table3", accTree);
+
+ // display:table, display:table-row-group, display:table-row, and display:table-cell
+ accTree =
+ { SECTION: [
+ { SECTION: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] };
+ testAccessibleTree("table4", accTree);
+
+ // display:inline-table
+ accTree =
+ { TEXT_CONTAINER: [
+ { TEXT_CONTAINER: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] };
+ testAccessibleTree("table5", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ title=" div with display:table exposes table semantics"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1007975">Mozilla Bug 1007975</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="table1" style="display:table">
+ table1
+ </div>
+
+ <div id="table2" style="display:table">
+ <div style="display:table-cell">table2</div>
+ </div>
+
+ <div id="table3" style="display:table">
+ <div style="display:table-row">
+ <div style="display:table-cell">table3</div>
+ </div>
+ </div>
+
+ <div id="table4" style="display:table">
+ <div style="display:table-row-group">
+ <div style="display:table-row">
+ <div style="display:table-cell">table4</div>
+ </div>
+ </div>
+ </div>
+
+ <div>
+ <span id="table5" style="display:inline-table">
+ <span style="display:table-row">
+ <span style="display:table-cell">table5</div>
+ </span>
+ </span>
+ </div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_headers_ariagrid.html b/accessible/tests/mochitest/table/test_headers_ariagrid.html
new file mode 100644
index 0000000000..7b2c3f3dbf
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_headers_ariagrid.html
@@ -0,0 +1,183 @@
+<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en">
+<html>
+<head>
+ <title>Table header information cells for ARIA grid</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // column and row headers from markup
+
+ let headerInfoMap = [
+ {
+ cell: "table_dc_1",
+ rowHeaderCells: [ "table_rh_1" ],
+ columnHeaderCells: [ "table_ch_2" ],
+ },
+ {
+ cell: "table_dc_2",
+ rowHeaderCells: [ "table_rh_1" ],
+ columnHeaderCells: [ "table_ch_3" ],
+ },
+ {
+ cell: "table_dc_3",
+ rowHeaderCells: [ "table_rh_2" ],
+ columnHeaderCells: [ "table_ch_2" ],
+ },
+ {
+ cell: "table_dc_4",
+ rowHeaderCells: [ "table_rh_2" ],
+ columnHeaderCells: [ "table_ch_3" ],
+ },
+ {
+ cell: "table_rh_1",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table_ch_1" ],
+ },
+ {
+ cell: "table_rh_2",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table_ch_1" ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+
+ // ////////////////////////////////////////////////////////////////////////
+ // column and row headers from markup for grid.
+
+ headerInfoMap = [
+ {
+ // not focusable cell (ARIAGridCellAccessible is used)
+ cell: "table2_dc_1",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table2_ch_1" ],
+ },
+ {
+ // focusable cell (ARIAGridCellAccessible is used)
+ cell: "table2_dc_2",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table2_ch_2" ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+
+ // ////////////////////////////////////////////////////////////////////////
+ // column and row headers from markup for one more grid.
+
+ headerInfoMap = [
+ {
+ // ARIAGridCellAccessible is used
+ cell: "t3_dc_1",
+ rowHeaderCells: [ "t3_rh_1" ],
+ columnHeaderCells: [ ],
+ },
+ {
+ // ARIAGridCellAccessible is used (inside rowgroup)
+ cell: "t3_dc_2",
+ rowHeaderCells: [ "t3_rh_2" ],
+ columnHeaderCells: [ ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ title="implement IAccessibleTable2"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=512424">Mozilla Bug 512424</a>
+ <a target="_blank"
+ title="nsHTMLTableCellAccessible is used in dojo's ARIA grid"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=513848">Mozilla Bug 513848</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div role="grid">
+ <div role="row">
+ <span id="table_ch_1" role="columnheader">col_1</span>
+ <span id="table_ch_2" role="columnheader">col_2</span>
+ <span id="table_ch_3" role="columnheader">col_3</span>
+ </div>
+ <div role="row">
+ <span id="table_rh_1" role="rowheader">row_1</span>
+ <span id="table_dc_1" role="gridcell">cell1</span>
+ <span id="table_dc_2" role="gridcell">cell2</span>
+ </div>
+ <div role="row">
+ <span id="table_rh_2" role="rowheader">row_2</span>
+ <span id="table_dc_3" role="gridcell">cell3</span>
+ <span id="table_dc_4" role="gridcell">cell4</span>
+ </div>
+ </div>
+
+ <div role="grid">
+ <div role="row">
+ <table role="presentation">
+ <tr>
+ <td id="table2_ch_1" role="columnheader">header1</td>
+ <td id="table2_ch_2" role="columnheader">header2</td>
+ </tr>
+ </table>
+ </div>
+ <div role="row">
+ <table role="presentation">
+ <tr>
+ <td id="table2_dc_1" role="gridcell">cell1</td>
+ <td id="table2_dc_2" role="gridcell" tabindex="-1">cell2</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+
+ <div role="grid">
+ <table role="presentation">
+ <tbody role="presentation">
+ <tr role="row">
+ <th id="t3_rh_1" role="rowheader">Row 1</th>
+ <td id="t3_dc_1" role="gridcell" tabindex="-1">
+ Apple Inc.
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <div role="rowgroup" tabindex="0">
+ <table role="presentation">
+ <tbody role="presentation">
+ <tr role="row">
+ <th id="t3_rh_2" role="rowheader">Row 2</th>
+ <td id="t3_dc_2" role="gridcell" tabindex="-1">
+ Apple-Shmapple Inc.
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_headers_ariatable.html b/accessible/tests/mochitest/table/test_headers_ariatable.html
new file mode 100644
index 0000000000..1af3813cdc
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_headers_ariatable.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en">
+<html>
+<head>
+ <title>Table header information cells for ARIA table</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // column and row headers from markup
+
+ const headerInfoMap = [
+ {
+ cell: "table_dc_1",
+ rowHeaderCells: [ "table_rh_1" ],
+ columnHeaderCells: [ "table_ch_2" ],
+ },
+ {
+ cell: "table_dc_2",
+ rowHeaderCells: [ "table_rh_1" ],
+ columnHeaderCells: [ "table_ch_3" ],
+ },
+ {
+ cell: "table_dc_3",
+ rowHeaderCells: [ "table_rh_2" ],
+ columnHeaderCells: [ "table_ch_2" ],
+ },
+ {
+ cell: "table_dc_4",
+ rowHeaderCells: [ "table_rh_2" ],
+ columnHeaderCells: [ "table_ch_3" ],
+ },
+ {
+ cell: "table_rh_1",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table_ch_1" ],
+ },
+ {
+ cell: "table_rh_2",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table_ch_1" ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ title="support ARIA table and cell roles"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1173364">Bug 1173364</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div role="table">
+ <div role="row">
+ <span id="table_ch_1" role="columnheader">col_1</span>
+ <span id="table_ch_2" role="columnheader">col_2</span>
+ <span id="table_ch_3" role="columnheader">col_3</span>
+ </div>
+ <div role="row">
+ <span id="table_rh_1" role="rowheader">row_1</span>
+ <span id="table_dc_1" role="cell">cell1</span>
+ <span id="table_dc_2" role="cell">cell2</span>
+ </div>
+ <div role="row">
+ <span id="table_rh_2" role="rowheader">row_2</span>
+ <span id="table_dc_3" role="cell">cell3</span>
+ <span id="table_dc_4" role="cell">cell4</span>
+ </div>
+ </div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_headers_table.html b/accessible/tests/mochitest/table/test_headers_table.html
new file mode 100644
index 0000000000..0d7dafff4b
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_headers_table.html
@@ -0,0 +1,756 @@
+<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en">
+<html>
+<head>
+ <title>Table header information cells for HTML table</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // column header from thead and row header from @scope inside of tfoot
+
+ var headerInfoMap = [
+ {
+ cell: "table1_cell_1",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table1_weekday", "table1_date" ],
+ },
+ {
+ cell: "table1_cell_2",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table1_day", "table1_date" ],
+ },
+ {
+ cell: "table1_cell_3",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table1_qty" ],
+ },
+ {
+ cell: "table1_cell_4",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table1_weekday", "table1_date" ],
+ },
+ {
+ cell: "table1_cell_5",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table1_day", "table1_date" ],
+ },
+ {
+ cell: "table1_cell_6",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table1_qty" ],
+ },
+ {
+ cell: "table1_cell_7",
+ rowHeaderCells: [ "table1_total" ],
+ columnHeaderCells: [ "table1_qty" ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // column and row headers from thead and @scope
+
+ headerInfoMap = [
+ {
+ cell: "table2_cell_2",
+ rowHeaderCells: [ "table2_rh_1" ],
+ columnHeaderCells: [ "table2_ch_2" ],
+ },
+ {
+ cell: "table2_cell_3",
+ rowHeaderCells: [ "table2_rh_1" ],
+ columnHeaderCells: [ "table2_ch_3" ],
+ },
+ {
+ cell: "table2_cell_5",
+ rowHeaderCells: [ "table2_rh_2" ],
+ columnHeaderCells: [ "table2_ch_2" ],
+ },
+ {
+ cell: "table2_cell_6",
+ rowHeaderCells: [ "table2_rh_2" ],
+ columnHeaderCells: [ "table2_ch_3" ],
+ },
+ {
+ cell: "table2_rh_1",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table2_ch_1" ],
+ },
+ {
+ cell: "table2_rh_2",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table2_ch_1" ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // column headers from @headers
+
+ headerInfoMap = [
+ {
+ cell: "table3_cell_1",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table3_ch_1" ],
+ },
+ {
+ cell: "table3_cell_2",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table3_ch_2" ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // table consisted of one column
+
+ headerInfoMap = [
+ {
+ cell: "table4_cell",
+ rowHeaderCells: [],
+ columnHeaderCells: [ "table4_ch" ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // table consisted of one row
+
+ headerInfoMap = [
+ {
+ cell: "table5_cell",
+ rowHeaderCells: [ "table5_rh" ],
+ columnHeaderCells: [ ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // @headers points to table cells
+
+ headerInfoMap = [
+ {
+ cell: "table6_cell",
+ rowHeaderCells: [ "table6_rh" ],
+ columnHeaderCells: [ "table6_ch" ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // @scope="rowgroup" and @scope="row"
+
+ headerInfoMap = [
+ {
+ cell: "t7_r1c1",
+ rowHeaderCells: [ "t7_Mary", "t7_Females" ],
+ columnHeaderCells: [ "t7_1km" ],
+ },
+ {
+ cell: "t7_r1c2",
+ rowHeaderCells: [ "t7_Mary", "t7_Females" ],
+ columnHeaderCells: [ "t7_5km" ],
+ },
+ {
+ cell: "t7_r1c3",
+ rowHeaderCells: [ "t7_Mary", "t7_Females" ],
+ columnHeaderCells: [ "t7_10km" ],
+ },
+ {
+ cell: "t7_r2c1",
+ rowHeaderCells: [ "t7_Betsy", "t7_Females" ],
+ columnHeaderCells: [ "t7_1km" ],
+ },
+ {
+ cell: "t7_r2c2",
+ rowHeaderCells: [ "t7_Betsy", "t7_Females" ],
+ columnHeaderCells: [ "t7_5km" ],
+ },
+ {
+ cell: "t7_r2c3",
+ rowHeaderCells: [ "t7_Betsy", "t7_Females" ],
+ columnHeaderCells: [ "t7_10km" ],
+ },
+ {
+ cell: "t7_r3c1",
+ rowHeaderCells: [ "t7_Matt", "t7_Males" ],
+ columnHeaderCells: [ "t7_1km" ],
+ },
+ {
+ cell: "t7_r3c2",
+ rowHeaderCells: [ "t7_Matt", "t7_Males" ],
+ columnHeaderCells: [ "t7_5km" ],
+ },
+ {
+ cell: "t7_r3c3",
+ rowHeaderCells: [ "t7_Matt", "t7_Males" ],
+ columnHeaderCells: [ "t7_10km" ],
+ },
+ {
+ cell: "t7_r4c1",
+ rowHeaderCells: [ "t7_Todd", "t7_Males" ],
+ columnHeaderCells: [ "t7_1km" ],
+ },
+ {
+ cell: "t7_r4c2",
+ rowHeaderCells: [ "t7_Todd", "t7_Males" ],
+ columnHeaderCells: [ "t7_5km" ],
+ },
+ {
+ cell: "t7_r4c3",
+ rowHeaderCells: [ "t7_Todd", "t7_Males" ],
+ columnHeaderCells: [ "t7_10km" ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // @scope="colgroup" and @scope="col"
+
+ headerInfoMap = [
+ {
+ cell: "t8_r1c1",
+ rowHeaderCells: [ "t8_1km" ],
+ columnHeaderCells: [ "t8_Mary", "t8_Females" ],
+ },
+ {
+ cell: "t8_r1c2",
+ rowHeaderCells: [ "t8_1km" ],
+ columnHeaderCells: [ "t8_Betsy", "t8_Females" ],
+ },
+ {
+ cell: "t8_r1c3",
+ rowHeaderCells: [ "t8_1km" ],
+ columnHeaderCells: [ "t8_Matt", "t8_Males" ],
+ },
+ {
+ cell: "t8_r1c4",
+ rowHeaderCells: [ "t8_1km" ],
+ columnHeaderCells: [ "t8_Todd", "t8_Males" ],
+ },
+ {
+ cell: "t8_r2c1",
+ rowHeaderCells: [ "t8_5km" ],
+ columnHeaderCells: [ "t8_Mary", "t8_Females" ],
+ },
+ {
+ cell: "t8_r2c2",
+ rowHeaderCells: [ "t8_5km" ],
+ columnHeaderCells: [ "t8_Betsy", "t8_Females" ],
+ },
+ {
+ cell: "t8_r2c3",
+ rowHeaderCells: [ "t8_5km" ],
+ columnHeaderCells: [ "t8_Matt", "t8_Males" ],
+ },
+ {
+ cell: "t8_r2c4",
+ rowHeaderCells: [ "t8_5km" ],
+ columnHeaderCells: [ "t8_Todd", "t8_Males" ],
+ },
+ {
+ cell: "t8_r3c1",
+ rowHeaderCells: [ "t8_10km" ],
+ columnHeaderCells: [ "t8_Mary", "t8_Females" ],
+ },
+ {
+ cell: "t8_r3c2",
+ rowHeaderCells: [ "t8_10km" ],
+ columnHeaderCells: [ "t8_Betsy", "t8_Females" ],
+ },
+ {
+ cell: "t8_r3c3",
+ rowHeaderCells: [ "t8_10km" ],
+ columnHeaderCells: [ "t8_Matt", "t8_Males" ],
+ },
+ {
+ cell: "t8_r3c4",
+ rowHeaderCells: [ "t8_10km" ],
+ columnHeaderCells: [ "t8_Todd", "t8_Males" ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // spanned table header cells (v1), @headers define header order
+
+ headerInfoMap = [
+ {
+ cell: "t9_r1c1",
+ rowHeaderCells: [ "t9_females", "t9_mary" ],
+ columnHeaderCells: [ "t9_1km" ],
+ },
+ {
+ cell: "t9_r1c2",
+ rowHeaderCells: [ "t9_females", "t9_mary" ],
+ columnHeaderCells: [ "t9_5km" ],
+ },
+ {
+ cell: "t9_r1c3",
+ rowHeaderCells: [ "t9_females", "t9_mary" ],
+ columnHeaderCells: [ "t9_10km" ],
+ },
+ {
+ cell: "t9_r2c1",
+ rowHeaderCells: [ "t9_females", "t9_betsy" ],
+ columnHeaderCells: [ "t9_1km" ],
+ },
+ {
+ cell: "t9_r2c2",
+ rowHeaderCells: [ "t9_females", "t9_betsy" ],
+ columnHeaderCells: [ "t9_5km" ],
+ },
+ {
+ cell: "t9_r2c3",
+ rowHeaderCells: [ "t9_females", "t9_betsy" ],
+ columnHeaderCells: [ "t9_10km" ],
+ },
+ {
+ cell: "t9_r3c1",
+ rowHeaderCells: [ "t9_males", "t9_matt" ],
+ columnHeaderCells: [ "t9_1km" ],
+ },
+ {
+ cell: "t9_r3c2",
+ rowHeaderCells: [ "t9_males", "t9_matt" ],
+ columnHeaderCells: [ "t9_5km" ],
+ },
+ {
+ cell: "t9_r3c3",
+ rowHeaderCells: [ "t9_males", "t9_matt" ],
+ columnHeaderCells: [ "t9_10km" ],
+ },
+ {
+ cell: "t9_r4c1",
+ rowHeaderCells: [ "t9_males", "t9_todd" ],
+ columnHeaderCells: [ "t9_1km" ],
+ },
+ {
+ cell: "t9_r4c2",
+ rowHeaderCells: [ "t9_males", "t9_todd" ],
+ columnHeaderCells: [ "t9_5km" ],
+ },
+ {
+ cell: "t9_r4c3",
+ rowHeaderCells: [ "t9_males", "t9_todd" ],
+ columnHeaderCells: [ "t9_10km" ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // spanned table header cells (v2), @headers define header order
+
+ headerInfoMap = [
+ {
+ cell: "t10_r1c1",
+ rowHeaderCells: [ "t10_1km" ],
+ columnHeaderCells: [ "t10_females", "t10_mary" ],
+ },
+ {
+ cell: "t10_r1c2",
+ rowHeaderCells: [ "t10_1km" ],
+ columnHeaderCells: [ "t10_females", "t10_betsy" ],
+ },
+ {
+ cell: "t10_r1c3",
+ rowHeaderCells: [ "t10_1km" ],
+ columnHeaderCells: [ "t10_males", "t10_matt" ],
+ },
+ {
+ cell: "t10_r1c4",
+ rowHeaderCells: [ "t10_1km" ],
+ columnHeaderCells: [ "t10_males", "t10_todd" ],
+ },
+ {
+ cell: "t10_r2c1",
+ rowHeaderCells: [ "t10_5km" ],
+ columnHeaderCells: [ "t10_females", "t10_mary" ],
+ },
+ {
+ cell: "t10_r2c2",
+ rowHeaderCells: [ "t10_5km" ],
+ columnHeaderCells: [ "t10_females", "t10_betsy" ],
+ },
+ {
+ cell: "t10_r2c3",
+ rowHeaderCells: [ "t10_5km" ],
+ columnHeaderCells: [ "t10_males", "t10_matt" ],
+ },
+ {
+ cell: "t10_r2c4",
+ rowHeaderCells: [ "t10_5km" ],
+ columnHeaderCells: [ "t10_males", "t10_todd" ],
+ },
+ {
+ cell: "t10_r3c1",
+ rowHeaderCells: [ "t10_10km" ],
+ columnHeaderCells: [ "t10_females", "t10_mary" ],
+ },
+ {
+ cell: "t10_r3c2",
+ rowHeaderCells: [ "t10_10km" ],
+ columnHeaderCells: [ "t10_females", "t10_betsy" ],
+ },
+ {
+ cell: "t10_r3c3",
+ rowHeaderCells: [ "t10_10km" ],
+ columnHeaderCells: [ "t10_males", "t10_matt" ],
+ },
+ {
+ cell: "t10_r3c4",
+ rowHeaderCells: [ "t10_10km" ],
+ columnHeaderCells: [ "t10_males", "t10_todd" ],
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Ensure correct column headers after colspan in a previous row.
+ headerInfoMap = [
+ {
+ cell: "t11r1c1",
+ columnHeaderCells: [],
+ rowHeaderCells: [],
+ },
+ {
+ cell: "t11r1c2",
+ columnHeaderCells: [],
+ rowHeaderCells: [],
+ },
+ {
+ cell: "t11r2c1_2",
+ columnHeaderCells: ["t11r1c1"],
+ rowHeaderCells: [],
+ },
+ {
+ cell: "t11r3c1",
+ columnHeaderCells: ["t11r1c1"],
+ rowHeaderCells: [],
+ },
+ {
+ cell: "t11r3c2",
+ columnHeaderCells: ["t11r1c2"],
+ rowHeaderCells: [],
+ },
+ ];
+ testHeaderCells(headerInfoMap);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ title="implement IAccessibleTable2"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=512424">
+ Bug 512424
+ </a>
+ <a target="_blank"
+ title="Table headers not associated when header is a td element with no scope"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=704465">
+ Bug 704465
+ </a>
+ <a target="_blank"
+ title="Support rowgroup and colgroup HTML scope"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1141978">
+ Bug 1141978
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <table id="table1" border="1">
+ <thead>
+ <tr>
+ <th id="table1_date" colspan="2">Date</th>
+ <th id="table1_qty" rowspan="2">Qty</th>
+ </tr>
+ <tr>
+ <th id="table1_weekday">Weekday</th>
+ <th id="table1_day">Day</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td id="table1_cell_1">Mon</td>
+ <td id="table1_cell_2">1</td>
+ <td id="table1_cell_3">20</td>
+ </tr>
+ <tr>
+ <td id="table1_cell_4">Thu</td>
+ <td id="table1_cell_5">2</td>
+ <td id="table1_cell_6">15</td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr>
+ <th id="table1_total" scope="row" colspan="2">Total</th>
+ <td id="table1_cell_7">35</td>
+ </tr>
+ </tfoot>
+ </table>
+
+ <table id="table2" border="1">
+ <thead>
+ <tr>
+ <th id="table2_ch_1">col1</th>
+ <th id="table2_ch_2">col2</th>
+ <td id="table2_ch_3" scope="col">col3</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th id="table2_rh_1">row1</th>
+ <td id="table2_cell_2">cell1</td>
+ <td id="table2_cell_3">cell2</td>
+ </tr>
+ <tr>
+ <td id="table2_rh_2" scope="row">row2</td>
+ <td id="table2_cell_5">cell3</td>
+ <td id="table2_cell_6">cell4</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table id="table3" border="1">
+ <tr>
+ <td id="table3_cell_1" headers="table3_ch_1">cell1</td>
+ <td id="table3_cell_2" headers="table3_ch_2">cell2</td>
+ </tr>
+ <tr>
+ <td id="table3_ch_1" scope="col">col1</td>
+ <td id="table3_ch_2" scope="col">col2</td>
+ </tr>
+ </table>
+
+ <table id="table4">
+ <thead>
+ <tr>
+ <th id="table4_ch">colheader</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td id="table4_cell">bla</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table id="table5">
+ <tr>
+ <th id="table5_rh">rowheader</th>
+ <td id="table5_cell">cell</td>
+ </tr>
+ </table>
+
+ <table id="table6">
+ <tr>
+ <td>empty cell</th>
+ <td id="table6_ch">colheader</td>
+ </tr>
+ <tr>
+ <td id="table6_rh">rowheader</th>
+ <td id="table6_cell" headers="table6_ch table6_rh">cell</td>
+ </tr>
+ </table>
+
+ <table id="table7" class="data complex" border="1">
+ <caption>Version 1 with rowgroup</caption>
+ <thead>
+ <tr>
+ <td colspan="2">&nbsp;</td>
+ <th id="t7_1km" scope="col">1 km</th>
+ <th id="t7_5km" scope="col">5 km</th>
+ <th id="t7_10km" scope="col">10 km</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th id="t7_Females" rowspan="2" scope="rowgroup">Females</th>
+ <th id="t7_Mary" scope="row">Mary</th>
+ <td id="t7_r1c1">8:32</td>
+ <td id="t7_r1c2">28:04</td>
+ <td id="t7_r1c3">1:01:16</td>
+ </tr>
+ <tr>
+ <th id="t7_Betsy" scope="row">Betsy</th>
+ <td id="t7_r2c1">7:43</td>
+ <td id="t7_r2c2">26:47</td>
+ <td id="t7_r2c3">55:38</td>
+ </tr>
+ <tr>
+ <th id="t7_Males" rowspan="2" scope="rowgroup">Males</th>
+ <th id="t7_Matt" scope="row">Matt</th>
+ <td id="t7_r3c1">7:55</td>
+ <td id="t7_r3c2">27:29</td>
+ <td id="t7_r3c3">57:04</td>
+ </tr>
+ <tr>
+ <th id="t7_Todd" scope="row">Todd</th>
+ <td id="t7_r4c1">7:01</td>
+ <td id="t7_r4c2">24:21</td>
+ <td id="t7_r4c3">50:35</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table id="table8" class="data complex" border="1">
+ <caption>Version 2 with colgroup</caption>
+ <thead>
+ <tr>
+ <td rowspan="2">&nbsp;</td>
+ <th id="t8_Females" colspan="2" scope="colgroup">Females</th>
+ <th id="t8_Males" colspan="2" scope="colgroup">Males</th>
+ </tr>
+ <tr>
+ <th id="t8_Mary" scope="col">Mary</th>
+ <th id="t8_Betsy" scope="col">Betsy</th>
+ <th id="t8_Matt" scope="col">Matt</th>
+ <th id="t8_Todd" scope="col">Todd</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th id="t8_1km" scope="row">1 km</th>
+ <td id="t8_r1c1">8:32</td>
+ <td id="t8_r1c2">7:43</td>
+ <td id="t8_r1c3">7:55</td>
+ <td id="t8_r1c4">7:01</td>
+ </tr>
+ <tr>
+ <th id="t8_5km" scope="row">5 km</th>
+ <td id="t8_r2c1">28:04</td>
+ <td id="t8_r2c2">26:47</td>
+ <td id="t8_r2c3">27:27</td>
+ <td id="t8_r2c4">24:21</td>
+ </tr>
+ <tr>
+ <th id="t8_10km" scope="row">10 km</th>
+ <td id="t8_r3c1">1:01:16</td>
+ <td id="t8_r3c2">55:38</td>
+ <td id="t8_r3c3">57:04</td>
+ <td id="t8_r3c4">50:35</td>
+ </tr>
+
+ </tbody>
+ </table>
+
+ <table id="table9" border="1">
+ <caption>
+ Example 1 (row group headers):
+ </caption>
+ <tr>
+ <td colspan="2"><span class="offscreen">empty</span></td>
+ <th id="t9_1km" width="40">1 km</th>
+ <th id="t9_5km" width="35">5 km</th>
+ <th id="t9_10km" width="42">10 km</th>
+ </tr>
+ <tr>
+ <th id="t9_females" width="56" rowspan="2">Females</th>
+ <th id="t9_mary" width="39">Mary</th>
+ <td id="t9_r1c1" headers="t9_females t9_mary t9_1km">8:32</td>
+ <td id="t9_r1c2" headers="t9_females t9_mary t9_5km">28:04</td>
+ <td id="t9_r1c3" headers="t9_females t9_mary t9_10km">1:01:16</td>
+ </tr>
+ <tr>
+ <th id="t9_betsy">Betsy</th>
+ <td id="t9_r2c1" headers="t9_females t9_betsy t9_1km">7:43</td>
+ <td id="t9_r2c2" headers="t9_females t9_betsy t9_5km">26:47</td>
+ <td id="t9_r2c3" headers="t9_females t9_betsy t9_10km">55:38</td>
+ </tr>
+ <tr>
+ <th id="t9_males" rowspan="2">Males</th>
+ <th id="t9_matt">Matt</th>
+ <td id="t9_r3c1" headers="t9_males t9_matt t9_1km">7:55</td>
+ <td id="t9_r3c2" headers="t9_males t9_matt t9_5km">27:29</td>
+ <td id="t9_r3c3" headers="t9_males t9_matt t9_10km">57:04</td>
+ </tr>
+ <tr>
+ <th id="t9_todd">Todd</th>
+ <td id="t9_r4c1" headers="t9_males t9_todd t9_1km">7:01</td>
+ <td id="t9_r4c2" headers="t9_males t9_todd t9_5km">24:21</td>
+ <td id="t9_r4c3" headers="t9_males t9_todd t9_10km">50:35</td>
+ </tr>
+ </table>
+
+ <table id="table10" border="1">
+ <caption>
+ Example 2 (column group headers):
+ </caption>
+ <tr>
+ <td rowspan="2"><span class="offscreen">empty</span></td>
+ <th colspan="2" id="t10_females">Females</th>
+ <th colspan="2" id="t10_males">Males</th>
+ </tr>
+ <tr>
+ <th width="40" id="t10_mary">Mary</th>
+ <th width="35" id="t10_betsy">Betsy</th>
+ <th width="42" id="t10_matt">Matt</th>
+ <th width="42" id="t10_todd">Todd</th>
+ </tr>
+ <tr>
+ <th width="39" id="t10_1km">1 km</th>
+ <td headers="t10_females t10_mary t10_1km" id="t10_r1c1">8:32</td>
+ <td headers="t10_females t10_betsy t10_1km" id="t10_r1c2">7:43</td>
+ <td headers="t10_males t10_matt t10_1km" id="t10_r1c3">7:55</td>
+ <td headers="t10_males t10_todd t10_1km" id="t10_r1c4">7:01</td>
+ </tr>
+ <tr>
+ <th id="t10_5km">5 km</th>
+ <td headers="t10_females t10_mary t10_5km" id="t10_r2c1">28:04</td>
+ <td headers="t10_females t10_betsy t10_5km" id="t10_r2c2">26:47</td>
+ <td headers="t10_males t10_matt t10_5km" id="t10_r2c3">27:29</td>
+ <td headers="t10_males t10_todd t10_5km" id="t10_r2c4">24:21</td>
+ </tr>
+ <tr>
+ <th id="t10_10km">10 km</th>
+ <td headers="t10_females t10_mary t10_10km" id="t10_r3c1">1:01:16</td>
+ <td headers="t10_females t10_betsy t10_10km" id="t10_r3c2">55:38</td>
+ <td headers="t10_males t10_matt t10_10km" id="t10_r3c3">57:04</td>
+ <td headers="t10_males t10_todd t10_10km" id="t10_r3c4">50:35</td>
+ </tr>
+ </table>
+
+ <table id="table11">
+ <tr>
+ <th id="t11r1c1">a</th>
+ <th id="t11r1c2">b</th>
+ </tr>
+ <tr>
+ <td id="t11r2c1_2" colspan="2"></td>
+ </tr>
+ <tr>
+ <td id="t11r3c1">e</td>
+ <td id="t11r3c2">f</td>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_headers_tree.xhtml b/accessible/tests/mochitest/table/test_headers_tree.xhtml
new file mode 100644
index 0000000000..46e8f43126
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_headers_tree.xhtml
@@ -0,0 +1,100 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Table header information cells for XUL tree">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../table.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "debug";
+
+ function doTest()
+ {
+ var treeAcc = getAccessible("tree", [nsIAccessibleTable]);
+
+ var headerInfoMap = [
+ {
+ cell: treeAcc.getCellAt(0, 0),
+ rowHeaderCells: [],
+ columnHeaderCells: [ "col" ]
+ },
+ {
+ cell: treeAcc.getCellAt(0, 1),
+ rowHeaderCells: [],
+ columnHeaderCells: [ "scol" ]
+ },
+ {
+ cell: treeAcc.getCellAt(1, 0),
+ rowHeaderCells: [],
+ columnHeaderCells: [ "col" ]
+ },
+ {
+ cell: treeAcc.getCellAt(1, 1),
+ rowHeaderCells: [],
+ columnHeaderCells: [ "scol" ]
+ },
+ {
+ cell: treeAcc.getCellAt(2, 0),
+ rowHeaderCells: [],
+ columnHeaderCells: [ "col" ]
+ },
+ {
+ cell: treeAcc.getCellAt(2, 1),
+ rowHeaderCells: [],
+ columnHeaderCells: [ "scol" ]
+ },
+ ];
+
+ testHeaderCells(headerInfoMap);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yXULTreeLoadEvent(doTest, "tree", new nsTableTreeView(3));
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=512424"
+ title="implement IAccessibleTable2">
+ Mozilla Bug 512424
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="debug"/>
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col" flex="1" primary="true" label="column"/>
+ <treecol id="scol" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren id="treechildren"/>
+ </tree>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/table/test_indexes_ariagrid.html b/accessible/tests/mochitest/table/test_indexes_ariagrid.html
new file mode 100644
index 0000000000..564e141a70
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_indexes_ariagrid.html
@@ -0,0 +1,159 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Table indexes for ARIA grid tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA grid
+ var idxes = [
+ [0, 1, 2],
+ [3, 4, 5],
+ [6, 7, 8],
+ [9, 10, 11],
+ ];
+ testTableIndexes("grid", idxes);
+
+ idxes = [
+ [0, 1, 2],
+ [3, 4, 5],
+ [6, 7, 8],
+ [9, 10, 11],
+ ];
+ testTableIndexes("grid-rowgroups", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // a bit strange ARIA grid
+ idxes = [
+ [0, 1],
+ [2, 3],
+ ];
+ testTableIndexes("grid2", idxes);
+
+ // an ARIA grid with div wrapping cell and div wrapping row
+ idxes = [
+ [0, 1],
+ [2, 3],
+ [4, 5],
+ ];
+ testTableIndexes("grid3", idxes);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=386813"
+ title="support nsIAccessibleTable on ARIA grid/treegrid">Mozilla Bug 386813</a>
+ <a target="_blank"
+ title="nsHTMLTableCellAccessible is used in dojo's ARIA grid"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=513848">Mozilla Bug 513848</a>
+ <a target="_blank"
+ title="ARIA grid with rowgroup breaks table row/col counting and indices"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=761853">Mozilla Bug 761853</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div role="grid" id="grid">
+ <div role="row">
+ <span role="columnheader">column1</span>
+ <span role="columnheader">column2</span>
+ <span role="columnheader">column3</span>
+ </div>
+ <div role="row">
+ <span role="rowheader">row1</span>
+ <span role="gridcell">cell1</span>
+ <span role="gridcell">cell2</span>
+ </div>
+ <div role="row">
+ <span role="rowheader">row2</span>
+ <span role="gridcell">cell3</span>
+ <span role="gridcell">cell4</span>
+ </div>
+ <div role="row">
+ <span role="rowheader">row3</span>
+ <span role="gridcell">cell5</span>
+ <span role="gridcell">cell6</span>
+ </div>
+ </div>
+
+ <div role="grid" id="grid-rowgroups">
+ <div role="row">
+ <span role="columnheader">grid-rowgroups-col1</span>
+ <span role="columnheader">grid-rowgroups-col2</span>
+ <span role="columnheader">grid-rowgroups-col3</span>
+ </div>
+ <div role="rowgroup">
+ <div role="row">
+ <span role="rowheader">grid-rowgroups-row1</span>
+ <span role="gridcell">grid-rowgroups-cell1</span>
+ <span role="gridcell">grid-rowgroups-cell2</span>
+ </div>
+ <div role="row">
+ <span role="rowheader">grid-rowgroups-row2</span>
+ <span role="gridcell">grid-rowgroups-cell3</span>
+ <span role="gridcell">grid-rowgroups-cell4</span>
+ </div>
+ </div>
+ <div role="row">
+ <span role="rowheader">grid-rowgroups-row3</span>
+ <span role="gridcell">grid-rowgroups-cell5</span>
+ <span role="gridcell">grid-rowgroups-cell6</span>
+ </div>
+ </div>
+
+ <div role="grid" id="grid2">
+ <div role="row">
+ <table role="presentation">
+ <tr>
+ <td role="columnheader">header1</td>
+ <td role="columnheader">header2</td>
+ </tr>
+ </table>
+ </div>
+ <div role="row">
+ <table role="presentation">
+ <tr>
+ <td role="gridcell">cell1</td>
+ <td role="gridcell" tabindex="-1">cell2</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+
+ <div role="grid" id="grid3">
+ <div role="row">
+ <div role="gridcell">Normal cell</div>
+ <div role="gridcell">1</div>
+ </div>
+ <div role="row">
+ <div role="gridcell">Div</div>
+ <div><div role="gridcell">2</div></div>
+ </div>
+ <div tabindex="-1"><div role="row">
+ <div role="gridcell">Cell in row in div</div>
+ <div role="gridcell">3</div>
+ </div></div>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_indexes_table.html b/accessible/tests/mochitest/table/test_indexes_table.html
new file mode 100644
index 0000000000..c43dbad8f7
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_indexes_table.html
@@ -0,0 +1,481 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=410052
+-->
+<head>
+ <title>Table indexes chrome tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // table
+ var idxes = [
+ [0, 1, 2],
+ [3, 4, 5],
+ [6, 7, 7],
+ [6, 8, 9],
+ ];
+
+ testTableIndexes("table", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // tableborder
+ idxes = [
+ [0, 1, 2],
+ [3, 4, 5],
+ [6, 7, 7],
+ [6, 8, 9],
+ ];
+
+ testTableIndexes("tableborder", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // table
+ idxes = [
+ [ 0, 1, 2, 2, 3, 4, 5, 6],
+ [ 7, 8, 9, 10, 11, 12, 13, 6],
+ [14, 15, 15, 16, 17, 18, 19, 6],
+ [20, 15, 15, 21, 22, 18, 23, 6],
+ ];
+
+ testTableIndexes("table2", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // tableinsane1 (empty row groups)
+ idxes = [
+ [0, 1, 2],
+ [3, 4, 5],
+ [6, 7, 7],
+ [6, 8, 9],
+ ];
+
+ testTableIndexes("tableinsane1", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // tableinsane2 (empry rows)
+ idxes = [
+ [-1, -1, -1],
+ [-1, -1, -1],
+ [ 0, 1, 2],
+ ];
+
+ testTableIndexes("tableinsane2", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // tableinsane3 (cell holes)
+ idxes = [
+ [0, 1, -1],
+ [2, 3, 4],
+ ];
+
+ testTableIndexes("tableinsane3", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // tableinsane3.2 (cell holes, row spans, fixed in bug 417912)
+ idxes = [
+ [0, 1, 2],
+ [3, -1, 2],
+ [4, 5, 2],
+ ];
+
+ testTableIndexes("tableinsane3.2", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // tableinsane4 (empty row groups/rows and cell holes)
+ idxes = [
+ [ 0, 1, 2],
+ [-1, -1, -1],
+ [ 3, 4, 5],
+ [ 6, 6, 7],
+ [ 8, -1, 7],
+ [ 9, 9, 9],
+ ];
+ testTableIndexes("tableinsane4", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // tableinsane5 (just a strange table)
+ idxes = [
+ [ 0, 1, 2, -1, -1],
+ [-1, -1, -1, -1, -1],
+ [ 3, 4, 5, -1, -1],
+ [ 6, 7, -1, -1, -1],
+ [ 6, 8, 9, -1, -1],
+ [ 6, 10, 9, 11, 12],
+ ];
+ testTableIndexes("tableinsane5", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // tableinsane6 (overlapping cells, mad table)
+ idxes = [
+ [ 0, 1, 2, -1, -1],
+ [-1, -1, -1, -1, -1],
+ [ 3, 4, 5, -1, -1],
+ [ 6, 6, 7, -1, -1],
+ [ 8, 9, 7, -1, -1],
+ [ 10, 9, 7, 11, 12],
+ ];
+ testTableIndexes("tableinsane6", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Table with a cell that has display: block; style
+ idxes = [
+ [0, 1],
+ ];
+ testTableIndexes("tablewithcelldisplayblock", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // A table with a cell that has display: block; and a cell with colspan.
+ // This makes us fall back to the ARIAGridCellAccessible implementation.
+ idxes = [
+ [0, 0, 1],
+ ];
+ testTableIndexes("tablewithcolspanandcelldisplayblock", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // A table with all elements being display:block, including a row group.
+ // This makes us fall back to the ARIAGridRowAccessible, and we must
+ // make sure the index is 0. Strange example from Gmail.
+ idxes = [
+ [0],
+ ];
+ testTableIndexes("tablealldisplayblock", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Table that has display: block; style
+ idxes = [
+ [0, 1],
+ ];
+ testTableIndexes("tablewithdisplayblock", idxes);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // tbody that has display: block; style
+ idxes = [
+ [0, 1],
+ ];
+ testTableIndexes("tbodywithdisplayblock", idxes);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="GetIndexAt and GetRowAtIndex and GetColumnAtIndex on HTML tables are inconsistent"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=410052">
+ Bug 410052
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!--
+ If you change the structure of the table please make sure to change
+ the indexes count in 'for' statement in the script above.
+ -->
+ <table border="1" id="table">
+ <caption><strong><b><font size="29">this is a caption for this table</font></b></strong></caption>
+ <thead>
+ <tr>
+ <th>col1</th>
+ <th>col2</th>
+ <th>col3</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>1</td>
+ <td>2</td>
+ <td>3</td>
+ </tr>
+ <tr>
+ <td rowspan="0">4</td>
+ <td colspan="2">5</td>
+ </tr>
+ <tr>
+ <td>6</td>
+ <td>7</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table border="1" id="tableborder" style="border-collapse:collapse">
+ <caption><strong><b><font size="29">this is a caption for this bc table</font></b></strong></caption>
+ <thead>
+ <tr>
+ <th>col1</th>
+ <th>col2</th>
+ <th>col3</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>1</td>
+ <td>2</td>
+ <td>3</td>
+ </tr>
+ <tr>
+ <td rowspan="2">4</td>
+ <td colspan="2">5</td>
+ </tr>
+ <tr>
+ <td>6</td>
+ <td>7</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table cellpadding="2" cellspacing="2" border="1" width="50%" id="table2">
+ <caption>column and row spans</caption>
+ <tbody>
+ <tr>
+ <td>0</td>
+ <td>1</td>
+ <td rowspan="1" colspan="2">2</td>
+ <td>3</td>
+ <td>4</td>
+ <td>5</td>
+ <td rowspan="4" colspan="1">6</td>
+ </tr>
+ <tr>
+ <td>7</td>
+ <td>8</td>
+ <td>8</td>
+ <td>10</td>
+ <td>11</td>
+ <td>12</td>
+ <td>13</td>
+ </tr>
+ <tr>
+ <td>14</td>
+ <td rowspan="2" colspan="2">15</td>
+ <td>16</td>
+ <td>17</td>
+ <td rowspan="2" colspan="1">18</td>
+ <td>19</td>
+ </tr>
+ <tr>
+ <td>20</td>
+ <td>21</td>
+ <td>22</td>
+ <td>23</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table border="1" id="tableinsane1">
+ <caption>test empty row groups</caption>
+ <thead>
+ <tr>
+ <th>col1</th>
+ <th>col2</th>
+ <th>col3</th>
+ </tr>
+ </thead>
+ <tbody></tbody>
+ <tbody></tbody>
+ <tbody></tbody>
+ <tbody>
+ <tr>
+ <td>1</td>
+ <td>2</td>
+ <td>3</td>
+ </tr>
+ <tr>
+ <td rowspan="2">4</td>
+ <td colspan="2">5</td>
+ </tr>
+ <tr>
+ <td>6</td>
+ <td>7</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table border="1" id="tableinsane2">
+ <caption>empty rows</caption>
+ <tbody><tr></tr><tr></tr></tbody>
+ <tbody></tbody>
+ <tbody>
+ <tr>
+ <td>0</td>
+ <td>1</td>
+ <td>2</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table border="1" id="tableinsane3">
+ <caption>missed cell</caption>
+ <tbody>
+ <tr>
+ <td>0</td>
+ <td>1</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <td>2</td>
+ <td>3</td>
+ <td>4</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table cellpadding="2" cellspacing="2" border="1" id="tableinsane3.2">
+ <tr><td>1</td><td>2</td><td rowspan=3>3</td>
+ <tr><td>4</td>
+ <tr><td>5</td><td>6</td>
+ </table>
+
+ <table border="1" id="tableinsane4">
+ <caption>test empty rows + cellmap holes</caption>
+ <thead>
+ <tr>
+ <th>col1</th>
+ <th>col2</th>
+ <th>col3</th>
+ </tr>
+ </thead>
+ <tbody><tr></tr></tbody>
+ <tbody></tbody>
+ <tbody></tbody>
+ <tbody>
+ <tr>
+ <td>1</td>
+ <td>2</td>
+ <td>3</td>
+ </tr>
+ <tr>
+ <td colspan="2">4</td>
+ <td rowspan="2">5</td>
+ </tr>
+ <tr>
+ <td>6</td>
+ </tr>
+ <tr>
+ <td colspan="3">7</td>
+ </tr>
+
+ </tbody>
+ </table>
+
+ <table border="1" id="tableinsane5">
+ <caption>just a strange table</caption>
+ <thead>
+ <tr>
+ <th>col1</th>
+ <th>col2</th>
+ <th>col3</th>
+ </tr>
+ </thead>
+ <tbody><tr></tr></tbody>
+ <tbody></tbody>
+ <tbody></tbody>
+ <tbody>
+ <tr>
+ <td>1</td>
+ <td>2</td>
+ <td>3</td>
+ </tr>
+ <tr>
+ <td rowspan="0">4</td>
+ <td colspan="0">5</td>
+ </tr>
+ <tr>
+ <td>6</td>
+ <td rowspan="0">7</td>
+ </tr>
+ <tr>
+ <td>8</td>
+ <td>9</td>
+ <td>10</td>
+ </tr>
+
+ </tbody>
+ </table>
+
+ <table border="1" id="tableinsane6" >
+ <caption>overlapping cells</caption>
+ <thead>
+ <tr>
+ <th>header cell 0</th>
+ <th>header cell 1</th>
+ <th>header cell 2</th>
+ </tr>
+ </thead>
+ <tbody><tr></tr></tbody>
+ <tbody></tbody>
+ <tbody></tbody>
+ <tbody>
+ <tr>
+ <td>3</td>
+ <td>4</td>
+ <td>5</td>
+ </tr>
+ <tr>
+ <td colspan="2">6</td>
+ <td rowspan="0">7</td>
+ </tr>
+ <tr>
+ <td>8</td>
+ <td rowspan="0">9</td>
+ </tr>
+ <tr>
+ <td colspan="3">10</td>
+ <td>11</td>
+ <td>12</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table id="tablewithcelldisplayblock">
+ <tr>
+ <th>a</th>
+ <td style="display: block;">b</td>
+ </tr>
+ </table>
+
+ <table id="tablewithcolspanandcelldisplayblock">
+ <tr>
+ <th colspan="2">a</th>
+ <td style="display: block;" >b</td>
+ </tr>
+ </table>
+
+ <table id="tablealldisplayblock" style="display:block;">
+ <tbody style="display:block;">
+ <tr style="display:block;">
+ <td style="display:block;">text</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table id="tablewithdisplayblock" style="display: block;">
+ <tr><th>a</th><td>b</td></tr>
+ </table>
+
+ <table id="tbodywithdisplayblock">
+ <tbody style="display: block;">
+ <tr>
+ <th>a</th>
+ <td>b</td>
+ </tr>
+ </tbody>
+ </table>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_indexes_tree.xhtml b/accessible/tests/mochitest/table/test_indexes_tree.xhtml
new file mode 100644
index 0000000000..0b1b6b2625
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_indexes_tree.xhtml
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible Table indexes tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../table.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "debug";
+
+ function doTest()
+ {
+ var idxes = [
+ [0, 1],
+ [2, 3],
+ [4, 5]
+ ];
+ testTableIndexes("tree", idxes);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yXULTreeLoadEvent(doTest, "tree", new nsTableTreeView(3));
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727"
+ title="Reorganize implementation of XUL tree accessibility">
+ Mozilla Bug 503727
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="debug"/>
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col" flex="1" primary="true" label="column"/>
+ <treecol id="scol" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren id="treechildren"/>
+ </tree>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/table/test_layoutguess.html b/accessible/tests/mochitest/table/test_layoutguess.html
new file mode 100644
index 0000000000..f516c3920c
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_layoutguess.html
@@ -0,0 +1,567 @@
+<html>
+<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=495388 -->
+<head>
+ <title>test HTMLTableAccessible::IsProbablyForLayout implementation</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+ function isLayoutTable(id) {
+ // This helps us know if the absence of layout-guess is simply because
+ // it is not a table.
+ ok(isAccessible(id, nsIAccessibleTable), `${id} has table interface`);
+ testAttrs(id, { "layout-guess": "true" }, true);
+ }
+ function isDataTable(id) {
+ testAbsentAttrs(id, { "layout-guess": "true" });
+ }
+
+ function doTest() {
+ // table with role of grid
+ isDataTable("table1");
+ // table with role of grid and datatable="0"
+ isDataTable("table1.1");
+
+ // table with landmark role
+ isDataTable("table2");
+
+ // table with summary
+ isDataTable("table3");
+
+ // table with caption
+ isDataTable("table4");
+
+ // layout table with empty caption
+ isLayoutTable("table4.2");
+
+ // table with thead element
+ isDataTable("table5");
+
+ // table with tfoot element
+ isDataTable("table5.1");
+
+ // table with colgroup or col elements
+ isDataTable("table5.2");
+ isDataTable("table5.3");
+
+ // table with th element
+ isDataTable("table6");
+
+ // table with headers attribute
+ isDataTable("table6.2");
+
+ // table with scope attribute
+ isDataTable("table6.2.2");
+
+ // table with abbr attribute
+ isDataTable("table6.2.3");
+
+ // table with abbr element
+ isDataTable("table6.3");
+
+ // table with abbr element having empty text node
+ isDataTable("table6.4");
+
+ // table with abbr element and non-empty text node
+ isLayoutTable("table6.5");
+
+ // layout table with nested table
+ isLayoutTable("table9");
+
+ // layout table with 1 column
+ isLayoutTable("table10");
+
+ // layout table with 1 row
+ isLayoutTable("table11");
+
+ // table with 5 columns
+ isDataTable("table12");
+
+ // table with a bordered cell
+ isDataTable("table13");
+
+ // table with alternating row background colors
+ isDataTable("table14");
+
+ // table with 3 columns and 21 rows
+ isDataTable("table15");
+
+ // layout table that has a 100% width
+ isLayoutTable("table16");
+
+ // layout table that has a 95% width in pixels
+ isLayoutTable("table17");
+
+ // layout table with less than 10 columns
+ isLayoutTable("table18");
+
+ // layout table with embedded iframe
+ isLayoutTable("table19");
+
+ // tree grid, no layout table
+ isDataTable("table20");
+
+ // layout table containing nested data table (having data structures)
+ isLayoutTable("table21");
+ isLayoutTable("table21.2");
+ isLayoutTable("table21.3");
+ isLayoutTable("table21.4");
+ isLayoutTable("table21.5");
+ isLayoutTable("table21.6");
+
+ // layout table having datatable="0" attribute and containing data table structure (tfoot element)
+ isLayoutTable("table22");
+
+ // repurposed table for tabbed UI
+ isLayoutTable("table23");
+
+ // data table that has a nested table but has non-zero border width on a cell
+ isDataTable("table24");
+
+ // layout display:block table with 1 column
+ isLayoutTable("displayblock_table1");
+
+ // matrix
+ isDataTable("mtable1");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=495388"
+ title="Don't treat tables that have a landmark role as layout table">
+ Mozilla Bug 495388
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=690222"
+ title="Data table elements used to determine layout-guess attribute shouldn't be picked from nested tables">
+ Mozilla Bug 690222
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=696975"
+ title="Extend the list of legitimate data table structures">
+ Mozilla Bug 696975
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- Table with role of grid -->
+ <table id="table1" role="grid">
+ <tr>
+ <th>Sender</th>
+ <th>Subject</th>
+ <th>Date</th>
+ </tr>
+ <tr>
+ <td>Marco</td>
+ <td>Test</td>
+ <td>June 12</td>
+ </tr>
+ <tr>
+ <td>David</td>
+ <td>Another test</td>
+ <td>June 12</td>
+ </tr>
+ <tr>
+ <td>Alex</td>
+ <td>Third test</td>
+ <td>June 12</td>
+ </tr>
+ </table>
+ <!-- table with role of grid and datatable="0"-->
+ <table id="table1.1" role="grid" datatable="0">
+ <tr>
+ <td>Cell1</td><td>cell2</td>
+ </tr>
+ </table>
+
+ <!-- table with landmark role -->
+ <table id="table2" role="main">
+ <tr>
+ <th>Sender</th>
+ <th>Subject</th>
+ <th>Date</th>
+ </tr>
+ <tr>
+ <td>Marco</td>
+ <td>Test</td>
+ <td>June 12</td>
+ </tr>
+ <tr>
+ <td>David</td>
+ <td>Another test</td>
+ <td>June 12</td>
+ </tr>
+ <tr>
+ <td>Alex</td>
+ <td>Third test</td>
+ <td>June 12</td>
+ </tr>
+ </table>
+
+ <!-- table with summary -->
+ <table id="table3" summary="This is a table">
+ <tr>
+ <td>Cell1</td><td>cell2</td>
+ </tr>
+ </table>
+
+ <!-- table with caption -->
+ <table id="table4">
+ <caption>This is a table</caption>
+ <tr>
+ <td>Cell1</td><td>cell2</td>
+ </tr>
+ </table>
+
+ <!-- layout table with empty caption -->
+ <table id="table4.2">
+ <caption> </caption>
+ <tr>
+ <td>Cell1</td><td>cell2</td>
+ </tr>
+ </table>
+
+ <!-- table with thead element -->
+ <table id="table5">
+ <thead>
+ <tr>
+ <td>Cell1</td><td>cell2</td>
+ </tr>
+ </thead>
+ </table>
+
+ <!-- table with tfoot element -->
+ <table id="table5.1">
+ <tfoot>
+ <tr>
+ <td>Cell1</td><td>cell2</td>
+ </tr>
+ </tfoot>
+ </table>
+
+ <!-- table with colgroup and col elements -->
+ <table id="table5.2">
+ <colgroup width="20"></colgroup>
+ <tr>
+ <td>Cell1</td><td>cell2</td>
+ </tr>
+ </table>
+ <table id="table5.3">
+ <col width="20">
+ <tr>
+ <td>Cell1</td><td>cell2</td>
+ </tr>
+ </table>
+
+ <!-- table with th element -->
+ <table id="table6">
+ <tr>
+ <th>Cell1</th><th>cell2</th>
+ </tr>
+ </table>
+
+ <!-- table with headers attribute -->
+ <table id="table6.2">
+ <tr>
+ <td headers="a">table6.2 cell</td>
+ </tr>
+ </table>
+
+ <!-- table with scope attribute -->
+ <table id="table6.2.2">
+ <tr>
+ <td scope="a">table6.2.2 cell</td>
+ </tr>
+ </table>
+
+ <!-- table with abbr attribute -->
+ <table id="table6.2.3">
+ <tr>
+ <td abbr="table6.2.3">table6.2.3 cell1</td>
+ </tr>
+ </table>
+
+ <!-- table with abbr element -->
+ <table id="table6.3">
+ <tr>
+ <td>table6.3 cell1</td>
+ <td><abbr>table6.3 cell2</abbr></td>
+ </tr>
+ </table>
+
+ <!-- table with abbr element having empty text node -->
+ <table id="table6.4">
+ <tr>
+ <td>
+ <abbr>abbr</abbr>
+ </td>
+ </tr>
+ </table>
+
+ <!-- table with abbr element and non-empty text node -->
+ <table id="table6.5">
+ <tr>
+ <td>
+ This is a really long text (<abbr>tiarlt</abbr>) inside layout table
+ </td>
+ </tr>
+ </table>
+
+ <!-- layout table with nested table -->
+ <table id="table9">
+ <tr>
+ <td><table><tr><td>Cell</td></tr></table></td>
+ </tr>
+ </table>
+
+ <!-- layout table with 1 column -->
+ <table id="table10">
+ <tr><td>Row1</td></tr>
+ <tr><td>Row2</td></tr>
+ </table>
+
+ <!-- layout table with 1 row and purposely many columns -->
+ <table id="table11">
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td><td>Col4</td><td>Col5</td></tr>
+ </table>
+
+ <!-- table with 5 columns -->
+ <table id="table12">
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td><td>Col4</td><td>Col5</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td><td>Col4</td><td>Col5</td></tr>
+ </table>
+
+ <!-- table with a bordered cell -->
+ <table id="table13" border="1" width="100%" bordercolor="#0000FF">
+ <tr>
+ <td bordercolor="#000000"> </td>
+ <td bordercolor="#000000"> </td>
+ <td bordercolor="#000000"> </td>
+ </tr>
+ <tr>
+ <td bordercolor="#000000"> </td>
+ <td bordercolor="#000000"> </td>
+ <td bordercolor="#000000"> </td>
+ </tr>
+ </table>
+
+ <!-- table with alternating row background colors -->
+ <table id="table14" width="100%">
+ <tr style="background-color: #0000FF;">
+ <td> </td>
+ <td> </td>
+ <td> </td>
+ </tr>
+ <tr style="background-color: #00FF00;">
+ <td> </td>
+ <td> </td>
+ <td> </td>
+ </tr>
+ </table>
+
+ <!-- table with 3 columns and 21 rows -->
+ <table id="table15" border="0">
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ </table>
+
+ <!-- layout table that has a 100% width -->
+ <table id="table16" width="100%">
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ </table>
+
+ <!-- layout table that has a 95% width in pixels -->
+ <table id="table17" width="98%">
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr>
+ </table>
+
+ <!-- layout table with less than 10 columns -->
+ <table id="table18">
+ <tr>
+ <td>Marco</td>
+ <td>Test</td>
+ <td>June 12</td>
+ </tr>
+ <tr>
+ <td>David</td>
+ <td>Another test</td>
+ <td>June 12</td>
+ </tr>
+ <tr>
+ <td>Alex</td>
+ <td>Third test</td>
+ <td>June 12</td>
+ </tr>
+ </table>
+
+ <!-- layout table with embedded iframe -->
+ <table id="table19">
+ <tr><td><iframe id="frame"></iframe></td><td> </td><td> </td></tr>
+ <tr><td> </td><td> </td><td> </td></tr>
+ <tr><td> </td><td> </td><td> </td></tr>
+ <tr><td> </td><td> </td><td> </td></tr>
+ </table>
+
+ <!-- tree grid, no layout table -->
+ <table id="table20" role="treegrid">
+ <tr role="treeitem"><td>Cell1</td><td>Cell2</td></tr>
+ </table>
+
+ <!-- layout table with nested data table containing data table elements -->
+ <table id="table21">
+ <tr>
+ <td>
+ <table>
+ <caption>table</caption>
+ <tr><td>Cell</td></tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ <table id="table21.2">
+ <tr>
+ <td>
+ <table>
+ <colgroup width="20"></colgroup>
+ <tr><th>Cell</th></tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ <table id="table21.3">
+ <tr>
+ <td>
+ <table>
+ <col width="20"></col>
+ <tr><th>Cell</th></tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ <table id="table21.4">
+ <tr>
+ <td>
+ <table>
+ <tr><th>Cell</th></tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ <table id="table21.5">
+ <tr>
+ <td>
+ <table>
+ <thead>
+ <tr><td>Cell</td></tr>
+ </thead>
+ </table>
+ </td>
+ </tr>
+ </table>
+ <table id="table21.6">
+ <tr>
+ <td>
+ <table>
+ <tfoot>
+ <tr><td>Cell</td></tr>
+ </tfoot>
+ </table>
+ </td>
+ </tr>
+ </table>
+
+ <!-- layout table with datatable="0" and tfoot element-->
+ <table id="table22" datatable="0">
+ <tfoot>
+ <tr>
+ <td>Cell1</td><td>cell2</td>
+ </tr>
+ </tfoot>
+ </table>
+
+ <table id="table23" border="1">
+ <tr role="tablist">
+ <td role="tab">Tab 1</td><td role="tab">Tab 2</td>
+ </tr>
+ <tr>
+ <td role="tabpanel" colspan="2">Hello</td>
+ </tr>
+ </table>
+
+ <table id="table24">
+ <tr></tr>
+ <tr>
+ <td style="width: 1px;"></td>
+ <td>
+ <table></table>
+ </td>
+ </tr>
+ </table>
+
+ <!-- display:block table -->
+ <table id="displayblock_table1" style="display:block">
+ <tr><td>Row1</td></tr>
+ <tr><td>Row2</td></tr>
+ </table>
+
+ <!-- MathML matrix -->
+ <math>
+ <mtable id="mtable1">
+ <mtr>
+ <mtd>
+ <mn>1</mn>
+ </mtd>
+ <mtd>
+ <mn>0</mn>
+ </mtd>
+ </mtr>
+ <mtr>
+ <mtd>
+ <mn>0</mn>
+ </mtd>
+ <mtd>
+ <mn>1</mn>
+ </mtd>
+ </mtr>
+ </mtable>
+ </math>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_mtable.html b/accessible/tests/mochitest/table/test_mtable.html
new file mode 100644
index 0000000000..aa79b3b98c
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_mtable.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>MathML table tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // 'Simple' table
+ var idxes = [
+ [0, 1],
+ [2, 3],
+ ];
+ testTableIndexes("simple", idxes);
+ var cellsArray = [
+ [kDataCell, kDataCell],
+ [kDataCell, kDataCell],
+ ];
+ var rowsArray = [ROLE_MATHML_TABLE_ROW, ROLE_MATHML_TABLE_ROW];
+ testTableStruct("simple", cellsArray, kNoColumnHeader,
+ "", "", kMathTable, rowsArray);
+
+ // 'Complex' table
+ idxes = [
+ [0, 0, 0],
+ [1, 1, 2],
+ [1, 1, 3],
+ ];
+ testTableIndexes("complex", idxes);
+ cellsArray = [
+ [kDataCell, kColSpanned, kColSpanned],
+ [kDataCell, kColSpanned, kDataCell],
+ [kRowSpanned, kSpanned, kDataCell],
+ ];
+ rowsArray = [
+ ROLE_MATHML_TABLE_ROW,
+ ROLE_MATHML_TABLE_ROW,
+ ROLE_MATHML_TABLE_ROW,
+ ];
+ testTableStruct("complex", cellsArray, kNoColumnHeader,
+ "", "", kMathTable, rowsArray);
+
+ // 'Simple' table with mlabeledtr
+ // At the moment we do not implement mlabeledtr but just hide the label
+ // with display: none. Thus we just test the role for now. See bug 689641.
+ idxes = [[0]];
+ testTableIndexes("simple_label", idxes);
+ cellsArray = [[kDataCell]];
+ rowsArray = [ROLE_MATHML_LABELED_ROW];
+ testTableStruct("simple_label", cellsArray, kNoColumnHeader,
+ "", "", kMathTable, rowsArray);
+
+ // Test that a non-table display style still generates the proper
+ // roles in the accessibility tree.
+ const table_tree = {
+ MATHML_TABLE: [{
+ MATHML_TABLE_ROW: [{ MATHML_CELL: [{ TEXT_LEAF: [] }] }]
+ }],
+ };
+ testAccessibleTree("table_with_display_block_mtd", table_tree);
+
+ // Equivalent to the above test but with display: block mtr.
+ testAccessibleTree("table_with_display_block_mtr", table_tree);
+
+ // Equivalent to the above test but with display: block mtable.
+ testAccessibleTree("table_with_display_block", table_tree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <math>
+ <mtable id="simple">
+ <mtr>
+ <mtd>
+ <mn>1</mn>
+ </mtd>
+ <mtd>
+ <mn>0</mn>
+ </mtd>
+ </mtr>
+ <mtr>
+ <mtd>
+ <mn>0</mn>
+ </mtd>
+ <mtd>
+ <mn>1</mn>
+ </mtd>
+ </mtr>
+ </mtable>
+
+ <mtable id="complex">
+ <mtr>
+ <mtd columnspan="3">
+ <mtext>1 x 3</mtext>
+ </mtd>
+ </mtr>
+ <mtr>
+ <mtd rowspan="2" columnspan="2">
+ <mtext>2 x 2</mtext>
+ </mtd>
+ <mtd>
+ <mtext>1 x 1</mtext>
+ </mtd>
+ </mtr>
+ <mtr>
+ <mtd>
+ <mtext>1 x 1</mtext>
+ </mtd>
+ </mtr>
+ </mtable>
+
+ <mtable id="simple_label">
+ <mlabeledtr>
+ <mtd><mtext>1</mtext></mtd>
+ <mtd><mtext>label</mtext></mtd>
+ </mlabeledtr>
+ </mtable>
+
+ <mtable id="table_with_display_block_mtd">
+ <mtr>
+ <mtd style="display: block">test</mtd>
+ </mtr>
+ </mtable>
+
+ <mtable id="table_with_display_block_mtr">
+ <mtr style="display: block">
+ <mtd>test</mtd>
+ </mtr>
+ </mtable>
+
+ <mtable id="table_with_display_block" style="display: block">
+ <mtr>
+ <mtd>test</mtd>
+ </mtr>
+ </mtable>
+
+ </math>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_sels_ariagrid.html b/accessible/tests/mochitest/table/test_sels_ariagrid.html
new file mode 100644
index 0000000000..6a4f065bd8
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_sels_ariagrid.html
@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=410052
+-->
+<head>
+ <title>nsIAccesible selection methods testing for ARIA grid</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA grid
+ var cellsArray =
+ [
+ [ true, true, false, true],
+ [ true, false, true, true],
+ [ true, false, false, true],
+ [ true, true, true, true],
+ [ true, true, true, true],
+ ];
+
+ testTableSelection("table", cellsArray);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // a bit strange ARIA grid
+ cellsArray =
+ [
+ [ false, false],
+ [ false, false],
+ ];
+
+ testTableSelection("grid2", cellsArray);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA grid (column and row headers)
+
+ cellsArray =
+ [
+ [ undefined, true, false],
+ [ undefined, true, false],
+ ];
+
+ testTableSelection("grid3", cellsArray);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="implement nsIAccessibleTable selection methods for ARIA grids"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=410052">Bug 410052</a>
+ <a target="_blank"
+ title="nsHTMLTableCellAccessible is used in dojo's ARIA grid"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=513848">Bug 513848</a>
+ <a target="_blank"
+ title="ARIA columnheader/rowheader shouldn't be selectable by default"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=888247">Bug 888247</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div role="grid" id="table">
+ <div role="row">
+ <span role="gridcell" aria-selected="true">cell1</span>
+ <span role="gridcell" aria-selected="true">cell2</span>
+ <span role="gridcell">cell3</span>
+ <span role="gridcell" aria-selected="true">cell4</span>
+ </div>
+ <div role="row">
+ <span role="gridcell" aria-selected="true">cell5</span>
+ <span role="gridcell">cell6</span>
+ <span role="gridcell" aria-selected="true">cell7</span>
+ <span role="gridcell" aria-selected="true">cell8</span>
+ </div>
+ <div role="row">
+ <span role="gridcell" aria-selected="true">cell9</span>
+ <span role="gridcell">cell10</span>
+ <span role="gridcell">cell11</span>
+ <span role="gridcell" aria-selected="true">cell12</span>
+ </div>
+ <div role="row" aria-selected="true">
+ <span role="gridcell">cell13</span>
+ <span role="gridcell">cell14</span>
+ <span role="gridcell">cell15</span>
+ <span role="gridcell">cell16</span>
+ </div>
+ <div role="row">
+ <span role="gridcell" aria-selected="true">cell17</span>
+ <span role="gridcell" aria-selected="true">cell18</span>
+ <span role="gridcell" aria-selected="true">cell19</span>
+ <span role="gridcell" aria-selected="true">cell20</span>
+ </div>
+ </div>
+
+ <div role="grid" id="grid2">
+ <div role="row">
+ <table role="presentation">
+ <tr>
+ <td role="columnheader" aria-selected="false">header1</td>
+ <td role="columnheader" aria-selected="false">header2</td>
+ </tr>
+ </table>
+ </div>
+ <div role="row">
+ <table role="presentation">
+ <tr>
+ <td role="gridcell">cell1</td>
+ <td role="gridcell" tabindex="-1">cell2</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+
+ <div role="grid" id="grid3">
+ <div role="row">
+ <div role="columnheader" id="colheader_default">col header1</div>
+ <div role="columnheader" id="colheader_selected" aria-selected="true">col header2</div>
+ <div role="columnheader" id="colheader_notselected" aria-selected="false">col header3</div>
+ </div>
+ <div role="row">
+ <div role="rowheader" id="rowheader_default">row header1</div>
+ <div role="rowheader" id="rowheader_selected" aria-selected="true">row header2</div>
+ <div role="rowheader" id="rowheader_notselected" aria-selected="false">row header3</div>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_sels_table.html b/accessible/tests/mochitest/table/test_sels_table.html
new file mode 100644
index 0000000000..b0d53ab42c
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_sels_table.html
@@ -0,0 +1,155 @@
+<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en">
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <title>nsIAccesible selection methods testing for HTML table</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+
+ <script type="text/javascript">
+
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // table
+
+ var cellsArray =
+ [
+ [false, false, false, kColSpanned, false, false, false, false],
+ [false, false, false, false, false, false, false, kRowSpanned],
+ [false, false, kColSpanned, false, false, false, false, kRowSpanned],
+ [false, kRowSpanned, kSpanned, false, false, kRowSpanned, false, kRowSpanned],
+ ];
+
+ testTableSelection("table", cellsArray);
+
+ var accTable = getAccessible("table", [nsIAccessibleTable]);
+ ok(!accTable.isProbablyForLayout(), "table is not for layout");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // table instane
+
+ cellsArray =
+ [
+ [false, false, false, -1, -1],
+ [false, false, false, -1, -1],
+ [false, false, kColSpanned, kColSpanned, -1],
+ [kRowSpanned, false, false, -1, -1],
+ [kRowSpanned, false, kRowSpanned, false, false],
+ ];
+
+ testTableSelection("tableinsane", cellsArray);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+ </head>
+ <body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=410052"
+ title="Fix our nsHTMLAccessibleTable class so GetIndexAt and GetRowAtIndex and GetColumnAtIndex behave consistently">
+ Mozilla Bug 410052
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=501635"
+ title="nsHTMLTableAccessible::GetSelectedCells contains index duplicates for spanned rows or columns">
+ Mozilla Bug 501635
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=501659"
+ title="HTML table's isRowSelected/isColumnSelected shouldn't fail if row or column has cell holes">
+ Mozilla Bug 501659
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- Test Table -->
+ <br><br><b> Testing Table:</b><br><br>
+ <center>
+ <table cellpadding="2" cellspacing="2" border="1" width="50%" id="table">
+ <tbody>
+ <tr>
+ <td><br></td>
+ <td><br></td>
+ <td rowspan="1" colspan="2"><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td rowspan="4" colspan="1"><br></td>
+ </tr>
+ <tr>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ </tr>
+ <tr>
+ <td><br></td>
+ <td rowspan="2" colspan="2">c1</td>
+ <td><br></td>
+ <td><br></td>
+ <td rowspan="2" colspan="1"><br></td>
+ <td><br></td>
+ </tr>
+ <tr>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table border="1" id="tableinsane">
+ <thead>
+ <tr>
+ <th>col1</th>
+ <th>col2</th>
+ <th>col3</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>1</td>
+ <td>2</td>
+ <td>3</td>
+ </tr>
+ <tr>
+ <td rowspan="3">4</td>
+ <td colspan="4">5</td>
+ </tr>
+ <tr>
+ <td>6</td>
+ <td rowspan="2">7</td>
+ </tr>
+ <tr>
+ <td>8</td>
+ <td>9</td>
+ <td>10</td>
+ </tr>
+ </tbody>
+ </table>
+
+ </center>
+ </body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_sels_tree.xhtml b/accessible/tests/mochitest/table/test_sels_tree.xhtml
new file mode 100644
index 0000000000..7b93d59d47
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_sels_tree.xhtml
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible Table selection tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../table.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "debug";
+
+ function doTest()
+ {
+ var cellsArray =
+ [
+ [false, false],
+ [false, false],
+ [false, false]
+ ];
+
+ testTableSelection("tree", cellsArray);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yXULTreeLoadEvent(doTest, "tree", new nsTableTreeView(3));
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727"
+ title="Reorganize implementation of XUL tree accessibility">
+ Mozilla Bug 503727
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="debug"/>
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col" flex="1" primary="true" label="column"/>
+ <treecol id="scol" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren id="treechildren"/>
+ </tree>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/table/test_struct_ariagrid.html b/accessible/tests/mochitest/table/test_struct_ariagrid.html
new file mode 100644
index 0000000000..92821e19c2
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_struct_ariagrid.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Table accessible tree and table interface tests for ARIA grid</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // Pure ARIA grid
+ var cellsArray = [
+ [kColHeaderCell, kColHeaderCell, kColHeaderCell],
+ [kRowHeaderCell, kDataCell, kDataCell],
+ [kRowHeaderCell, kDataCell, kDataCell],
+ ];
+
+ testTableStruct("table", cellsArray);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML table based ARIA grid
+ cellsArray = [
+ [kColHeaderCell, kColHeaderCell, kColHeaderCell],
+ [kDataCell, kDataCell, kDataCell],
+ [kDataCell, kDataCell, kDataCell],
+ ];
+
+ testTableStruct("grid", cellsArray);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA grid with HTML table elements
+ cellsArray = [
+ [kColHeaderCell, kColHeaderCell],
+ [kDataCell, kDataCell],
+ ];
+
+ testTableStruct("grid2", cellsArray);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA grid of wrong markup
+ cellsArray = [ ];
+ testTableStruct("grid3", cellsArray);
+
+ cellsArray = [ [] ];
+ testTableStruct("grid4", cellsArray);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA table with tr inside a shadow root (bug 1698097).
+ testTableStruct("tableShadow", [ [ kDataCell ] ]);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ title="ARIA grid based on HTML table"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=491683">Mozilla Bug 491683</a>
+ <a target="_blank"
+ title="implement IAccessibleTable2"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=512424">Mozilla Bug 512424</a>
+ <a target="_blank"
+ title="nsHTMLTableCellAccessible is used in dojo's ARIA grid"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=513848">Mozilla Bug 513848</a>
+ <a target="_blank"
+ title="Crash [@ AccIterator::GetNext()]"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=675861">Mozilla Bug 675861</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- Not usual markup to avoid text accessible between cell accessibles -->
+ <div id="table" role="grid">
+ <div role="row"><span
+ id="table_ch_1" role="columnheader">col_1</span><span
+ id="table_ch_2" role="columnheader">col_2</span><span
+ id="table_ch_3" role="columnheader">col_3</span></div>
+ <div role="row"><span
+ id="table_rh_1" role="rowheader">row_1</span><span
+ id="table_dc_1" role="gridcell">cell1</span><span
+ id="table_dc_2" role="gridcell">cell2</span></div>
+ <div role="row"><span
+ id="table_rh_2" role="rowheader">row_2</span><span
+ id="table_dc_3" role="gridcell">cell3</span><span
+ id="table_dc_4" role="gridcell">cell4</span></div>
+ </div>
+
+ <table role="grid" id="grid" border="1" cellpadding="10" cellspacing="0">
+ <thead>
+ <tr role="row">
+ <th role="columnheader">subject</td>
+ <th role="columnheader">sender</th>
+ <th role="columnheader">date</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr role="row">
+ <td role="gridcell" tabindex="0">about everything</td>
+ <td role="gridcell">president</td>
+ <td role="gridcell">today</td>
+ </tr>
+ <tr role="row">
+ <td role="gridcell">new bugs</td>
+ <td role="gridcell">mozilla team</td>
+ <td role="gridcell">today</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <!-- ARIA grid containing presentational HTML:table with HTML:td used as ARIA
+ grid cells (focusable and not focusable cells) -->
+ <div role="grid" id="grid2">
+ <div role="row">
+ <table role="presentation">
+ <tr>
+ <td role="columnheader">header1</td>
+ <td role="columnheader">header2</td>
+ </tr>
+ </table>
+ </div>
+ <div role="row">
+ <table role="presentation">
+ <tr>
+ <td role="gridcell">cell1</td>
+ <td role="gridcell" tabindex="-1">cell2</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+
+ <!-- Wrong markup ARIA grid -->
+ <div role="grid" id="grid3"></div>
+ <div role="grid" id="grid4"><div role="row"></div></div>
+
+ <div id="tableShadow" role="table"></div>
+ <script>
+ let host = document.getElementById("tableShadow");
+ let shadow = host.attachShadow({mode: "open"});
+ let tr = document.createElement("tr");
+ shadow.append(tr);
+ tr.setAttribute("role", "row");
+ let td = document.createElement("td");
+ tr.append(td);
+ td.textContent = "test";
+ </script>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_struct_ariatreegrid.html b/accessible/tests/mochitest/table/test_struct_ariatreegrid.html
new file mode 100644
index 0000000000..8d36a2b350
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_struct_ariatreegrid.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Table accessible tree and table interface tests for ARIA tree grid</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML based ARIA tree grid
+
+ var cellsArray = [
+ [kColHeaderCell, kColHeaderCell, kColHeaderCell],
+ [kDataCell, kDataCell, kDataCell],
+ [kDataCell, kDataCell, kDataCell],
+ ];
+
+ testTableStruct("treegrid", cellsArray, kNoColumnHeader, "", "",
+ kTreeTable);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ title="ARIA treegrid role on HTML:table makes thead/tbody accessible"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=491683">Mozilla Bug 516133</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <table role="treegrid" id="treegrid"
+ border="1" cellpadding="10" cellspacing="0">
+ <thead>
+ <tr role="row">
+ <th role="columnheader">subject</td>
+ <th role="columnheader">sender</th>
+ <th role="columnheader">date</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr role="row">
+ <td role="gridcell">about everything</td>
+ <td role="gridcell">president</td>
+ <td role="gridcell">today</td>
+ </tr>
+ <tr role="row">
+ <td role="gridcell">new bugs</td>
+ <td role="gridcell">mozilla team</td>
+ <td role="gridcell">today</td>
+ </tr>
+ </tbody>
+ </table>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_struct_table.html b/accessible/tests/mochitest/table/test_struct_table.html
new file mode 100644
index 0000000000..46bad05c62
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_struct_table.html
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Table accessible tree and table interface tests for HTML tables</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // column headers from thead and tfoot
+
+ cellsArray = [
+ [kColHeaderCell, kColHeaderCell, kColSpanned],
+ [kRowSpanned, kColHeaderCell, kColHeaderCell],
+ [kDataCell, kDataCell, kDataCell],
+ [kColHeaderCell, kColHeaderCell, kColHeaderCell],
+ ];
+
+ testTableStruct("table1", cellsArray);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // row and column headers from thead and @scope
+
+ var cellsArray = [
+ [kColHeaderCell, kColHeaderCell, kColHeaderCell],
+ [kRowHeaderCell, kDataCell, kDataCell],
+ [kRowHeaderCell, kDataCell, kDataCell],
+ ];
+
+ testTableStruct("table2", cellsArray);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // caption and @summary
+
+ cellsArray = [
+ [kColHeaderCell, kColHeaderCell, kColHeaderCell, kColHeaderCell],
+ [kRowHeaderCell, kDataCell, kDataCell, kDataCell],
+ [kRowHeaderCell, kDataCell, kDataCell, kDataCell],
+ ];
+
+ testTableStruct("table3", cellsArray, kNoColumnHeader,
+ "Test Table",
+ "this is a test table for nsIAccessibleTable");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // row and column spans
+
+ cellsArray = [
+ [kDataCell, kDataCell, kDataCell, kColSpanned, kDataCell, kDataCell, kDataCell, kDataCell],
+ [kDataCell, kDataCell, kDataCell, kDataCell, kDataCell, kDataCell, kDataCell, kRowSpanned],
+ [kDataCell, kDataCell, kColSpanned, kDataCell, kDataCell, kDataCell, kDataCell, kRowSpanned],
+ [kDataCell, kRowSpanned, kSpanned, kDataCell, kDataCell, kRowSpanned, kDataCell, kRowSpanned],
+ ];
+
+ testTableStruct("table4", cellsArray);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Table with a cell that has display: block; style
+
+ cellsArray = [
+ [kRowHeaderCell, kDataCell],
+ ];
+
+ testTableStruct("table5", cellsArray);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Fix our nsHTMLAccessibleTable class so GetIndexAt and GetRowAtIndex and GetColumnAtIndex behave consistently"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=410052">Mozilla Bug 410052</a>
+ <a target="_blank"
+ title="GetCellDataAt callers that expect an error if no cell is found are wrong"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=417912">Mozilla Bug 417912</a>
+ <a target="_blank"
+ title="create accessibles for HTML tr"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=493695">Mozilla Bug 493695</a>
+ <a target="_blank"
+ title="implement IAccessibleTable2"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=512424">Mozilla Bug 512424</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <table id="table1">
+ <thead>
+ <tr>
+ <th rowspan="2">col1</th><th colspan="2">col2</th>
+ </tr>
+ <tr>
+ <th>col2sub1</th><th>col2sub2</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>cell1</td><td>cell2</td><td>cell3</td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr>
+ <th>col1</th><th>col2</th><th>col3</th>
+ </tr>
+ </tfoot>
+ </table>
+
+ <table id="table2">
+ <thead>
+ <tr>
+ <th id="table1_0">col1</th>
+ <th id="table1_1">col2</th>
+ <td id="table1_2" scope="col">col3</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th id="table1_3">row1</th>
+ <td id="table1_4">cell1</td>
+ <td id="table1_5">cell2</td>
+ </tr>
+ <tr>
+ <td id="table1_6" scope="row">row2</td>
+ <td id="table1_7">cell3</td>
+ <td id="table1_8">cell4</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table id="table3" border="1"
+ summary="this is a test table for nsIAccessibleTable">
+ <caption>Test Table</caption>
+ <thead>
+ <tr>
+ <th></th>
+ <th>columnHeader_1</th>
+ <th id ="col2a">columnHeader_2</th>
+ <th>columnHeader_3</th>
+ </tr>
+ </thead>
+ <tr>
+ <th id="row2a">rowHeader_1</th>
+ <td id="row2b">row1_column1</td>
+ <td id ="col2b">row1_column2</td>
+ <td id="row2c">row1_column3</td>
+ </tr>
+ <tr>
+ <th>rowHeader_2</th>
+ <td>row2_column1</td>
+ <td id ="col2c">row2_column2</td>
+ <td>row2_column3</td>
+ </tr>
+ </table>
+
+ <table id="table4" cellpadding="2" cellspacing="2" border="1" width="50%">
+ <tbody>
+ <tr>
+ <td><br></td>
+ <td><br></td>
+ <td rowspan="1" colspan="2"><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td rowspan="4" colspan="1"><br></td>
+ </tr>
+ <tr>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ </tr>
+ <tr>
+ <td><br></td>
+ <td rowspan="2" colspan="2">c1</td>
+ <td><br></td>
+ <td><br></td>
+ <td rowspan="2" colspan="1"><br></td>
+ <td><br></td>
+ </tr>
+ <tr>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ <td><br></td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table id="table5">
+ <tr>
+ <th>a</th>
+ <td style="display: block;">b</td>
+ </tr>
+ </table>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_struct_tree.xhtml b/accessible/tests/mochitest/table/test_struct_tree.xhtml
new file mode 100644
index 0000000000..6710bd2e8b
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_struct_tree.xhtml
@@ -0,0 +1,73 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Table accessible tree and table interface tests for XUL trees">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../table.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "debug";
+
+ function doTest()
+ {
+ var cellsArray = [
+ [kDataCell, kDataCell],
+ [kDataCell, kDataCell],
+ [kDataCell, kDataCell]
+ ];
+
+ testTableStruct("table", cellsArray, kTreeColumnHeader);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yXULTreeLoadEvent(doTest, "table", new nsTableTreeView(3));
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=512424"
+ title="implement IAccessibleTable2">
+ Mozilla Bug 512424
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="debug"/>
+ <tree id="table" flex="1">
+ <treecols>
+ <treecol id="col" flex="1" label="column"/>
+ <treecol id="scol" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren id="treechildren"/>
+ </tree>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/table/test_table_1.html b/accessible/tests/mochitest/table/test_table_1.html
new file mode 100644
index 0000000000..b1331a5cc3
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_table_1.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en">
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+
+ <script type="application/javascript">
+
+function doTest() {
+ var accTable = getAccessible("table", [nsIAccessibleTable]);
+
+ var s = window.getSelection();
+ if (s.rangeCount > 0)
+ s.removeAllRanges();
+
+ var cell = getNode("col2b");
+ var range = document.createRange();
+ range.selectNode(cell);
+ s.addRange(range);
+
+ is(accTable.selectedCellCount, 1, "only one cell selected");
+ cell = getNode("col2a");
+ range = document.createRange();
+ range.selectNode(cell);
+ s.addRange(range);
+ cell = getNode("col2c");
+ range = document.createRange();
+ range.selectNode(cell);
+ s.addRange(range);
+ is(accTable.selectedColumnCount, 1, "only one column selected");
+
+ cell = getNode("row2a");
+ range = document.createRange();
+ range.selectNode(cell);
+ s.addRange(range);
+ cell = getNode("row2b");
+ range = document.createRange();
+ range.selectNode(cell);
+ s.addRange(range);
+ range = document.createRange();
+ cell = getNode("row2c");
+ range.selectNode(cell);
+ s.addRange(range);
+
+ is(accTable.selectedRowCount, 1, "no cells selected");
+
+ // These shouldn't throw.
+ try {
+ accTable.getColumnDescription(1);
+ accTable.getRowDescription(1);
+ } catch (ex) {
+ ok(false, "getColumnDescription/getRowDescription shouldn't throw.");
+ }
+ SimpleTest.finish();
+}
+SimpleTest.waitForExplicitFinish();
+addA11yLoadEvent(doTest);
+ </script>
+ </head>
+ <body >
+
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=410052">Mozilla Bug 410052</a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=760878"
+ title="decomtaminate Get Row / Column Description() on accessible tables">
+ Mozilla Bug 760878
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- Test Table -->
+ <br><br><b> Testing Table:</b><br><br>
+ <center>
+ <table id="table" border="1"
+ summary="this is a test table for nsIAccessibleTable" >
+ <caption>Test Table</caption>
+ <thead>
+ <tr>
+ <th></th>
+ <th>columnHeader_1</th>
+ <th id ="col2a">columnHeader_2</th>
+ <th>columnHeader_3</th>
+ </tr>
+ </thead>
+ <tr>
+ <th id="row2a">rowHeader_1</th>
+ <td id="row2b">row1_column1</td>
+ <td id ="col2b">row1_column2</td>
+ <td id="row2c">row1_column3</td>
+ </tr>
+ <tr>
+ <th>rowHeader_2</th>
+ <td>row2_column1</td>
+ <td id ="col2c">row2_column2</td>
+ <td>row2_column3</td>
+ </tr>
+ </table>
+ </center>
+ </body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_table_2.html b/accessible/tests/mochitest/table/test_table_2.html
new file mode 100644
index 0000000000..6bd7c56b37
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_table_2.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en">
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="text/javascript">
+
+function doTest() {
+ // Test table with role=alert.
+ var tableInterfaceExposed = true;
+ var accTable3 = getAccessible("table3", [nsIAccessibleTable], null, DONOTFAIL_IF_NO_INTERFACE);
+ if (!accTable3)
+ tableInterfaceExposed = false;
+ ok(tableInterfaceExposed, "table interface is not exposed");
+
+ if (tableInterfaceExposed) {
+ testRole(accTable3, ROLE_ALERT);
+
+ is(accTable3.getCellAt(0, 0).firstChild.name, "cell0", "wrong cell");
+ is(accTable3.getCellAt(0, 1).firstChild.name, "cell1", "wrong cell");
+ }
+
+ // Test table with role=log and aria property in tr. We create accessible for
+ // tr in this case.
+ tableInterfaceExposed = true;
+ var accTable4 = getAccessible("table4", [nsIAccessibleTable], null, DONOTFAIL_IF_NO_INTERFACE);
+ if (!accTable4)
+ tableInterfaceExposed = false;
+ ok(tableInterfaceExposed, "table interface is not exposed");
+
+ if (tableInterfaceExposed) {
+ let accNotCreated = (!isAccessible("tr"));
+ ok(!accNotCreated, "missed tr accessible");
+
+ testRole(accTable4, ROLE_TABLE);
+
+ is(accTable4.getCellAt(0, 0).firstChild.name, "cell0", "wrong cell");
+ is(accTable4.getCellAt(0, 1).firstChild.name, "cell1", "wrong cell");
+ is(accTable4.getCellAt(1, 0).firstChild.name, "cell2", "wrong cell");
+ is(accTable4.getCellAt(1, 1).firstChild.name, "cell3", "wrong cell");
+ }
+
+ SimpleTest.finish();
+}
+SimpleTest.waitForExplicitFinish();
+addA11yLoadEvent(doTest);
+ </script>
+ </head>
+
+ <body >
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=419811">Mozilla Bug 419811</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- Test Table -->
+ <br><br><b> Testing Table:</b><br><br>
+ <center>
+ <table id="table3" border="1" role="alert">
+ <tr>
+ <td>cell0</td>
+ <td>cell1</td>
+ </tr>
+ </table>
+
+ <table id="table4" border="1" role="log">
+ <tr aria-live="polite" id="tr">
+ <td>cell0</td>
+ <td>cell1</td>
+ </tr>
+ <tr>
+ <td>cell2</td>
+ <td>cell3</td>
+ </tr>
+ </table>
+
+ </center>
+ </body>
+</html>
diff --git a/accessible/tests/mochitest/table/test_table_mutation.html b/accessible/tests/mochitest/table/test_table_mutation.html
new file mode 100644
index 0000000000..671e627244
--- /dev/null
+++ b/accessible/tests/mochitest/table/test_table_mutation.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en">
+<html>
+<head>
+ <title>Table mutation</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+
+ async function doTest() {
+ let headers = [
+ {
+ cell: "t1r1c1",
+ columnHeaderCells: [],
+ rowHeaderCells: [],
+ },
+ // t1r2 is hidden
+ {
+ cell: "t1r3c1",
+ columnHeaderCells: ["t1r1c1"],
+ rowHeaderCells: [],
+ },
+ ];
+ testHeaderCells(headers);
+
+ info("Remove row");
+ let reordered = waitForEvent(EVENT_REORDER, "t1");
+ getNode("t1r1").hidden = true;
+ await reordered;
+ headers = [
+ // t1r1 and t1r2 are hidden
+ {
+ cell: "t1r3c1",
+ columnHeaderCells: [],
+ rowHeaderCells: [],
+ },
+ ];
+ testHeaderCells(headers);
+
+ info("Add rows");
+ reordered = waitForEvent(EVENT_REORDER, "t1");
+ getNode("t1r1").hidden = false;
+ getNode("t1r2").hidden = false;
+ await reordered;
+ headers = [
+ {
+ cell: "t1r1c1",
+ columnHeaderCells: [],
+ rowHeaderCells: [],
+ },
+ {
+ cell: "t1r2c1",
+ columnHeaderCells: ["t1r1c1"],
+ rowHeaderCells: [],
+ },
+ {
+ cell: "t1r3c1",
+ columnHeaderCells: ["t1r2c1", "t1r1c1"],
+ rowHeaderCells: [],
+ },
+ ];
+ testHeaderCells(headers);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <table id="t1">
+ <tr id="t1r1">
+ <th id="t1r1c1"></th>
+ </tr>
+ <tr id="t1r2" hidden>
+ <th id="t1r2c1"></th>
+ </tr>
+ <tr id="t1r3">
+ <td id="t1r3c1"></td>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/test_OuterDocAccessible.html b/accessible/tests/mochitest/test_OuterDocAccessible.html
new file mode 100644
index 0000000000..b7a719aba5
--- /dev/null
+++ b/accessible/tests/mochitest/test_OuterDocAccessible.html
@@ -0,0 +1,87 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=441519
+-->
+<head>
+ <title>nsOuterDocAccessible chrome tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="common.js"></script>
+ <script type="application/javascript"
+ src="states.js"></script>
+ <script type="application/javascript"
+ src="role.js"></script>
+
+ <script type="application/javascript">
+ // needed error return value
+ const ns_error_invalid_arg = Cr.NS_ERROR_INVALID_ARG;
+
+ function doTest() {
+ // Get accessible for body tag.
+ var docAcc = getAccessible(document);
+
+ if (docAcc) {
+ var outerDocAcc = getAccessible(docAcc.parent);
+
+ if (outerDocAcc) {
+ testRole(outerDocAcc, ROLE_INTERNAL_FRAME);
+
+ // check if it is focusable.
+ testStates(outerDocAcc, STATE_FOCUSABLE, 0);
+
+ // see bug 428954: No name wanted for internal frame
+ is(outerDocAcc.name, null, "Wrong name for internal frame!");
+
+ // see bug 440770, no actions wanted on outer doc
+ is(outerDocAcc.actionCount, 0,
+ "Wrong number of actions for internal frame!");
+
+ try {
+ outerDocAcc.getActionName(0);
+ throw new Error("No exception thrown for actionName!");
+ } catch (e) {
+ is(e.result, ns_error_invalid_arg,
+ "Wrong return value for actionName call!");
+ }
+
+ try {
+ outerDocAcc.getActionDescription(0);
+ throw new Error("No exception thrown for actionDescription!");
+ } catch (e) {
+ is(e.result, ns_error_invalid_arg,
+ "Wrong return value for actionDescription call!");
+ }
+
+ try {
+ outerDocAcc.doAction(0);
+ throw new Error("No exception thrown for doAction!");
+ } catch (e) {
+ is(e.result, ns_error_invalid_arg,
+ "Wrong return value for doAction call!");
+ }
+ }
+ }
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=441519"
+ title="nsOuterDocAccessible chrome tests">
+ Mozilla Bug 441519
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/test_aria_token_attrs.html b/accessible/tests/mochitest/test_aria_token_attrs.html
new file mode 100644
index 0000000000..ad0bc25970
--- /dev/null
+++ b/accessible/tests/mochitest/test_aria_token_attrs.html
@@ -0,0 +1,417 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=452388
+-->
+<head>
+ <title>An NMTOKEN based ARIA property is undefined if the ARIA attribute is not present, or is set to "" or "undefined"</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="common.js"></script>
+ <script type="application/javascript"
+ src="role.js"></script>
+ <script type="application/javascript"
+ src="states.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // test aria-pressed state mapping to roles PUSHBUTTON vs TOGGLEBUTTON
+ testRole("button_pressed_true", ROLE_TOGGLE_BUTTON);
+ testRole("button_pressed_false", ROLE_TOGGLE_BUTTON);
+ testRole("button_pressed_empty", ROLE_PUSHBUTTON);
+ testRole("button_pressed_undefined", ROLE_PUSHBUTTON);
+ testRole("button_pressed_absent", ROLE_PUSHBUTTON);
+
+ // test button aria-pressed states
+ testStates("button_pressed_true", STATE_PRESSED, 0, STATE_CHECKABLE);
+ testStates("button_pressed_false", 0, 0, STATE_CHECKABLE | STATE_PRESSED);
+ testStates("button_pressed_empty", 0, 0, STATE_PRESSED | STATE_CHECKABLE);
+ testStates("button_pressed_undefined", 0, 0, STATE_PRESSED | STATE_CHECKABLE);
+ testStates("button_pressed_absent", 0, 0, STATE_PRESSED | STATE_CHECKABLE);
+
+ // test (checkbox) checkable and checked states
+ testStates("checkbox_checked_true", (STATE_CHECKABLE | STATE_CHECKED));
+ testStates("checkbox_checked_mixed", (STATE_CHECKABLE | STATE_MIXED), 0, STATE_CHECKED);
+ testStates("checkbox_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("checkbox_checked_empty", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("checkbox_checked_undefined", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("checkbox_checked_absent", STATE_CHECKABLE, 0, STATE_CHECKED);
+
+ // test native checkbox checked state and aria-checked state (if conflict, native wins)
+ testStates("native_checkbox_nativechecked_ariatrue", (STATE_CHECKABLE | STATE_CHECKED));
+ testStates("native_checkbox_nativechecked_ariafalse", (STATE_CHECKABLE | STATE_CHECKED));
+ testStates("native_checkbox_nativechecked_ariaempty", (STATE_CHECKABLE | STATE_CHECKED));
+ testStates("native_checkbox_nativechecked_ariaundefined", (STATE_CHECKABLE | STATE_CHECKED));
+ testStates("native_checkbox_nativechecked_ariaabsent", (STATE_CHECKABLE | STATE_CHECKED));
+
+ testStates("native_checkbox_nativeunchecked_ariatrue", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("native_checkbox_nativeunchecked_ariafalse", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("native_checkbox_nativeunchecked_ariaempty", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("native_checkbox_nativeunchecked_ariaundefined", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("native_checkbox_nativeunchecked_ariaabsent", STATE_CHECKABLE, 0, STATE_CHECKED);
+
+ // test (checkbox) readonly states
+ testStates("checkbox_readonly_true", STATE_READONLY);
+ testStates("checkbox_readonly_false", 0, 0, STATE_READONLY);
+ testStates("checkbox_readonly_empty", 0, 0, STATE_READONLY);
+ testStates("checkbox_readonly_undefined", 0, 0, STATE_READONLY);
+ testStates("checkbox_readonly_absent", 0, 0, STATE_READONLY);
+
+ // test (checkbox) required states
+ testStates("checkbox_required_true", STATE_REQUIRED);
+ testStates("checkbox_required_false", 0, 0, STATE_REQUIRED);
+ testStates("checkbox_required_empty", 0, 0, STATE_REQUIRED);
+ testStates("checkbox_required_undefined", 0, 0, STATE_REQUIRED);
+ testStates("checkbox_required_absent", 0, 0, STATE_REQUIRED);
+
+ // test (checkbox) invalid states
+ testStates("checkbox_invalid_true", STATE_INVALID);
+ testStates("checkbox_invalid_false", 0, 0, STATE_INVALID);
+ testStates("checkbox_invalid_empty", 0, 0, STATE_INVALID);
+ testStates("checkbox_invalid_undefined", 0, 0, STATE_INVALID);
+ testStates("checkbox_invalid_absent", 0, 0, STATE_INVALID);
+
+ // test (checkbox) disabled states
+ testStates("checkbox_disabled_true", STATE_UNAVAILABLE);
+ testStates("checkbox_disabled_false", 0, 0, STATE_UNAVAILABLE);
+ testStates("checkbox_disabled_empty", 0, 0, STATE_UNAVAILABLE);
+ testStates("checkbox_disabled_undefined", 0, 0, STATE_UNAVAILABLE);
+ testStates("checkbox_disabled_absent", 0, 0, STATE_UNAVAILABLE);
+
+ // test (listbox) multiselectable states
+ testStates("listbox_multiselectable_true", STATE_MULTISELECTABLE | STATE_EXTSELECTABLE);
+ testStates("listbox_multiselectable_false", 0, 0, STATE_MULTISELECTABLE | STATE_EXTSELECTABLE);
+ testStates("listbox_multiselectable_empty", 0, 0, STATE_MULTISELECTABLE | STATE_EXTSELECTABLE);
+ testStates("listbox_multiselectable_undefined", 0, 0, STATE_MULTISELECTABLE | STATE_EXTSELECTABLE);
+ testStates("listbox_multiselectable_absent", 0, 0, STATE_MULTISELECTABLE | STATE_EXTSELECTABLE);
+
+ // test (option) checkable and checked states
+ testStates("option_checked_true", (STATE_CHECKABLE | STATE_CHECKED));
+ testStates("option_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("option_checked_empty", 0, 0, STATE_CHECKABLE | STATE_CHECKED);
+ testStates("option_checked_undefined", 0, 0, STATE_CHECKABLE | STATE_CHECKED);
+ testStates("option_checked_absent", 0, 0, STATE_CHECKABLE | STATE_CHECKED);
+
+ // test (menuitem) checkable and checked states, which are unsupported on this role
+ testStates("menuitem_checked_true", 0, 0, (STATE_CHECKABLE | STATE_CHECKED));
+ testStates("menuitem_checked_mixed", 0, 0, (STATE_CHECKABLE | STATE_CHECKED | STATE_MIXED));
+ testStates("menuitem_checked_false", 0, 0, (STATE_CHECKABLE | STATE_CHECKED));
+ testStates("menuitem_checked_empty", 0, 0, (STATE_CHECKABLE | STATE_CHECKED));
+ testStates("menuitem_checked_undefined", 0, 0, (STATE_CHECKABLE | STATE_CHECKED));
+ testStates("menuitem_checked_absent", 0, 0, (STATE_CHECKABLE | STATE_CHECKED));
+
+ // test (menuitemcheckbox) checkable and checked states
+ testStates("menuitemcheckbox_checked_true", (STATE_CHECKABLE | STATE_CHECKED));
+ testStates("menuitemcheckbox_checked_mixed", (STATE_CHECKABLE | STATE_MIXED), 0, STATE_CHECKED);
+ testStates("menuitemcheckbox_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("menuitemcheckbox_checked_empty", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("menuitemcheckbox_checked_undefined", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("menuitemcheckbox_checked_absent", STATE_CHECKABLE, 0, STATE_CHECKED);
+
+ // test (menuitemcheckbox) readonly states
+ testStates("menuitemcheckbox_readonly_true", STATE_READONLY);
+ testStates("menuitemcheckbox_readonly_false", 0, 0, STATE_READONLY);
+ testStates("menuitemcheckbox_readonly_empty", 0, 0, STATE_READONLY);
+ testStates("menuitemcheckbox_readonly_undefined", 0, 0, STATE_READONLY);
+ testStates("menuitemcheckbox_readonly_absent", 0, 0, STATE_READONLY);
+
+ // test (menuitemradio) checkable and checked states
+ testStates("menuitemradio_checked_true", (STATE_CHECKABLE | STATE_CHECKED));
+ testStates("menuitemradio_checked_mixed", STATE_CHECKABLE, 0, (STATE_MIXED | STATE_CHECKED));
+ testStates("menuitemradio_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("menuitemradio_checked_empty", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("menuitemradio_checked_undefined", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("menuitemradio_checked_absent", STATE_CHECKABLE, 0, STATE_CHECKED);
+
+ // test (menuitemradio) readonly states
+ testStates("menuitemradio_readonly_true", STATE_READONLY);
+ testStates("menuitemradio_readonly_false", 0, 0, STATE_READONLY);
+ testStates("menuitemradio_readonly_empty", 0, 0, STATE_READONLY);
+ testStates("menuitemradio_readonly_undefined", 0, 0, STATE_READONLY);
+ testStates("menuitemradio_readonly_absent", 0, 0, STATE_READONLY);
+
+ // test (radio) checkable and checked states
+ testStates("radio_checked_true", (STATE_CHECKABLE | STATE_CHECKED));
+ testStates("radio_checked_mixed", STATE_CHECKABLE, 0, (STATE_MIXED | STATE_CHECKED));
+ testStates("radio_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("radio_checked_empty", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("radio_checked_undefined", STATE_CHECKABLE, 0, STATE_CHECKED);
+ testStates("radio_checked_absent", STATE_CHECKABLE, 0, STATE_CHECKED);
+
+ // test (radiogroup) readonly states
+ testStates("radiogroup_readonly_true", STATE_READONLY);
+ testStates("radiogroup_readonly_false", 0, 0, STATE_READONLY);
+ testStates("radiogroup_readonly_empty", 0, 0, STATE_READONLY);
+ testStates("radiogroup_readonly_undefined", 0, 0, STATE_READONLY);
+ testStates("radiogroup_readonly_absent", 0, 0, STATE_READONLY);
+
+ // test (switch) readonly states
+ testStates("switch_readonly_true", STATE_READONLY);
+ testStates("switch_readonly_false", 0, 0, STATE_READONLY);
+ testStates("switch_readonly_empty", 0, 0, STATE_READONLY);
+ testStates("switch_readonly_undefined", 0, 0, STATE_READONLY);
+ testStates("switch_readonly_absent", 0, 0, STATE_READONLY);
+
+ // test (textbox) multiline states
+ testStates("textbox_multiline_true", 0, EXT_STATE_MULTI_LINE);
+ testStates("textbox_multiline_false", 0, EXT_STATE_SINGLE_LINE);
+ testStates("textbox_multiline_empty", 0, EXT_STATE_SINGLE_LINE);
+ testStates("textbox_multiline_undefined", 0, EXT_STATE_SINGLE_LINE);
+ testStates("textbox_multiline_absent", 0, EXT_STATE_SINGLE_LINE);
+
+ // test (textbox) readonly states
+ testStates("textbox_readonly_true", STATE_READONLY);
+ testStates("textbox_readonly_false", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+ testStates("textbox_readonly_empty", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+ testStates("textbox_readonly_undefined", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+ testStates("textbox_readonly_absent", 0, EXT_STATE_EDITABLE, STATE_READONLY);
+
+ // test native textbox readonly state and aria-readonly state (if conflict, native wins)
+ testStates("native_textbox_nativereadonly_ariatrue", STATE_READONLY);
+ testStates("native_textbox_nativereadonly_ariafalse", STATE_READONLY);
+ testStates("native_textbox_nativereadonly_ariaempty", STATE_READONLY);
+ testStates("native_textbox_nativereadonly_ariaundefined", STATE_READONLY);
+ testStates("native_textbox_nativereadonly_ariaabsent", STATE_READONLY);
+
+ testStates("native_textbox_nativeeditable_ariatrue", 0, 0, STATE_READONLY);
+ testStates("native_textbox_nativeeditable_ariafalse", 0, 0, STATE_READONLY);
+ testStates("native_textbox_nativeeditable_ariaempty", 0, 0, STATE_READONLY);
+ testStates("native_textbox_nativeeditable_ariaundefined", 0, 0, STATE_READONLY);
+ testStates("native_textbox_nativeeditable_ariaabsent", 0, 0, STATE_READONLY);
+
+ // test (treeitem) selectable and selected states
+ testStates("treeitem_selected_true", (STATE_SELECTABLE | STATE_SELECTED));
+ testStates("treeitem_selected_false", STATE_SELECTABLE, 0, STATE_SELECTED);
+ testStates("treeitem_selected_empty", STATE_SELECTABLE, 0, STATE_SELECTED);
+ testStates("treeitem_selected_undefined", STATE_SELECTABLE, 0, STATE_SELECTED);
+ testStates("treeitem_selected_absent", STATE_SELECTABLE, 0, STATE_SELECTED);
+
+ // test (treeitem) haspopup states
+ testStates("treeitem_haspopup_true", STATE_HASPOPUP);
+ testStates("treeitem_haspopup_false", 0, 0, STATE_HASPOPUP);
+ testStates("treeitem_haspopup_empty", 0, 0, STATE_HASPOPUP);
+ testStates("treeitem_haspopup_undefined", 0, 0, STATE_HASPOPUP);
+ testStates("treeitem_haspopup_absent", 0, 0, STATE_HASPOPUP);
+
+ // test (treeitem) expandable and expanded/collapsed states
+ testStates("treeitem_expanded_true", STATE_EXPANDED, EXT_STATE_EXPANDABLE);
+ testStates("treeitem_expanded_false", STATE_COLLAPSED, EXT_STATE_EXPANDABLE);
+ testStates("treeitem_expanded_empty", 0, 0, STATE_EXPANDED | STATE_COLLAPSED, EXT_STATE_EXPANDABLE);
+ testStates("treeitem_expanded_undefined", 0, 0, STATE_EXPANDED | STATE_COLLAPSED, EXT_STATE_EXPANDABLE);
+ testStates("treeitem_expanded_absent", 0, 0, STATE_EXPANDED | STATE_COLLAPSED, EXT_STATE_EXPANDABLE);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=452388">
+ Mozilla Bug 452388
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=499653"
+ title="Unify ARIA state attributes mapping rules">
+ Mozilla Bug 499653
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=989958"
+ title="Pressed state is not exposed on a button element with aria-pressed attribute"
+ Mozilla Bug 989958
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="button_pressed_true" role="button" aria-pressed="true">This button has aria-pressed="true" and should get ROLE_TOGGLE_BUTTON. It should also get STATE_PRESSED.</div>
+ <div id="button_pressed_false" role="button" aria-pressed="false">This button has aria-pressed="false" and should get ROLE_TOGGLE_BUTTON.</div>
+ <div id="button_pressed_empty" role="button" aria-pressed="">This button has aria-pressed="" and should <emph>not</emph> get ROLE_BUTTON.</div>
+ <div id="button_pressed_undefined" role="button" aria-pressed="undefined">This button has aria-pressed="undefined" and should <emph>not</emph> get ROLE_TOGGLE_BUTTON.</div>
+ <div id="button_pressed_absent" role="button">This button has <emph>no</emph> aria-pressed attribute and should <emph>not</emph> get ROLE_TOGGLE_BUTTON.</div>
+
+ <div id="checkbox_checked_true" role="checkbox" aria-checked="true">This checkbox has aria-checked="true" and should get STATE_CHECKABLE. It should also get STATE_checked.</div>
+ <div id="checkbox_checked_mixed" role="checkbox" aria-checked="mixed">This checkbox has aria-checked="mixed" and should get STATE_CHECKABLE. It should also get STATE_MIXED.</div>
+ <div id="checkbox_checked_false" role="checkbox" aria-checked="false">This checkbox has aria-checked="false" and should get STATE_CHECKABLE.</div>
+ <div id="checkbox_checked_empty" role="checkbox" aria-checked="">This checkbox has aria-checked="" and should <emph>not</emph> get STATE_CHECKABLE.</div>
+ <div id="checkbox_checked_undefined" role="checkbox" aria-checked="undefined">This checkbox has aria-checked="undefined" and should <emph>not</emph> get STATE_CHECKABLE.</div>
+ <div id="checkbox_checked_absent" role="checkbox">This checkbox has <emph>no</emph> aria-checked attribute and should get STATE_CHECKABLE.</div>
+
+ <form action="">
+ <input id="native_checkbox_nativechecked_ariatrue" type="checkbox" checked="checked" aria-checked="true"/>
+ <input id="native_checkbox_nativechecked_ariafalse" type="checkbox" checked="checked" aria-checked="false"/>
+ <input id="native_checkbox_nativechecked_ariaempty" type="checkbox" checked="checked" aria-checked=""/>
+ <input id="native_checkbox_nativechecked_ariaundefined" type="checkbox" checked="checked" aria-checked="undefined"/>
+ <input id="native_checkbox_nativechecked_ariaabsent" type="checkbox" checked="checked"/>
+
+ <input id="native_checkbox_nativeunchecked_ariatrue" type="checkbox" aria-checked="true"/>
+ <input id="native_checkbox_nativeunchecked_ariafalse" type="checkbox" aria-checked="false"/>
+ <input id="native_checkbox_nativeunchecked_ariaempty" type="checkbox" aria-checked=""/>
+ <input id="native_checkbox_nativeunchecked_ariaundefined" type="checkbox" aria-checked="undefined"/>
+ <input id="native_checkbox_nativeunchecked_ariaabsent" type="checkbox"/>
+ </form>
+
+ <div id="checkbox_readonly_true" role="checkbox" aria-readonly="true">This checkbox has aria-readonly="true" and should get STATE_READONLY.</div>
+ <div id="checkbox_readonly_false" role="checkbox" aria-readonly="false">This checkbox has aria-readonly="false" and should <emph>not</emph> get STATE_READONLY.</div>
+ <div id="checkbox_readonly_empty" role="checkbox" aria-readonly="">This checkbox has aria-readonly="" and should <emph>not</emph> get STATE_READONLY.</div>
+ <div id="checkbox_readonly_undefined" role="checkbox" aria-readonly="undefined">This checkbox has aria-readonly="undefined" and should <emph>not</emph> get STATE_READONLY.</div>
+ <div id="checkbox_readonly_absent" role="checkbox">This checkbox has <emph>no</emph> aria-readonly attribute and should <emph>not</emph> get STATE_READONLY.</div>
+
+ <div id="checkbox_required_true" role="checkbox" aria-required="true">This checkbox has aria-required="true" and should get STATE_REQUIRED.</div>
+ <div id="checkbox_required_false" role="checkbox" aria-required="false">This checkbox has aria-required="false" and should <emph>not</emph> get STATE_REQUIRED.</div>
+ <div id="checkbox_required_empty" role="checkbox" aria-required="">This checkbox has aria-required="" and should <emph>not</emph> get STATE_REQUIRED.</div>
+ <div id="checkbox_required_undefined" role="checkbox" aria-required="undefined">This checkbox has aria-required="undefined" and should <emph>not</emph> get STATE_REQUIRED.</div>
+ <div id="checkbox_required_absent" role="checkbox">This checkbox has <emph>no</emph> aria-required attribute and should <emph>not</emph> get STATE_REQUIRED.</div>
+
+ <div id="checkbox_invalid_true" role="checkbox" aria-invalid="true">This checkbox has aria-invalid="true" and should get STATE_INVALID.</div>
+ <div id="checkbox_invalid_false" role="checkbox" aria-invalid="false">This checkbox has aria-invalid="false" and should <emph>not</emph> get STATE_INVALID.</div>
+ <div id="checkbox_invalid_empty" role="checkbox" aria-invalid="">This checkbox has aria-invalid="" and should <emph>not</emph> get STATE_INVALID.</div>
+ <div id="checkbox_invalid_undefined" role="checkbox" aria-invalid="undefined">This checkbox has aria-invalid="undefined" and should <emph>not</emph> get STATE_INVALID.</div>
+ <div id="checkbox_invalid_absent" role="checkbox">This checkbox has <emph>no</emph> aria-invalid attribute and should <emph>not</emph> get STATE_INVALID.</div>
+
+ <div id="checkbox_disabled_true" role="checkbox" aria-disabled="true" tabindex="0">This checkbox has aria-disabled="true" and should get STATE_DISABLED.</div>
+ <div id="checkbox_disabled_false" role="checkbox" aria-disabled="false">This checkbox has aria-disabled="false" and should <emph>not</emph> get STATE_DISABLED.</div>
+ <div id="checkbox_disabled_empty" role="checkbox" aria-disabled="">This checkbox has aria-disabled="" and should <emph>not</emph> get STATE_DISABLED.</div>
+ <div id="checkbox_disabled_undefined" role="checkbox" aria-disabled="undefined">This checkbox has aria-disabled="undefined" and should <emph>not</emph> get STATE_DISABLED.</div>
+ <div id="checkbox_disabled_absent" role="checkbox">This checkbox has <emph>no</emph> aria-disabled attribute and should <emph>not</emph> get STATE_DISABLED.</div>
+
+ <div id="listbox_multiselectable_true" role="listbox" aria-multiselectable="true">
+ <div id="option_checked_true" role="option" aria-checked="true">item</div>
+ </div>
+ <div id="listbox_multiselectable_false" role="listbox" aria-multiselectable="false">
+ <div id="option_checked_false" role="option" aria-checked="false">item</div>
+ </div>
+ <div id="listbox_multiselectable_empty" role="listbox" aria-multiselectable="">
+ <div id="option_checked_empty" role="option" aria-checked="">item</div>
+ </div>
+ <div id="listbox_multiselectable_undefined" role="listbox" aria-multiselectable="undefined">
+ <div id="option_checked_undefined" role="option" aria-checked="undefined">item</div>
+ </div>
+ <div id="listbox_multiselectable_absent" role="listbox">
+ <div id="option_checked_absent" role="option">item</div>
+ </div>
+
+ <div role="menu">
+ <div id="menuitem_checked_true" role="menuitem" aria-checked="true">Generic menuitems don't support aria-checked.</div>
+ <div id="menuitem_checked_mixed" role="menuitem" aria-checked="mixed">Generic menuitems don't support aria-checked.</div>
+ <div id="menuitem_checked_false" role="menuitem" aria-checked="false">Generic menuitems don't support aria-checked.</div>
+ <div id="menuitem_checked_empty" role="menuitem" aria-checked="">Generic menuitems don't support aria-checked.</div>
+ <div id="menuitem_checked_undefined" role="menuitem" aria-checked="undefined">Generic menuitems don't support aria-checked.</div>
+ <div id="menuitem_checked_absent" role="menuitem">Generic menuitems don't support aria-checked.</div>
+
+ <div id="menuitemcheckbox_checked_true" role="menuitemcheckbox" aria-checked="true">This menuitemcheckbox has aria-checked="true" and should get STATE_CHECKABLE. It should also get STATE_checked.</div>
+ <div id="menuitemcheckbox_checked_mixed" role="menuitemcheckbox" aria-checked="mixed">This menuitemcheckbox has aria-checked="mixed" and should get STATE_CHECKABLE. It should also get STATE_MIXED.</div>
+ <div id="menuitemcheckbox_checked_false" role="menuitemcheckbox" aria-checked="false">This menuitemcheckbox has aria-checked="false" and should get STATE_CHECKABLE.</div>
+ <div id="menuitemcheckbox_checked_empty" role="menuitemcheckbox" aria-checked="">This menuitemcheckbox has aria-checked="" and should <emph>not</emph> get STATE_CHECKABLE.</div>
+ <div id="menuitemcheckbox_checked_undefined" role="menuitemcheckbox" aria-checked="undefined">This menuitemcheckbox has aria-checked="undefined" and should <emph>not</emph> get STATE_CHECKABLE.</div>
+ <div id="menuitemcheckbox_checked_absent" role="menuitemcheckbox">This menuitemcheckbox has <emph>no</emph> aria-checked attribute and should <emph>not</emph> get STATE_CHECKABLE.</div>
+
+ <div id="menuitemcheckbox_readonly_true" role="menuitemcheckbox" aria-readonly="true">This menuitemcheckbox has aria-readonly="true" and should get STATE_READONLY.</div>
+ <div id="menuitemcheckbox_readonly_false" role="menuitemcheckbox" aria-readonly="false">This menuitemcheckbox has aria-readonly="false" and should <emph>not</emph> get STATE_READONLY.</div>
+ <div id="menuitemcheckbox_readonly_empty" role="menuitemcheckbox" aria-readonly="">This menuitemcheckbox has aria-readonly="" and should <emph>not</emph> get STATE_READONLY.</div>
+ <div id="menuitemcheckbox_readonly_undefined" role="menuitemcheckbox" aria-readonly="undefined">This menuitemcheckbox has aria-readonly="undefined" and should <emph>not</emph> get STATE_READONLY.</div>
+ <div id="menuitemcheckbox_readonly_absent" role="menuitemcheckbox">This menuitemcheckbox has <emph>no</emph> aria-readonly attribute and should <emph>not</emph> get STATE_READONLY.</div>
+
+ <div id="menuitemradio_checked_true" role="menuitemradio" aria-checked="true">This menuitem has aria-checked="true" and should get STATE_CHECKABLE. It should also get STATE_checked.</div>
+ <div id="menuitemradio_checked_mixed" role="menuitemradio" aria-checked="mixed">This menuitem has aria-checked="mixed" and should get STATE_CHECKABLE. It should not get STATE_MIXED.</div>
+ <div id="menuitemradio_checked_false" role="menuitemradio" aria-checked="false">This menuitem has aria-checked="false" and should get STATE_CHECKABLE.</div>
+ <div id="menuitemradio_checked_empty" role="menuitemradio" aria-checked="">This menuitem has aria-checked="" and should <emph>not</emph> get STATE_CHECKABLE.</div>
+ <div id="menuitemradio_checked_undefined" role="menuitemradio" aria-checked="undefined">This menuitem has aria-checked="undefined" and should <emph>not</emph> get STATE_CHECKABLE.</div>
+ <div id="menuitemradio_checked_absent" role="menuitemradio">This menuitem has <emph>no</emph> aria-checked attribute but should get STATE_CHECKABLE.</div>
+ </div>
+
+ <div id="menuitemradio_readonly_true" role="menuitemradio" aria-readonly="true">This menuitemradio has aria-readonly="true" and should get STATE_READONLY.</div>
+ <div id="menuitemradio_readonly_false" role="menuitemradio" aria-readonly="false">This menuitemradio has aria-readonly="false" and should <emph>not</emph> get STATE_READONLY.</div>
+ <div id="menuitemradio_readonly_empty" role="menuitemradio" aria-readonly="">This menuitemradio has aria-readonly="" and should <emph>not</emph> get STATE_READONLY.</div>
+ <div id="menuitemradio_readonly_undefined" role="menuitemradio" aria-readonly="undefined">This menuitemradio has aria-readonly="undefined" and should <emph>not</emph> get STATE_READONLY.</div>
+ <div id="menuitemradio_readonly_absent" role="menuitemradio">This menuitemradio has <emph>no</emph> aria-readonly attribute and should <emph>not</emph> get STATE_READONLY.</div>
+
+ <div id="radio_checked_true" role="radio" aria-checked="true">This menuitem has aria-checked="true" and should get STATE_CHECKABLE. It should also get STATE_CHECKED.</div>
+ <div id="radio_checked_mixed" role="radio" aria-checked="mixed">This radio button has aria-checked="mixed" and should get STATE_CHECKABLE. It should not get STATE_MIXED.</div>
+ <div id="radio_checked_false" role="radio" aria-checked="false">This menuitem has aria-checked="false" and should get STATE_CHECKABLE.</div>
+ <div id="radio_checked_empty" role="radio" aria-checked="">This menuitem has aria-checked="" and should <emph>not</emph> get STATE_CHECKABLE.</div>
+ <div id="radio_checked_undefined" role="radio" aria-checked="undefined">This menuitem has aria-checked="undefined" and should <emph>not</emph> get STATE_CHECKABLE.</div>
+ <div id="radio_checked_absent" role="radio">This menuitem has <emph>no</emph> aria-checked attribute but should get STATE_CHECKABLE.</div>
+
+ <div id="radiogroup_readonly_true" role="radiogroup" aria-readonly="true">
+ <div role="radio">yes</div>
+ <div role="radio">no</div>
+ </div>
+ <div id="radiogroup_readonly_false" role="radiogroup" aria-readonly="false">
+ <div role="radio">yes</div>
+ <div role="radio">no</div>
+ </div>
+ <div id="radiogroup_readonly_empty" role="radiogroup" aria-readonly="">
+ <div role="radio">yes</div>
+ <div role="radio">no</div>
+ </div>
+ <div id="radiogroup_readonly_undefined" role="radiogroup" aria-readonly="undefined">
+ <div role="radio">yes</div>
+ <div role="radio">no</div>
+ </div>
+ <div id="radiogroup_readonly_absent" role="radiogroup">
+ <div role="radio">yes</div>
+ <div role="radio">no</div>
+ </div>
+
+ <div id="switch_readonly_true" role="switch" aria-readonly="true">This switch has aria-readonly="true" and should get STATE_READONLY.</div>
+ <div id="switch_readonly_false" role="switch" aria-readonly="false">This switch has aria-readonly="false" and should <emph>not</emph> get STATE_READONLY.</div>
+ <div id="switch_readonly_empty" role="switch" aria-readonly="">This switch has aria-readonly="" and should <emph>not</emph> get STATE_READONLY.</div>
+ <div id="switch_readonly_undefined" role="switch" aria-readonly="undefined">This switch has aria-readonly="undefined" and should <emph>not</emph> get STATE_READONLY.</div>
+ <div id="switch_readonly_absent" role="switch">This switch has <emph>no</emph> aria-readonly attribute and should <emph>not</emph> get STATE_READONLY.</div>
+
+ <div id="textbox_readonly_true" role="textbox" aria-readonly="true"></div>
+ <div id="textbox_readonly_false" role="textbox" aria-readonly="false"></div>
+ <div id="textbox_readonly_empty" role="textbox" aria-readonly=""></div>
+ <div id="textbox_readonly_undefined" role="textbox" aria-readonly="undefined"></div>
+ <div id="textbox_readonly_absent" role="textbox"></div>
+
+ <div id="textbox_multiline_true" role="textbox" aria-multiline="true"></div>
+ <div id="textbox_multiline_false" role="textbox" aria-multiline="false"></div>
+ <div id="textbox_multiline_empty" role="textbox" aria-multiline=""></div>
+ <div id="textbox_multiline_undefined" role="textbox" aria-multiline="undefined"></div>
+ <div id="textbox_multiline_absent" role="textbox"></div>
+
+ <form action="">
+ <input id="native_textbox_nativereadonly_ariatrue" readonly="readonly" aria-readonly="true"/>
+ <input id="native_textbox_nativereadonly_ariafalse" readonly="readonly" aria-readonly="false"/>
+ <input id="native_textbox_nativereadonly_ariaempty" readonly="readonly" aria-readonly=""/>
+ <input id="native_textbox_nativereadonly_ariaundefined" readonly="readonly" aria-readonly="undefined"/>
+ <input id="native_textbox_nativereadonly_ariaabsent" readonly="readonly"/>
+
+ <input id="native_textbox_nativeeditable_ariatrue" aria-readonly="true"/>
+ <input id="native_textbox_nativeeditable_ariafalse" aria-readonly="false"/>
+ <input id="native_textbox_nativeeditable_ariaempty" aria-readonly=""/>
+ <input id="native_textbox_nativeeditable_ariaundefined" aria-readonly="undefined"/>
+ <input id="native_textbox_nativeeditable_ariaabsent"/>
+ </form>
+
+ <div role="tree">
+ <div id="treeitem_selected_true" role="treeitem" aria-selected="true">This treeitem has aria-selected="true" and should get STATE_SELECTABLE. It should also get STATE_SELECTED.</div>
+ <div id="treeitem_selected_false" role="treeitem" aria-selected="false">This treeitem has aria-selected="false" and should get STATE_SELECTABLE.</div>
+ <div id="treeitem_selected_empty" role="treeitem" aria-selected="">This treeitem has aria-selected="" and should <emph>not</emph> get STATE_SELECTABLE.</div>
+ <div id="treeitem_selected_undefined" role="treeitem" aria-selected="undefined">This treeitem has aria-selected="undefined" and should <emph>not</emph> get STATE_SELECTABLE.</div>
+ <div id="treeitem_selected_absent" role="treeitem">This treeitem has <emph>no</emph> aria-selected attribute and should <emph>not</emph> get STATE_SELECTABLE.</div>
+
+ <div id="treeitem_haspopup_true" role="treeitem" aria-haspopup="true">This treeitem has aria-haspopup="true" and should get STATE_HASPOPUP.</div>
+ <div id="treeitem_haspopup_false" role="treeitem" aria-haspopup="false">This treeitem has aria-haspopup="false" and should get STATE_HASPOPUP.</div>
+ <div id="treeitem_haspopup_empty" role="treeitem" aria-haspopup="">This treeitem has aria-haspopup="" and should <emph>not</emph> get STATE_HASPOPUP.</div>
+ <div id="treeitem_haspopup_undefined" role="treeitem" aria-haspopup="undefined">This treeitem has aria-haspopup="undefined" and should <emph>not</emph> get STATE_HASPOPUP.</div>
+ <div id="treeitem_haspopup_absent" role="treeitem">This treeitem has <emph>no</emph> aria-haspopup attribute and should <emph>not</emph> get STATE_HASPOPUP.</div>
+
+ <div id="treeitem_expanded_true" role="treeitem" aria-expanded="true">This treeitem has aria-expanded="true" and should get STATE_EXPANDABLE. It should also get STATE_EXPANDED.</div>
+ <div id="treeitem_expanded_false" role="treeitem" aria-expanded="false">This treeitem has aria-expanded="false" and should get STATE_EXPANDABLE. It should also get STATE_COLLAPSED.</div>
+ <div id="treeitem_expanded_empty" role="treeitem" aria-expanded="">This treeitem has aria-expanded="" and should <emph>not</emph> get STATE_EXPANDABLE.</div>
+ <div id="treeitem_expanded_undefined" role="treeitem" aria-expanded="undefined">This treeitem has aria-expanded="undefined" and should <emph>not</emph> get STATE_EXPANDABLE.</div>
+ <div id="treeitem_expanded_absent" role="treeitem">This treeitem has <emph>no</emph> aria-expanded attribute and should <emph>not</emph> get STATE_EXPANDABLE.</div>
+ </div>
+
+ </body>
+</html>
diff --git a/accessible/tests/mochitest/test_bug420863.html b/accessible/tests/mochitest/test_bug420863.html
new file mode 100644
index 0000000000..4f3a608fe1
--- /dev/null
+++ b/accessible/tests/mochitest/test_bug420863.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=420863
+-->
+<head>
+ <title>Table indexes chrome tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="common.js"></script>
+ <script type="application/javascript"
+ src="events.js"></script>
+ <script type="application/javascript"
+ src="actions.js"></script>
+
+ <script type="application/javascript">
+ var gClickHandler = null;
+
+ function doTest() {
+ // Actions should be exposed on any accessible having related DOM node
+ // with registered 'click' event handler.
+
+ // ////////////////////////////////////////////////////////////////////////
+ // generic td
+ var td1Acc = getAccessible("td1");
+ if (!td1Acc) {
+ SimpleTest.finish();
+ return;
+ }
+
+ is(td1Acc.actionCount, 0,
+ "Simple table cell shouldn't have any actions");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // one td with 'onclick' attribute and one with registered click handler
+ var td3Node = getNode("td3");
+
+ // register 'click' event handler
+ gClickHandler = {
+ handleEvent: function handleEvent(aEvent) {
+ },
+ };
+ td3Node.addEventListener("click", gClickHandler);
+
+ // check actions
+ var actionsArray = [
+ {
+ ID: "td2", // "onclick" attribute
+ actionName: "click",
+ actionIndex: 0,
+ events: CLICK_EVENTS,
+ },
+ {
+ ID: td3Node,
+ actionName: "click",
+ actionIndex: 0,
+ events: CLICK_EVENTS,
+ checkOnClickEvent: function check(aEvent) {
+ // unregister click event handler
+ this.ID.removeEventListener("click", gClickHandler);
+
+ // check actions
+ is(getAccessible(this.ID).actionCount, 0,
+ "td3 shouldn't have actions");
+ },
+ },
+ ];
+
+ testActions(actionsArray); // will call SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=420863"
+ title="If an HTML element has an onClick attribute, expose its click action on the element rather than its child text leaf node."
+ target="_blank">Mozilla Bug 420863</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <table>
+ <tr>
+ <td id="td1">Can't click this cell</td>
+ <td onclick="gTdClickAttr = true;"
+ id="td2">Cell with 'onclick' attribute</td>
+ <td id="td3">Cell with registered 'click' event handler</td>
+ </tr>
+ </table>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/test_custom_element_accessibility_defaults.html b/accessible/tests/mochitest/test_custom_element_accessibility_defaults.html
new file mode 100644
index 0000000000..672b5c36ad
--- /dev/null
+++ b/accessible/tests/mochitest/test_custom_element_accessibility_defaults.html
@@ -0,0 +1,382 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1665151
+-->
+<head>
+ <title>Test for default accessibility semantics for custom elements</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="common.js"></script>
+ <script src="attributes.js"></script>
+ <script src="role.js"></script>
+ <script src="states.js"></script>
+ <script src="value.js"></script>
+ <script src="name.js"></script>
+ <script src="promisified-events.js"></script>
+ <script>
+[
+ ["role", "math", () => {}],
+ ["atomic", "toolbar", internals => internals.ariaAtomic = "true"],
+ ["autocomplete", "textbox", internals => internals.ariaAutoComplete = "inline"],
+ ["busy", "feed", internals => internals.ariaBusy = "true"],
+ ["checked", "checkbox", internals => internals.ariaChecked = "true"],
+ ["colcount", "grid", internals => internals.ariaColCount = "1"],
+ ["col", "gridcell", internals => {
+ internals.ariaColIndex = "1";
+ internals.ariaColIndexText = "Default";
+ internals.ariaColSpan = "1";
+ }],
+ ["current", "listitem", internals => internals.ariaCurrent = "page"],
+ ["description", "note", internals => internals.ariaDescription = "Default"],
+ ["disabled", "button", internals => internals.ariaDisabled = "true"],
+ ["expanded", "button", internals => internals.ariaExpanded = "true"],
+ ["haspopup", "button", internals => internals.ariaHasPopup = "true"],
+ ["hidden", "region", internals => internals.ariaHidden = "false"],
+ ["invalid", "textbox", internals => internals.ariaInvalid = "true"],
+ ["keyshortcuts", "button", internals => internals.ariaKeyShortcuts = "Alt+Shift+A"],
+ ["label", "button", internals => internals.ariaLabel = "Default"],
+ ["level", "heading", internals => internals.ariaLevel = "1"],
+ ["live", "region", internals => internals.ariaLive = "polite"],
+ ["modal", "dialog", internals => internals.ariaModal = "true"],
+ ["multiline", "textbox", internals => internals.ariaMultiLine = "true"],
+ ["multiselectable", "listbox", internals => internals.ariaMultiSelectable = "true"],
+ ["orientation", "menu", internals => internals.ariaOrientation = "vertical"],
+ ["placeholder", "textbox", internals => internals.ariaPlaceholder = "Default"],
+ ["posinset", "option", internals => internals.ariaPosInSet = "1"],
+ ["pressed", "button", internals => internals.ariaPressed = "true"],
+ ["readonly", "textbox", internals => internals.ariaReadOnly = "true"],
+ ["relevant", "region", internals => internals.ariaRelevant = "all"],
+ ["required", "textbox", internals => internals.ariaRequired = "true"],
+ ["roledescription", "region", internals => internals.ariaRoleDescription = "Default"],
+ ["rowcount", "grid", internals => internals.ariaRowCount = "1"],
+ ["row", "row", internals => {
+ internals.ariaRowIndex = "1";
+ internals.ariaRowIndexText = "Default";
+ }],
+ ["rowspan", "cell", internals => internals.ariaRowSpan = "1"],
+ ["selected", "option", internals => internals.ariaSelected = "true"],
+ ["setsize", "listitem", internals => internals.ariaSetSize = "1"],
+ ["sort", "columnheader", internals => internals.ariaSort = "ascending"],
+ ["value", "slider", internals => {
+ internals.ariaValueNow = "5";
+ internals.ariaValueMin = "1";
+ internals.ariaValueMax = "10";
+ internals.ariaValueText = "Default";
+ }],
+].forEach(([name, role, apply]) => {
+ customElements.define(`custom-${name}`,
+ class extends HTMLElement {
+ constructor() {
+ super();
+ this._internals = this.attachInternals();
+ this._internals.role = role;
+ apply(this._internals);
+ }
+ get internals() {
+ return this._internals;
+ }
+ }
+ );
+});
+
+async function runTest() {
+ // Test for proper overriding of default attributes.
+ testAttrs("default-role", {"xml-roles": "math"}, true);
+ testAttrs("custom-role", {"xml-roles": "note"}, true);
+
+ testAttrs("default-atomic", {"atomic": "true"}, true);
+ testAbsentAttrs("custom-atomic", {"atomic": "false"});
+
+ testAttrs("default-autocomplete", {"autocomplete": "inline"}, true);
+ testAttrs("custom-autocomplete", {"autocomplete": "list"}, true);
+
+ testStates("default-busy", STATE_BUSY);
+ testStates("custom-busy", 0, 0, STATE_BUSY);
+
+ testStates("default-checked", STATE_CHECKED);
+ testStates("custom-checked", 0, 0, STATE_CHECKED);
+
+ testAttrs("default-colCount", {"colcount": "1"}, true);
+ testAttrs("default-col", {"colindex": "1"}, true);
+ testAttrs("default-col", {"colindextext": "Default"}, true);
+ testAttrs("default-col", {"colspan": "1"}, true);
+ testAttrs("custom-colCount", {"colcount": "3"}, true);
+ testAttrs("custom-col", {"colindex": "2"}, true);
+ testAttrs("custom-col", {"colindextext": "Custom"}, true);
+ testAttrs("custom-col", {"colspan": "2"}, true);
+
+ testAttrs("default-current", {"current": "page"}, true);
+ testAttrs("custom-current", {"current": "step"}, true);
+
+ testDescr("default-description", "Default");
+ testDescr("custom-description", "Custom");
+
+ testStates("default-disabled", STATE_UNAVAILABLE);
+ testStates("custom-disabled", 0, 0, STATE_UNAVAILABLE);
+
+ testStates("default-expanded", STATE_EXPANDED);
+ testStates("custom-expanded", STATE_COLLAPSED);
+
+ testAttrs("default-haspopup", {"haspopup": "true"}, true);
+ testAbsentAttrs("custom-haspopup", {"haspopup": "false"});
+
+ ok(isAccessible("default-hidden"), "Accessible for not aria-hidden");
+ ok(!isAccessible("custom-hidden"), "No accessible for aria-hidden");
+
+ testStates("default-invalid", STATE_INVALID);
+ testStates("custom-invalid", 0, 0, STATE_INVALID);
+
+ testAttrs("default-keyshortcuts", {"keyshortcuts": "Alt+Shift+A"}, true);
+ testAttrs("custom-keyshortcuts", {"keyshortcuts": "A"}, true);
+
+ testName("default-label", "Default");
+ testName("custom-label", "Custom");
+
+ testAttrs("default-level", {"level": "1"}, true);
+ testAttrs("custom-level", {"level": "2"}, true);
+
+ testAttrs("default-live", {"live": "polite"}, true);
+ testAttrs("custom-live", {"live": "assertive"}, true);
+
+ testStates("default-modal", 0, EXT_STATE_MODAL);
+ testStates("custom-modal", 0, 0, 0, EXT_STATE_MODAL);
+
+ testStates("default-multiline", 0, EXT_STATE_MULTI_LINE, 0, EXT_STATE_SINGLE_LINE);
+ testStates("custom-multiline", 0, EXT_STATE_SINGLE_LINE, 0, EXT_STATE_MULTI_LINE);
+
+ testStates("default-multiselectable", STATE_MULTISELECTABLE);
+ testStates("custom-multiselectable", 0, 0, STATE_MULTISELECTABLE);
+
+ testStates("default-orientation", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL);
+ testStates("custom-orientation", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL);
+
+ testAttrs("default-posinset", {"posinset": "1"}, true);
+ testAttrs("custom-posinset", {"posinset": "2"}, true);
+
+ testStates("default-pressed", STATE_PRESSED);
+ testStates("custom-pressed", 0, 0, STATE_PRESSED);
+
+ testStates("default-readonly", STATE_READONLY);
+ testStates("custom-readonly", 0, 0, STATE_READONLY);
+
+ testAttrs("default-relevant", {"relevant": "all"}, true);
+ testAttrs("custom-relevant", {"relevant": "text"}, true);
+
+ testStates("default-required", STATE_REQUIRED);
+ testStates("custom-required", 0, 0, STATE_REQUIRED);
+
+ testAttrs("default-roledescription", {"roledescription": "Default"}, true);
+ testAttrs("custom-roledescription", {"roledescription": "Custom"}, true);
+
+ testAttrs("default-rowcount", {"rowcount": "1"}, true);
+ testAttrs("default-row", {"rowindex": "1"}, true);
+ testAttrs("default-row", {"rowindextext": "Default"}, true);
+ testAttrs("default-rowspan", {"rowspan": "1"}, true);
+ testAttrs("custom-rowcount", {"rowcount": "3"}, true);
+ testAttrs("custom-row", {"rowindex": "2"}, true);
+ testAttrs("custom-row", {"rowindextext": "Custom"}, true);
+ testAttrs("custom-rowspan", {"rowspan": "2"}, true);
+
+ testStates("default-selected", STATE_SELECTED);
+ testStates("custom-selected", 0, 0, STATE_SELECTED);
+
+ testAttrs("default-setsize", {"setsize": "1"}, true);
+ testAttrs("custom-setsize", {"setsize": "2"}, true);
+
+ testAttrs("default-sort", {"sort": "ascending"}, true);
+ testAttrs("custom-sort", {"sort": "descending"}, true);
+
+ testValue("default-value", "Default", 5, 1, 10, 0);
+ testValue("custom-value", "Custom", 15, 10, 20, 0);
+
+ // Test that changes of defaults fire the proper events.
+ info("Changing ElementInternals ariaLabel");
+ let nameChanged = waitForEvent(EVENT_NAME_CHANGE, "default-label");
+ let customLabelElement = document.getElementById("default-label");
+ customLabelElement.internals.ariaLabel = "Changed Default";
+ await nameChanged;
+ testName("default-label", "Changed Default");
+
+ info("Changing ElementInternals ariaRequired");
+ let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "default-required");
+ let requiredElement = document.getElementById("default-required");
+ requiredElement.internals.ariaRequired = "false";
+ await stateChanged;
+ testStates("default-required", 0, 0, STATE_REQUIRED);
+
+ info("Changing ElementInternals ariaSort");
+ let attributeChanged = waitForEvent(EVENT_OBJECT_ATTRIBUTE_CHANGED, "default-sort");
+ let sortElement = document.getElementById("default-sort");
+ sortElement.internals.ariaSort = "descending";
+ await attributeChanged;
+ testAttrs("default-sort", {"sort": "descending"}, true);
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addA11yLoadEvent(runTest);
+ </script>
+</head>
+<body>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1665151">Bug 1665151</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test">
+<custom-role id="default-role"></custom-role>
+<custom-role id="custom-role" role="note"></custom-role>
+
+<custom-autocomplete id="default-autocomplete"></custom-autocomplete>
+<custom-autocomplete id="custom-autocomplete" aria-autocomplete="list"></custom-autocomplete>
+
+<custom-atomic id="default-atomic"></custom-atomic>
+<custom-atomic id="custom-atomic" aria-atomic="false"></custom-atomic>
+
+<custom-busy id="default-busy"></custom-busy>
+<custom-busy id="custom-busy" aria-busy="false"></custom-busy>
+
+<custom-checked id="default-checked"></custom-checked>
+<custom-checked id="custom-checked" aria-checked="false"></custom-checked>
+
+<custom-colcount id="default-colCount">
+ <div role="rowgroup">
+ <div role="row">
+ <custom-col id="default-col"></custom-col>
+ </div>
+ </div>
+</custom-colcount>
+<custom-colcount id="custom-colCount" aria-colcount="3">
+ <div role="rowgroup">
+ <div role="row">
+ <custom-col
+ id="custom-col"
+ aria-colindex="2"
+ aria-colindextext="Custom"
+ aria-colspan="2">
+ </custom-col>
+ </div>
+ </div>
+</custom-colcount>
+
+<custom-current id="default-current"></custom-current>
+<custom-current id="custom-current" aria-current="step"></custom-current>
+
+<custom-description id="default-description"></custom-description>
+<custom-description id="custom-description" aria-description="Custom"></custom-description>
+
+<custom-disabled id="default-disabled"></custom-disabled>
+<custom-disabled id="custom-disabled" aria-disabled="false"></custom-disabled>
+
+<custom-expanded id="default-expanded"></custom-expanded>
+<custom-expanded id="custom-expanded" aria-expanded="false"></custom-expanded>
+
+<custom-haspopup id="default-haspopup"></custom-haspopup>
+<custom-haspopup id="custom-haspopup" aria-haspopup="false"></custom-haspopup>
+
+<custom-hidden id="default-hidden"></custom-hidden>
+<custom-hidden id="custom-hidden" aria-hidden="true"></custom-hidden>
+
+<custom-invalid id="default-invalid"></custom-invalid>
+<custom-invalid id="custom-invalid" aria-invalid="false"></custom-invalid>
+
+<custom-keyshortcuts id="default-keyshortcuts"></custom-keyshortcuts>
+<custom-keyshortcuts id="custom-keyshortcuts" aria-keyshortcuts="A"></custom-keyshortcuts>
+
+<custom-label id="default-label"></custom-label>
+<custom-label id="custom-label" aria-label="Custom"></custom-label>
+
+<custom-level id="default-level"></custom-level>
+<custom-level id="custom-level" aria-level="2"></custom-level>
+
+<custom-live id="default-live"></custom-live>
+<custom-live id="custom-live" aria-live="assertive"></custom-live>
+
+<custom-modal id="default-modal"></custom-modal>
+<custom-modal id="custom-modal" aria-modal="false"></custom-modal>
+
+<custom-multiline id="default-multiline"></custom-multiline>
+<custom-multiline id="custom-multiline" aria-multiline="false"></custom-multiline>
+
+<custom-multiselectable id="default-multiselectable"></custom-multiselectable>
+<custom-multiselectable id="custom-multiselectable" aria-multiselectable="false"></custom-multiselectable>
+
+<custom-orientation id="default-orientation"></custom-orientation>
+<custom-orientation id="custom-orientation" aria-orientation="horizontal"></custom-orientation>
+
+<custom-posinset id="default-posinset"></custom-posinset>
+<custom-posinset id="custom-posinset" aria-posinset="2"></custom-posinset>
+
+<custom-pressed id="default-pressed"></custom-pressed>
+<custom-pressed id="custom-pressed" aria-pressed="false"></custom-pressed>
+
+<custom-readonly id="default-readonly"></custom-readonly>
+<custom-readonly id="custom-readonly" aria-readonly="false"></custom-readonly>
+
+<custom-relevant id="default-relevant"></custom-relevant>
+<custom-relevant id="custom-relevant" aria-relevant="text"></custom-relevant>
+
+<custom-required id="default-required"></custom-required>
+<custom-required id="custom-required" aria-required="false"></custom-required>
+
+<custom-roledescription id="default-roledescription"></custom-roledescription>
+<custom-roledescription id="custom-roledescription" aria-roledescription="Custom"></custom-roledescription>
+
+<custom-rowcount id="default-rowcount">
+ <div role="rowgroup">
+ <custom-row id="default-row">
+ <custom-rowspan id="default-rowspan"></custom-rowspan>
+ </custom-row>
+ </div>
+</custom-rowcount>
+<custom-rowcount id="custom-rowcount" aria-rowcount="3">
+ <div role="rowgroup">
+ <custom-row
+ id="custom-row"
+ aria-rowindex="2"
+ aria-rowindextext="Custom">
+ <custom-rowspan id="custom-rowspan" aria-rowspan="2"></custom-rowspan>
+ </custom-row>
+ </div>
+</custom-rowcount>
+
+<custom-selected id="default-selected"></custom-selected>
+<custom-selected id="custom-selected" aria-selected="false"></custom-selected>
+
+<div role="listbox">
+ <custom-setsize id="default-setsize"></custom-setsize>
+</div>
+<div role="listbox">
+ <custom-setsize id="custom-setsize" aria-setsize="2"></custom-setsize>
+ <div role="listitem" aria-setsize="2"></div>
+</div>
+
+<div role="grid">
+ <div role="rowgroup">
+ <div role="row">
+ <custom-sort id="default-sort"></custom-sort>
+ </div>
+ </div>
+</div>
+<div role="grid">
+ <div role="rowgroup">
+ <div role="row">
+ <custom-sort id="custom-sort" aria-sort="descending"></custom-sort>
+ </div>
+ </div>
+</div>
+
+<custom-value id="default-value"></custom-value>
+<custom-value
+ id="custom-value"
+ aria-valuenow="15"
+ aria-valuemin="10"
+ aria-valuemax="20"
+ aria-valuetext="Custom">
+</custom-value>
+</pre>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/test_descr.html b/accessible/tests/mochitest/test_descr.html
new file mode 100644
index 0000000000..c386ee5dc1
--- /dev/null
+++ b/accessible/tests/mochitest/test_descr.html
@@ -0,0 +1,134 @@
+<html>
+
+<head>
+ <title>nsIAccessible::description tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="common.js"></script>
+ <script type="application/javascript"
+ src="name.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // Description from aria-describedby attribute
+ testDescr("img1", "aria description");
+
+ // No description from @title attribute because it is used to generate
+ // name.
+ testDescr("img2", "");
+
+ // Description from @title attribute, name is generated from @alt
+ // attribute.
+ testDescr("img3", "description");
+
+ // No description from aria-describedby since it is the same as the
+ // @alt attribute which is used as the name
+ testDescr("img4", "");
+
+ // No description from @title attribute since it is the same as the
+ // @alt attribute which is used as the name
+ testDescr("img5", "");
+
+ // Description from content of h2.
+ testDescr("p", "heading");
+
+ // Description from aria-description attribute
+ testDescr("p2", "I describe");
+
+ // Description from contents of h2 when both aria-describedby and
+ // aria-description are present
+ testDescr("p3", "heading");
+
+ // From table summary (caption is used as a name)
+ testDescr("table1", "summary");
+
+ // Empty (summary is used as a name)
+ testDescr("table2", "");
+
+ // From title (summary is used as a name)
+ testDescr("table3", "title");
+
+ // No description from <desc> element since it is the same as the
+ // <title> element.
+ testDescr("svg", "");
+
+ // role="alert" referenced by aria-describedby should include subtree.
+ testDescr("inputDescribedByAlert", "Error");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=489944"
+ title="@title attribute no longer exposed on accDescription">
+ Mozilla Bug 489944
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=666212"
+ title="summary attribute content mapped to accessible name in MSAA">
+ Mozilla Bug 666212
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi/id=1031188"
+ title="Ensure that accDescription never duplicates AccessibleName">
+ Mozilla Bug 1031188
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <p id="description">aria description</p>
+ <img id="img1" aria-describedby="description" />
+ <img id="img2" title="title" />
+ <img id="img3" alt="name" title="description" />
+ <img id="img4" alt="aria description" aria-describedby="description">
+ <img id="img5" alt="image" title="image">
+
+ <h2 id="heading">heading</h2>
+ <p id="p" aria-describedby="heading" role="button">click me</p>
+ <p id="p2" aria-description="I describe" role="button">click me</p>
+ <p id="p3" aria-description="I do not describe" aria-describedby="heading" role="button">click me</p>
+
+ <table id="table1" summary="summary">
+ <caption>caption</caption>
+ <tr><td>cell</td></tr>
+ </table>
+
+ <table id="table2" summary="summary">
+ <tr><td>cell</td></tr>
+ </table>
+
+ <table id="table3" summary="summary" title="title">
+ <tr><td>cell</td></tr>
+ </table>
+
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1"
+ viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice"
+ id="svg"
+ style="width:100px; height:100px;">
+ <title>SVG Image</title>
+ <desc>SVG Image</desc>
+ <linearGradient id="gradient">
+ <stop class="begin" offset="0%"/>
+ <stop class="end" offset="100%"/>
+ </linearGradient>
+ <rect x="0" y="0" width="100" height="100" style="fill:url(#gradient)" />
+ <circle cx="50" cy="50" r="30" style="fill:url(#gradient)" />
+ </svg>
+
+ <div id="alert" role="alert">Error</div>
+ <input type="text" id="inputDescribedByAlert" aria-describedby="alert">
+</body>
+</html>
diff --git a/accessible/tests/mochitest/test_nsIAccessibleDocument.html b/accessible/tests/mochitest/test_nsIAccessibleDocument.html
new file mode 100644
index 0000000000..7758f63805
--- /dev/null
+++ b/accessible/tests/mochitest/test_nsIAccessibleDocument.html
@@ -0,0 +1,94 @@
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=441737
+-->
+<head>
+ <title>nsIAccessibleDocument chrome tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="common.js"></script>
+ <script type="application/javascript"
+ src="role.js"></script>
+ <script type="application/javascript"
+ src="states.js"></script>
+
+ <script src="chrome://mochikit/content/chrome-harness.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var docAcc = getAccessible(document, [nsIAccessibleDocument]);
+ if (docAcc) {
+ // nsIAccessible
+ is(docAcc.name, "nsIAccessibleDocument chrome tests",
+ "Name for document accessible not correct!");
+
+ testRole(docAcc, ROLE_DOCUMENT);
+
+ // check if it is focusable, read-only.
+ testStates(docAcc, (STATE_READONLY | STATE_FOCUSABLE));
+
+ // No actions wanted on doc
+ is(docAcc.actionCount, 0, "Wrong number of actions for document!");
+
+ // attributes should contain tag:body
+ const attributes = docAcc.attributes;
+ is(attributes.getStringProperty("tag"), "body",
+ "Wrong attribute on document!");
+
+ // Document URL.
+ var rootDir = getRootDirectory(window.location.href);
+ is(docAcc.URL, rootDir + "test_nsIAccessibleDocument.html",
+ "Wrong URL for document!");
+
+ // Document title and mime type.
+ is(docAcc.title, "nsIAccessibleDocument chrome tests",
+ "Wrong title for document!");
+ is(docAcc.mimeType, "text/html",
+ "Wrong mime type for document!");
+
+ // DocAccessible::getDocType currently returns NS_ERROR_FAILURE.
+ // See bug 442005. After fixing, please remove this comment and
+ // uncomment the below two lines to enable the test.
+// is(docAcc.docType, "HTML",
+// "Wrong type of document!");
+
+ // Test for correct Document retrieval.
+ var domDoc = null;
+ try {
+ domDoc = docAcc.DOMDocument;
+ } catch (e) {}
+ ok(domDoc, "no Document for this doc accessible!");
+ is(domDoc, document, "Document nodes do not match!");
+
+ // Test for correct nsIDOMWindow retrieval.
+ var domWindow = null;
+ try {
+ domWindow = docAcc.window;
+ } catch (e) {}
+ ok(domWindow, "no nsIDOMWindow for this doc accessible!");
+ is(domWindow, window, "Window nodes do not match!");
+ }
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=441737"
+ title="nsAccessibleDocument chrome tests">
+ Mozilla Bug 441737
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/test_nsIAccessibleImage.html b/accessible/tests/mochitest/test_nsIAccessibleImage.html
new file mode 100644
index 0000000000..1d8ee2022d
--- /dev/null
+++ b/accessible/tests/mochitest/test_nsIAccessibleImage.html
@@ -0,0 +1,198 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=429659
+-->
+<head>
+ <title>nsIAccessibleImage chrome tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="common.js"></script>
+ <script type="application/javascript"
+ src="role.js"></script>
+ <script type="application/javascript"
+ src="attributes.js"></script>
+ <script type="application/javascript"
+ src="layout.js"></script>
+
+ <script type="application/javascript">
+ function testCoordinates(aID, aAcc, aWidth, aHeight) {
+ var screenX = {}, screenY = {}, windowX = {}, windowY = {}, parentX = {},
+ parentY = {};
+
+ // get screen coordinates.
+ aAcc.getImagePosition(
+ nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE,
+ screenX, screenY);
+ // get window coordinates.
+ aAcc.getImagePosition(
+ nsIAccessibleCoordinateType.COORDTYPE_WINDOW_RELATIVE,
+ windowX, windowY);
+ // get parent related coordinates.
+ aAcc.getImagePosition(
+ nsIAccessibleCoordinateType.COORDTYPE_PARENT_RELATIVE,
+ parentX, parentY);
+ // XXX For linked images, a negative parentY value is returned, and the
+ // screenY coordinate is the link's screenY coordinate minus 1.
+ // Until this is fixed, set parentY to -1 if it's negative.
+ if (parentY.value < 0)
+ parentY.value = -1;
+
+ // See if asking image for child at image's screen coordinates gives
+ // correct accessible. getChildAtPoint operates on screen coordinates.
+ var tempAcc = null;
+ try {
+ tempAcc = aAcc.getChildAtPoint(screenX.value, screenY.value);
+ } catch (e) {}
+ is(tempAcc, aAcc,
+ "Wrong accessible returned for position of " + aID + "!");
+
+ // get image's parent.
+ var imageParentAcc = null;
+ try {
+ imageParentAcc = aAcc.parent;
+ } catch (e) {}
+ ok(imageParentAcc, "no parent accessible for " + aID + "!");
+
+ if (imageParentAcc) {
+ // See if parent's screen coordinates plus image's parent relative
+ // coordinates equal to image's screen coordinates.
+ var parentAccX = {}, parentAccY = {}, parentAccWidth = {},
+ parentAccHeight = {};
+ imageParentAcc.getBounds(parentAccX, parentAccY, parentAccWidth,
+ parentAccHeight);
+ is(parentAccX.value + parentX.value, screenX.value,
+ "Wrong screen x coordinate for " + aID + "!");
+// XXX see bug 456344 is(parentAccY.value + parentY.value, screenY.value,
+// "Wrong screen y coordinate for " + aID + "!");
+ }
+
+ var [expected_w, expected_h] = CSSToDevicePixels(window, aWidth, aHeight);
+ var width = {}, height = {};
+ aAcc.getImageSize(width, height);
+ is(width.value, expected_w, "Wrong width for " + aID + "!");
+ is(height.value, expected_h, "wrong height for " + aID + "!");
+ }
+
+ function testThis(aID, aSRC, aWidth, aHeight,
+ aActionCount, aActionNames) {
+ var acc = getAccessible(aID, [nsIAccessibleImage]);
+ if (!acc)
+ return;
+
+ // Test role
+ testRole(aID, ROLE_GRAPHIC);
+
+ // test coordinates and size
+ testCoordinates(aID, acc, aWidth, aHeight);
+
+ // bug 429659: Make sure the SRC attribute is set for any image
+ var attributes = {"src": aSRC};
+ testAttrs(acc, attributes, true);
+
+ var actionCount = aActionCount || 0;
+ is(acc.actionCount, actionCount,
+ "Wrong number of actions for " + aID + "!");
+ if (actionCount) {
+ for (let index = 0; index < aActionNames.length; index++) {
+ is(acc.getActionName(index), aActionNames[index],
+ "Wrong action name for " + aID + ", index " + index + "!");
+ }
+ }
+ }
+
+ function doTest() {
+ // Test non-linked image
+ testThis("nonLinkedImage", "moz.png", 89, 38);
+
+ // Test linked image
+ var actionNamesArray = ["click ancestor"];
+ testThis("linkedImage", "moz.png", 89, 38, 1,
+ actionNamesArray);
+
+ // Image with long desc
+ actionNamesArray = ["showlongdesc"];
+ testThis("longdesc", "moz.png", 89, 38, 1,
+ actionNamesArray);
+
+ // Image with invalid url in long desc
+ testThis("invalidLongdesc", "moz.png", 89, 38, 0);
+
+ // Image with click and long desc
+ actionNamesArray = null;
+ actionNamesArray = ["click", "showlongdesc"];
+ testThis("clickAndLongdesc", "moz.png",
+ 89, 38, 2, actionNamesArray);
+
+ // Image with click
+ actionNamesArray = null;
+ actionNamesArray = ["click"];
+ testThis("click", "moz.png",
+ 89, 38, 1, actionNamesArray);
+
+ // Image with long desc
+ actionNamesArray = null;
+ actionNamesArray = ["showlongdesc"];
+ testThis("longdesc2", "moz.png",
+ 89, 38, 1, actionNamesArray);
+
+ // Image described by HTML:a@href with whitespaces
+ actionNamesArray = null;
+ actionNamesArray = ["showlongdesc"];
+ testThis("longdesc3", "moz.png",
+ 89, 38, 1, actionNamesArray);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=429659">Mozilla Bug 429659</a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=652635"
+ title="fall back missing @longdesc to aria-describedby pointing to a href">
+ Mozilla Bug 652635
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <br>Simple image:<br>
+ <img id="nonLinkedImage" src="moz.png"/>
+ <br>Linked image:<br>
+ <a href="http://www.mozilla.org"><img id="linkedImage" src="moz.png"></a>
+ <br>Image with longdesc:<br>
+ <img id="longdesc" src="moz.png" longdesc="longdesc_src.html"
+ alt="Image of Mozilla logo"/>
+ <br>Image with invalid url in longdesc:<br>
+ <img id="invalidLongdesc" src="moz.png" longdesc="longdesc src.html"
+ alt="Image of Mozilla logo"/>
+ <br>Image with click and longdesc:<br>
+ <img id="clickAndLongdesc" src="moz.png" longdesc="longdesc_src.html"
+ alt="Another image of Mozilla logo" onclick="alert('Clicked!');"/>
+
+ <br>image described by a link to be treated as longdesc<br>
+ <img id="longdesc2" src="moz.png" aria-describedby="describing_link"
+ alt="Second Image of Mozilla logo"/>
+ <a id="describing_link" href="longdesc_src.html">link to description of image</a>
+
+ <br>Image described by a link to be treated as longdesc with whitespaces<br>
+ <img id="longdesc3" src="moz.png" aria-describedby="describing_link2"
+ alt="Second Image of Mozilla logo"/>
+ <a id="describing_link2" href="longdesc src.html">link to description of image</a>
+
+ <br>Image with click:<br>
+ <img id="click" src="moz.png"
+ alt="A third image of Mozilla logo" onclick="alert('Clicked, too!');"/>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/text.js b/accessible/tests/mochitest/text.js
new file mode 100644
index 0000000000..21392336a1
--- /dev/null
+++ b/accessible/tests/mochitest/text.js
@@ -0,0 +1,814 @@
+/* import-globals-from common.js */
+
+// //////////////////////////////////////////////////////////////////////////////
+// Public
+
+const BOUNDARY_CHAR = nsIAccessibleText.BOUNDARY_CHAR;
+const BOUNDARY_WORD_START = nsIAccessibleText.BOUNDARY_WORD_START;
+const BOUNDARY_WORD_END = nsIAccessibleText.BOUNDARY_WORD_END;
+const BOUNDARY_LINE_START = nsIAccessibleText.BOUNDARY_LINE_START;
+const BOUNDARY_LINE_END = nsIAccessibleText.BOUNDARY_LINE_END;
+const BOUNDARY_PARAGRAPH = nsIAccessibleText.BOUNDARY_PARAGRAPH;
+
+const kTextEndOffset = nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT;
+const kCaretOffset = nsIAccessibleText.TEXT_OFFSET_CARET;
+
+const EndPoint_Start = nsIAccessibleTextRange.EndPoint_Start;
+const EndPoint_End = nsIAccessibleTextRange.EndPoint_End;
+
+const kTodo = 1; // a test is expected to fail
+const kOk = 2; // a test doesn't fail
+
+/**
+ * Test characterCount for the given array of accessibles.
+ *
+ * @param aCount [in] the expected character count
+ * @param aIDs [in] array of accessible identifiers to test
+ * @param aTodoFlag [in, optional] either kOk or kTodo
+ */
+function testCharacterCount(aIDs, aCount, aTodoFlag) {
+ var ids = aIDs instanceof Array ? aIDs : [aIDs];
+ var isFunc = aTodoFlag == kTodo ? todo_is : is;
+ for (var i = 0; i < ids.length; i++) {
+ var textacc = getAccessible(ids[i], [nsIAccessibleText]);
+ isFunc(
+ textacc.characterCount,
+ aCount,
+ "Wrong character count for " + prettyName(ids[i])
+ );
+ }
+}
+
+/**
+ * Test text between two given offsets.
+ *
+ * @param aIDs [in] an array of accessible IDs to test
+ * @param aStartOffset [in] the start offset within the text to test
+ * @param aEndOffset [in] the end offset up to which the text is tested
+ * @param aText [in] the expected result from the test
+ * @param aTodoFlag [in, optional] either kOk or kTodo
+ */
+function testText(aIDs, aStartOffset, aEndOffset, aText, aTodoFlag) {
+ var ids = aIDs instanceof Array ? aIDs : [aIDs];
+ var isFunc = aTodoFlag == kTodo ? todo_is : is;
+ for (var i = 0; i < ids.length; i++) {
+ var acc = getAccessible(ids[i], nsIAccessibleText);
+ try {
+ isFunc(
+ acc.getText(aStartOffset, aEndOffset),
+ aText,
+ "getText: wrong text between start and end offsets '" +
+ aStartOffset +
+ "', '" +
+ aEndOffset +
+ " for '" +
+ prettyName(ids[i]) +
+ "'"
+ );
+ } catch (e) {
+ ok(
+ false,
+ "getText fails between start and end offsets '" +
+ aStartOffset +
+ "', '" +
+ aEndOffset +
+ " for '" +
+ prettyName(ids[i]) +
+ "'"
+ );
+ }
+ }
+}
+
+/**
+ * Test getTextAtOffset for BOUNDARY_CHAR over different elements.
+ *
+ * @param aIDs [in] the accessible identifier or array of accessible
+ * identifiers
+ * @param aOffset [in] the offset to get a character at it
+ * @param aChar [in] the expected character
+ * @param aStartOffset [in] expected start offset of the character
+ * @param aEndOffset [in] expected end offset of the character
+ */
+function testCharAtOffset(aIDs, aOffset, aChar, aStartOffset, aEndOffset) {
+ var IDs = aIDs instanceof Array ? aIDs : [aIDs];
+ for (var i = 0; i < IDs.length; i++) {
+ var acc = getAccessible(IDs[i], nsIAccessibleText);
+ testTextHelper(
+ IDs[i],
+ aOffset,
+ BOUNDARY_CHAR,
+ aChar,
+ aStartOffset,
+ aEndOffset,
+ kOk,
+ kOk,
+ kOk,
+ acc.getTextAtOffset,
+ "getTextAtOffset "
+ );
+ }
+}
+
+/**
+ * Test getTextAtOffset function over different elements.
+ *
+ * @param aIDs [in] ID or array of IDs
+ * @param aBoundaryType [in] boundary type for text to be retrieved
+ * @param aTestList [in] array of sets:
+ * offset1 and offset2 defining the offset range
+ * the text in the range
+ * start offset of the text in the range
+ * end offset of the text in the range
+ *
+ * or
+ *
+ * @param aOffset [in] the offset to get the text at
+ * @param aBoundaryType [in] Boundary type for text to be retrieved
+ * @param aText [in] expected return text for getTextAtOffset
+ * @param aStartOffset [in] expected return start offset for getTextAtOffset
+ * @param aEndOffset [in] expected return end offset for getTextAtOffset
+ * @param ... [in] list of ids or list of tuples made of:
+ * element identifier
+ * kTodo or kOk for returned text
+ * kTodo or kOk for returned start offset
+ * kTodo or kOk for returned offset result
+ */
+function testTextAtOffset() {
+ testTextSuperHelper("getTextAtOffset", arguments);
+}
+
+/**
+ * Test getTextAfterOffset for BOUNDARY_CHAR over different elements.
+ *
+ * @param aIDs [in] the accessible identifier or array of accessible
+ * identifiers
+ * @param aOffset [in] the offset to get a character after it
+ * @param aChar [in] the expected character
+ * @param aStartOffset [in] expected start offset of the character
+ * @param aEndOffset [in] expected end offset of the character
+ */
+function testCharAfterOffset(aIDs, aOffset, aChar, aStartOffset, aEndOffset) {
+ var IDs = aIDs instanceof Array ? aIDs : [aIDs];
+ for (var i = 0; i < IDs.length; i++) {
+ var acc = getAccessible(IDs[i], nsIAccessibleText);
+ testTextHelper(
+ IDs[i],
+ aOffset,
+ BOUNDARY_CHAR,
+ aChar,
+ aStartOffset,
+ aEndOffset,
+ kOk,
+ kOk,
+ kOk,
+ acc.getTextAfterOffset,
+ "getTextAfterOffset "
+ );
+ }
+}
+
+/**
+ * Test getTextAfterOffset function over different elements
+ *
+ * @param aIDs [in] ID or array of IDs
+ * @param aBoundaryType [in] boundary type for text to be retrieved
+ * @param aTestList [in] array of sets:
+ * offset1 and offset2 defining the offset range
+ * the text in the range
+ * start offset of the text in the range
+ * end offset of the text in the range
+ *
+ * or
+ *
+ * @param aOffset [in] the offset to get the text after
+ * @param aBoundaryType [in] Boundary type for text to be retrieved
+ * @param aText [in] expected return text for getTextAfterOffset
+ * @param aStartOffset [in] expected return start offset for getTextAfterOffset
+ * @param aEndOffset [in] expected return end offset for getTextAfterOffset
+ * @param ... [in] list of ids or list of tuples made of:
+ * element identifier
+ * kTodo or kOk for returned text
+ * kTodo or kOk for returned start offset
+ * kTodo or kOk for returned offset result
+ */
+function testTextAfterOffset(
+ aOffset,
+ aBoundaryType,
+ aText,
+ aStartOffset,
+ aEndOffset
+) {
+ testTextSuperHelper("getTextAfterOffset", arguments);
+}
+
+/**
+ * Test getTextBeforeOffset for BOUNDARY_CHAR over different elements.
+ *
+ * @param aIDs [in] the accessible identifier or array of accessible
+ * identifiers
+ * @param aOffset [in] the offset to get a character before it
+ * @param aChar [in] the expected character
+ * @param aStartOffset [in] expected start offset of the character
+ * @param aEndOffset [in] expected end offset of the character
+ */
+function testCharBeforeOffset(aIDs, aOffset, aChar, aStartOffset, aEndOffset) {
+ var IDs = aIDs instanceof Array ? aIDs : [aIDs];
+ for (var i = 0; i < IDs.length; i++) {
+ var acc = getAccessible(IDs[i], nsIAccessibleText);
+ testTextHelper(
+ IDs[i],
+ aOffset,
+ BOUNDARY_CHAR,
+ aChar,
+ aStartOffset,
+ aEndOffset,
+ kOk,
+ kOk,
+ kOk,
+ acc.getTextBeforeOffset,
+ "getTextBeforeOffset "
+ );
+ }
+}
+
+/**
+ * Test getTextBeforeOffset function over different elements
+ *
+ * @param aIDs [in] ID or array of IDs
+ * @param aBoundaryType [in] boundary type for text to be retrieved
+ * @param aTestList [in] array of sets:
+ * offset1 and offset2 defining the offset range
+ * the text in the range
+ * start offset of the text in the range
+ * end offset of the text in the range
+ *
+ * or
+ *
+ * @param aOffset [in] the offset to get the text before
+ * @param aBoundaryType [in] Boundary type for text to be retrieved
+ * @param aText [in] expected return text for getTextBeforeOffset
+ * @param aStartOffset [in] expected return start offset for getTextBeforeOffset
+ * @param aEndOffset [in] expected return end offset for getTextBeforeOffset
+ * @param ... [in] list of ids or list of tuples made of:
+ * element identifier
+ * kTodo or kOk for returned text
+ * kTodo or kOk for returned start offset
+ * kTodo or kOk for returned offset result
+ */
+function testTextBeforeOffset(
+ aOffset,
+ aBoundaryType,
+ aText,
+ aStartOffset,
+ aEndOffset
+) {
+ testTextSuperHelper("getTextBeforeOffset", arguments);
+}
+
+/**
+ * Test word count for an element.
+ *
+ * @param aElement [in] element identifier
+ * @param aCount [in] Expected word count
+ * @param aToDoFlag [in] kTodo or kOk for returned text
+ */
+function testWordCount(aElement, aCount, aToDoFlag) {
+ var isFunc = aToDoFlag == kTodo ? todo_is : is;
+ var acc = getAccessible(aElement, nsIAccessibleText);
+ var startOffsetObj = {},
+ endOffsetObj = {};
+ var length = acc.characterCount;
+ var offset = 0;
+ var wordCount = 0;
+ while (true) {
+ acc.getTextAtOffset(
+ offset,
+ BOUNDARY_WORD_START,
+ startOffsetObj,
+ endOffsetObj
+ );
+ if (offset >= length) {
+ break;
+ }
+
+ wordCount++;
+ offset = endOffsetObj.value;
+ }
+ isFunc(
+ wordCount,
+ aCount,
+ "wrong words count for '" +
+ acc.getText(0, -1) +
+ "': " +
+ wordCount +
+ " in " +
+ prettyName(aElement)
+ );
+}
+
+/**
+ * Test word at a position for an element.
+ *
+ * @param aElement [in] element identifier
+ * @param aWordIndex [in] index of the word to test
+ * @param aText [in] expected text for that word
+ * @param aToDoFlag [in] kTodo or kOk for returned text
+ */
+function testWordAt(aElement, aWordIndex, aText, aToDoFlag) {
+ var isFunc = aToDoFlag == kTodo ? todo_is : is;
+ var acc = getAccessible(aElement, nsIAccessibleText);
+
+ var textLength = acc.characterCount;
+ var wordIdx = aWordIndex;
+ var startOffsetObj = { value: 0 },
+ endOffsetObj = { value: 0 };
+ for (let offset = 0; offset < textLength; offset = endOffsetObj.value) {
+ acc.getTextAtOffset(
+ offset,
+ BOUNDARY_WORD_START,
+ startOffsetObj,
+ endOffsetObj
+ );
+
+ wordIdx--;
+ if (wordIdx < 0) {
+ break;
+ }
+ }
+
+ if (wordIdx >= 0) {
+ ok(
+ false,
+ "the given word index '" +
+ aWordIndex +
+ "' exceeds words amount in " +
+ prettyName(aElement)
+ );
+
+ return;
+ }
+
+ var startWordOffset = startOffsetObj.value;
+ var endWordOffset = endOffsetObj.value;
+
+ // Calculate the end word offset.
+ acc.getTextAtOffset(
+ endOffsetObj.value,
+ BOUNDARY_WORD_END,
+ startOffsetObj,
+ endOffsetObj
+ );
+ if (startOffsetObj.value != textLength) {
+ endWordOffset = startOffsetObj.value;
+ }
+
+ if (endWordOffset <= startWordOffset) {
+ todo(
+ false,
+ "wrong start and end offset for word at index '" +
+ aWordIndex +
+ "': " +
+ " of text '" +
+ acc.getText(0, -1) +
+ "' in " +
+ prettyName(aElement)
+ );
+
+ return;
+ }
+
+ let text = acc.getText(startWordOffset, endWordOffset);
+ isFunc(
+ text,
+ aText,
+ "wrong text for word at index '" +
+ aWordIndex +
+ "': " +
+ " of text '" +
+ acc.getText(0, -1) +
+ "' in " +
+ prettyName(aElement)
+ );
+}
+
+/**
+ * Test words in a element.
+ *
+ * @param aElement [in] element identifier
+ * @param aWords [in] array of expected words
+ * @param aToDoFlag [in, optional] kTodo or kOk for returned text
+ */
+function testWords(aElement, aWords, aToDoFlag) {
+ if (aToDoFlag == null) {
+ aToDoFlag = kOk;
+ }
+
+ testWordCount(aElement, aWords.length, aToDoFlag);
+
+ for (var i = 0; i < aWords.length; i++) {
+ testWordAt(aElement, i, aWords[i], aToDoFlag);
+ }
+}
+
+/**
+ * Remove all selections.
+ *
+ * @param aID [in] Id, DOM node, or acc obj
+ */
+function cleanTextSelections(aID) {
+ var acc = getAccessible(aID, [nsIAccessibleText]);
+
+ while (acc.selectionCount > 0) {
+ acc.removeSelection(0);
+ }
+}
+
+/**
+ * Test addSelection method.
+ *
+ * @param aID [in] Id, DOM node, or acc obj
+ * @param aStartOffset [in] start offset for the new selection
+ * @param aEndOffset [in] end offset for the new selection
+ * @param aSelectionsCount [in] expected number of selections after addSelection
+ */
+function testTextAddSelection(aID, aStartOffset, aEndOffset, aSelectionsCount) {
+ var acc = getAccessible(aID, [nsIAccessibleText]);
+ var text = acc.getText(0, -1);
+
+ acc.addSelection(aStartOffset, aEndOffset);
+
+ is(
+ acc.selectionCount,
+ aSelectionsCount,
+ text +
+ ": failed to add selection from offset '" +
+ aStartOffset +
+ "' to offset '" +
+ aEndOffset +
+ "': selectionCount after"
+ );
+}
+
+/**
+ * Test removeSelection method.
+ *
+ * @param aID [in] Id, DOM node, or acc obj
+ * @param aSelectionIndex [in] index of the selection to be removed
+ * @param aSelectionsCount [in] expected number of selections after
+ * removeSelection
+ */
+function testTextRemoveSelection(aID, aSelectionIndex, aSelectionsCount) {
+ var acc = getAccessible(aID, [nsIAccessibleText]);
+ var text = acc.getText(0, -1);
+
+ acc.removeSelection(aSelectionIndex);
+
+ is(
+ acc.selectionCount,
+ aSelectionsCount,
+ text +
+ ": failed to remove selection at index '" +
+ aSelectionIndex +
+ "': selectionCount after"
+ );
+}
+
+/**
+ * Test setSelectionBounds method.
+ *
+ * @param aID [in] Id, DOM node, or acc obj
+ * @param aStartOffset [in] new start offset for the selection
+ * @param aEndOffset [in] new end offset for the selection
+ * @param aSelectionIndex [in] index of the selection to set
+ * @param aSelectionsCount [in] expected number of selections after
+ * setSelectionBounds
+ */
+function testTextSetSelection(
+ aID,
+ aStartOffset,
+ aEndOffset,
+ aSelectionIndex,
+ aSelectionsCount
+) {
+ var acc = getAccessible(aID, [nsIAccessibleText]);
+ var text = acc.getText(0, -1);
+
+ acc.setSelectionBounds(aSelectionIndex, aStartOffset, aEndOffset);
+
+ is(
+ acc.selectionCount,
+ aSelectionsCount,
+ text +
+ ": failed to set selection at index '" +
+ aSelectionIndex +
+ "': selectionCount after"
+ );
+}
+
+/**
+ * Test selectionCount method.
+ *
+ * @param aID [in] Id, DOM node, or acc obj
+ * @param aCount [in] expected selection count
+ */
+function testTextSelectionCount(aID, aCount) {
+ var acc = getAccessible(aID, [nsIAccessibleText]);
+ var text = acc.getText(0, -1);
+
+ is(acc.selectionCount, aCount, text + ": wrong selectionCount: ");
+}
+
+/**
+ * Test getSelectionBounds method.
+ *
+ * @param aID [in] Id, DOM node, or acc obj
+ * @param aStartOffset [in] expected start offset for the selection
+ * @param aEndOffset [in] expected end offset for the selection
+ * @param aSelectionIndex [in] index of the selection to get
+ */
+function testTextGetSelection(aID, aStartOffset, aEndOffset, aSelectionIndex) {
+ var acc = getAccessible(aID, [nsIAccessibleText]);
+ var text = acc.getText(0, -1);
+
+ var startObj = {},
+ endObj = {};
+ acc.getSelectionBounds(aSelectionIndex, startObj, endObj);
+
+ is(
+ startObj.value,
+ aStartOffset,
+ text + ": wrong start offset for index '" + aSelectionIndex + "'"
+ );
+ is(
+ endObj.value,
+ aEndOffset,
+ text + ": wrong end offset for index '" + aSelectionIndex + "'"
+ );
+}
+
+function testTextRange(
+ aRange,
+ aRangeDescr,
+ aStartContainer,
+ aStartOffset,
+ aEndContainer,
+ aEndOffset,
+ aText,
+ aCommonContainer,
+ aChildren
+) {
+ isObject(
+ aRange.startContainer,
+ getAccessible(aStartContainer),
+ "Wrong start container of " + aRangeDescr
+ );
+ is(aRange.startOffset, aStartOffset, "Wrong start offset of " + aRangeDescr);
+ isObject(
+ aRange.endContainer,
+ getAccessible(aEndContainer),
+ "Wrong end container of " + aRangeDescr
+ );
+ is(aRange.endOffset, aEndOffset, "Wrong end offset of " + aRangeDescr);
+
+ if (aText === undefined) {
+ return;
+ }
+
+ is(aRange.text, aText, "Wrong text of " + aRangeDescr);
+
+ var children = aRange.embeddedChildren;
+ is(
+ children ? children.length : 0,
+ aChildren ? aChildren.length : 0,
+ "Wrong embedded children count of " + aRangeDescr
+ );
+
+ isObject(
+ aRange.container,
+ getAccessible(aCommonContainer),
+ "Wrong container of " + aRangeDescr
+ );
+
+ if (aChildren) {
+ for (var i = 0; i < aChildren.length; i++) {
+ var expectedChild = getAccessible(aChildren[i]);
+ var actualChild = children.queryElementAt(i, nsIAccessible);
+ isObject(
+ actualChild,
+ expectedChild,
+ "Wrong child at index '" + i + "' of " + aRangeDescr
+ );
+ }
+ }
+}
+
+// //////////////////////////////////////////////////////////////////////////////
+// Private
+
+function testTextSuperHelper(aFuncName, aArgs) {
+ // List of tests.
+ if (aArgs[2] instanceof Array) {
+ let ids = aArgs[0] instanceof Array ? aArgs[0] : [aArgs[0]];
+ let boundaryType = aArgs[1];
+ let list = aArgs[2];
+ for (let i = 0; i < list.length; i++) {
+ let offset1 = list[i][0],
+ offset2 = list[i][1];
+ let text = list[i][2],
+ startOffset = list[i][3],
+ endOffset = list[i][4];
+ let failureList = list[i][5];
+ for (let offset = offset1; offset <= offset2; offset++) {
+ for (let idIdx = 0; idIdx < ids.length; idIdx++) {
+ let id = ids[idIdx];
+
+ let flagOk1 = kOk,
+ flagOk2 = kOk,
+ flagOk3 = kOk;
+ if (failureList) {
+ for (let fIdx = 0; fIdx < failureList.length; fIdx++) {
+ if (
+ offset == failureList[fIdx][0] &&
+ id == failureList[fIdx][1]
+ ) {
+ flagOk1 = failureList[fIdx][2];
+ flagOk2 = failureList[fIdx][3];
+ flagOk3 = failureList[fIdx][4];
+ break;
+ }
+ }
+ }
+
+ let acc = getAccessible(id, nsIAccessibleText);
+ testTextHelper(
+ id,
+ offset,
+ boundaryType,
+ text,
+ startOffset,
+ endOffset,
+ flagOk1,
+ flagOk2,
+ flagOk3,
+ acc[aFuncName],
+ aFuncName + " "
+ );
+ }
+ }
+ }
+ return;
+ }
+
+ // Test at single offset. List of IDs.
+ var offset = aArgs[0];
+ var boundaryType = aArgs[1];
+ var text = aArgs[2];
+ var startOffset = aArgs[3];
+ var endOffset = aArgs[4];
+ if (aArgs[5] instanceof Array) {
+ let ids = aArgs[5];
+ for (let i = 0; i < ids.length; i++) {
+ let acc = getAccessible(ids[i], nsIAccessibleText);
+ testTextHelper(
+ ids[i],
+ offset,
+ boundaryType,
+ text,
+ startOffset,
+ endOffset,
+ kOk,
+ kOk,
+ kOk,
+ acc[aFuncName],
+ aFuncName + " "
+ );
+ }
+
+ return;
+ }
+
+ // Each ID is tested separately.
+ for (let i = 5; i < aArgs.length; i = i + 4) {
+ let ID = aArgs[i];
+ let acc = getAccessible(ID, nsIAccessibleText);
+ let toDoFlag1 = aArgs[i + 1];
+ let toDoFlag2 = aArgs[i + 2];
+ let toDoFlag3 = aArgs[i + 3];
+
+ testTextHelper(
+ ID,
+ offset,
+ boundaryType,
+ text,
+ startOffset,
+ endOffset,
+ toDoFlag1,
+ toDoFlag2,
+ toDoFlag3,
+ acc[aFuncName],
+ aFuncName + " "
+ );
+ }
+}
+
+function testTextHelper(
+ aID,
+ aOffset,
+ aBoundaryType,
+ aText,
+ aStartOffset,
+ aEndOffset,
+ aToDoFlag1,
+ aToDoFlag2,
+ aToDoFlag3,
+ aTextFunc,
+ aTextFuncName
+) {
+ var exceptionFlag =
+ aToDoFlag1 == undefined ||
+ aToDoFlag2 == undefined ||
+ aToDoFlag3 == undefined;
+
+ var startMsg = aTextFuncName + "(" + boundaryToString(aBoundaryType) + "): ";
+ var endMsg = ", id: " + prettyName(aID) + ";";
+
+ try {
+ var startOffsetObj = {},
+ endOffsetObj = {};
+ var text = aTextFunc(aOffset, aBoundaryType, startOffsetObj, endOffsetObj);
+
+ if (exceptionFlag) {
+ ok(false, startMsg + "no expected failure at offset " + aOffset + endMsg);
+ return;
+ }
+
+ var isFunc1 = aToDoFlag1 == kTodo ? todo : ok;
+ var isFunc2 = aToDoFlag2 == kTodo ? todo : ok;
+ var isFunc3 = aToDoFlag3 == kTodo ? todo : ok;
+
+ isFunc1(
+ text == aText,
+ startMsg +
+ "wrong text " +
+ "(got '" +
+ text +
+ "', expected: '" +
+ aText +
+ "')" +
+ ", offset: " +
+ aOffset +
+ endMsg
+ );
+ isFunc2(
+ startOffsetObj.value == aStartOffset,
+ startMsg +
+ "wrong start offset" +
+ "(got '" +
+ startOffsetObj.value +
+ "', expected: '" +
+ aStartOffset +
+ "')" +
+ ", offset: " +
+ aOffset +
+ endMsg
+ );
+ isFunc3(
+ endOffsetObj.value == aEndOffset,
+ startMsg +
+ "wrong end offset" +
+ "(got '" +
+ endOffsetObj.value +
+ "', expected: '" +
+ aEndOffset +
+ "')" +
+ ", offset: " +
+ aOffset +
+ endMsg
+ );
+ } catch (e) {
+ var okFunc = exceptionFlag ? todo : ok;
+ okFunc(
+ false,
+ startMsg + "failed at offset " + aOffset + endMsg + ", exception: " + e
+ );
+ }
+}
+
+function boundaryToString(aBoundaryType) {
+ switch (aBoundaryType) {
+ case BOUNDARY_CHAR:
+ return "char";
+ case BOUNDARY_WORD_START:
+ return "word start";
+ case BOUNDARY_WORD_END:
+ return "word end";
+ case BOUNDARY_LINE_START:
+ return "line start";
+ case BOUNDARY_LINE_END:
+ return "line end";
+ case BOUNDARY_PARAGRAPH:
+ return "paragraph";
+ }
+ return "unknown";
+}
diff --git a/accessible/tests/mochitest/text/a11y.toml b/accessible/tests/mochitest/text/a11y.toml
new file mode 100644
index 0000000000..740073611b
--- /dev/null
+++ b/accessible/tests/mochitest/text/a11y.toml
@@ -0,0 +1,33 @@
+[DEFAULT]
+support-files = [ "doc.html",
+ "!/accessible/tests/mochitest/*.js"]
+
+["test_atcaretoffset.html"]
+
+["test_charboundary.html"]
+
+["test_doc.html"]
+
+["test_dynamic.html"]
+
+["test_general.xhtml"]
+
+["test_gettext.html"]
+
+["test_hypertext.html"]
+
+["test_lineboundary.html"]
+
+["test_paragraphboundary.html"]
+
+["test_passwords.html"]
+
+["test_selection.html"]
+
+["test_settext_input_event.html"]
+
+["test_textBounds.html"]
+
+["test_wordboundary.html"]
+
+["test_words.html"]
diff --git a/accessible/tests/mochitest/text/doc.html b/accessible/tests/mochitest/text/doc.html
new file mode 100644
index 0000000000..d57406c226
--- /dev/null
+++ b/accessible/tests/mochitest/text/doc.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script type="application/javascript">
+ document.documentElement.appendChild(document.createTextNode("outbody"));
+ </script>
+</head>
+<body>inbody</body>
+</html>
diff --git a/accessible/tests/mochitest/text/test_atcaretoffset.html b/accessible/tests/mochitest/text/test_atcaretoffset.html
new file mode 100644
index 0000000000..33fe7cd793
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_atcaretoffset.html
@@ -0,0 +1,425 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test: nsIAccessibleText getText* functions at caret offset</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+
+ <script type="application/javascript">
+ // gA11yEventDumpToConsole = true; // debugging
+
+ function traverseTextByLines(aQueue, aID, aLines) {
+ var wholeText = "";
+ for (var i = 0; i < aLines.length ; i++)
+ wholeText += aLines[i][0] + aLines[i][1];
+
+ var baseInvokerFunc = synthClick;
+ var charIter = new charIterator(wholeText, aLines);
+ // charIter.debugOffset = 10; // enable to run tests at given offset only
+
+ while (charIter.next()) {
+ aQueue.push(new tmpl_moveTo(aID, baseInvokerFunc, wholeText, charIter));
+ baseInvokerFunc = synthRightKey;
+ }
+ }
+
+ /**
+ * Used to get test list for each traversed character.
+ */
+ function charIterator(aWholeText, aLines) {
+ this.next = function charIterator_next() {
+ // Don't increment offset if we are at end of the wrapped line
+ // (offset is shared between end of this line and start of next line).
+ if (this.mAtWrappedLineEnd) {
+ this.mAtWrappedLineEnd = false;
+ this.mLine = this.mLine.nextLine;
+ return true;
+ }
+
+ this.mOffset++;
+ if (this.mOffset > aWholeText.length)
+ return false;
+
+ var nextLine = this.mLine.nextLine;
+ if (!nextLine.isFakeLine() && this.mOffset == nextLine.start) {
+ if (nextLine.start == this.mLine.end)
+ this.mAtWrappedLineEnd = true;
+ else
+ this.mLine = nextLine;
+ }
+
+ return true;
+ };
+
+ Object.defineProperty(this, "offset", { get() { return this.mOffset; },
+ });
+
+ Object.defineProperty(this, "offsetDescr", { get() {
+ return this.mOffset + " offset (" + this.mLine.number + " line, " +
+ (this.mOffset - this.mLine.start) + " offset on the line)";
+ },
+ });
+
+ Object.defineProperty(this, "tests", { get() {
+ // Line boundary tests.
+ var cLine = this.mLine;
+ var pLine = cLine.prevLine;
+ var ppLine = pLine.prevLine;
+ var nLine = cLine.nextLine;
+ var nnLine = nLine.nextLine;
+
+ var lineTests = [
+ [ testTextBeforeOffset, BOUNDARY_LINE_START, pLine.start, cLine.start],
+ [ testTextBeforeOffset, BOUNDARY_LINE_END, ppLine.end, pLine.end],
+ [ testTextAtOffset, BOUNDARY_LINE_START, cLine.start, nLine.start],
+ [ testTextAtOffset, BOUNDARY_LINE_END, pLine.end, cLine.end],
+ [ testTextAfterOffset, BOUNDARY_LINE_START, nLine.start, nnLine.start],
+ [ testTextAfterOffset, BOUNDARY_LINE_END, cLine.end, nLine.end],
+ ];
+
+ // Word boundary tests.
+ var cWord = this.mLine.firstWord;
+ var nWord = cWord.nextWord, pWord = cWord.prevWord;
+
+ // The current word is a farthest word starting at or after the offset.
+ if (this.mOffset >= nWord.start) {
+ while (this.mOffset >= nWord.start && !this.mLine.isLastWord(cWord)) {
+ cWord = nWord;
+ nWord = nWord.nextWord;
+ }
+ pWord = cWord.prevWord;
+ } else if (this.mOffset < cWord.start) {
+ while (this.mOffset < cWord.start) {
+ cWord = pWord;
+ pWord = pWord.prevWord;
+ }
+ nWord = cWord.nextWord;
+ }
+
+ var nnWord = nWord.nextWord, ppWord = pWord.prevWord;
+
+ var isAfterWordEnd =
+ this.mOffset > cWord.end || cWord.line != this.mLine;
+ var isAtOrAfterWordEnd = (this.mOffset >= cWord.end);
+ var useNextWordForAtWordEnd =
+ isAtOrAfterWordEnd && this.mOffset != aWholeText.length;
+
+ var wordTests = [
+ [ testTextBeforeOffset, BOUNDARY_WORD_START,
+ pWord.start, cWord.start ],
+ [ testTextBeforeOffset, BOUNDARY_WORD_END,
+ (isAfterWordEnd ? pWord : ppWord).end,
+ (isAfterWordEnd ? cWord : pWord).end ],
+ [ testTextAtOffset, BOUNDARY_WORD_START,
+ cWord.start, nWord.start ],
+ [ testTextAtOffset, BOUNDARY_WORD_END,
+ (useNextWordForAtWordEnd ? cWord : pWord).end,
+ (useNextWordForAtWordEnd ? nWord : cWord).end ],
+ [ testTextAfterOffset, BOUNDARY_WORD_START,
+ nWord.start, nnWord.start ],
+ [ testTextAfterOffset, BOUNDARY_WORD_END,
+ (isAfterWordEnd ? nWord : cWord).end,
+ (isAfterWordEnd ? nnWord : nWord).end ],
+ ];
+
+ // Character boundary tests.
+ var prevOffset = this.offset > 1 ? this.offset - 1 : 0;
+ var nextOffset = this.offset >= aWholeText.length ?
+ this.offset : this.offset + 1;
+ var nextAfterNextOffset = nextOffset >= aWholeText.length ?
+ nextOffset : nextOffset + 1;
+
+ var charTests = [
+ [ testTextBeforeOffset, BOUNDARY_CHAR,
+ prevOffset, this.offset ],
+ [ testTextAtOffset, BOUNDARY_CHAR,
+ this.offset,
+ this.mAtWrappedLineEnd ? this.offset : nextOffset ],
+ [ testTextAfterOffset, BOUNDARY_CHAR,
+ this.mAtWrappedLineEnd ? this.offset : nextOffset,
+ this.mAtWrappedLineEnd ? nextOffset : nextAfterNextOffset ],
+ ];
+
+ return lineTests.concat(wordTests.concat(charTests));
+ },
+ });
+
+ Object.defineProperty(this, "failures", { get() {
+ if (this.mOffset == this.mLine.start)
+ return this.mLine.lineStartFailures;
+ if (this.mOffset == this.mLine.end)
+ return this.mLine.lineEndFailures;
+ return [];
+ },
+ });
+
+ this.mOffset = -1;
+ this.mLine = new line(aWholeText, aLines, 0);
+ this.mAtWrappedLineEnd = false;
+ this.mWord = this.mLine.firstWord;
+ }
+
+ /**
+ * A line object. Allows to navigate by lines and by words.
+ */
+ function line(aWholeText, aLines, aIndex) {
+ Object.defineProperty(this, "prevLine", { get() {
+ return new line(aWholeText, aLines, aIndex - 1);
+ },
+ });
+ Object.defineProperty(this, "nextLine", { get() {
+ return new line(aWholeText, aLines, aIndex + 1);
+ },
+ });
+
+ Object.defineProperty(this, "start", { get() {
+ if (aIndex < 0)
+ return 0;
+
+ if (aIndex >= aLines.length)
+ return aWholeText.length;
+
+ return aLines[aIndex][2];
+ },
+ });
+ Object.defineProperty(this, "end", { get() {
+ if (aIndex < 0)
+ return 0;
+
+ if (aIndex >= aLines.length)
+ return aWholeText.length;
+
+ return aLines[aIndex][3];
+ },
+ });
+
+ Object.defineProperty(this, "number", { get() { return aIndex; },
+ });
+ Object.defineProperty(this, "wholeText", { get() { return aWholeText; },
+ });
+ this.isFakeLine = function line_isFakeLine() {
+ return aIndex < 0 || aIndex >= aLines.length;
+ };
+
+ Object.defineProperty(this, "lastWord", { get() {
+ if (aIndex < 0)
+ return new word(this, [], -1);
+ if (aIndex >= aLines.length)
+ return new word(this, [], 0);
+
+ var words = aLines[aIndex][4].words;
+ return new word(this, words, words.length - 2);
+ },
+ });
+ Object.defineProperty(this, "firstWord", { get() {
+ if (aIndex < 0)
+ return new word(this, [], -1);
+ if (aIndex >= aLines.length)
+ return new word(this, [], 0);
+
+ var words = aLines[aIndex][4].words;
+ return new word(this, words, 0);
+ },
+ });
+
+ this.isLastWord = function line_isLastWord(aWord) {
+ var lastWord = this.lastWord;
+ return lastWord.start == aWord.start && lastWord.end == aWord.end;
+ };
+
+ Object.defineProperty(this, "lineStartFailures", { get() {
+ if (aIndex < 0 || aIndex >= aLines.length)
+ return [];
+
+ return aLines[aIndex][4].lsf || [];
+ },
+ });
+ Object.defineProperty(this, "lineEndFailures", { get() {
+ if (aIndex < 0 || aIndex >= aLines.length)
+ return [];
+
+ return aLines[aIndex][4].lef || [];
+ },
+ });
+ }
+
+ /**
+ * A word object. Allows to navigate by words.
+ */
+ function word(aLine, aWords, aIndex) {
+ Object.defineProperty(this, "prevWord", { get() {
+ if (aIndex >= 2)
+ return new word(aLine, aWords, aIndex - 2);
+
+ var prevLineLastWord = aLine.prevLine.lastWord;
+ if (this.start == prevLineLastWord.start && !this.isFakeStartWord())
+ return prevLineLastWord.prevWord;
+ return prevLineLastWord;
+ },
+ });
+ Object.defineProperty(this, "nextWord", { get() {
+ if (aIndex + 2 < aWords.length)
+ return new word(aLine, aWords, aIndex + 2);
+
+ var nextLineFirstWord = aLine.nextLine.firstWord;
+ if (this.end == nextLineFirstWord.end && !this.isFakeEndWord())
+ return nextLineFirstWord.nextWord;
+ return nextLineFirstWord;
+ },
+ });
+
+ Object.defineProperty(this, "line", { get() { return aLine; } });
+
+ Object.defineProperty(this, "start", { get() {
+ if (this.isFakeStartWord())
+ return 0;
+
+ if (this.isFakeEndWord())
+ return aLine.end;
+ return aWords[aIndex];
+ },
+ });
+ Object.defineProperty(this, "end", { get() {
+ if (this.isFakeStartWord())
+ return 0;
+
+ return this.isFakeEndWord() ? aLine.end : aWords[aIndex + 1];
+ },
+ });
+
+ this.toString = function word_toString() {
+ var start = this.start, end = this.end;
+ return "'" + aLine.wholeText.substring(start, end) +
+ "' at [" + start + ", " + end + "]";
+ };
+
+ this.isFakeStartWord = function() { return aIndex < 0; };
+ this.isFakeEndWord = function() { return aIndex >= aWords.length; };
+ }
+
+ /**
+ * A template invoker to move through the text.
+ */
+ function tmpl_moveTo(aID, aInvokerFunc, aWholeText, aCharIter) {
+ this.offset = aCharIter.offset;
+
+ var checker = new caretMoveChecker(this.offset, true, aID);
+ this.__proto__ = new (aInvokerFunc)(aID, checker);
+
+ this.finalCheck = function genericMoveTo_finalCheck() {
+ if (this.noTests())
+ return;
+
+ for (var i = 0; i < this.tests.length; i++) {
+ var func = this.tests[i][0];
+ var boundary = this.tests[i][1];
+ var startOffset = this.tests[i][2];
+ var endOffset = this.tests[i][3];
+ var text = aWholeText.substring(startOffset, endOffset);
+
+ var isOk1 = kOk, isOk2 = kOk, isOk3 = kOk;
+ for (var fIdx = 0; fIdx < this.failures.length; fIdx++) {
+ var failure = this.failures[fIdx];
+ if (func.name.includes(failure[0]) && boundary == failure[1]) {
+ isOk1 = failure[2];
+ isOk2 = failure[3];
+ isOk3 = failure[4];
+ }
+ }
+
+ func(kCaretOffset, boundary, text, startOffset, endOffset,
+ aID, isOk1, isOk2, isOk3);
+ }
+ };
+
+ this.getID = function genericMoveTo_getID() {
+ return "move to " + this.offsetDescr;
+ };
+
+ this.noTests = function tmpl_moveTo_noTests() {
+ return ("debugOffset" in aCharIter) &&
+ (aCharIter.debugOffset != this.offset);
+ };
+
+ this.offsetDescr = aCharIter.offsetDescr;
+ this.tests = this.noTests() ? null : aCharIter.tests;
+ this.failures = aCharIter.failures;
+ }
+
+ var gQueue = null;
+ function doTest() {
+ gQueue = new eventQueue();
+
+ // __a__w__o__r__d__\n
+ // 0 1 2 3 4 5
+ // __t__w__o__ (soft line break)
+ // 6 7 8 9
+ // __w__o__r__d__s
+ // 10 11 12 13 14 15
+
+ traverseTextByLines(gQueue, "textarea",
+ [ [ "aword", "\n", 0, 5, { words: [ 0, 5 ] } ],
+ [ "two ", "", 6, 10, { words: [ 6, 9 ] } ],
+ [ "words", "", 10, 15, { words: [ 10, 15 ] } ],
+ ] );
+
+ var line4 = [ // "riend "
+ [ "TextBeforeOffset", BOUNDARY_WORD_END,
+ kOk, kOk, kOk],
+ [ "TextAfterOffset", BOUNDARY_WORD_END,
+ kOk, kOk, kOk ],
+ ];
+ traverseTextByLines(gQueue, "ta_wrapped",
+ [ [ "hi ", "", 0, 3, { words: [ 0, 2 ] } ],
+ [ "hello ", "", 3, 9, { words: [ 3, 8 ] } ],
+ [ "my ", "", 9, 12, { words: [ 9, 11 ] } ],
+ [ "longf", "", 12, 17, { words: [ 12, 17 ] } ],
+ [ "riend ", "", 17, 23, { words: [ 17, 22 ], lsf: line4 } ],
+ [ "t sq ", "", 23, 28, { words: [ 23, 24, 25, 27 ] } ],
+ [ "t", "", 28, 29, { words: [ 28, 29 ] } ],
+ ] );
+
+ gQueue.invoke(); // will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="nsIAccessibleText getText related functions tests at caret offset"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=852021">
+ Bug 852021
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+
+ <textarea id="textarea" cols="5">aword
+two words</textarea>
+
+ <!-- scrollbar-width: none is needed so that the width of the scrollbar
+ doesn't incorrectly affect the width of the textarea on some systems.
+ See bug 1600170 and bug 33654.
+ -->
+ <textarea id="ta_wrapped" cols="5" style="scrollbar-width: none;">hi hello my longfriend t sq t</textarea>
+ </pre>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/text/test_charboundary.html b/accessible/tests/mochitest/text/test_charboundary.html
new file mode 100644
index 0000000000..5ca4120a47
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_charboundary.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Char boundary text tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ //
+ // __h__e__l__l__o__ __m__y__ __f__r__i__e__n__d__
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+
+ var IDs = [ "i1", "d1", "e1", "t1" ];
+
+ testCharBeforeOffset(IDs, 0, "", 0, 0);
+ testCharBeforeOffset(IDs, 1, "h", 0, 1);
+ testCharBeforeOffset(IDs, 14, "n", 13, 14);
+ testCharBeforeOffset(IDs, 15, "d", 14, 15);
+
+ testCharAtOffset(IDs, 0, "h", 0, 1);
+ testCharAtOffset(IDs, 1, "e", 1, 2);
+ testCharAtOffset(IDs, 14, "d", 14, 15);
+ testCharAtOffset(IDs, 15, "", 15, 15);
+
+ testCharAfterOffset(IDs, 0, "e", 1, 2);
+ testCharAfterOffset(IDs, 1, "l", 2, 3);
+ testCharAfterOffset(IDs, 14, "", 15, 15);
+ testCharAfterOffset(IDs, 15, "", 15, 15);
+
+ // ////////////////////////////////////////////////////////////////////////
+ //
+ // __B__r__a__v__e__ __S__i__r__ __ __R__o__b__i__n__ __ __ __r__a__n
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
+
+ IDs = [ "i2", "d2", "e2", "t2" ];
+
+ testCharBeforeOffset(IDs, 0, "", 0, 0);
+ testCharBeforeOffset(IDs, 1, "B", 0, 1);
+ testCharBeforeOffset(IDs, 6, " ", 5, 6);
+ testCharBeforeOffset(IDs, 10, " ", 9, 10);
+ testCharBeforeOffset(IDs, 11, " ", 10, 11);
+ testCharBeforeOffset(IDs, 17, " ", 16, 17);
+ testCharBeforeOffset(IDs, 19, " ", 18, 19);
+
+ testCharAtOffset(IDs, 0, "B", 0, 1);
+ testCharAtOffset(IDs, 1, "r", 1, 2);
+ testCharAtOffset(IDs, 5, " ", 5, 6);
+ testCharAtOffset(IDs, 9, " ", 9, 10);
+ testCharAtOffset(IDs, 10, " ", 10, 11);
+ testCharAtOffset(IDs, 17, " ", 17, 18);
+
+ testCharAfterOffset(IDs, 0, "r", 1, 2);
+ testCharAfterOffset(IDs, 1, "a", 2, 3);
+ testCharAfterOffset(IDs, 4, " ", 5, 6);
+ testCharAfterOffset(IDs, 5, "S", 6, 7);
+ testCharAfterOffset(IDs, 8, " ", 9, 10);
+ testCharAfterOffset(IDs, 9, " ", 10, 11);
+ testCharAfterOffset(IDs, 10, "R", 11, 12);
+ testCharAfterOffset(IDs, 15, " ", 16, 17);
+ testCharAfterOffset(IDs, 16, " ", 17, 18);
+ testCharAfterOffset(IDs, 17, " ", 18, 19);
+ testCharAfterOffset(IDs, 18, "r", 19, 20);
+
+ // ////////////////////////////////////////////////////////////////////////
+ //
+ // __o__n__e__w__o__r__d__\n
+ // 0 1 2 3 4 5 6 7
+ // __\n
+ // 8
+ // __t__w__o__ __w__o__r__d__s__\n
+ // 9 10 11 12 13 14 15 16 17 18
+
+ IDs = ["d3", "dbr3", "e3", "ebr3", "t3"];
+
+ testCharBeforeOffset(IDs, 8, "\n", 7, 8);
+ testCharBeforeOffset(IDs, 9, "\n", 8, 9);
+ testCharBeforeOffset(IDs, 10, "t", 9, 10);
+
+ testCharAtOffset(IDs, 7, "\n", 7, 8);
+ testCharAtOffset(IDs, 8, "\n", 8, 9);
+ testCharAtOffset(IDs, 9, "t", 9, 10);
+
+ testCharAfterOffset(IDs, 6, "\n", 7, 8);
+ testCharAfterOffset(IDs, 7, "\n", 8, 9);
+ testCharAfterOffset(IDs, 8, "t", 9, 10);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="i1" value="hello my friend"/>
+ <div id="d1">hello my friend</div>
+ <div id="e1" contenteditable="true">hello my friend</div>
+ <textarea id="t1" contenteditable="true">hello my friend</textarea>
+
+ <input id="i2" value="Brave Sir Robin ran"/>
+ <pre>
+ <div id="d2">Brave Sir Robin ran</div>
+ <div id="e2" contenteditable="true">Brave Sir Robin ran</div>
+ </pre>
+ <textarea id="t2" cols="300">Brave Sir Robin ran</textarea>
+
+ <pre>
+ <div id="d3">oneword
+
+two words
+</div>
+ <div id="dbr3">oneword<br/><br/>two words<br/></div>
+ <div id="e3" contenteditable="true">oneword
+
+two words
+</div>
+ <div id="ebr3" contenteditable="true">oneword<br/><br/>two words<br/></div>
+ <textarea id="t3" cols="300">oneword
+
+two words</textarea>
+ </pre>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/text/test_doc.html b/accessible/tests/mochitest/text/test_doc.html
new file mode 100644
index 0000000000..88b75b98c4
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_doc.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>nsIAccessibleText getText related function tests for document accessible</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+ <script type="application/javascript">
+
+ function doTest() {
+ var iframeDoc = [ getNode("iframe").contentDocument ];
+ testCharacterCount(iframeDoc, 15);
+ testText(iframeDoc, 0, 15, "outbody inbody ");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Elements appended outside the body aren't accessible"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=608887">Mozilla Bug 608887</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <iframe id="iframe" src="doc.html"></iframe>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/text/test_dynamic.html b/accessible/tests/mochitest/text/test_dynamic.html
new file mode 100644
index 0000000000..63889fc664
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_dynamic.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>nsIAccessibleText getText related function tests for tree mutations</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function insertBefore(aId, aEl, aTextBefore, aTextAfter, aStartIdx, aEndIdx) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, aId),
+ ];
+
+ this.invoke = function insertBefore_invoke() {
+ testText(aId, 0, -1, aTextBefore);
+ getNode(aId).insertBefore(aEl, getNode(aId).firstChild);
+ };
+
+ this.finalCheck = function insertBefore_finalCheck() {
+ testText(aId, aStartIdx, aEndIdx, aTextAfter);
+ };
+
+ this.getID = function insertTextBefore_getID() {
+ return "insert " + prettyName(aEl) + " before";
+ };
+ }
+
+ function insertTextBefore(aId, aTextBefore, aText) {
+ var el = document.createTextNode(aText);
+ this.__proto__ = new insertBefore(aId, el, aTextBefore,
+ aText + aTextBefore, 0, -1);
+ }
+
+ function insertImgBefore(aId, aTextBefore) {
+ var el = document.createElement("img");
+ el.setAttribute("src", "../moz.png");
+ el.setAttribute("alt", "mozilla");
+
+ this.__proto__ = new insertBefore(aId, el, aTextBefore,
+ kEmbedChar + aTextBefore, 0, -1);
+ }
+
+ function insertTextBefore2(aId) {
+ var el = document.createTextNode("hehe");
+ this.__proto__ = new insertBefore(aId, el, "ho", "ho", 4, -1);
+ }
+
+ var gQueue = null;
+ function doTest() {
+ gQueue = new eventQueue();
+ gQueue.push(new insertTextBefore("c1", "ho", "ha"));
+ gQueue.push(new insertImgBefore("c1", "haho"));
+ gQueue.push(new insertImgBefore("c2", kEmbedChar));
+ gQueue.push(new insertTextBefore2("c3"));
+ gQueue.invoke(); // will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="c1">ho</div>
+ <div id="c2"><img src="../moz.png" alt="mozilla"></div>
+ <div id="c3">ho</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/text/test_general.xhtml b/accessible/tests/mochitest/text/test_general.xhtml
new file mode 100644
index 0000000000..df0ffcc0c6
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_general.xhtml
@@ -0,0 +1,79 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Tests: XUL label text interface">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Testing
+
+ var gQueue = null;
+ function doTests()
+ {
+ //////////////////////////////////////////////////////////////////////////
+ // XUL label
+
+ var ids = ["label1", "label2"];
+
+ testCharacterCount(ids, 5);
+
+ testText(ids, 0, -1, "Hello");
+ testText(ids, 0, 1, "H");
+
+ testCharAfterOffset(ids, 0, "e", 1, 2);
+ testCharBeforeOffset(ids, 1, "H", 0, 1);
+ testCharAtOffset(ids, 1, "e", 1, 2);
+
+ //////////////////////////////////////////////////////////////////////////
+ // HTML input
+
+ testTextAtOffset([ getNode("tbox1") ], BOUNDARY_LINE_START,
+ [ [ 0, 4, "test", 0, 4 ] ]);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ ]]>
+ </script>
+
+ <vbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=396166"
+ title="xul:label@value accessible should implement nsIAccessibleText">
+ Bug 396166
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=899433"
+ title="Accessibility returns empty line for last line in certain cases">
+ Bug 899433
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+ <label id="label1" value="Hello"/>
+ <label id="label2">Hello</label>
+
+ <html:input id="tbox1" value="test"/>
+ </vbox>
+</window>
diff --git a/accessible/tests/mochitest/text/test_gettext.html b/accessible/tests/mochitest/text/test_gettext.html
new file mode 100644
index 0000000000..2f221a416b
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_gettext.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Get text between offsets tests</title>
+ <meta charset="utf-8">
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ //
+ // __h__e__l__l__o__ __m__y__ __f__r__i__e__n__d__
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+
+ var IDs = [ "i1", "d1", "d1wrap", "e1", "t1" ];
+
+ testCharacterCount(IDs, 15);
+
+ testText(IDs, 0, 1, "h");
+ testText(IDs, 1, 3, "el");
+ testText(IDs, 14, 15, "d");
+ testText(IDs, 0, 15, "hello my friend");
+ testText(IDs, 0, -1, "hello my friend");
+
+ // ////////////////////////////////////////////////////////////////////////
+ //
+ // __B__r__a__v__e__ __S__i__r__ __ __R__o__b__i__n__ __ __ __r__a__n
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
+
+ IDs = [ "i2", "dpre2", "epre2", "t2" ];
+
+ testCharacterCount(IDs, 22);
+
+ testText(IDs, 0, 1, "B");
+ testText(IDs, 5, 6, " ");
+ testText(IDs, 9, 11, " ");
+ testText(IDs, 16, 19, " ");
+ testText(IDs, 0, 22, "Brave Sir Robin ran");
+ testText(IDs, 0, -1, "Brave Sir Robin ran");
+
+ testCharacterCount(["d2", "e2"], 19);
+ testText(["d2", "e2"], 0, 19, "Brave Sir Robin ran");
+
+ // ////////////////////////////////////////////////////////////////////////
+ //
+ // __o__n__e__w__o__r__d__\n
+ // 0 1 2 3 4 5 6 7
+ // __\n
+ // 8
+ // __t__w__o__ __w__o__r__d__s__\n
+ // 9 10 11 12 13 14 15 16 17 18
+
+ IDs = ["d3", "dbr3", "e3", "ebr3", "t3"];
+
+ testCharacterCount(IDs, 19);
+
+ testText(IDs, 0, 19, "oneword\n\ntwo words\n");
+ testText(IDs, 0, -1, "oneword\n\ntwo words\n");
+
+ // ////////////////////////////////////////////////////////////////////////
+ //
+ // CSS text-transform
+ //
+ // Content with `text-transform:uppercase | lowercase | capitalize` returns
+ // the transformed content.
+ //
+ testText(["d4a"], 0, -1, "HELLO MY FRIEND");
+ testText(["d4b"], 0, -1, "hello my friend");
+ testText(["d4c"], 0, -1, "Hello My Friend");
+
+ // `text-transform: full-width | full-size-kana` should not be reflected in
+ // a11y.
+ testText(["d5a"], 0, -1, "hello my friend");
+ testText(["d5b"], 0, -1, "ゕゖヵヶ");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="i1" value="hello my friend"/>
+ <div id="d1">hello my friend</div>
+ <div id="d1wrap" style="word-wrap:break-word; width:1px">hello my friend</div>
+ <div id="e1" contenteditable="true">hello my friend</div>
+ <textarea id="t1">hello my friend</textarea>
+
+ <input id="i2" value="Brave Sir Robin ran"/>
+ <pre><div id="dpre2">Brave Sir Robin ran</div></pre>
+ <pre><div id="epre2" contenteditable="true">Brave Sir Robin ran</div></pre>
+ <textarea id="t2" cols="300">Brave Sir Robin ran</textarea>
+ <div id="d2">Brave Sir Robin ran</div>
+ <div id="e2" contenteditable="true">Brave Sir Robin ran</div>
+
+ <pre>
+ <div id="d3">oneword
+
+two words
+</div>
+ <div id="dbr3">oneword<br/><br/>two words<br/></div>
+ <div id="e3" contenteditable="true">oneword
+
+two words
+</div>
+ <div id="ebr3" contenteditable="true">oneword<br/><br/>two words<br/></div>
+ <textarea id="t3" cols="300">oneword
+
+two words
+</textarea>
+ </pre>
+
+ <div id="d4a" style="text-transform:uppercase">Hello My Friend</div>
+ <div id="d4b" style="text-transform:lowercase">Hello My Friend</div>
+ <div id="d4c" style="text-transform:capitalize">hello my friend</div>
+
+ <div id="d5a" style="text-transform:full-width">hello my friend</div>
+ <div id="d5b" style="text-transform:full-size-kana">ゕゖヵヶ</div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/text/test_hypertext.html b/accessible/tests/mochitest/text/test_hypertext.html
new file mode 100644
index 0000000000..b8a289ea52
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_hypertext.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>nsIAccessibleText getText related function tests for rich text</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <style>
+ #listitemnone {
+ list-style-type: none;
+ }
+ h6.gencontent:before {
+ content: "aga"
+ }
+ </style>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // null getText
+ // ////////////////////////////////////////////////////////////////////////
+
+ var emptyTextAcc = getAccessible("nulltext", [nsIAccessibleText]);
+ is(emptyTextAcc.getText(0, -1), "", "getText() END_OF_TEXT with null string");
+ is(emptyTextAcc.getText(0, 0), "", "getText() Len==0 with null string");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // hypertext
+ // ////////////////////////////////////////////////////////////////////////
+
+ // ! - embedded object char
+ // __h__e__l__l__o__ __!__ __s__e__e__ __!__
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13
+
+ var IDs = [ "hypertext", "hypertext2", "ht_displaycontents" ];
+
+ // //////////////////////////////////////////////////////////////////////
+ // characterCount
+
+ testCharacterCount(IDs, 13);
+
+ // //////////////////////////////////////////////////////////////////////
+ // getText
+
+ testText(IDs, 0, 1, "h");
+ testText(IDs, 5, 7, " " + kEmbedChar);
+ testText(IDs, 10, 13, "e " + kEmbedChar);
+ testText(IDs, 0, 13, "hello " + kEmbedChar + " see " + kEmbedChar);
+
+ // //////////////////////////////////////////////////////////////////////
+ // getTextAtOffset line boundary
+
+ testTextAtOffset(0, BOUNDARY_LINE_START, "line ", 0, 5,
+ "hypertext3", kOk, kOk, kOk);
+
+ // XXX: see bug 634202.
+ testTextAtOffset(0, BOUNDARY_LINE_START, "line ", 0, 5,
+ "hypertext4", kOk, kOk, kOk);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // list
+ // ////////////////////////////////////////////////////////////////////////
+
+ IDs = [ "list" ];
+ testCharacterCount(IDs, 2);
+ testText(IDs, 0, 2, kEmbedChar + kEmbedChar);
+
+ IDs = [ "listitem" ];
+ testCharacterCount(IDs, 6);
+ testText(IDs, 0, 6, "1. foo");
+
+ IDs = [ "listitemnone" ];
+ testCharacterCount(IDs, 3);
+ testText(IDs, 0, 3, "bar");
+
+ testText(["testbr"], 0, 3, "foo");
+
+ testTextAtOffset(2, nsIAccessibleText.BOUNDARY_CHAR, "o", 2, 3, "testbr",
+ kOk, kOk, kOk);
+ testTextAtOffset(2, nsIAccessibleText.BOUNDARY_WORD_START, "foo\n", 0, 4,
+ "testbr", kOk, kOk, kOk);
+ testTextBeforeOffset(2, nsIAccessibleText.BOUNDARY_LINE_START, "foo\n",
+ 0, 4, "testbr", kTodo, kOk, kTodo);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Fix getText"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=630001">
+ Bug 630001, part3
+ </a>
+ <a target="_blank"
+ title="getTextAtOffset line boundary may return more than one line"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=638326">
+ Bug 638326
+ </a>
+ <a target="_blank"
+ title="getText(0, -1) fails with empty text"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=749810">
+ Bug 749810
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="nulltext"></div>
+
+ <div id="hypertext">hello <a>friend</a> see <img src="about:blank"></div>
+ <div id="hypertext2">hello <a>friend</a> see <input></div>
+ <div id="ht_displaycontents">hello <a>friend</a> see <ul id="ul" style="display: contents;">
+ <li>Supermarket 1</li>
+ <li>Supermarket 2</li>
+ </ul></div>
+ <ol id="list">
+ <li id="listitem">foo</li>
+ <li id="listitemnone">bar</li>
+ </ol>
+
+ <div id="hypertext3">line
+<!-- haha -->
+<!-- hahaha -->
+<h6>heading</h6>
+ </div>
+
+ <div id="hypertext4">line
+<!-- haha -->
+<!-- hahaha -->
+<h6 role="presentation" class="gencontent">heading</h6>
+ </div>
+
+ <div id="testbr">foo<br/></div>
+
+ <div></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/text/test_lineboundary.html b/accessible/tests/mochitest/text/test_lineboundary.html
new file mode 100644
index 0000000000..77b35ece5d
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_lineboundary.html
@@ -0,0 +1,422 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Line boundary getText* functions tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+ <script type="application/javascript">
+ function doTest() {
+ testTextAtOffset("line_test_1", BOUNDARY_LINE_START,
+ [[0, 6, "Line 1 ", 0, 7],
+ // See the kOk test below.
+ // [7, 7, kEmbedChar, 7, 8],
+ [8, 15, "Line 3 ", 8, 15]]);
+ testTextAtOffset(/* aOffset */ 7, BOUNDARY_LINE_START,
+ kEmbedChar, /* aStartOffset */ 7, /* aEndOffset */ 8,
+ "line_test_1",
+ /* returned text */ kOk,
+ /* returned start offset */ kOk, /* returned end offset */ kOk);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // __h__e__l__l__o__ __m__y__ __f__r__i__e__n__d__
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+
+ var IDs = [ "input", "div", "editable", "textarea",
+ getNode("ta", getNode("ta_cntr").contentDocument) ];
+
+ testTextBeforeOffset(IDs, BOUNDARY_LINE_START,
+ [ [ 0, 15, "", 0, 0 ] ]);
+ testTextBeforeOffset(IDs, BOUNDARY_LINE_END,
+ [ [ 0, 15, "", 0, 0 ] ]);
+
+ testTextAtOffset(IDs, BOUNDARY_LINE_START,
+ [ [ 0, 15, "hello my friend", 0, 15 ] ]);
+ testTextAtOffset(IDs, BOUNDARY_LINE_END,
+ [ [ 0, 15, "hello my friend", 0, 15 ] ]);
+
+ testTextAfterOffset(IDs, BOUNDARY_LINE_START,
+ [ [ 0, 15, "", 15, 15 ] ]);
+ testTextAfterOffset(IDs, BOUNDARY_LINE_END,
+ [ [ 0, 15, "", 15, 15 ] ]);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // __o__n__e__w__o__r__d__\n
+ // 0 1 2 3 4 5 6 7
+ // __\n
+ // 8
+ // __t__w__o__ __w__o__r__d__s__\n
+ // 9 10 11 12 13 14 15 16 17 18
+
+ IDs = [ "ml_div", "ml_divbr", "ml_editable", "ml_editablebr", "ml_textarea"];
+
+ testTextBeforeOffset(IDs, BOUNDARY_LINE_START,
+ [ [ 0, 7, "", 0, 0 ],
+ [ 8, 8, "oneword\n", 0, 8 ],
+ [ 9, 18, "\n", 8, 9 ],
+ [ 19, 19, "two words\n", 9, 19 ]]);
+
+ testTextBeforeOffset(IDs, BOUNDARY_LINE_END,
+ [ [ 0, 7, "", 0, 0 ],
+ [ 8, 8, "oneword", 0, 7 ],
+ [ 9, 18, "\n", 7, 8 ],
+ [ 19, 19, "\ntwo words", 8, 18 ]]);
+
+ testTextAtOffset(IDs, BOUNDARY_LINE_START,
+ [ [ 0, 7, "oneword\n", 0, 8 ],
+ [ 8, 8, "\n", 8, 9 ],
+ [ 9, 18, "two words\n", 9, 19 ],
+ [ 19, 19, "", 19, 19 ]]);
+ testTextAtOffset(IDs, BOUNDARY_LINE_END,
+ [ [ 0, 7, "oneword", 0, 7 ],
+ [ 8, 8, "\n", 7, 8 ],
+ [ 9, 18, "\ntwo words", 8, 18 ],
+ [ 19, 19, "\n", 18, 19 ]]);
+
+ testTextAfterOffset(IDs, BOUNDARY_LINE_START,
+ [ [ 0, 7, "\n", 8, 9 ],
+ [ 8, 8, "two words\n", 9, 19 ],
+ [ 9, 19, "", 19, 19 ]]);
+ testTextAfterOffset(IDs, BOUNDARY_LINE_END,
+ [ [ 0, 7, "\n", 7, 8 ],
+ [ 8, 8, "\ntwo words", 8, 18 ],
+ [ 9, 18, "\n", 18, 19 ],
+ [ 19, 19, "", 19, 19 ]]);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // a * b (* is embedded char for link)
+ testTextBeforeOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_START,
+ [ [ 0, 5, "", 0, 0 ] ]);
+
+ testTextBeforeOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_END,
+ [ [ 0, 5, "", 0, 0 ] ]);
+
+ testTextAtOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_START,
+ [ [ 0, 5, "a " + kEmbedChar + " c", 0, 5 ] ]);
+
+ testTextAtOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_END,
+ [ [ 0, 5, "a " + kEmbedChar + " c", 0, 5 ] ]);
+
+ testTextAfterOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_START,
+ [ [ 0, 5, "", 5, 5 ] ]);
+
+ testTextAfterOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_END,
+ [ [ 0, 5, "", 5, 5 ] ]);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // foo<br> and foo<br><br>
+
+ testTextAtOffset([ getAccessible("ht_2").firstChild.firstChild ],
+ BOUNDARY_LINE_START,
+ [ [ 0, 3, "foo\n", 0, 4 ] ]);
+ testTextAtOffset([ getAccessible("ht_3").firstChild.firstChild ],
+ BOUNDARY_LINE_START,
+ [ [ 0, 3, "foo\n", 0, 4 ], [ 4, 4, "\n", 4, 5 ] ]);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // 'Hello world ' (\n is rendered as space)
+
+ testTextAtOffset([ "ht_4" ], BOUNDARY_LINE_START,
+ [ [ 0, 12, "Hello world ", 0, 12 ] ]);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // list items
+
+ testTextAtOffset([ "li1" ], BOUNDARY_LINE_START,
+ [ [ 0, 6, kDiscBulletText + "Item", 0, 6 ] ]);
+ testTextAtOffset([ "li2" ], BOUNDARY_LINE_START,
+ [ [ 0, 2, kDiscBulletText, 0, 2 ] ]);
+ testTextAtOffset([ "li3" ], BOUNDARY_LINE_START,
+ [ [ 0, 8, kDiscBulletText + "a long ", 0, 9 ],
+ [ 9, 12, "and ", 9, 13 ] ]);
+ testTextAtOffset([ "li4" ], BOUNDARY_LINE_START,
+ [ [ 0, 7, kDiscBulletText + "a " + kEmbedChar + " c", 0, 7 ] ]);
+ testTextAtOffset([ "li5" ], BOUNDARY_LINE_START,
+ [ [ 0, 2, kDiscBulletText + "\n", 0, 3 ],
+ [ 3, 7, "hello", 3, 8 ] ]);
+ testTextAtOffset([ "ul1" ], BOUNDARY_LINE_START,
+ [ [ 0, 0, kEmbedChar, 0, 1 ],
+ [ 1, 1, kEmbedChar, 1, 2 ],
+ [ 2, 2, kEmbedChar, 2, 3 ],
+ [ 3, 3, kEmbedChar, 3, 4 ],
+ [ 4, 5, kEmbedChar, 4, 5 ] ]);
+
+ testTextAtOffset([ "li6" ], BOUNDARY_LINE_START,
+ [ [ 0, 7, "1. Item", 0, 7 ] ]);
+ testTextAtOffset([ "li7" ], BOUNDARY_LINE_START,
+ [ [ 0, 3, "2. ", 0, 3 ] ]);
+ testTextAtOffset([ "li8" ], BOUNDARY_LINE_START,
+ [ [ 0, 9, "3. a long ", 0, 10 ],
+ [ 10, 13, "and ", 10, 14 ] ]);
+ testTextAtOffset([ "li9" ], BOUNDARY_LINE_START,
+ [ [ 0, 8, "4. a " + kEmbedChar + " c", 0, 8 ] ]);
+ testTextAtOffset([ "li10" ], BOUNDARY_LINE_START,
+ [ [ 0, 3, "5. \n", 0, 4 ],
+ [ 4, 8, "hello", 4, 9 ] ]);
+ testTextAtOffset([ "ol1" ], BOUNDARY_LINE_START,
+ [ [ 0, 0, kEmbedChar, 0, 1 ],
+ [ 1, 1, kEmbedChar, 1, 2 ],
+ [ 2, 2, kEmbedChar, 2, 3 ],
+ [ 3, 3, kEmbedChar, 3, 4 ],
+ [ 4, 5, kEmbedChar, 4, 5 ] ]);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Nested hypertexts
+
+ testTextAtOffset(["ht_5" ], BOUNDARY_LINE_START,
+ [ [ 0, 0, kEmbedChar, 0, 1 ] ]);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Block followed by list
+
+ testTextAtOffset([ "block_then_ul" ], BOUNDARY_LINE_START,
+ [ [ 0, 0, kEmbedChar, 0, 1 ],
+ [ 1, 1, kEmbedChar, 1, 2 ] ]);
+
+ // Embedded char containing a line break breaks line offsets in parent.
+ testTextAtOffset([ "brInEmbed" ], BOUNDARY_LINE_START,
+ [ [0, 1, "a " + kEmbedChar, 0, 3],
+ [2, 2, "a " + kEmbedChar + " d", 0, 5],
+ [3, 5, kEmbedChar + " d", 2, 5] ]);
+ testTextAtOffset([ "brInEmbedAndBefore" ], BOUNDARY_LINE_START,
+ [ [0, 1, "a\n", 0, 2],
+ [2, 3, "b " + kEmbedChar, 2, 5],
+ [4, 4, "b " + kEmbedChar + " e", 2, 7],
+ [5, 7, kEmbedChar + " e", 4, 7] ]);
+ testTextAtOffset([ "brInEmbedAndAfter" ], BOUNDARY_LINE_START,
+ [ [0, 1, "a " + kEmbedChar, 0, 3],
+ [2, 2, "a " + kEmbedChar + " d\n", 0, 6],
+ [3, 5, kEmbedChar + " d\n", 2, 6],
+ [6, 7, "e", 6, 7] ]);
+ testTextAtOffset([ "brInEmbedAndBlockElementAfter" ], BOUNDARY_LINE_START,
+ [ [0, 2, "a " + kEmbedChar, 0, 3],
+ [3, 4, kEmbedChar, 3, 4] ]);
+ testTextAtOffset([ "brInEmbedThenTextThenBlockElement" ], BOUNDARY_LINE_START,
+ [ [0, 1, "a " + kEmbedChar, 0, 3],
+ [2, 2, "a " + kEmbedChar + " d", 0, 5],
+ [3, 4, kEmbedChar + " d", 2, 5],
+ [5, 6, kEmbedChar, 5, 6] ]);
+ testTextAtOffset([ "noBrInEmbedButOneBefore" ], BOUNDARY_LINE_START,
+ [ [0, 1, "a\n", 0, 2],
+ [2, 7, "b " + kEmbedChar + " d", 2, 7] ]);
+ testTextAtOffset([ "noBrInEmbedButOneAfter" ], BOUNDARY_LINE_START,
+ [ [0, 3, "a " + kEmbedChar + "\n", 0, 4],
+ [4, 5, "c", 4, 5] ]);
+ testTextAtOffset([ "twoEmbedsWithBRs" ], BOUNDARY_LINE_START,
+ [ [0, 1, "a " + kEmbedChar, 0, 3],
+ [2, 2, "a " + kEmbedChar + kEmbedChar, 0, 4],
+ [3, 3, kEmbedChar + kEmbedChar + " f", 2, 6],
+ [4, 6, kEmbedChar + " f", 3, 6] ]);
+
+ // Inline block span with nested spans and BRs
+ testTextAtOffset([ "inlineBlockWithSpansAndBrs" ], BOUNDARY_LINE_START,
+ [ [0, 1, "a\n", 0, 2],
+ [2, 3, "b\n", 2, 4],
+ [4, 5, "c", 4, 5] ]);
+
+ // Spans with BRs and whitespaces.
+ testTextAtOffset([ "spansWithWhitespaces" ], BOUNDARY_LINE_START,
+ [ [0, 6, "Line 1\n", 0, 7],
+ [7, 13, "Line 2\n", 7, 14],
+ [14, 20, "Line 3\n", 14, 21],
+ [21, 27, "Line 4\n", 21, 28],
+ [28, 28, "", 28, 28] ]);
+
+ // A line with an empty display: contents leaf in the middle.
+ testTextAtOffset([ "displayContents" ], BOUNDARY_LINE_START,
+ // See the kOk test below.
+ // [ [0, 3, `a${kEmbedChar}b`, 0, 3] ]);
+ [ [0, 0, `a${kEmbedChar}b`, 0, 3],
+ [2, 3, `a${kEmbedChar}b`, 0, 3] ]);
+ testTextAtOffset(/* aOffset */ 1, BOUNDARY_LINE_START,
+ `a${kEmbedChar}b`, /* aStartOffset */ 0, /* aEndOffset */ 3,
+ "displayContents",
+ /* returned text */ kOk,
+ /* returned start offset */ kOk,
+ /* returned end offset */ kOk);
+
+ // A line which wraps, followed by a br, followed by another line.
+ testTextAtOffset([ "brAfterWrapped" ], BOUNDARY_LINE_START,
+ [ [0, 1, "a ", 0, 2],
+ [2, 3, "b\n", 2, 4],
+ [4, 5, "c", 4, 5] ]);
+
+ testTextAtOffset([ "inlineInput" ], BOUNDARY_LINE_END,
+ [ [0, 1, "a", 0, 1],
+ [2, 7, `\nb ${kEmbedChar} d`, 1, 7,
+ [ [ 4, "inlineInput", kOk, kOk, kOk] ] ] ]);
+
+ testTextAtOffset([ "inlineInput2" ], BOUNDARY_LINE_END,
+ [ [0, 1, "a", 0, 1],
+ [2, 7, `\n${kEmbedChar} c d`, 1, 7,
+ [ [ 2, "inlineInput2", kOk, kOk, kOk] ] ] ]);
+
+ testTextAtOffset([ "inlineInput3" ], BOUNDARY_LINE_END,
+ [ [0, 1, "a", 0, 1],
+ [2, 8, `\nb${kEmbedChar} c d`, 1, 8,
+ [ [ 3, "inlineInput3", kOk, kOk, kOk] ] ] ]);
+
+ testTextAtOffset([ "inlineInput4" ], BOUNDARY_LINE_END,
+ [ [0, 1, "a", 0, 1],
+ [2, 7, `\n${kEmbedChar}b c d`, 1, 8,
+ [ [ 2, "inlineInput4", kOk, kOk, kOk ] ] ] ]);
+
+ testTextAtOffset(/* aOffset */ 0, BOUNDARY_LINE_START,
+ kEmbedChar, 0, 1, "contentEditableTable",
+ /* returned text */ kOk,
+ /* returned start offset */ kOk,
+ /* returned end offset */ kOk);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="getTextAtOffset for word boundaries: beginning of a new life"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=853340">
+ Bug 853340
+ </a>
+ <a target="_blank"
+ title="getTextBeforeOffset for word boundaries: evolving"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=855732">
+ Bug 855732
+ </a>
+ <a target="_blank"
+ title=" getTextAfterOffset for line boundary on new rails"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=882292">
+ Bug 882292
+ </a>
+ <a target="_blank"
+ title="getTextAtOffset broken for last object when closing tag is preceded by newline char"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=947170">
+ Bug 947170
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="input" value="hello my friend"/>
+ <div id="div">hello my friend</div>
+ <div id="editable" contenteditable="true">hello my friend</div>
+ <textarea id="textarea">hello my friend</textarea>
+ <iframe id="ta_cntr"
+ src="data:text/html,<html><body><textarea id='ta'>hello my friend</textarea></body></html>"></iframe>
+
+ <pre>
+ <div id="ml_div" style="border-style:outset;">oneword
+
+two words
+</div>
+ <div id="ml_divbr" style="border-style:outset;">oneword<br/><br/>two words<br/></div>
+ <div id="ml_editable" style="border-style:outset;" contenteditable="true">oneword
+
+two words
+</div>
+ <div id="ml_editablebr" contenteditable="true" style="border-style:outset;">oneword<br/><br/>two words<br/></div>
+ <textarea id="ml_textarea" cols="300">oneword
+
+two words
+</textarea>
+ </pre>
+
+ <iframe id="ht_1" src="data:text/html,<html><body>a <a href=''>b</a> c</body></html>"></iframe>
+
+ <iframe id="ht_2" src="data:text/html,<div contentEditable='true'>foo<br/></div>"></iframe>
+ <iframe id="ht_3" src="data:text/html,<div contentEditable='true'>foo<br/><br/></div>"></iframe>
+
+ <p id="ht_4">Hello world
+</p>
+
+ <ul id="ul1">
+ <li id="li1">Item</li>
+ <li id="li2"></li>
+ <li id="li3" style="font-family:monospace; font-size:10pt; width:8ch;">a long and winding road that lead me to your door</li>
+ <li id="li4">a <a href=''>b</a> c</li>
+ <li id="li5"><br>hello</li>
+ </ul>
+
+ <ol id="ol1">
+ <li id="li6">Item</li>
+ <li id="li7"></li>
+ <li id="li8" style="font-family:monospace; font-size:10pt; width:8ch;">a long and winding road that lead me to your door</li>
+ <li id="li9">a <a href=''>b</a> c</li>
+ <li id="li10"><br>hello</li>
+ </ol>
+
+ <div id="ht_5">
+ <div>
+ <p>sectiounus</p>
+ <p>seciofarus</p>
+ </div>
+ </div>
+ <div id="line_test_1">
+ Line 1
+ <center><input type="TEXT"><input value="Button" type="SUBMIT"></center>
+ Line 3
+ </div>
+
+ <div id="block_then_ul">
+ <p>Block</p>
+ <ul><li>Li</li></ul>
+ </div>
+ <div id="brInEmbed" contenteditable>a <a href="https://mozilla.org/">b<br>c</a> d</div>
+ <div id="brInEmbedAndBefore">a<br>b <a href="https://mozilla.org/">c<br>d</a> e</div>
+ <div id="brInEmbedAndAfter">a <a href="https://mozilla.org/">b<br>c</a> d<br>e</div>
+ <div id="brInEmbedAndBlockElementAfter">a <a href="https://mozilla.org/">b<br>c</a><p>d</p></div>
+ <div id="brInEmbedThenTextThenBlockElement">a <a href="https://mozilla.org/">b<br>c</a> d<p>e</p></div>
+ <div id="noBrInEmbedButOneBefore">a<br>b <a href="https://mozilla.org/">c</a> d</div>
+ <div id="noBrInEmbedButOneAfter">a <a href="https://mozilla.org/">b</a><br>c</div>
+ <div id="twoEmbedsWithBRs">a <a href="https://mozilla.org">b<br>c</a><a href="https://mozilla.org">d<br>e</a> f</div>
+ <span id="inlineBlockWithSpansAndBrs" style="display: inline-block;"><span>a<br>b<br><span></span></span>c</span>
+ <div id="spansWithWhitespaces"> <!-- Don't indent the following block -->
+<span>Line 1<br/>
+</span>
+<span>Line 2<br/>
+</span>
+<span>Line 3<br/>
+</span>
+<span>Line 4<br/>
+</span></div><!-- OK to indent again -->
+ <div id="displayContents">a<ul style="display: contents;"><li style="display: contents;"></li></ul>b</div>
+ <div id="brAfterWrapped" style="width: 10px;">a b<br>c</div>
+ <div id="inlineInput">a<br>b <input value="c"> d</div>
+ <div id="inlineInput2">a<br><input value="b"> c d</div>
+ <div id="inlineInput3">a<br> b<input value=""> c d</div>
+ <div id="inlineInput4">a<br><input value="">b c d</div>
+ <div id="contentEditableTable" contenteditable>
+ <table style="display: inline-table">
+ <thead>
+ <th>Foo</th>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Bar</td>
+ </tr>
+ </tbody>
+ </table>
+ <br>
+ <table style="display: inline-table">
+ <thead>
+ <th>Foo</th>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Bar</td>
+ </tr>
+ </tbody>
+ </table>
+ <br>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/text/test_paragraphboundary.html b/accessible/tests/mochitest/text/test_paragraphboundary.html
new file mode 100644
index 0000000000..8b270b661c
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_paragraphboundary.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Paragraph boundary getText* functions tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+ <script type="application/javascript">
+ function doTest() {
+ // First, test the contentEditable.
+ testTextAtOffset("ce", BOUNDARY_PARAGRAPH,
+ [[0, 0, kEmbedChar, 0, 1],
+ [1, 2, kEmbedChar, 1, 2]]);
+
+ // Now, test each paragraph.
+ var ID = getNode("ce").firstElementChild;
+ testTextAtOffset(ID, BOUNDARY_PARAGRAPH,
+ [[0, 15, "hello my friend", 0, 15]]);
+ ID = getNode("ce").lastElementChild;
+ testTextAtOffset(ID, BOUNDARY_PARAGRAPH,
+ [[0, 11, "hello again", 0, 11]]);
+
+ // Test a paragraph whose line forcefully wraps.
+ testTextAtOffset("forced_wrap", BOUNDARY_PARAGRAPH,
+ [[0, 2, "ab", 0, 2]]);
+
+ // Test paragraphs with a few line breaks.
+ testTextAtOffset("forced_br", BOUNDARY_PARAGRAPH,
+ [[0, 1, "a\n", 0, 2], // a and br treated as a paragraph
+ [2, 3, "b\n", 2, 4], // b treated as a paragraph, excl 2nd line break
+ [4, 4, "\n", 4, 5], // second br treated as a separate paragraph
+ [5, 6, "c", 5, 6]]); // Last paragraph treated as usual
+ testTextAtOffset("br_at_beginning", BOUNDARY_PARAGRAPH,
+ [[0, 0, "\n", 0, 1], // br treated as a separate paragraph
+ [1, 2, "a\n", 1, 3], // a and br treated as a paragraph
+ [3, 4, "b", 3, 4]]); // b treated as last paragraph
+
+ // Test a paragraph with an embedded link.
+ testTextAtOffset("pWithLink", BOUNDARY_PARAGRAPH,
+ [[0, 3, "a" + kEmbedChar + "d", 0, 3]]);
+ testTextAtOffset("link", BOUNDARY_PARAGRAPH,
+ [[0, 2, "bc", 0, 2]]);
+
+ // Paragraph with link that contains a line break.
+ testTextAtOffset("pWithLinkWithBr", BOUNDARY_PARAGRAPH,
+ [[0, 0, "a" + kEmbedChar, 0, 2],
+ [1, 1, "a" + kEmbedChar + "d", 0, 3],
+ [2, 3, kEmbedChar + "d", 1, 3]]);
+
+ // Test a list and list item
+ testTextAtOffset("ul", BOUNDARY_PARAGRAPH,
+ [[0, 0, kEmbedChar, 0, 1],
+ [1, 2, kEmbedChar, 1, 2]]);
+ testTextAtOffset("li1", BOUNDARY_PARAGRAPH,
+ [[0, 3, "• a", 0, 3]]);
+ testTextAtOffset("li2", BOUNDARY_PARAGRAPH,
+ [[0, 3, "• a", 0, 3]]);
+ // Test a list item containing multiple text leaf nodes.
+ testTextAtOffset("liMultiLeaf", BOUNDARY_PARAGRAPH,
+ [[0, 4, "• ab", 0, 4]]);
+
+ // Test line breaks in a textarea.
+ testTextAtOffset("textarea", BOUNDARY_PARAGRAPH,
+ [[0, 1, "a\n", 0, 2],
+ [2, 3, "b\n", 2, 4],
+ [4, 4, "\n", 4, 5],
+ [5, 6, "c", 5, 6]]);
+
+ // Test that a textarea has a blank paragraph at the end if it contains
+ // a line break as its last character.
+ testTextAtOffset("textarea_with_trailing_br", BOUNDARY_PARAGRAPH,
+ [[0, 15, "This is a test.\n", 0, 16],
+ [16, 16, "", 16, 16]]);
+
+ // Paragraph with a presentational line break.
+ testTextAtOffset("presentational_br", BOUNDARY_PARAGRAPH,
+ [[0, 3, "a b", 0, 3]]);
+
+ // Two paragraphs in a div, non-editable case.
+ testTextAtOffset("two_paragraphs", BOUNDARY_PARAGRAPH,
+ [[0, 0, kEmbedChar, 0, 1],
+ [1, 2, kEmbedChar, 1, 2]]);
+
+ // Div containing a paragraph containing a link
+ testTextAtOffset("divWithParaWithLink", BOUNDARY_PARAGRAPH,
+ [[0, 0, kEmbedChar, 0, 1],
+ [1, 2, "b", 1, 2]]);
+
+ // Two text nodes and a br
+ testTextAtOffset("twoTextNodesAndBr", BOUNDARY_PARAGRAPH,
+ [[0, 2, "ab\n", 0, 3],
+ [3, 3, "", 3, 3]]);
+
+ // Link followed by a paragraph.
+ testTextAtOffset("linkThenPara", BOUNDARY_PARAGRAPH,
+ [[0, 0, kEmbedChar, 0, 1],
+ [1, 2, kEmbedChar, 1, 2]]);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="getTextAtOffset for paragraph boundaries"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1520779">
+ Bug 1520779
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="ce" contenteditable="true">
+ <p>hello my friend</p>
+ <p>hello again</p>
+ </div>
+ <p id="forced_wrap" style="width: 1px; word-break: break-all;">ab</p>
+ <p id="forced_br">a<br>b<br><br>c</p>
+ <p id="br_at_beginning"><br>a<br>b</p>
+ <p id="pWithLink">a<a id="link" href="https://example.com/">bc</a>d</p>
+ <p id="pWithLinkWithBr">a<a href="#">b<br>c</a>d</p>
+ <ul id="ul"><li id="li1">a</li><li>b</li></ul>
+ <style>#li2::marker { content:'\2022\0020'; }</style>
+ <ul id="ul"><li id="li2">a</li><li>b</li></ul>
+ <ul><li id="liMultiLeaf">a<span>b</span></li></ul>
+ <textarea id="textarea">a
+b
+
+c</textarea> <!-- This must be outdented for a correct test case -->
+ <textarea id="textarea_with_trailing_br">This is a test.
+</textarea> <!-- This must be outdented for a correct test case -->
+ <p id="presentational_br" style="white-space: pre-wrap;">a<span> <br role="presentation"></span>b</p>
+ <div id="two_paragraphs"><p>a</p><p>b</p></div>
+ <div id ="divWithParaWithLink"><p><a href="#">a</a></p>b</div>
+ <p id="twoTextNodesAndBr">a<span>b</span><br></p>
+ <div id="linkThenPara"><a href="#">a</a><p>b</p></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/text/test_passwords.html b/accessible/tests/mochitest/text/test_passwords.html
new file mode 100644
index 0000000000..fc184f2d45
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_passwords.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>nsIAccessibleText getText related function tests for text and password inputs</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // regular text and password inputs
+ // ////////////////////////////////////////////////////////////////////////
+
+ // //////////////////////////////////////////////////////////////////////
+ // characterCount and getText for regular text field
+
+ var IDs = [ "username" ];
+ testCharacterCount(IDs, 4);
+ testText(IDs, 0, 4, "test");
+
+ // //////////////////////////////////////////////////////////////////////
+ // characterCount and getText for password field
+
+ IDs = [ "password" ];
+ testCharacterCount(IDs, 4);
+ let password = document.getElementById("password");
+ let editor = SpecialPowers.wrap(password).editor;
+ let passwordMask = editor.passwordMask;
+ testText(IDs, 0, 4, `${passwordMask}${passwordMask}${passwordMask}${passwordMask}`);
+ // a11y data is updated at next tick so that we need to refresh here.
+ editor.unmask(0, 2);
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0);
+ testText(IDs, 0, 4, `te${passwordMask}${passwordMask}`);
+ editor.unmask(2, 4);
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0);
+ testText(IDs, 0, 4, `${passwordMask}${passwordMask}st`);
+ editor.unmask(0, 4);
+ SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0);
+ testText(IDs, 0, 4, `test`);
+ SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="mochitest for getText for password fields"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=415943">Mozilla Bug 415943</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <form action="post.php" method="post">
+ <label for="username">User name:</label>
+ <input id="username" value="test"><br />
+ <label for="password">Password:</label>
+ <input type="password" id="password" value="test"/>
+ </form>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/text/test_selection.html b/accessible/tests/mochitest/text/test_selection.html
new file mode 100644
index 0000000000..ce83e57086
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_selection.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test text selection functions</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+
+ <script type="application/javascript">
+
+ function doTest() {
+ // Test selection count: clean selection / check count.
+ testTextAddSelection("div0", 0, 2, 1); // |Test selection...
+ cleanTextSelections("div0");
+ testTextSelectionCount("div0", 0);
+
+ // Test addition: adding two equal selections, the second one should
+ // not be added.
+ testTextAddSelection("div1", 7, 9, 1); // Test ad|di|ng two...
+ testTextAddSelection("div1", 7, 9, 1); // Test ad|di|ng two...
+ testTextGetSelection("div1", 7, 9, 0);
+
+ // Test overlapping selections: adding three selections, one adjacent.
+ testTextAddSelection("div2", 0, 3, 1); // |Tes|t adding 3...
+ testTextAddSelection("div2", 7, 9, 2); // |Tes|t ad|di|ng 3...
+ testTextAddSelection("div2", 3, 4, 3); // |Tes||t| ad|di|ng 3...
+ testTextGetSelection("div2", 0, 3, 0);
+ testTextGetSelection("div2", 3, 4, 1);
+ testTextGetSelection("div2", 7, 9, 2);
+
+ // Test selection re-ordering: adding two selections.
+ // NOTE: removeSelections aSelectionIndex is from start of document.
+ testTextAddSelection("div3", 0, 3, 1); // |Tes|t adding 2...
+ testTextAddSelection("div3", 7, 9, 2); // |Tes|t ad|di|ng 2...
+ testTextRemoveSelection("div3", 4, 1); // Test ad|di|ng 2...
+
+ // Test extending existing selection.
+ // NOTE: setSelectionBounds aSelectionIndex is from start of document.
+ testTextAddSelection("div4", 4, 5, 1); // Test| |extending...
+ testTextSetSelection("div4", 4, 9, 6, 1); // Test| exte|nding...
+
+ // Test moving an existing selection.
+ // NOTE: setSelectionBounds aSelectionIndex is from start of document.
+ testTextAddSelection("div5", 1, 3, 1); // T|es|t moving...
+ testTextSetSelection("div5", 5, 9, 6, 1); // Test |movi|ng...
+
+ // Test adding selections to multiple inner elements.
+ testTextAddSelection("div71", 0, 3, 1); // |Tes|t adding...
+ testTextAddSelection("div71", 7, 8, 2); // |Tes|t ad|d|ing...
+ testTextAddSelection("div72", 4, 6, 1); // Test| a|dding...
+ testTextAddSelection("div72", 7, 8, 2); // Test| a|d|d|ing...
+
+ // Test adding selection to parent element.
+ // NOTE: If inner elements are represented as embedded chars
+ // we count their internal selections.
+ testTextAddSelection("div7", 7, 8, 5); // Test ad|d|ing...
+
+ // Test attempt to selected generated content.
+ // range's start is clipped to end of generated content.
+ testTextAddSelection("div8", 1, 8, 1);
+ testTextGetSelection("div8", 6, 8, 0);
+ // range's end is expanded to end of container hypertext.
+ testTextAddSelection("div8", 10, 15, 2);
+ testTextGetSelection("div8", 10, 23, 1);
+
+ testTextAddSelection("li", 0, 8, 1);
+ testTextGetSelection("li", 3, 8, 0);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+
+</script>
+</head>
+
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="div0">Test selection count</div>
+ </br>
+ <div id="div1">Test adding two equal selections </div>
+ <div id="div2">Test adding 3 selections one adjacent </div>
+ <div id="div3">Test adding 2 selections, remove first one </div>
+ <div id="div4">Test extending a selection </div>
+ <div id="div5">Test moving a selection </div>
+ </br>
+ <div id="div7">Test adding selections to parent element
+ <div id="div71">Test adding selections to inner element1 </div>
+ <div id="div72">Test adding selections to inner element2 </div>
+ </div>
+ <style>
+ #div8:before {
+ content: 'hello ';
+ }
+ #div8:after {
+ content: ', i love you';
+ }
+ </style>
+ <div id="div8">world</div>
+ <ol>
+ <li id="li">Number one</li>
+ </ol>
+
+</body>
+
+</html>
diff --git a/accessible/tests/mochitest/text/test_settext_input_event.html b/accessible/tests/mochitest/text/test_settext_input_event.html
new file mode 100644
index 0000000000..2f0ecacf30
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_settext_input_event.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test that setTextContents only sends one DOM input event</title>
+ <meta charset="utf-8" />
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript">
+ async function doTest() {
+ let input = getAccessible("input", [nsIAccessibleEditableText]);
+ let eventPromise = new Promise(resolve =>
+ document.getElementById("input").addEventListener(
+ "input", resolve, { once: true }));
+
+ input.setTextContents("goodbye");
+ let inputEvent = await eventPromise;
+ is(inputEvent.target.value, "goodbye", "input set to new value.");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="HyperTextAccessible::ReplaceText causes two distinct DOM 'input' events"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1490840">Mozilla Bug 1490840</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <input id="input" value="hello">
+</body>
+</html>
diff --git a/accessible/tests/mochitest/text/test_textBounds.html b/accessible/tests/mochitest/text/test_textBounds.html
new file mode 100644
index 0000000000..66e7f1a93f
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_textBounds.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>TextBounds tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../layout.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // Returned rect should be all 0 if no frame; e.g. display: contents.
+ testTextBounds(
+ "displayContents", 0, 0, [0, 0, 0, 0], COORDTYPE_SCREEN_RELATIVE
+ );
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <section id="displayContents" style="display: contents;"></section>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/text/test_wordboundary.html b/accessible/tests/mochitest/text/test_wordboundary.html
new file mode 100644
index 0000000000..49d4d95561
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_wordboundary.html
@@ -0,0 +1,361 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Word boundary text tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // "hello"
+ // __h__e__l__l__o__
+ // 0 1 2 3 4 5
+ var ids = [ "i1", "d1", "e1", "t1" ];
+ testTextBeforeOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "", 0, 0 ] ]);
+ testTextBeforeOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 5, "", 0, 0 ] ]);
+
+ testTextAtOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "hello", 0, 5 ] ]);
+ testTextAtOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 5, "hello", 0, 5 ] ]);
+
+ testTextAfterOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "", 5, 5 ] ]);
+ testTextAfterOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 5, "", 5, 5 ] ]);
+
+ // "hello "
+ // __h__e__l__l__o__ __
+ // 0 1 2 3 4 5 6
+ ids = [ "i2", "d2", "p2", "e2", "t2" ];
+ testTextBeforeOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 6, "", 0, 0 ] ]);
+ testTextBeforeOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 5, "", 0, 0 ],
+ [ 6, 6, "hello", 0, 5 ],
+ ]);
+
+ testTextAtOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 6, "hello ", 0, 6 ] ]);
+ testTextAtOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 4, "hello", 0, 5 ],
+ [ 5, 6, " ", 5, 6 ],
+ ]);
+
+ testTextAfterOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 6, "", 6, 6 ] ]);
+ testTextAfterOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 5, " ", 5, 6 ],
+ [ 6, 6, "", 6, 6 ],
+ ]);
+
+ // "hello all"
+ // __h__e__l__l__o__ __a__l__l__
+ // 0 1 2 3 4 5 6 7 8 9
+ ids = [ "i6", "d6", "e6", "t6" ];
+ testTextBeforeOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "", 0, 0 ],
+ [ 6, 9, "hello ", 0, 6 ]]);
+ testTextBeforeOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 5, "", 0, 0 ],
+ [ 6, 9, "hello", 0, 5 ] ]);
+
+ testTextAtOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "hello ", 0, 6 ],
+ [ 6, 9, "all", 6, 9 ] ]);
+ testTextAtOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 4, "hello", 0, 5 ],
+ [ 5, 9, " all", 5, 9 ] ]);
+
+ testTextAfterOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "all", 6, 9 ],
+ [ 6, 9, "", 9, 9 ] ]);
+ testTextAfterOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 5, " all", 5, 9 ],
+ [ 6, 9, "", 9, 9 ] ]);
+
+ // " hello all " (with whitespace collapsing)
+ // __h__e__l__l__o__ __a__l__l__ __
+ // 0 1 2 3 4 5 6 7 8 9 10
+ ids = [ "d6a", "e6a" ];
+ testTextBeforeOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "", 0, 0 ],
+ [ 6, 10, "hello ", 0, 6 ] ]);
+ testTextBeforeOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 5, "", 0, 0 ],
+ [ 6, 9, "hello", 0, 5 ],
+ [ 10, 10, " all", 5, 9 ] ]);
+
+ testTextAtOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "hello ", 0, 6 ],
+ [ 6, 10, "all ", 6, 10 ] ]);
+ testTextAtOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 4, "hello", 0, 5 ],
+ [ 5, 8, " all", 5, 9 ],
+ [ 9, 10, " ", 9, 10 ] ]);
+
+ testTextAfterOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "all ", 6, 10 ],
+ [ 6, 10, "", 10, 10 ] ]);
+ testTextAfterOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 5, " all", 5, 9 ],
+ [ 6, 9, " ", 9, 10 ],
+ [ 10, 10, "", 10, 10 ] ]);
+
+ // "hello my friend"
+ // __h__e__l__l__o__ __m__y__ __f__r__i__e__n__d__
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+ ids = [ "i7", "d7", "e7", "t7", "w7" ];
+ testTextBeforeOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "", 0, 0 ],
+ [ 6, 8, "hello ", 0, 6 ],
+ [ 9, 15, "my ", 6, 9 ] ]);
+ testTextBeforeOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 5, "", 0, 0 ],
+ [ 6, 8, "hello", 0, 5 ],
+ [ 9, 15, " my", 5, 8 ] ]);
+
+ testTextAtOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "hello ", 0, 6 ],
+ [ 6, 8, "my ", 6, 9 ],
+ [ 9, 15, "friend", 9, 15] ]);
+ testTextAtOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 4, "hello", 0, 5 ],
+ [ 5, 7, " my", 5, 8 ],
+ [ 8, 15, " friend", 8, 15] ]);
+
+ testTextAfterOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "my ", 6, 9 ],
+ [ 6, 8, "friend", 9, 15 ],
+ [ 9, 15, "", 15, 15 ] ]);
+ testTextAfterOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 5, " my", 5, 8 ],
+ [ 6, 8, " friend", 8, 15 ],
+ [ 9, 15, "", 15, 15 ] ]);
+
+ // "Brave Sir Robin ran"
+ // __B__r__a__v__e__ __S__i__r__ __ __R__o__b__i__n__ __ __ __r__a__n__
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
+ ids = [ "i8", "d8", "e8", "t8" ];
+ testTextBeforeOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "", 0, 0 ],
+ [ 6, 10, "Brave ", 0, 6 ],
+ [ 11, 18, "Sir ", 6, 11 ],
+ [ 19, 22, "Robin ", 11, 19 ] ]);
+ testTextBeforeOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 5, "", 0, 0 ],
+ [ 6, 9, "Brave", 0, 5 ],
+ [ 10, 16, " Sir", 5, 9 ],
+ [ 17, 22, " Robin", 9, 16 ] ]);
+
+ testTextAtOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "Brave ", 0, 6 ],
+ [ 6, 10, "Sir ", 6, 11 ],
+ [ 11, 18, "Robin ", 11, 19 ],
+ [ 19, 22, "ran", 19, 22 ] ]);
+ testTextAtOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 4, "Brave", 0, 5 ],
+ [ 5, 8, " Sir", 5, 9 ],
+ [ 9, 15, " Robin", 9, 16 ],
+ [ 16, 22, " ran", 16, 22 ] ]);
+
+ testTextAfterOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 5, "Sir ", 6, 11 ],
+ [ 6, 10, "Robin ", 11, 19 ],
+ [ 11, 18, "ran", 19, 22 ],
+ [ 19, 22, "", 22, 22 ] ]);
+ testTextAfterOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 5, " Sir", 5, 9 ],
+ [ 6, 9, " Robin", 9, 16 ],
+ [ 10, 16, " ran", 16, 22 ],
+ [ 17, 22, "", 22, 22 ] ]);
+
+ // 'oneword
+ // '
+ // 'two words
+ // '
+ // __o__n__e__w__o__r__d__\n
+ // 0 1 2 3 4 5 6 7
+ // __\n
+ // 8
+ // __t__w__o__ __w__o__r__d__s__\n__
+ // 9 10 11 12 13 14 15 16 17 18 19
+
+ ids = ["ml_div1", "ml_divbr1", "ml_ediv1", "ml_edivbr1", "ml_t1"];
+ testTextBeforeOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 8, "", 0, 0 ],
+ [ 9, 12, "oneword\n\n", 0, 9 ],
+ [ 13, 19, "two ", 9, 13 ] ]);
+ testTextBeforeOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 7, "", 0, 0 ],
+ [ 8, 12, "oneword", 0, 7,
+ [ [ 8, "ml_divbr1", kOk, kOk, kOk ],
+ [ 8, "ml_edivbr1", kOk, kOk, kOk ],
+ [ 9, "ml_divbr1", kOk, kOk, kOk ],
+ [ 9, "ml_edivbr1", kOk, kOk, kOk ] ] ],
+ [ 13, 18, "\n\ntwo", 7, 12 ],
+ [ 19, 19, " words", 12, 18,
+ [ [ 19, "ml_divbr1", kOk, kOk, kOk ],
+ [ 19, "ml_edivbr1", kOk, kOk, kOk ] ] ],
+ ] );
+
+ testTextAtOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 8, "oneword\n\n", 0, 9 ],
+ [ 9, 12, "two ", 9, 13 ],
+ [ 13, 19, "words\n", 13, 19 ] ]);
+ testTextAtOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 6, "oneword", 0, 7 ],
+ [ 7, 11, "\n\ntwo", 7, 12 ],
+ [ 12, 17, " words", 12, 18 ],
+ [ 18, 19, "\n", 18, 19,
+ [ [ 18, "ml_divbr1", kOk, kOk, kOk ],
+ [ 18, "ml_edivbr1", kOk, kOk, kOk ],
+ [ 19, "ml_divbr1", kOk, kOk, kOk ],
+ [ 19, "ml_edivbr1", kOk, kOk, kOk ] ] ] ]);
+
+ testTextAfterOffset(ids, BOUNDARY_WORD_START,
+ [ [ 0, 8, "two ", 9, 13 ],
+ [ 9, 12, "words\n", 13, 19 ],
+ [ 13, 19, "", 19, 19 ] ]);
+ testTextAfterOffset(ids, BOUNDARY_WORD_END,
+ [ [ 0, 7, "\n\ntwo", 7, 12 ],
+ [ 8, 12, " words", 12, 18 ],
+ [ 13, 18, "\n", 18, 19,
+ [ [ 18, "ml_divbr1", kOk, kOk, kOk ],
+ [ 18, "ml_edivbr1", kOk, kOk, kOk ] ] ],
+ [ 19, 19, "", 19, 19 ] ]);
+
+ // a <a href="#">b</a>
+ // a *
+ testTextBeforeOffset("cntr_1", BOUNDARY_WORD_START,
+ [ [ 0, 1, "", 0, 0 ],
+ [ 2, 3, "a ", 0, 2 ] ]);
+
+ testTextAtOffset("cntr_1", BOUNDARY_WORD_START,
+ [ [ 0, 1, "a ", 0, 2 ],
+ [ 2, 3, kEmbedChar, 2, 3 ] ]);
+ testTextAfterOffset("cntr_1", BOUNDARY_WORD_START,
+ [ [ 0, 1, kEmbedChar, 2, 3 ],
+ [ 2, 3, "", 3, 3 ] ]);
+
+ // Punctuation tests.
+ testTextAtOffset("punc_alone", BOUNDARY_WORD_START, [
+ [ 0, 1, "a ", 0, 2 ],
+ [ 2, 4, "@@ ", 2, 5 ],
+ [ 5, 6, "b", 5, 6 ]
+ ]);
+ testTextAtOffset("punc_begin", BOUNDARY_WORD_START, [
+ [ 0, 1, "a ", 0, 2 ],
+ [ 2, 5, "@@b ", 2, 6 ],
+ [ 6, 7, "c", 6, 7 ]
+ ]);
+ testTextAtOffset("punc_end", BOUNDARY_WORD_START, [
+ [ 0, 1, "a ", 0, 2 ],
+ [ 2, 5, "b@@ ", 2, 6 ],
+ [ 6, 7, "c", 6, 7 ]
+ ]);
+ testTextAtOffset("punc_middle", BOUNDARY_WORD_START, [
+ [ 0, 1, "a ", 0, 2 ],
+ [ 2, 4, "b@@", 2, 5 ],
+ [ 5, 6, "c ", 5, 7 ],
+ [ 7, 8, "d", 7, 8 ]
+ ]);
+ testTextAtOffset("punc_everywhere", BOUNDARY_WORD_START, [
+ [ 0, 1, "a ", 0, 2 ],
+ [ 2, 6, "@@b@@", 2, 7 ],
+ [ 7, 10, "c@@ ", 7, 11 ],
+ [ 11, 12, "d", 11, 12 ]
+ ]);
+
+ // Multi-word embedded object test.
+ testTextAtOffset("multiword_embed", BOUNDARY_WORD_START, [
+ [ 0, 1, "a ", 0, 2 ],
+ [ 2, 3, `${kEmbedChar} `, 2, 4, [
+ // Word at offset 2 returns end offset 3, should be 4.
+ [ 2, "multiword_embed", kOk, kOk, kOk ]
+ ] ],
+ [ 4, 5, "b", 4, 5 ]
+ ]);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="i1" value="hello"/>
+ <div id="d1">hello</div>
+ <div id="e1" contenteditable="true">hello</div>
+ <textarea id="t1">hello</textarea>
+
+ <input id="i2" value="hello "/>
+ <div id="d2"> hello </div>
+ <pre><div id="p2">hello </div></pre>
+ <div id="e2" contenteditable="true" style='white-space:pre'>hello </div>
+ <textarea id="t2">hello </textarea>
+
+ <input id="i6" value="hello all"/>
+ <div id="d6"> hello all</div>
+ <div id="e6" contenteditable="true">hello all</div>
+ <textarea id="t6">hello all</textarea>
+
+ <div id="d6a"> hello all </div>
+ <div id="e6a" contenteditable="true"> hello all </div>
+
+ <input id="i7" value="hello my friend"/>
+ <div id="d7"> hello my friend</div>
+ <div id="e7" contenteditable="true">hello my friend</div>
+ <textarea id="t7">hello my friend</textarea>
+ <div id="w7" style="width:1em"> hello my friend</div>
+
+ <input id="i8" value="Brave Sir Robin ran"/>
+ <pre>
+ <div id="d8">Brave Sir Robin ran</div>
+ <div id="e8" contenteditable="true">Brave Sir Robin ran</div>
+ </pre>
+ <textarea id="t8" cols="300">Brave Sir Robin ran</textarea>
+
+ <pre>
+<div id="ml_div1">oneword
+
+two words
+</div>
+<div id="ml_divbr1">oneword<br/><br/>two words<br/></div>
+<div id="ml_ediv1" contenteditable="true">oneword
+
+two words
+</div>
+<div id="ml_edivbr1" contenteditable="true">oneword<br/><br/>two words<br/></div>
+<textarea id="ml_t1" cols="300">oneword
+
+two words
+</textarea>
+ </pre>
+
+ <div id="cntr_1">a <a href="#">b</a></div>
+
+ <p id="punc_alone">a @@ b</p>
+ <p id="punc_begin">a @@b c</p>
+ <p id="punc_end">a b@@ c</p>
+ <p id="punc_middle">a b@@c d</p>
+ <p id="punc_everywhere">a @@b@@c@@ d</p>
+
+ <p id="multiword_embed">a <a href="#">x y</a> b</p>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/text/test_words.html b/accessible/tests/mochitest/text/test_words.html
new file mode 100644
index 0000000000..db61dc09ce
--- /dev/null
+++ b/accessible/tests/mochitest/text/test_words.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>nsIAccessibleText getText related function tests for html:input,html:div and html:textarea</title>
+ <meta charset="utf-8" />
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+ <script type="application/javascript">
+ if (navigator.platform.startsWith("Mac")) {
+ SimpleTest.expectAssertions(0, 1);
+ } else {
+ SimpleTest.expectAssertions(0, 1);
+ }
+
+ async function doTest() {
+ test_common();
+ for (let newSegmenter of [true, false]) {
+ for (let stopAtPunctuation of [true, false]) {
+ await test_per_segmenter(newSegmenter, stopAtPunctuation);
+ }
+ }
+ SimpleTest.finish();
+ }
+
+ function test_common() {
+ // "one two"
+ testWords("div1", ["one", "two"]);
+
+ // "one two"
+ testWords("div2", ["one", "two"]);
+
+ // "one,two"
+ testWordCount("div3", 2, kOk);
+ testWordAt("div3", 0, "one", kTodo);
+ testWordAt("div3", 1, "two", kOk);
+
+ // "one, two"
+ testWordCount("div4", 2, kOk);
+ testWordAt("div4", 0, "one", kTodo);
+ testWordAt("div4", 1, "two", kOk);
+
+ // "one+two"
+ testWordCount("div5", 2, kOk);
+ testWordAt("div5", 0, "one", kTodo);
+ testWordAt("div5", 1, "two", kOk);
+
+ // "one+two "
+ testWordCount("div6", 2, kOk);
+ testWordAt("div6", 0, "one", kTodo);
+ testWordAt("div6", 1, "two", kOk);
+
+ // "one\ntwo"
+ testWordCount("div7", 2, kOk);
+ testWordAt("div7", 0, "one", kOk);
+ testWordAt("div7", 1, "two", kTodo);
+
+ // "345"
+ testWords("div9", ["345"]);
+
+ // "3a A4"
+ testWords("div10", ["3a", "A4"]);
+
+ // "カタカナ"
+ testWords("div13", ["カタカナ"], kOk);
+
+ // "3+4*5=23"
+ testWordCount("div16", 4, kOk);
+ testWordAt("div16", 0, "3", kTodo);
+ testWordAt("div16", 1, "4", kTodo);
+ testWordAt("div16", 2, "5", kTodo);
+ testWordAt("div16", 3, "23", kTodo);
+
+ // "Hello. Friend, are you here?!"
+ testWordCount("div17", 5, kOk);
+ testWordAt("div17", 0, "Hello", kTodo);
+ testWordAt("div17", 1, "Friend", kTodo);
+ testWordAt("div17", 2, "are", kOk);
+ testWordAt("div17", 3, "you", kOk);
+ testWordAt("div17", 4, "here", kTodo);
+
+ testWords("input_1", ["foo", "bar"]);
+ }
+
+ async function test_per_segmenter(aNewSegmenter, aStopAtPunctuation) {
+ // If aNewSegmenter is true, use UAX#14/#29 compatible segmenter.
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["intl.icu4x.segmenter.enabled", aNewSegmenter],
+ ["layout.word_select.stop_at_punctuation", aStopAtPunctuation],
+ ]});
+
+ // "one.two"
+ if (!aStopAtPunctuation) {
+ testWordCount("div8", 1, kOk);
+ testWordAt("div8", 0, "one.two", kOk);
+ } else {
+ testWordCount("div8", 2, kOk);
+ testWordAt("div8", 0, "one", kTodo);
+ testWordAt("div8", 1, "two", kOk);
+ }
+
+ // "3.1416"
+ testWords("div11", ["3.1416"], !aStopAtPunctuation ? kOk : kTodo);
+
+ // "4,261.01"
+ testWords("div12", ["4,261.01"], !aStopAtPunctuation ? kOk: kTodo);
+
+ // "Peter's car"
+ testWords("div14", ["Peter's", "car"], !aStopAtPunctuation ? kOk : kTodo);
+
+ // "N.A.T.O."
+ testWords("div15", ["N.A.T.O."], !aStopAtPunctuation ? kOk : kTodo);
+
+ await SpecialPowers.popPrefEnv();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="nsIAccessibleText test word boundaries"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=452769">Mozilla Bug 452769</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ <div id="div1">one two</div>
+ <div id="div2">one two</div>
+ <div id="div3">one,two</div>
+ <div id="div4">one, two</div>
+ <div id="div5">one+two</div>
+ <div id="div6">one+two </div>
+ <div id="div7">one<br/>two</div>
+ <div id="div8">one.two</div>
+ <div id="div9">345</div>
+ <div id="div10">3a A4</div>
+ <div id="div11">3.1416</div>
+ <div id="div12">4,261.01</div>
+ <div id="div13">カタカナ</div>
+ <div id="div14">Peter's car</div>
+ <div id="div15">N.A.T.O.</div>
+ <div id="div16">3+4*5=23</div>
+ <div id="div17">Hello. Friend, are you here?!</div>
+ </pre>
+ <input id="input_1" type="text" value="foo bar" placeholder="something or other">
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/textattrs/a11y.toml b/accessible/tests/mochitest/textattrs/a11y.toml
new file mode 100644
index 0000000000..795f1fa9b7
--- /dev/null
+++ b/accessible/tests/mochitest/textattrs/a11y.toml
@@ -0,0 +1,17 @@
+[DEFAULT]
+prefs = "mathml.legacy_mathvariant_attribute.disabled=true"
+support-files = [
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/moz.png"]
+
+["test_general.html"]
+
+["test_general.xhtml"]
+
+["test_invalid.html"]
+
+["test_mathml.html"]
+
+["test_spelling.html"]
+
+["test_svg.html"]
diff --git a/accessible/tests/mochitest/textattrs/test_general.html b/accessible/tests/mochitest/textattrs/test_general.html
new file mode 100644
index 0000000000..7b29f409a2
--- /dev/null
+++ b/accessible/tests/mochitest/textattrs/test_general.html
@@ -0,0 +1,813 @@
+<html>
+
+<head>
+ <title>Text attributes tests</title>
+ <meta charset="utf-8">
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <style>
+ .gencontent:before { content: "*"; }
+ .gencontent:after { content: "*"; }
+ </style>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script src="../common.js"></script>
+ <script src="../attributes.js"></script>
+ <script src="../events.js"></script>
+
+ <script>
+ var gComputedStyle = null;
+
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // area1
+ var ID = "area1";
+ var defAttrs = buildDefaultTextAttrs(ID, "10pt");
+ testDefaultTextAttrs(ID, defAttrs);
+
+ var attrs = {};
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 7);
+
+ attrs = { "font-weight": kBoldFontWeight };
+ testTextAttrs(ID, 7, attrs, defAttrs, 7, 11);
+
+ attrs = {};
+ testTextAttrs(ID, 12, attrs, defAttrs, 11, 18);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area2
+ ID = "area2";
+ defAttrs = buildDefaultTextAttrs(ID, "14pt");
+ testDefaultTextAttrs(ID, defAttrs);
+
+ attrs = {};
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 7);
+
+ attrs = { "font-weight": kBoldFontWeight };
+ testTextAttrs(ID, 7, attrs, defAttrs, 7, 12);
+
+ var tempElem = getNode(ID).firstChild.nextSibling.firstChild.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"font-style": gComputedStyle.fontStyle,
+ "font-weight": kBoldFontWeight };
+ testTextAttrs(ID, 13, attrs, defAttrs, 12, 19);
+
+ attrs = { "font-weight": kBoldFontWeight };
+ testTextAttrs(ID, 20, attrs, defAttrs, 19, 23);
+
+ attrs = {};
+ testTextAttrs(ID, 24, attrs, defAttrs, 23, 30);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area3
+ ID = "area3";
+ defAttrs = buildDefaultTextAttrs(ID, "12pt");
+ testDefaultTextAttrs(ID, defAttrs);
+
+ tempElem = getNode(ID).firstChild.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"color": gComputedStyle.color};
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 6);
+
+ tempElem = tempElem.firstChild.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"color": gComputedStyle.color};
+ testTextAttrs(ID, 6, attrs, defAttrs, 6, 26);
+
+ tempElem = tempElem.parentNode;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"color": gComputedStyle.color};
+ testTextAttrs(ID, 26, attrs, defAttrs, 26, 27);
+
+ tempElem = tempElem.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"color": gComputedStyle.color,
+ "background-color": gComputedStyle.backgroundColor};
+ testTextAttrs(ID, 27, attrs, defAttrs, 27, 50);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area4
+ ID = "area4";
+ defAttrs = buildDefaultTextAttrs(ID, "12pt");
+ testDefaultTextAttrs(ID, defAttrs);
+
+ tempElem = getNode(ID).firstChild.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"color": gComputedStyle.color};
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 16);
+
+ tempElem = tempElem.nextSibling.firstChild.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"color": gComputedStyle.color};
+ testTextAttrs(ID, 16, attrs, defAttrs, 16, 33);
+
+ tempElem = tempElem.parentNode;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"color": gComputedStyle.color};
+ testTextAttrs(ID, 34, attrs, defAttrs, 33, 46);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area5: "Green!*!RedNormal"
+ ID = "area5";
+ defAttrs = buildDefaultTextAttrs(ID, "12pt");
+ testDefaultTextAttrs(ID, defAttrs);
+
+ // Green
+ tempElem = getNode(ID).firstChild.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"color": gComputedStyle.color};
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 5);
+
+ // br
+ attrs = {};
+ testTextAttrs(ID, 5, attrs, defAttrs, 5, 6);
+
+ // img, embedded accessible, no attributes
+ attrs = {};
+ testTextAttrs(ID, 6, attrs, {}, 6, 7);
+
+ // br
+ attrs = {};
+ testTextAttrs(ID, 7, attrs, defAttrs, 7, 8);
+
+ // Red
+ tempElem = tempElem.nextSibling.nextSibling.nextSibling.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"color": gComputedStyle.color};
+ testTextAttrs(ID, 9, attrs, defAttrs, 8, 11);
+
+ // Normal
+ attrs = {};
+ testTextAttrs(ID, 11, attrs, defAttrs, 11, 18);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area6 (CSS vertical-align property, refer to bug 445938 for details
+ // and sup and sub elements, refer to bug 735645 for details)
+ ID = "area6";
+ defAttrs = buildDefaultTextAttrs(ID, "12pt");
+ testDefaultTextAttrs(ID, defAttrs);
+
+ attrs = {};
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 5);
+
+ // Embedded object (sup) has no attributes but takes up one character.
+ testTextAttrs(ID, 5, {}, {}, 5, 6);
+
+ attrs = {};
+ testTextAttrs(ID, 6, attrs, defAttrs, 6, 20);
+
+ attrs = { "text-position": "super" };
+ testTextAttrs(ID, 20, attrs, defAttrs, 20, 28);
+
+ attrs = {};
+ testTextAttrs(ID, 28, attrs, defAttrs, 28, 32);
+
+ // Embedded object (sub) has no attributes but takes up one character.
+ testTextAttrs(ID, 32, {}, {}, 32, 33);
+
+ attrs = {};
+ testTextAttrs(ID, 33, attrs, defAttrs, 33, 38);
+
+ attrs = { "text-position": "sub" };
+ testTextAttrs(ID, 38, attrs, defAttrs, 38, 47);
+
+ attrs = {};
+ testTextAttrs(ID, 47, attrs, defAttrs, 47, 52);
+
+ attrs = { "text-position": "super" };
+ testTextAttrs(ID, 52, attrs, defAttrs, 52, 67);
+
+ attrs = {};
+ testTextAttrs(ID, 67, attrs, defAttrs, 67, 72);
+
+ attrs = { "text-position": "sub" };
+ testTextAttrs(ID, 72, attrs, defAttrs, 72, 85);
+
+ attrs = {};
+ testTextAttrs(ID, 85, attrs, defAttrs, 85, 90);
+
+ attrs = { "text-position": "super" };
+ testTextAttrs(ID, 90, attrs, defAttrs, 90, 106);
+
+ attrs = {};
+ testTextAttrs(ID, 106, attrs, defAttrs, 106, 111);
+
+ attrs = { "text-position": "sub" };
+ testTextAttrs(ID, 111, attrs, defAttrs, 111, 125);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area7
+ ID = "area7";
+ defAttrs = buildDefaultTextAttrs(ID, "12pt");
+ defAttrs.language = "en";
+ testDefaultTextAttrs(ID, defAttrs);
+
+ attrs = {"language": "ru"};
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 6);
+
+ attrs = {};
+ testTextAttrs(ID, 6, attrs, defAttrs, 6, 7);
+
+ tempElem = getNode(ID).firstChild.nextSibling.nextSibling.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = { "background-color": gComputedStyle.backgroundColor};
+ testTextAttrs(ID, 13, attrs, defAttrs, 7, 20);
+
+ attrs = {};
+ testTextAttrs(ID, 20, attrs, defAttrs, 20, 21);
+
+ attrs = {"language": "de"};
+ testTextAttrs(ID, 21, attrs, defAttrs, 21, 36);
+
+ attrs = {};
+ testTextAttrs(ID, 36, attrs, defAttrs, 36, 44);
+
+ attrs = {};
+ testTextAttrs(ID, 37, attrs, defAttrs, 36, 44);
+
+ tempElem = tempElem.nextSibling.nextSibling.nextSibling.nextSibling.firstChild.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"color": gComputedStyle.color};
+ testTextAttrs(ID, 44, attrs, defAttrs, 44, 51);
+
+ tempElem = tempElem.firstChild.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"font-weight": kBoldFontWeight,
+ "color": gComputedStyle.color};
+ testTextAttrs(ID, 51, attrs, defAttrs, 51, 55);
+
+ tempElem = tempElem.parentNode;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"color": gComputedStyle.color};
+ testTextAttrs(ID, 55, attrs, defAttrs, 55, 62);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area9, different single style spans in styled paragraph
+ ID = "area9";
+ defAttrs = buildDefaultTextAttrs(ID, "10pt");
+ testDefaultTextAttrs(ID, defAttrs);
+
+ attrs = {};
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 6);
+
+ attrs = { "font-size": "12pt" };
+ testTextAttrs(ID, 7, attrs, defAttrs, 6, 12);
+
+ attrs = {};
+ testTextAttrs(ID, 13, attrs, defAttrs, 12, 21);
+
+ // Walk to the span with the different background color
+ tempElem = getNode(ID).firstChild.nextSibling.nextSibling.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = { "background-color": gComputedStyle.backgroundColor };
+ testTextAttrs(ID, 22, attrs, defAttrs, 21, 36);
+
+ attrs = {};
+ testTextAttrs(ID, 37, attrs, defAttrs, 36, 44);
+
+ // Walk from the background color span to the one with font-style
+ tempElem = tempElem.nextSibling.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = { "font-style": gComputedStyle.fontStyle };
+ testTextAttrs(ID, 45, attrs, defAttrs, 44, 61);
+
+ attrs = {};
+ testTextAttrs(ID, 62, attrs, defAttrs, 61, 69);
+
+ // Walk from span with font-style to the one with font-family.
+ tempElem = tempElem.nextSibling.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = { "font-family": kMonospaceFontFamily };
+ testTextAttrs(ID, 70, attrs, defAttrs, 69, 83);
+
+ attrs = {};
+ testTextAttrs(ID, 84, attrs, defAttrs, 83, 91);
+
+ attrs = {
+ "text-underline-style": "solid",
+ "text-underline-color": gComputedStyle.color,
+ };
+ testTextAttrs(ID, 92, attrs, defAttrs, 91, 101);
+
+ attrs = {};
+ testTextAttrs(ID, 102, attrs, defAttrs, 101, 109);
+
+ attrs = {
+ "text-line-through-style": "solid",
+ "text-line-through-color": gComputedStyle.color,
+ };
+ testTextAttrs(ID, 110, attrs, defAttrs, 109, 122);
+
+ attrs = {};
+ testTextAttrs(ID, 123, attrs, defAttrs, 122, 130);
+
+ attrs = {
+ "text-line-through-style": "solid",
+ "text-line-through-color": gComputedStyle.color,
+ };
+ testTextAttrs(ID, 131, attrs, defAttrs, 130, 143);
+
+ attrs = {};
+ testTextAttrs(ID, 144, attrs, defAttrs, 143, 151);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area10, different single style spans in non-styled paragraph
+ ID = "area10";
+ defAttrs = buildDefaultTextAttrs(ID, "12pt");
+ testDefaultTextAttrs(ID, defAttrs);
+
+ attrs = {};
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 7);
+
+ attrs = { "font-size": "14pt" };
+ testTextAttrs(ID, 7, attrs, defAttrs, 7, 13);
+
+ attrs = {};
+ testTextAttrs(ID, 13, attrs, defAttrs, 13, 22);
+
+ // Walk to the span with the different background color
+ tempElem = getNode(ID).firstChild.nextSibling.nextSibling.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = { "background-color": gComputedStyle.backgroundColor };
+ testTextAttrs(ID, 23, attrs, defAttrs, 22, 37);
+
+ attrs = {};
+ testTextAttrs(ID, 38, attrs, defAttrs, 37, 45);
+
+ // Walk from the background color span to the one with font-style
+ tempElem = tempElem.nextSibling.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = {"font-style": gComputedStyle.fontStyle};
+ testTextAttrs(ID, 46, attrs, defAttrs, 45, 62);
+
+ attrs = {};
+ testTextAttrs(ID, 63, attrs, defAttrs, 62, 70);
+
+ // Walk from span with font-style to the one with font-family.
+ tempElem = tempElem.nextSibling.nextSibling;
+ gComputedStyle = document.defaultView.getComputedStyle(tempElem);
+ attrs = { "font-family": kMonospaceFontFamily };
+ testTextAttrs(ID, 71, attrs, defAttrs, 70, 84);
+
+ attrs = {};
+ testTextAttrs(ID, 85, attrs, defAttrs, 84, 92);
+
+ attrs = {
+ "text-underline-style": "solid",
+ "text-underline-color": gComputedStyle.color,
+ };
+ testTextAttrs(ID, 93, attrs, defAttrs, 92, 102);
+
+ attrs = {};
+ testTextAttrs(ID, 103, attrs, defAttrs, 102, 110);
+
+ attrs = {
+ "text-line-through-style": "solid",
+ "text-line-through-color": gComputedStyle.color,
+ };
+ testTextAttrs(ID, 111, attrs, defAttrs, 110, 123);
+
+ attrs = {};
+ testTextAttrs(ID, 124, attrs, defAttrs, 123, 131);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area11, "font-weight" tests
+ ID = "area11";
+ defAttrs = buildDefaultTextAttrs(ID, "12pt", kBoldFontWeight);
+ testDefaultTextAttrs(ID, defAttrs);
+
+ attrs = { };
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 13);
+
+ attrs = { "font-weight": kNormalFontWeight };
+ testTextAttrs(ID, 13, attrs, defAttrs, 13, 20);
+
+ attrs = { };
+ testTextAttrs(ID, 20, attrs, defAttrs, 20, 27);
+
+ attrs = { "font-weight": kNormalFontWeight };
+ testTextAttrs(ID, 27, attrs, defAttrs, 27, 33);
+
+ attrs = { };
+ testTextAttrs(ID, 33, attrs, defAttrs, 33, 51);
+
+ attrs = { "font-weight": kNormalFontWeight };
+ testTextAttrs(ID, 51, attrs, defAttrs, 51, 57);
+
+ attrs = { };
+ testTextAttrs(ID, 57, attrs, defAttrs, 57, 97);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // test out of range offset
+ testTextAttrsWrongOffset("area12", -1);
+ testTextAttrsWrongOffset("area12", 500);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // test zero offset on empty hypertext accessibles
+ ID = "area13";
+ defAttrs = buildDefaultTextAttrs(ID, "12pt");
+ attrs = { };
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 0);
+
+ ID = "area14";
+ defAttrs = buildDefaultTextAttrs(ID, kInputFontSize,
+ kNormalFontWeight, kInputFontFamily);
+
+ attrs = { };
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 0);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area15, embed char tests, "*plain*plain**bold*bold*"
+ ID = "area15";
+ defAttrs = buildDefaultTextAttrs(ID, "12pt");
+
+ // p
+ testTextAttrs(ID, 0, { }, { }, 0, 1);
+ // plain
+ testTextAttrs(ID, 1, { }, defAttrs, 1, 6);
+ // p
+ testTextAttrs(ID, 6, { }, { }, 6, 7);
+ // plain
+ testTextAttrs(ID, 7, { }, defAttrs, 7, 12);
+ // p and img
+ testTextAttrs(ID, 12, { }, { }, 12, 14);
+ // bold
+ attrs = { "font-weight": kBoldFontWeight };
+ testTextAttrs(ID, 14, attrs, defAttrs, 14, 18);
+ // p
+ testTextAttrs(ID, 18, { }, { }, 18, 19);
+ // bold
+ attrs = { "font-weight": kBoldFontWeight };
+ testTextAttrs(ID, 19, attrs, defAttrs, 19, 23);
+ // p
+ testTextAttrs(ID, 23, { }, { }, 23, 24);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area16, "font-family" tests
+ ID = "area16";
+ defAttrs = buildDefaultTextAttrs(ID, "12pt");
+ testDefaultTextAttrs(ID, defAttrs);
+
+ attrs = { "font-family": kMonospaceFontFamily };
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 4);
+
+ attrs = { };
+ testTextAttrs(ID, 4, attrs, defAttrs, 4, 9);
+
+ attrs = { "font-family": kSerifFontFamily };
+ testTextAttrs(ID, 9, attrs, defAttrs, 9, 13);
+
+ attrs = { };
+ testTextAttrs(ID, 13, attrs, defAttrs, 13, 18);
+
+ attrs = { "font-family": kAbsentFontFamily };
+ testTextAttrs(ID, 18, attrs, defAttrs, 18, 22);
+
+ // bug 1224498 - this fails with 'cursive' fontconfig lookup
+ if (!LINUX) {
+ attrs = { };
+ testTextAttrs(ID, 22, attrs, defAttrs, 22, 27);
+
+ attrs = { "font-family": kCursiveFontFamily };
+ testTextAttrs(ID, 27, attrs, defAttrs, 27, 31);
+
+ attrs = { };
+ testTextAttrs(ID, 31, attrs, defAttrs, 31, 45);
+ }
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area17, "text-decoration" tests
+ ID = "area17";
+ defAttrs = buildDefaultTextAttrs(ID, "12pt");
+ testDefaultTextAttrs(ID, defAttrs);
+
+ attrs = {
+ "text-underline-style": "solid",
+ "text-underline-color": getSystemColor("CanvasText"),
+ };
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 10);
+
+ attrs = {
+ "text-underline-style": "solid",
+ "text-underline-color": "rgb(0, 0, 255)",
+ };
+ testTextAttrs(ID, 10, attrs, defAttrs, 10, 15);
+
+ attrs = {
+ "text-underline-style": "dotted",
+ "text-underline-color": getSystemColor("CanvasText"),
+ };
+ testTextAttrs(ID, 15, attrs, defAttrs, 15, 22);
+
+ attrs = {
+ "text-line-through-style": "solid",
+ "text-line-through-color": getSystemColor("CanvasText"),
+ };
+ testTextAttrs(ID, 22, attrs, defAttrs, 22, 34);
+
+ attrs = {
+ "text-line-through-style": "solid",
+ "text-line-through-color": "rgb(0, 0, 255)",
+ };
+ testTextAttrs(ID, 34, attrs, defAttrs, 34, 39);
+
+ attrs = {
+ "text-line-through-style": "wavy",
+ "text-line-through-color": getSystemColor("CanvasText"),
+ };
+ testTextAttrs(ID, 39, attrs, defAttrs, 39, 44);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area18, "auto-generation text" tests
+ ID = "area18";
+ defAttrs = buildDefaultTextAttrs(ID, "12pt");
+ testDefaultTextAttrs(ID, defAttrs);
+
+ attrs = {
+ "auto-generated": "true",
+ "font-family": "-moz-bullet-font",
+ };
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 2);
+ testTextAttrs(ID, 3, { }, defAttrs, 3, 7);
+ attrs = {
+ "auto-generated": "true",
+ };
+ testTextAttrs(ID, 7, attrs, defAttrs, 7, 8);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area19, "HTML5 mark tag" test
+ // text enclosed in mark tag will have a different background color
+ // However, since bug 982125, it is its own accessible.
+ // Therefore, anything other than the default background color is
+ // unexpected.
+ ID = "area19";
+ defAttrs = buildDefaultTextAttrs(ID, "12pt");
+
+ attrs = {};
+ testTextAttrs(ID, 0, attrs, defAttrs, 0, 10);
+
+ ID = "area19mark";
+ let defMarkAttrs = buildDefaultTextAttrs(ID, "12pt");
+ attrs = {};
+ testTextAttrs(ID, 0, attrs, defMarkAttrs, 0, 7);
+
+ ID = "area19";
+ attrs = {};
+ testTextAttrs(ID, 11, attrs, defAttrs, 11, 22);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // area20, "aOffset as -1 (Mozilla Bug 789621)" test
+
+ ID = "area20";
+ defAttrs = buildDefaultTextAttrs(ID, "15pt");
+ testDefaultTextAttrs(ID, defAttrs);
+
+ testTextAttrs(ID, -1, {}, defAttrs, 0, 11);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML sub tag offset test - verify attributes
+ ID = "sub_tag";
+ defAttrs = buildDefaultTextAttrs(ID, "10pt");
+ defAttrs["text-position"] = "sub";
+ testDefaultTextAttrs(ID, defAttrs);
+ testTextAttrs(ID, 0, {}, defAttrs, 0, 11, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML sup tag offset test - verify attributes
+ ID = "sup_tag";
+ defAttrs = buildDefaultTextAttrs(ID, "10pt");
+ defAttrs["text-position"] = "super";
+ testDefaultTextAttrs(ID, defAttrs);
+ testTextAttrs(ID, 0, {}, defAttrs, 0, 11, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA subscript role - verify text-position attribute
+ ID = "subscript_role";
+ defAttrs = { "text-position": "sub" };
+ testDefaultTextAttrs(ID, defAttrs, true);
+ testTextAttrs(ID, 0, {}, defAttrs, 0, 11, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA superscript role - verify text-position attribute
+ ID = "superscript_role";
+ defAttrs = { "text-position": "super" };
+ testDefaultTextAttrs(ID, defAttrs, true);
+ testTextAttrs(ID, 0, {}, defAttrs, 0, 11, true);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // test text-position attributes in various situations
+ ID = "superscript_role_in_div";
+ defAttrs = { "text-position": "super" };
+ testTextAttrs(ID, 0, {}, defAttrs, 0, 11, true);
+
+ ID = "sub_within_superscript_role";
+ defAttrs = { "text-position": "sub" };
+ testTextAttrs(ID, 0, {}, defAttrs, 0, 11, true);
+
+ ID = "sup_within_subscript_role";
+ defAttrs = { "text-position": "super" };
+ testTextAttrs(ID, 0, {}, defAttrs, 0, 11, true);
+
+ ID = "sub_within_sup";
+ defAttrs = { "text-position": "sub" };
+ testTextAttrs(ID, 0, {}, defAttrs, 0, 11, true);
+
+ ID = "sup_within_sub";
+ defAttrs = { "text-position": "super" };
+ testTextAttrs(ID, 0, {}, defAttrs, 0, 11, true);
+
+ ID = "css_sub_within_superscript_role";
+ attrs = { "text-position": "sub" };
+ testTextAttrs(ID, 0, attrs, {}, 0, 11, true);
+
+ ID = "css_super_within_subscript_role";
+ attrs = { "text-position": "super" };
+ testTextAttrs(ID, 0, attrs, {}, 0, 11, true);
+
+ ID = "sub_with_superscript_role";
+ defAttrs = { "text-position": "super" };
+ testTextAttrs(ID, 0, {}, defAttrs, 0, 11, true);
+
+ ID = "sup_with_subscript_role";
+ defAttrs = { "text-position": "sub" };
+ testTextAttrs(ID, 0, {}, defAttrs, 0, 11, true);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body style="font-size: 12pt">
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=345759"
+ title="Implement text attributes">
+ Mozilla Bug 345759
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=473569"
+ title="Restrict text-position to allowed values">
+ Mozilla Bug 473569
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=473576"
+ title="font-family text attribute should expose actual font used">
+ Mozilla Bug 473576
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=523304"
+ title="expose text-underline-color and text-line-through-color text attributes">
+ Mozilla Bug 523304
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=735645"
+ title="expose sub and sup elements in text attributes">
+ Mozilla Bug 735645
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=445516"
+ title="Support auto-generated text attribute on bullet lists">
+ Mozilla Bug 445516
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=789621"
+ title="getTextAttributes doesn't work with magic offsets">
+ Mozilla Bug 789621
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <p id="area1" style="font-size: smaller">Normal <b>Bold</b> Normal</p>
+ <p id="area2" style="font-size: 120%">Normal <b>Bold <i>Italic </i>Bold</b> Normal</p>
+ <p id="area3" style="background-color: blue;">
+ <span style="color: green; background-color: rgb(0, 0, 255)">
+ Green
+ <span style="color: red">but children are red</span>
+ </span><span style="color: green; background-color: rgb(255, 255, 0);">
+ Another green section.
+ </span>
+ </p>
+ <p id="area4">
+ <span style="color: green">
+ Green
+ </span><span style="color: green">
+ Green too
+ <span style="color: red">with red children</span>
+ Green again
+ </span>
+ </p>
+ <!-- Green!*!RedNormal-->
+ <p id="area5">
+ <span style="color: green">Green</span>
+ <img src="../moz.png" alt="image"/>
+ <span style="color: red">Red</span>Normal
+ </p>
+ <p id="area6">
+ This <sup>sentence</sup> has the word
+ <span style="vertical-align:super;">sentence</span> in
+ <sub>superscript</sub> and
+ <span style="vertical-align:sub;">subscript</span> and
+ <span style="vertical-align:20%;">superscript 20%</span> and
+ <span style="vertical-align:-20%;">subscript 20%</span> and
+ <span style="vertical-align:20px;">superscript 20px</span> and
+ <span style="vertical-align:-20px;">subscript 20px</span>
+ </p>
+
+ <p lang="en" id="area7">
+ <span lang="ru">Привет</span>
+ <span style="background-color: blue">Blue BG color</span>
+ <span lang="de">Ich bin/Du bist</span>
+ <span lang="en">
+ Normal
+ <span style="color: magenta">Magenta<b>Bold</b>Magenta</span>
+ </span>
+ </p>
+
+ <p id="area9" style="font-size: smaller">Small
+ <span style="font-size: 120%">bigger</span> smaller
+ <span style="background-color: blue;">background blue</span> normal
+ <span style="font-style: italic;">Different styling</span> normal
+ <span style="font-family: monospace;">Different font</span> normal
+ <span style="text-decoration: underline;">underlined</span> normal
+ <span style="text-decoration: line-through;">strikethrough</span> normal
+ <strike>strikethrough</strike> normal
+ </p>
+
+ <p id="area10">Normal
+ <span style="font-size: 120%">bigger</span> smaller
+ <span style="background-color: blue;">background blue</span> normal
+ <span style="font-style: italic;">Different styling</span> normal
+ <span style="font-family: monospace;">Different font</span> normal
+ <span style="text-decoration: underline;">underlined</span> normal
+ <span style="text-decoration: line-through;">strikethrough</span> normal
+ </p>
+
+ <p id="area11" style="font-weight: bolder;">
+ <span style="font-weight: bolder;">bolder</span>bolder
+ <span style="font-weight: lighter;">lighter</span>bolder
+ <span style="font-weight: normal;">normal</span>bolder
+ <b>bold</b>bolder
+ <span style="font-weight: 400;">normal</span>bolder
+ <span style="font-weight: 700;">bold</span>bolder
+ <span style="font-weight: bold;">bold</span>bolder
+ <span style="font-weight: 900;">bold</span>bolder
+ </p>
+
+ <p id="area12">hello</p>
+ <p id="area13"></p>
+ <input id="area14">
+
+ <!-- *plain*plain**bold*bold*-->
+ <div id="area15"><p>embed</p>plain<p>embed</p>plain<p>embed</p><img src="../moz.png" alt="image"/><b>bold</b><p>embed</p><b>bold</b><p>embed</p></div>
+
+ <p id="area16" style="font-family: sans-serif;">
+ <span style="font-family: monospace;">text</span>text
+ <span style="font-family: serif;">text</span>text
+ <span style="font-family: BodoniThatDoesntExist;">text</span>text
+ <span style="font-family: Comic Sans MS, cursive;">text</span>text
+ <span style="font-family: sans-serif, fantasy;">text</span>text
+ </p>
+
+ <p id="area17">
+ <span style="text-decoration-line: underline;">underline
+ </span><span style="text-decoration: underline; text-decoration-color: blue;">blue
+ </span><span style="text-decoration: underline; text-decoration-style: dotted;">dotted
+ </span><span style="text-decoration-line: line-through;">linethrough
+ </span><span style="text-decoration: line-through; text-decoration-color: blue;">blue
+ </span><span style="text-decoration: line-through; text-decoration-style: wavy;">wavy
+ </span>
+ </p>
+
+ <ul>
+ <li id="area18" class="gencontent">item</li>
+ </ul>
+
+ <p id="area19">uncolored
+ <mark id="area19mark">colored</mark> uncolored
+ </p>
+
+ <p id="area20" style="font-size: 15pt;">offset test</p>
+
+ <!-- subscript, superscript tests -->
+ <sub id="sub_tag">offset test</sub>
+ <sup id="sup_tag">offset test</sup>
+ <p id="subscript_role" role="subscript">offset test</p>
+ <p id="superscript_role" role="superscript">offset test</p>
+
+ <div><span id="superscript_role_in_div" role="superscript">offset test</span></div>
+ <p role="superscript"><sub id="sub_within_superscript_role">offset test</sub></p>
+ <p role="subscript"><sup id="sup_within_subscript_role">offset test</sup></p>
+ <sup><sub id="sub_within_sup">offset test</sub></sup>
+ <sub><sup id="sup_within_sub">offset test</sup></sub>
+ <p id="css_sub_within_superscript_role" role="superscript"><span style="vertical-align: sub">offset test</span></p>
+ <p id="css_super_within_subscript_role" role="subscript"><span style="vertical-align: super">offset test</span></p>
+ <sub id="sub_with_superscript_role" role="superscript">offset test</sub>
+ <sup id="sup_with_subscript_role" role="subscript">offset test</sup>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/textattrs/test_general.xhtml b/accessible/tests/mochitest/textattrs/test_general.xhtml
new file mode 100644
index 0000000000..de0556f10c
--- /dev/null
+++ b/accessible/tests/mochitest/textattrs/test_general.xhtml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Tests: XUL label text interface">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Testing
+
+ function doTests()
+ {
+ //////////////////////////////////////////////////////////////////////////
+ // XUL label
+
+ testTextAttrs("label1", 0, {}, {}, 0, 5, true);
+ testTextAttrs("label2", 0, {}, {}, 0, 5, true);
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ ]]>
+ </script>
+
+ <vbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+ <label id="label1" value="Hello"/>
+ <label id="label2">Hello</label>
+ </vbox>
+</window>
diff --git a/accessible/tests/mochitest/textattrs/test_invalid.html b/accessible/tests/mochitest/textattrs/test_invalid.html
new file mode 100644
index 0000000000..7028b32622
--- /dev/null
+++ b/accessible/tests/mochitest/textattrs/test_invalid.html
@@ -0,0 +1,59 @@
+<html>
+
+<head>
+ <title>Invalid text attribute</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+
+ <script type="application/javascript">
+ function doTests() {
+ testDefaultTextAttrs("aria_invalid_empty", {}, true);
+ testDefaultTextAttrs("aria_invalid_true", { "invalid": "true" }, true);
+ testDefaultTextAttrs("aria_invalid_false", { "invalid": "false" }, true);
+ testDefaultTextAttrs("aria_invalid_grammar", { "invalid": "grammar" }, true);
+ testDefaultTextAttrs("aria_invalid_spelling", { "invalid": "spelling" }, true);
+ testDefaultTextAttrs("aria_invalid_erroneous", { "invalid": "true" }, true);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=445510"
+ title="Support ARIA-based text attributes">
+ Mozilla Bug 445510
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="aria_invalid_empty" aria-invalid="">no invalid</div>
+ <div id="aria_invalid_true" aria-invalid="true">invalid:true</div>
+ <div id="aria_invalid_false" aria-invalid="false">invalid:false</div>
+ <div id="aria_invalid_grammar" aria-invalid="grammar">invalid:grammar</div>
+ <div id="aria_invalid_spelling" aria-invalid="spelling">invalid:spelling</div>
+ <div id="aria_invalid_erroneous" aria-invalid="erroneous">invalid:true</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/textattrs/test_mathml.html b/accessible/tests/mochitest/textattrs/test_mathml.html
new file mode 100644
index 0000000000..ea1eea1764
--- /dev/null
+++ b/accessible/tests/mochitest/textattrs/test_mathml.html
@@ -0,0 +1,54 @@
+<html>
+
+<head>
+ <title>MathML Text attributes tests</title>
+ <meta charset="utf-8">
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script src="../common.js"></script>
+ <script src="../attributes.js"></script>
+ <script src="../events.js"></script>
+
+ <script>
+ function doTest() {
+ const math = getNode("math");
+ const defAttrs = buildDefaultTextAttrs(math, "10pt");
+ testDefaultTextAttrs(math, defAttrs);
+
+ for (const id of ["mn", "mi", "annotation", "annotationXml"]) {
+ testTextAttrs(id, 0, {}, defAttrs, 0, 1);
+ }
+
+ // These elements contain a surrogate pair, so the end offset is 2.
+ for (const id of ["mn_double_struck", "mi_italic"]) {
+ testTextAttrs(id, 0, {}, defAttrs, 0, 2);
+ }
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body style="font-size: 12pt">
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <math id="math" style="font-size: smaller">
+ <mn id="mn">1</mn>
+ <mi id="mi" mathvariant="normal">x</mi>
+ <mn id="mn_double_struck">𝟙</mn>
+ <mi id="mi_italic">x</mi>
+ <!-- tabindex forces creation of an Accessible -->
+ <annotation id="annotation" tabindex="0">a</annotation>
+ <annotation-xml id="annotationXml" tabindex="0">a</annotation-xml>
+ </math>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/textattrs/test_spelling.html b/accessible/tests/mochitest/textattrs/test_spelling.html
new file mode 100644
index 0000000000..b8f3858353
--- /dev/null
+++ b/accessible/tests/mochitest/textattrs/test_spelling.html
@@ -0,0 +1,52 @@
+<html>
+
+<head>
+ <title>Spell check text attribute tests</title>
+ <meta charset="utf-8" />
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../attributes.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ async function doTest() {
+ const misspelledAttrs = {"invalid": "spelling"};
+
+ let editable = document.getElementById("div_after_misspelling");
+ // The attr change event gets fired on the last accessible containing a
+ // spelling error.
+ let spellDone = waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED, "div_after_misspelling_div2");
+ editable.focus();
+ await spellDone;
+ testTextAttrs("div_after_misspelling_div1", 0, {}, {}, 0, 5, true);
+ testTextAttrs("div_after_misspelling_div1", 5, misspelledAttrs, {}, 5, 9, true);
+ testTextAttrs("div_after_misspelling_div2", 0, {}, {}, 0, 5, true);
+ testTextAttrs("div_after_misspelling_div2", 5, misspelledAttrs, {}, 5, 9, true);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- Text attribute offsets for accessibles after first spelling error (bug 1479678) -->
+ <div id="div_after_misspelling" contenteditable="true" spellcheck="true" lang="en-US">
+ <div id="div_after_misspelling_div1">Test tset</div>
+ <div id="div_after_misspelling_div2">test tset</div>
+ </div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/textattrs/test_svg.html b/accessible/tests/mochitest/textattrs/test_svg.html
new file mode 100644
index 0000000000..9456a74936
--- /dev/null
+++ b/accessible/tests/mochitest/textattrs/test_svg.html
@@ -0,0 +1,56 @@
+<html>
+
+<head>
+ <title>SVG Text attributes tests</title>
+ <meta charset="utf-8">
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script src="../common.js"></script>
+ <script src="../attributes.js"></script>
+ <script src="../events.js"></script>
+
+ <script>
+ function doTest() {
+ const svg = getNode("svg");
+ const defAttrs = buildDefaultTextAttrs(svg, "10pt");
+ testDefaultTextAttrs(svg, defAttrs);
+ testTextAttrs(svg, 0, {}, defAttrs, 0, 2);
+
+ const g = getNode("g");
+ testTextAttrs(g, 0, {}, defAttrs, 0, 2);
+
+ const a = getNode("a");
+ const aDefAttrs = buildDefaultTextAttrs(a, "10pt");
+ testTextAttrs(a, 0, {}, aDefAttrs, 0, 1);
+
+ const f3 = getNode("f3");
+ testTextAttrs(f3, 0, {}, defAttrs, 0, 2);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body style="font-size: 12pt">
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <svg id="svg" style="font-size: smaller">
+ <foreignobject>f1</foreignobject>
+ <g id="g">
+ <title>g</title>
+ <foreignobject>f2</foreignobject>
+ </g>
+ <text><a href="#" id="a">a</a></text>
+ <foreignobject id="f3" role="button"><body>f3</body></foreignobject>
+ </svg>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/textcaret/a11y.toml b/accessible/tests/mochitest/textcaret/a11y.toml
new file mode 100644
index 0000000000..d89b87e1cf
--- /dev/null
+++ b/accessible/tests/mochitest/textcaret/a11y.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+support-files = "!/accessible/tests/mochitest/*.js"
+
+["test_general.html"]
diff --git a/accessible/tests/mochitest/textcaret/test_general.html b/accessible/tests/mochitest/textcaret/test_general.html
new file mode 100644
index 0000000000..bd949116fd
--- /dev/null
+++ b/accessible/tests/mochitest/textcaret/test_general.html
@@ -0,0 +1,174 @@
+<html>
+
+<head>
+ <title>Text accessible caret testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ /**
+ * Turn on/off the caret browsing mode.
+ */
+ function turnCaretBrowsing(aIsOn) {
+ Services.prefs.setBoolPref("accessibility.browsewithcaret", aIsOn);
+ }
+
+ /**
+ * Test caret offset for the given accessible.
+ */
+ function testCaretOffset(aID, aCaretOffset) {
+ var acc = getAccessible(aID, [nsIAccessibleText]);
+ is(acc.caretOffset, aCaretOffset,
+ "Wrong caret offset for " + aID);
+ }
+
+ function testCaretOffsets(aList) {
+ for (var i = 0; i < aList.length; i++)
+ testCaretOffset(aList[0][0], aList[0][1]);
+ }
+
+ function queueTraversalList(aList, aFocusNode) {
+ for (var i = 0 ; i < aList.length; i++) {
+ var node = aList[i].DOMPoint[0];
+ var nodeOffset = aList[i].DOMPoint[1];
+
+ var textAcc = aList[i].point[0];
+ var textOffset = aList[i].point[1];
+ var textList = aList[i].pointList;
+ var invoker =
+ new moveCaretToDOMPoint(textAcc, node, nodeOffset, textOffset,
+ ((i == 0) ? aFocusNode : null),
+ testCaretOffsets.bind(null, textList));
+ gQueue.push(invoker);
+ }
+ }
+
+ /**
+ * Do tests.
+ */
+ var gQueue = null;
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ function doTests() {
+ turnCaretBrowsing(true);
+
+ // test caret offsets
+ testCaretOffset(document, 0); // because of no selection ranges
+ testCaretOffset("textbox", -1);
+ testCaretOffset("textarea", -1);
+ testCaretOffset("p", -1);
+
+ // test caret move events and caret offsets
+ gQueue = new eventQueue();
+
+ gQueue.push(new setCaretOffset("textbox", 1, "textbox"));
+ gQueue.push(new setCaretOffset("link", 1, "link"));
+ gQueue.push(new setCaretOffset("heading", 1, document));
+
+ // a*b*c
+ var p2Doc = getNode("p2_container").contentDocument;
+ var traversalList = [
+ { // before 'a'
+ DOMPoint: [ getNode("p2", p2Doc).firstChild, 0 ],
+ point: [ getNode("p2", p2Doc), 0 ],
+ pointList: [ [ p2Doc, 0 ] ],
+ },
+ { // after 'a' (before anchor)
+ DOMPoint: [ getNode("p2", p2Doc).firstChild, 1 ],
+ point: [ getNode("p2", p2Doc), 1 ],
+ pointList: [ [ p2Doc, 0 ] ],
+ },
+ { // before 'b' (inside anchor)
+ DOMPoint: [ getNode("p2_a", p2Doc).firstChild, 0 ],
+ point: [ getNode("p2_a", p2Doc), 0 ],
+ pointList: [
+ [ getNode("p2", p2Doc), 1 ],
+ [ p2Doc, 0 ],
+ ],
+ },
+ { // after 'b' (inside anchor)
+ DOMPoint: [ getNode("p2_a", p2Doc).firstChild, 1 ],
+ point: [ getNode("p2_a", p2Doc), 1 ],
+ pointList: [
+ [ getNode("p2", p2Doc), 1 ],
+ [ p2Doc, 0 ],
+ ],
+ },
+ { // before 'c' (after anchor)
+ DOMPoint: [ getNode("p2", p2Doc).lastChild, 0 ],
+ point: [ getNode("p2", p2Doc), 2 ],
+ pointList: [ [ p2Doc, 0 ] ],
+ },
+ { // after 'c'
+ DOMPoint: [ getNode("p2", p2Doc).lastChild, 1 ],
+ point: [ getNode("p2", p2Doc), 3 ],
+ pointList: [ [ p2Doc, 0 ] ],
+ },
+ ];
+ queueTraversalList(traversalList, getNode("p2", p2Doc));
+
+ gQueue.onFinish = function() {
+ turnCaretBrowsing(false);
+ };
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=448744"
+ title="caretOffset should return -1 if the system caret is not currently with in that particular object">
+ Bug 448744
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=524115"
+ title="HyperText accessible should get focus when the caret is positioned inside of it, text is changed or copied into clipboard by ATs">
+ Bug 524115
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=546068"
+ title="Position is not being updated when atk_text_set_caret_offset is used">
+ Bug 546068
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=672717"
+ title="Broken caret when moving into/out of embedded objects with right arrow">
+ Bug 672717
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=725581"
+ title="caretOffset for textarea should be -1 when textarea doesn't have a focus">
+ Bug 725581
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input id="textbox" value="hello"/>
+ <textarea id="textarea">text<br>text</textarea>
+ <p id="p" contentEditable="true"><span>text</span><br/>text</p>
+ <a id="link" href="about:mozilla">about mozilla</a>
+ <h5 id="heading">heading</h5>
+ <iframe id="p2_container"
+ src="data:text/html,<p id='p2' contentEditable='true'>a<a id='p2_a' href='mozilla.org'>b</a>c</p>"></iframe>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/textrange/a11y.toml b/accessible/tests/mochitest/textrange/a11y.toml
new file mode 100644
index 0000000000..c7d4f270e4
--- /dev/null
+++ b/accessible/tests/mochitest/textrange/a11y.toml
@@ -0,0 +1,8 @@
+[DEFAULT]
+support-files = [
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/moz.png"]
+
+["test_general.html"]
+
+["test_selection.html"]
diff --git a/accessible/tests/mochitest/textrange/test_general.html b/accessible/tests/mochitest/textrange/test_general.html
new file mode 100644
index 0000000000..8ddbd77a9c
--- /dev/null
+++ b/accessible/tests/mochitest/textrange/test_general.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Text Range tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+ <script type="application/javascript"
+ src="../layout.js"></script>
+ <script type="application/javascript">
+
+ function doTest() {
+ const sel = window.getSelection();
+ const r1 = document.createRange();
+ r1.selectNode(getNode("p1"));
+ sel.addRange(r1);
+ const r2 = document.createRange();
+ r2.selectNode(getNode("p2"));
+ sel.addRange(r2);
+ const docAcc = getAccessible(document, [nsIAccessibleText]);
+ const accRanges = docAcc.selectionRanges;
+ const p1Range = accRanges.queryElementAt(0, nsIAccessibleTextRange);
+ const p1RangeCopy = docAcc.selectionRanges.queryElementAt(0, nsIAccessibleTextRange);
+ const p2Range = accRanges.queryElementAt(1, nsIAccessibleTextRange);
+
+ // TextRange::compare
+ ok(p1Range.compare(p1RangeCopy),
+ "p1 ranges should be equal");
+
+ ok(!p1Range.compare(p2Range),
+ "p1 and p2 ranges can't be equal");
+
+ // TextRange::compareEndPoints
+ var res = p1Range.compareEndPoints(EndPoint_End, p2Range, EndPoint_Start);
+ is(res, -1, "p1 range must be lesser with p2 range");
+
+ res = p2Range.compareEndPoints(EndPoint_Start, p1Range, EndPoint_End);
+ is(res, 1, "p2 range must be greater with p1 range");
+
+ res = p1Range.compareEndPoints(EndPoint_Start, p1Range, EndPoint_Start);
+ is(res, 0, "p1 range must be equal with p1 range");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Implement Text accessible text range methods"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=975065">Bug 975065</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <p id="p1">text <img id="p1_img", src="../moz.png"> text</p>
+ <p>between</p>
+ <p id="p2">text <a id="p2_a" href="www">link<img id="p2_img", src="../moz.png"></a> text</p>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/textrange/test_selection.html b/accessible/tests/mochitest/textrange/test_selection.html
new file mode 100644
index 0000000000..2a5d4da5c2
--- /dev/null
+++ b/accessible/tests/mochitest/textrange/test_selection.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Text Range selection tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../text.js"></script>
+ <script type="application/javascript"
+ src="../layout.js"></script>
+ <script type="application/javascript">
+
+ function doTest() {
+ var sel = window.getSelection();
+ var p = getNode("p1");
+ var a = getNode("p2_a");
+
+ var range = document.createRange();
+ sel.addRange(range);
+
+ // the accessible is contained by the range
+ range.selectNode(p);
+
+ var a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
+ var a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
+
+ testTextRange(a11yrange, "selection range #1", document, 3, document, 4);
+
+ ok(a11yrange.crop(getAccessible(a)), "Range failed to crop #1.");
+ testTextRange(a11yrange, "cropped range #1", a, 0, a, 5);
+
+ // the range is contained by the accessible
+ range.selectNode(a);
+ a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
+ a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
+
+ testTextRange(a11yrange, "selection range #2", p, 5, p, 6);
+
+ ok(a11yrange.crop(getAccessible(p)), "Range failed to crop #2.");
+ testTextRange(a11yrange, "cropped range #2", p, 5, p, 6);
+
+ // the range starts before the accessible and ends inside it
+ range.setStart(p, 0);
+ range.setEndAfter(a.firstChild, 4);
+ a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
+ a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
+
+ testTextRange(a11yrange, "selection range #3", p, 0, a, 4);
+
+ ok(a11yrange.crop(getAccessible(a)), "Range failed to crop #3.");
+ testTextRange(a11yrange, "cropped range #3", a, 0, a, 4);
+
+ // the range starts inside the accessible and ends after it
+ range.setStart(a.firstChild, 1);
+ range.setEndAfter(p);
+ a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
+ a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
+
+ testTextRange(a11yrange, "selection range #4", a, 1, document, 4);
+
+ ok(a11yrange.crop(getAccessible(a)), "Range failed to crop #4.");
+ testTextRange(a11yrange, "cropped range #4", a, 1, a, 5);
+
+ // the range ends before the accessible
+ range.setStart(p.firstChild, 0);
+ range.setEnd(p.firstChild, 4);
+ a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
+ a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
+
+ testTextRange(a11yrange, "selection range #5", p, 0, p, 4);
+ ok(!a11yrange.crop(getAccessible(a)), "Crop #5 succeeded while it shouldn't");
+
+ // the range starts after the accessible
+ range.setStart(p.lastChild, 0);
+ range.setEnd(p.lastChild, 4);
+ a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
+ a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
+
+ testTextRange(a11yrange, "selection range #6", p, 6, p, 10);
+
+ ok(!a11yrange.crop(getAccessible(a)), "Crop #6 succeeded while it shouldn't");
+
+ // crop a range by a table
+ range.selectNode(getNode("c2"));
+ a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
+ a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
+
+ testTextRange(a11yrange, "selection range #7", document, 4, document, 5);
+
+ ok(a11yrange.crop(getAccessible("table")), "Range failed to crop #7.");
+ testTextRange(a11yrange, "cropped range #7", "table", 0, "table", 1);
+
+ // test compare points for selection with start in nested node
+ range.setStart(a.firstChild, 2);
+ range.setEnd(p.lastChild, 3);
+ a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
+ a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
+ var res = a11yrange.compareEndPoints(EndPoint_Start, a11yrange, EndPoint_End);
+ is(res, -1, "start must be lesser than end");
+
+ res = a11yrange.compareEndPoints(EndPoint_End, a11yrange, EndPoint_Start);
+ is(res, 1, "end must be greater than start");
+
+ // Crop a range to its next sibling.
+ range.selectNode(getNode("c3p1").firstChild);
+ a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
+ a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
+ testTextRange(a11yrange, "selection range #8", "c3p1", 0, "c3p1", 1);
+ ok(!a11yrange.crop(getAccessible("c3p2")), "Crop #8 succeeded but shouldn't have.");
+ // Crop a range to its previous sibling.
+ range.selectNode(getNode("c3p2").firstChild);
+ a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges;
+ a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange);
+ testTextRange(a11yrange, "selection range #9", "c3p2", 0, "c3p2", 1);
+ ok(!a11yrange.crop(getAccessible("c3p1")), "Crop #9 succeeded but shouldn't have.");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Implement IAccessible2_3::selectionRanges"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1233118">Bug 1233118</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <p id="p1">text <a id="p2_a" href="www">link<img id="p2_img", src="../moz.png"></a> text</p>
+
+ <div id="c2">start<table id="table"><tr><td>cell</td></tr></table>end</div>
+
+ <div id="c3"><p id="c3p1">a</p><p id="c3p2">b</p></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/textselection/a11y.toml b/accessible/tests/mochitest/textselection/a11y.toml
new file mode 100644
index 0000000000..b87962bc41
--- /dev/null
+++ b/accessible/tests/mochitest/textselection/a11y.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files = "!/accessible/tests/mochitest/*.js"
+
+["test_general.html"]
+
+["test_userinput.html"]
diff --git a/accessible/tests/mochitest/textselection/test_general.html b/accessible/tests/mochitest/textselection/test_general.html
new file mode 100644
index 0000000000..92e7988a87
--- /dev/null
+++ b/accessible/tests/mochitest/textselection/test_general.html
@@ -0,0 +1,221 @@
+<html>
+
+<head>
+ <title>Text selection testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ /**
+ * Helper function to test selection bounds.
+ * @param {string} aID The ID to test.
+ * @param {nsIAccessibleText} acc The accessible to test.
+ * @param {int} index The selection's index to test.
+ * @param {array} offsets The start and end offset to test against.
+ * @param {string} msgStart The start of the message to return in test
+ * messages.
+ */
+ function testSelectionBounds(aID, acc, index, offsets, msgStart) {
+ const [expectedStart, expectedEnd] = offsets;
+ const startOffset = {}, endOffset = {};
+ acc.getSelectionBounds(index, startOffset, endOffset);
+
+ is(startOffset.value, Math.min(expectedStart, expectedEnd),
+ msgStart + ": Wrong start offset for " + aID);
+ is(endOffset.value, Math.max(expectedStart, expectedEnd),
+ msgStart + ": Wrong end offset for " + aID);
+ }
+
+ /**
+ * Test adding selections to accessibles.
+ * @param {string} aID The ID of the element to test.
+ * @param {array} aSelections Array of selection start and end indices.
+ */
+ async function addSelections(aID, aSelections) {
+ info("Test adding selections to " + aID);
+ const hyperText = getAccessible(aID, [ nsIAccessibleText ]);
+ const initialSelectionCount = hyperText.selectionCount;
+
+ // Multiple selection changes will be coalesced, so just listen for one.
+ const selectionChange = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, aID);
+ for (let [startOffset, endOffset] of aSelections) {
+ hyperText.addSelection(startOffset, endOffset);
+ }
+ await selectionChange;
+
+ is(hyperText.selectionCount,
+ aSelections.length + initialSelectionCount,
+ "addSelection: Wrong selection count for " + aID);
+
+ for (let i in aSelections) {
+ testSelectionBounds(aID, hyperText, initialSelectionCount + i,
+ aSelections[i], "addSelection");
+ }
+
+ is(hyperText.caretOffset, aSelections[hyperText.selectionCount -1][1],
+ "addSelection: caretOffset not at selection end for " + aID);
+ }
+
+ /**
+ * Test changing selections in accessibles.
+ * @param {string} aID The ID of the element to test.
+ * @param {int} aIndex The index of the selection to change.
+ * @param {array} aSelection Array of the selection's new start and end
+ * indices.
+ */
+ async function changeSelection(aID, aIndex, aSelection) {
+ info("Test changing the selection of " + aID + " at index " + aIndex);
+ const [startOffset, endOffset] = aSelection;
+ const hyperText = getAccessible(aID, [ nsIAccessibleText ]);
+
+ const selectionChanged = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, aID);
+ hyperText.setSelectionBounds(aIndex, startOffset, endOffset);
+ await selectionChanged;
+
+ testSelectionBounds(aID, hyperText, aIndex,
+ aSelection, "setSelectionBounds");
+
+ is(hyperText.caretOffset, endOffset,
+ "setSelectionBounds: caretOffset not at selection end for " + aID);
+ }
+
+ /**
+ * Test removing all selections from accessibles.
+ * @param {string} aID The ID of the element to test.
+ */
+ async function removeSelections(aID) {
+ info("Testing removal of all selections from " + aID);
+ const hyperText = getAccessible(aID, [ nsIAccessibleText ]);
+
+ let selectionsRemoved = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, document);
+ const selectionCount = hyperText.selectionCount;
+ for (let i = 0; i < selectionCount; i++) {
+ hyperText.removeSelection(0);
+ }
+ await selectionsRemoved;
+
+ is(hyperText.selectionCount, 0,
+ "removeSelection: Wrong selection count for " + aID);
+ }
+
+ /**
+ * Test that changing the DOM selection is reflected in the accessibles.
+ * @param {string} aID The container ID to test in
+ * @param {string} aNodeID1 The start node of the selection
+ * @param {int} aNodeOffset1 The offset where the selection should start
+ * @param {string} aNodeID2 The node in which the selection should end
+ * @param {int} aNodeOffset2 The index at which the selection should end
+ * @param {array} aTests An array of accessibles and their start and end
+ * offsets to test.
+ */
+ async function changeDOMSelection(aID, aNodeID1, aNodeOffset1,
+ aNodeID2, aNodeOffset2,
+ aTests) {
+ info("Test that DOM selection changes are reflected in the accessibles");
+
+ let selectionChanged = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, aID);
+ // HyperTextAccessible::GetSelectionDOMRanges ignores hidden selections.
+ // Here we may be focusing an editable element (and thus hiding the
+ // main document selection), so blur it so that we test what we want to
+ // test.
+ document.activeElement.blur();
+
+ const sel = window.getSelection();
+ const range = document.createRange();
+ range.setStart(getNode(aNodeID1), aNodeOffset1);
+ range.setEnd(getNode(aNodeID2), aNodeOffset2);
+ sel.addRange(range);
+ await selectionChanged;
+
+ for (let i = 0; i < aTests.length; i++) {
+ const text = getAccessible(aTests[i][0], nsIAccessibleText);
+ is(text.selectionCount, 1,
+ "setSelectionBounds: Wrong selection count for " + aID);
+ testSelectionBounds(aID, text, 0, [aTests[i][1], aTests[i][2]],
+ "setSelectionBounds");
+ }
+ }
+
+ /**
+ * Test expected and unexpected events for selecting
+ * all text and focusing both an input and text area. We expect a caret
+ * move, but not a text selection change.
+ * @param {string} aID The ID of the element to test.
+ */
+ async function eventsForSelectingAllTextAndFocus(aID) {
+ info("Test expected caretMove and unexpected textSelection events for " +aID);
+ let events = waitForEvents({
+ expected: [[EVENT_TEXT_CARET_MOVED, aID]],
+ unexpected: [[EVENT_TEXT_SELECTION_CHANGED, aID]]}, aID);
+ selectAllTextAndFocus(aID);
+ await events;
+ }
+
+ /**
+ * Do tests
+ */
+
+ async function doTests() {
+ await addSelections("paragraph", [[1, 3], [6, 10]]);
+ await changeSelection("paragraph", 0, [2, 4]);
+ await removeSelections("paragraph");
+
+ // reverse selection
+ await addSelections("paragraph", [[1, 3], [10, 6]]);
+ await removeSelections("paragraph");
+
+ await eventsForSelectingAllTextAndFocus("textbox");
+ await changeSelection("textbox", 0, [1, 3]);
+
+ // reverse selection
+ await changeSelection("textbox", 0, [3, 1]);
+
+ await eventsForSelectingAllTextAndFocus("textarea");
+ await changeSelection("textarea", 0, [1, 3]);
+
+ await changeDOMSelection("c1", "c1_span1", 0, "c1_span2", 0,
+ [["c1", 2, 2]]);
+ await changeDOMSelection("c2", "c2", 0, "c2_div2", 1,
+ [["c2", 0, 3], ["c2_div2", 0, 2]]);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=688126"
+ title="nsIAccessibleText::setSelectionBounds doesn't fire text selection changed events in some cases">
+ Bug 688126
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=688124"
+ title="no text selection changed event when selection is removed">
+ Bug 688124
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <p id="paragraph">hello world</p>
+ <input id="textbox" value="hello"/>
+ <textarea id="textarea">hello</textarea>
+ <div id="c1">hi<span id="c1_span1"></span><span id="c1_span2"></span>hi</div>
+ <div id="c2">hi<div id="c2_div2">hi</div></div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/textselection/test_userinput.html b/accessible/tests/mochitest/textselection/test_userinput.html
new file mode 100644
index 0000000000..1aa0b2abb6
--- /dev/null
+++ b/accessible/tests/mochitest/textselection/test_userinput.html
@@ -0,0 +1,76 @@
+<html>
+
+<head>
+ <title>Text selection by user input</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ async function doTests() {
+ // Tab to 't2' and then tab out of it: it must not have any selection.
+ info("Select all text in t1 and focus it");
+ let focused = waitForEvent(EVENT_FOCUS, "t1");
+ // Simulate tabbing to t1 by selecting all text before focusing it.
+ selectAllTextAndFocus("t1");
+ await focused;
+
+ info("Tab to t2");
+ const t2 = getNode("t2");
+ focused = waitForEvent(EVENT_FOCUS, t2);
+ synthesizeKey("VK_TAB");
+ await focused;
+
+ info("Tab to t3 and make sure there is no selection in t2 afterwards");
+ const t3 = getNode("t3");
+ focused = waitForEvent(EVENT_FOCUS, t3);
+ synthesizeKey("VK_TAB");
+ await focused;
+ const prevFocus = getAccessible(t2, [ nsIAccessibleText ]);
+ is(prevFocus.selectionCount, 0,
+ "Wrong selection count for t2");
+
+ let exceptionCaught = false;
+ try {
+ const startOffsetObj = {}, endOffsetObj = {};
+ prevFocus.getSelectionBounds(0, startOffsetObj, endOffsetObj);
+ } catch (e) {
+ exceptionCaught = true;
+ }
+
+ ok(exceptionCaught, "No selection was expected for t2");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=440590"
+ title="Text selection information is not updated when HTML and XUL entries lose focus">
+ Bug 440590
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input type="text" id="t1" maxlength="3" size="3" value="1">
+ <input type="text" id="t2" maxlength="3" size="3" value="1">
+ <input type="text" id="t3" maxlength="3" size="3" value="1">
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/a11y.toml b/accessible/tests/mochitest/tree/a11y.toml
new file mode 100644
index 0000000000..e9181c752e
--- /dev/null
+++ b/accessible/tests/mochitest/tree/a11y.toml
@@ -0,0 +1,105 @@
+[DEFAULT]
+support-files = [
+ "dockids.html",
+ "wnd.xhtml",
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/formimage.png",
+ "!/accessible/tests/mochitest/letters.gif",
+ "!/accessible/tests/mochitest/moz.png",
+ "!/accessible/tests/mochitest/tree/wnd.xhtml",
+ "!/dom/media/test/bug461281.ogg"]
+
+["test_applicationacc.xhtml"]
+skip-if = ["true"] # Bug 561508
+
+["test_aria_display_contents.html"]
+
+["test_aria_globals.html"]
+
+["test_aria_grid.html"]
+
+["test_aria_imgmap.html"]
+
+["test_aria_list.html"]
+
+["test_aria_menu.html"]
+
+["test_aria_owns.html"]
+
+["test_aria_presentation.html"]
+
+["test_aria_table.html"]
+
+["test_brokencontext.html"]
+
+["test_button.xhtml"]
+
+["test_canvas.html"]
+
+["test_combobox.xhtml"]
+
+["test_cssflexbox.html"]
+
+["test_cssoverflow.html"]
+
+["test_display_contents.html"]
+
+["test_divs.html"]
+
+["test_dochierarchy.html"]
+
+["test_dockids.html"]
+
+["test_filectrl.html"]
+
+["test_formctrl.html"]
+
+["test_formctrl.xhtml"]
+
+["test_gencontent.html"]
+
+["test_groupbox.xhtml"]
+
+["test_html_in_mathml.html"]
+
+["test_iframe.html"]
+
+["test_image.xhtml"]
+
+["test_img.html"]
+
+["test_invalid_img.xhtml"]
+
+["test_invalidationlist.html"]
+
+["test_list.html"]
+
+["test_map.html"]
+
+["test_media.html"]
+
+["test_select.html"]
+
+["test_svg.html"]
+
+["test_tabbox.xhtml"]
+
+["test_tabbrowser.xhtml"]
+skip-if = [
+ "os == 'linux' && debug", # Bug 1389365
+ "os == 'win' && ccov", # Bug 1423218
+]
+
+["test_table.html"]
+
+["test_table_2.html"]
+
+["test_table_3.html"]
+
+["test_tree.xhtml"]
+
+["test_txtcntr.html"]
+
+["test_txtctrl.html"]
+
+["test_txtctrl.xhtml"]
diff --git a/accessible/tests/mochitest/tree/dockids.html b/accessible/tests/mochitest/tree/dockids.html
new file mode 100644
index 0000000000..e964cd759e
--- /dev/null
+++ b/accessible/tests/mochitest/tree/dockids.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<html>
+ <head>
+ <link rel="next" href="http://www.mozilla.org">
+ <style>
+ head, link, a { display: block; }
+ link:after { content: "Link to " attr(href); }
+ </style>
+ <script>
+ window.onload = function() {
+ document.documentElement.appendChild(document.createElement("input"));
+
+ var l = document.createElement("link");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ l.href = "http://www.mozilla.org";
+ l.textContent = "Another ";
+ document.documentElement.appendChild(l);
+
+ l = document.createElement("a");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ l.href = "http://www.mozilla.org";
+ l.textContent = "Yet another link to mozilla";
+ document.documentElement.appendChild(l);
+ };
+ </script>
+ </head>
+ <body>
+ Hey, I'm a <body> with three links that are not inside me and an input
+ that's not inside me.
+ </body>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_applicationacc.xhtml b/accessible/tests/mochitest/tree/test_applicationacc.xhtml
new file mode 100644
index 0000000000..5e00a2878f
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_applicationacc.xhtml
@@ -0,0 +1,73 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible Application Accessible hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // Note: bug 560239 can be tested if this test runs in standalone mode only.
+
+ var gURL = "../tree/wnd.xhtml"
+ var gWnd = window.openDialog(gURL, "wnd", "chrome,width=600,height=600");
+
+ function doTest()
+ {
+ // Application accessible should contain two root document accessibles,
+ // one is for browser window, another one is for open dialog window.
+ var accTree = {
+ role: ROLE_APP_ROOT,
+ children: [
+ {
+ role: ROLE_CHROME_WINDOW,
+ name: "Accessibility Chrome Test Harness - Minefield"
+ },
+ {
+ role: ROLE_CHROME_WINDOW,
+ name: "Empty Window"
+ }
+ ]
+ };
+ testAccessibleTree(getApplicationAccessible(), accTree);
+
+ gWnd.close();
+
+ SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ // We need to open dialog window before accessibility is started.
+ addLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=560239"
+ title="no children of application accessible for windows open before accessibility was started">
+ Mozilla Bug 560239
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/tree/test_aria_display_contents.html b/accessible/tests/mochitest/tree/test_aria_display_contents.html
new file mode 100644
index 0000000000..5c6f7f20fb
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_aria_display_contents.html
@@ -0,0 +1,173 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>ARIA and style="display: contents;"</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // Test ARIA grids that have display: contents; on different elements.
+ // They should all have equivalent trees.
+ var accTree =
+ { TABLE: [
+ { ROW: [
+ { role: ROLE_COLUMNHEADER,
+ children: [ { TEXT_LEAF: [ ] }, ]
+ },
+ { role: ROLE_COLUMNHEADER,
+ children: [ { TEXT_LEAF: [ ] }, ]
+ },
+ ] },
+ { ROW: [
+ { ROWHEADER: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ { GRID_CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] };
+
+ testAccessibleTree("gridWithoutDisplayContents", accTree);
+ testAccessibleTree("gridWithDisplayContents", accTree);
+ testAccessibleTree("gridWithDisplayContentsRow", accTree);
+ testAccessibleTree("gridWithDisplayContentsColHeader", accTree);
+ testAccessibleTree("gridWithDisplayContentsRowHeader", accTree);
+ testAccessibleTree("gridWithDisplayContentsGridCell", accTree);
+
+ // Test divs with ARIA roles and attributes and display: contents to
+ // verify that Accessibles are created appropriately.
+ accTree =
+ { SECTION: [
+ { LIST: [
+ { LISTITEM: [
+ { TEXT_LEAF: [ ] }
+ ] },
+ ] },
+ { SECTION: [
+ { LISTITEM: [
+ { TEXT_LEAF: [ ] }
+ ] },
+ ] },
+ { LISTITEM: [
+ { TEXT_LEAF: [ ] }
+ ] },
+ ] };
+ testAccessibleTree("container", accTree);
+
+ // Test paragraph with display: contents. It should create a generic
+ // Accessible that reports the role correctly.
+ accTree =
+ { SECTION: [
+ { PARAGRAPH: [ { TEXT_LEAF: [ ] } ] },
+ { TEXT_LEAF: [ ] }, // space between paragraphs
+ { TEXT_LEAF: [ ] },
+ ] };
+ testAccessibleTree("paragraphContainer", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Element with ARIA role and display: contents doesn't get an accessible"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1494196">
+ Mozilla Bug 1494196
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="gridWithoutDisplayContents" role="grid">
+ <div role="row">
+ <div role="columnheader">col1</div>
+ <div role="columnheader">col2</div>
+ </div>
+ <div role="row">
+ <div role="rowheader">row1</div>
+ <div role="gridcell">cell1</div>
+ </div>
+ </div>
+ <div id="gridWithDisplayContents" role="grid" style="display:contents;">
+ <div role="row">
+ <div role="columnheader">col1</div>
+ <div role="columnheader">col2</div>
+ </div>
+ <div role="row">
+ <div role="rowheader">row1</div>
+ <div role="gridcell">cell1</div>
+ </div>
+ </div>
+ <div id="gridWithDisplayContentsRow" role="grid">
+ <div role="row" style="display:contents;">
+ <div role="columnheader">col1</div>
+ <div role="columnheader">col2</div>
+ </div>
+ <div role="row">
+ <div role="rowheader">row1</div>
+ <div role="gridcell">cell1</div>
+ </div>
+ </div>
+ <div id="gridWithDisplayContentsColHeader" role="grid">
+ <div role="row">
+ <div role="columnheader" style="display:contents;">col1</div>
+ <div role="columnheader">col2</div>
+ </div>
+ <div role="row">
+ <div role="rowheader">row1</div>
+ <div role="gridcell">cell1</div>
+ </div>
+ </div>
+ <div id="gridWithDisplayContentsRowHeader" role="grid">
+ <div role="row">
+ <div role="columnheader">col1</div>
+ <div role="columnheader">col2</div>
+ </div>
+ <div role="row">
+ <div role="rowheader" style="display:contents;">row1</div>
+ <div role="gridcell">cell1</div>
+ </div>
+ </div>
+ <div id="gridWithDisplayContentsGridCell" role="grid">
+ <div role="row">
+ <div role="columnheader">col1</div>
+ <div role="columnheader">col2</div>
+ </div>
+ <div role="row">
+ <div role="rowheader">row1</div>
+ <div role="gridcell" style="display:contents;">cell1</div>
+ </div>
+ </div>
+
+ <div id="container">
+ <div role="list" style="display: contents;">
+ <div role="listitem">test</div>
+ </div>
+ <div aria-label="test" style="display: contents;">
+ <div role="listitem">test</div>
+ </div>
+ <div role="none" style="display: contents;">
+ <div role="listitem">test</div>
+ </div>
+ </div>
+
+ <div id="paragraphContainer">
+ <p style="display: contents;">test</p>
+ <p style="display: contents;" role="none">test</p>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_aria_globals.html b/accessible/tests/mochitest/tree/test_aria_globals.html
new file mode 100644
index 0000000000..bb5fe14cdf
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_aria_globals.html
@@ -0,0 +1,127 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test Global ARIA States and Accessible Creation</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var globalIds = [
+ "atomic",
+ "busy",
+ "controls",
+ "describedby",
+ "description",
+ "disabled",
+ "dropeffect",
+ "flowto",
+ "grabbed",
+ "haspopup",
+ "invalid",
+ "label",
+ "labelledby",
+ "live",
+ "owns",
+ "relevant",
+ ];
+
+ // Elements having ARIA global state or properties or referred by another
+ // element must be accessible.
+ ok(isAccessible("pawn"),
+ "Must be accessible because referred by another element.");
+
+ for (let idx = 0; idx < globalIds.length; idx++) {
+ ok(isAccessible(globalIds[idx]),
+ "Must be accessible becuase of aria-" + globalIds[idx] +
+ " presence");
+ }
+
+ // Unfocusable elements, having ARIA global state or property with a valid
+ // IDREF value, and an inherited presentation role. A generic accessible
+ // is created (to prevent table cells text jamming).
+ ok(!isAccessible("td_nothing", nsIAccessibleTableCell),
+ "inherited presentation role takes a place");
+
+ for (let idx = 0; idx < globalIds.length; idx++) {
+ ok(isAccessible("td_" + globalIds[idx]),
+ "Inherited presentation role must be ignored becuase of " +
+ "aria-" + globalIds[idx] + " presence");
+ }
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Update universal ARIA attribute support to latest spec"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=551978">
+ Mozilla Bug 551978
+ </a>
+ <a target="_blank"
+ title="Presentational table related elements referred or having global ARIA attributes must be accessible"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=809751">
+ Mozilla Bug 809751
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- Test that global aria states and properties are enough to cause the
+ creation of accessible objects -->
+ <div id="global_aria_states_and_props" role="group">
+ <span id="pawn"></span>
+ <span id="atomic" aria-atomic="true"></span>
+ <span id="busy" aria-busy="false"></span>
+ <span id="controls" aria-controls="pawn"></span>
+ <span id="describedby" aria-describedby="pawn"></span>
+ <span id="description" aria-description="hi"></span>
+ <span id="disabled" aria-disabled="true"></span>
+ <span id="dropeffect" aria-dropeffect="move"></span>
+ <span id="flowto" aria-flowto="pawn"></span>
+ <span id="grabbed" aria-grabbed="false"></span>
+ <span id="haspopup" aria-haspopup="false"></span>
+ <span id="invalid" aria-invalid="false"></span>
+ <span id="label" aria-label="hi"></span>
+ <span id="labelledby" aria-labelledby="label"></span>
+ <span id="live" aria-live="polite"></span>
+ <span id="owns" aria-owns="pawn"></span>
+ <span id="relevant" aria-relevant="additions"></span>
+ </div>
+
+ <table role="presentation">
+ <tr>
+ <td id="td_nothing"></td>
+ <td id="td_atomic" aria-atomic="true"></td>
+ <td id="td_busy" aria-busy="false"></td>
+ <td id="td_controls" aria-controls="pawn"></td>
+ <td id="td_describedby" aria-describedby="pawn"></td>
+ <td id="td_description" aria-description="hi"></td>
+ <td id="td_disabled" aria-disabled="true"></td>
+ <td id="td_dropeffect" aria-dropeffect="move"></td>
+ <td id="td_flowto" aria-flowto="pawn"></td>
+ <td id="td_grabbed" aria-grabbed="false"></td>
+ <td id="td_haspopup" aria-haspopup="false"></td>
+ <td id="td_invalid" aria-invalid="false"></td>
+ <td id="td_label" aria-label="hi"></td>
+ <td id="td_labelledby" aria-labelledby="label"></td>
+ <td id="td_live" aria-live="polite"></td>
+ <td id="td_owns" aria-owns="pawn"></td>
+ <td id="td_relevant" aria-relevant="additions"></td>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_aria_grid.html b/accessible/tests/mochitest/tree/test_aria_grid.html
new file mode 100644
index 0000000000..80ff97095b
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_aria_grid.html
@@ -0,0 +1,318 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML table tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // grid having rowgroups
+
+ var accTree =
+ { TABLE: [
+ { GROUPING: [
+ { ROW: [
+ { GRID_CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] },
+ ] };
+
+ testAccessibleTree("grid", accTree);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // strange grids (mix of ARIA and HTML tables)
+
+ accTree = {
+ role: ROLE_TABLE,
+ children: [
+ { // div@role="row"
+ role: ROLE_ROW,
+ tagName: "DIV",
+ children: [
+ { // caption text leaf
+ role: ROLE_TEXT_LEAF,
+ name: "caption",
+ children: [ ],
+ },
+ { // th generic accessible
+ role: ROLE_TEXT_CONTAINER,
+ children: [
+ { // th text leaf
+ role: ROLE_TEXT_LEAF,
+ name: "header1",
+ children: [ ],
+ },
+ ],
+ },
+ { // td@role="columnheader"
+ role: ROLE_COLUMNHEADER,
+ name: "header2",
+ children: [ { TEXT_LEAF: [ ] } ],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree("strange_grid1", accTree);
+
+ accTree = {
+ role: ROLE_TABLE,
+ children: [
+ { // tr@role="row"
+ role: ROLE_ROW,
+ tagName: "TR",
+ children: [
+ { // td implicit role="gridcell"
+ role: ROLE_GRID_CELL,
+ children: [
+ { // td text leaf
+ role: ROLE_TEXT_LEAF,
+ name: "cell1",
+ children: [ ],
+ },
+ ],
+ },
+ { // td@role="gridcell"
+ role: ROLE_GRID_CELL,
+ name: "cell2",
+ children: [ { TEXT_LEAF: [ ] } ],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree("strange_grid2", accTree);
+
+ accTree = {
+ role: ROLE_TABLE,
+ children: [
+ { // div@role="row"
+ role: ROLE_ROW,
+ children: [
+ { // div@role="gridcell"
+ role: ROLE_GRID_CELL,
+ children: [
+ { // td generic accessible
+ role: ROLE_TEXT_CONTAINER,
+ children: [
+ { // text leaf from presentational table
+ role: ROLE_TEXT_LEAF,
+ name: "cell3",
+ children: [ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree("strange_grid3", accTree);
+
+ accTree = {
+ role: ROLE_TABLE,
+ children: [
+ { // div@role="row"
+ role: ROLE_ROW,
+ children: [
+ { // div@role="gridcell"
+ role: ROLE_GRID_CELL,
+ children: [
+ { // table
+ role: ROLE_TABLE,
+ children: [
+ { // tr
+ role: ROLE_ROW,
+ children: [
+ { // td
+ role: ROLE_CELL,
+ children: [
+ { // caption text leaf of presentational table
+ role: ROLE_TEXT_LEAF,
+ name: "caption",
+ children: [ ],
+ },
+ { // td generic accessible
+ role: ROLE_TEXT_CONTAINER,
+ children: [
+ { // td text leaf of presentational table
+ role: ROLE_TEXT_LEAF,
+ name: "cell4",
+ children: [ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
+ testAccessibleTree("strange_grid4", accTree);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // grids that could contain whitespace accessibles but shouldn't.
+
+ accTree =
+ { TREE_TABLE: [
+ { ROW: [
+ { GRID_CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ { GRID_CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ { GRID_CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] };
+
+ testAccessibleTree("whitespaces-grid", accTree);
+
+ // grids that could contain text container accessibles but shouldn't.
+
+ accTree =
+ { TABLE: [
+ { ROW: [
+ { GRID_CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ { GRID_CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ { ROW: [
+ { GRID_CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ { GRID_CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] };
+
+ testAccessibleTree("gridWithPresentationalBlockElement", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Support ARIA role rowgroup"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=525909">
+ Mozilla Bug 525909
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="grid" role="grid">
+ <div role="rowgroup">
+ <div role="row">
+ <div role="gridcell">cell</div>
+ </div>
+ </div>
+ </div>
+
+ <div id="strange_grid1" role="grid">
+ <div role="row">
+ <table role="presentation">
+ <caption>caption</caption>
+ <tr>
+ <th>header1</th>
+ <td role="columnheader">header2</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+
+ <div id="strange_grid2" role="grid">
+ <table role="presentation">
+ <tr role="row">
+ <td id="implicit_gridcell">cell1</td>
+ <td role="gridcell">cell2</td>
+ </tr>
+ </table>
+ </div>
+
+ <div id="strange_grid3" role="grid">
+ <div role="row">
+ <div role="gridcell">
+ <table role="presentation">
+ <tr>
+ <td>cell3</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+
+ <div id="strange_grid4" role="grid">
+ <div role="row">
+ <div role="gridcell">
+ <table>
+ <tr>
+ <td>
+ <table role="presentation">
+ <caption>caption</caption>
+ <tr><td>cell4</td></tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+
+ <div role="treegrid" id="whitespaces-grid">
+ <div role="row" aria-selected="false" tabindex="-1">
+ <span role="gridcell">03:30PM-04:30PM</span>
+ <span role="gridcell" style="font-weight:bold;">test</span>
+ <span role="gridcell">a user1</span>
+ </div>
+ </div>
+
+ <div id="gridWithPresentationalBlockElement" role="grid">
+ <span style="display: block;">
+ <div role="row">
+ <div role="gridcell">Cell 1</div>
+ <div role="gridcell">Cell 2</div>
+ </div>
+ </span>
+ <span style="display: block;">
+ <div role="row">
+ <span style="display: block;">
+ <div role="gridcell">Cell 3</div>
+ <div role="gridcell">Cell 4</div>
+ </span>
+ </div>
+ </span>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_aria_imgmap.html b/accessible/tests/mochitest/tree/test_aria_imgmap.html
new file mode 100644
index 0000000000..0cd4867cae
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_aria_imgmap.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test usemap elements and ARIA</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // gA11yEventDumpToConsole = true;
+ function doPreTest() {
+ waitForImageMap("imagemap", doTest);
+ }
+
+ function doTest() {
+ var accTree = {
+ role: ROLE_IMAGE_MAP,
+ children: [
+ {
+ role: ROLE_ENTRY,
+ name: "first name",
+ },
+ {
+ role: ROLE_ENTRY,
+ name: "last name",
+ },
+ {
+ role: ROLE_RADIOBUTTON,
+ name: "male",
+ },
+ {
+ role: ROLE_RADIOBUTTON,
+ name: "female",
+ },
+ {
+ role: ROLE_CHECKBUTTON,
+ name: "have bike",
+ },
+ {
+ role: ROLE_EDITCOMBOBOX,
+ name: "bike model",
+ },
+ {
+ role: ROLE_CHECKBUTTON,
+ name: "have car",
+ },
+ {
+ role: ROLE_CHECKBUTTON,
+ name: "have airplane",
+ },
+ {
+ role: ROLE_PUSHBUTTON,
+ name: "submit",
+ },
+ ],
+ };
+
+ // Test image map tree structure, roles, and names.
+ testAccessibleTree("imagemap", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doPreTest);
+ </script>
+
+</head>
+<body>
+
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=548291"
+ title="Accessible tree of ARIA image maps">
+Mozilla Bug 548291
+</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test">
+</pre>
+
+<img id="imagemap" src="../formimage.png" width="219" height="229" border="0" usemap="#ariaMap">
+<map id="ariaMap" name="ariaMap">
+ <area id="t1" role="textbox" shape="rect" tabindex="0" alt="" title="first name" coords="4,20,108,48" href="#" />
+ <area id="t2" role="textbox" shape="rect" alt="" title="last name" coords="111,21,215,50" href="#" />
+ <area id="rb1" role="radio" aria-checked="true" shape="circle" alt="" title="male" coords="60,75,11" href="#" />
+ <area id="rb2" role="radio" shape="circle" alt="" title="female" coords="73,94,11" href="#" />
+ <area id="cb1" role="checkbox" aria-checked="true" shape="rect" alt="" title="have bike" coords="95,123,118,145" href="#" />
+ <area id="cbox" role="combobox" shape="rect" alt="" title="bike model" coords="120,124,184,146" href="#" />
+ <area id="cb2" role="checkbox" shape="rect" alt="" title="have car" coords="90,145,114,164" href="#" />
+ <area id="cb3" role="checkbox" shape="rect" alt="" title="have airplane" coords="130,163,152,184" href="#" />
+ <area id="b1" role="button" shape="rect" alt="" title="submit" coords="4,198,67,224" href="#" />
+</map>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_aria_list.html b/accessible/tests/mochitest/tree/test_aria_list.html
new file mode 100644
index 0000000000..f3642c801d
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_aria_list.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>ARIA lists</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // list
+
+ var accTree =
+ { LIST: [
+ { LISTITEM: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] };
+
+ testAccessibleTree("list", accTree);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // strange list (mix of ARIA and HTML)
+
+ accTree = { // div@role="list"
+ role: ROLE_LIST,
+ children: [
+ { // li
+ role: ROLE_TEXT_CONTAINER,
+ children: [
+ { // li text leaf
+ role: ROLE_TEXT_LEAF,
+ name: "item1",
+ children: [ ],
+ },
+ ],
+ },
+ { // li@role="listitem"
+ role: ROLE_LISTITEM,
+ children: [
+ { // text leaf
+ role: ROLE_TEXT_LEAF,
+ name: "item2",
+ children: [ ],
+ },
+ ],
+ },
+ ],
+ };
+
+ testAccessibleTree("strange_list", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Build the context dependent tree"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=804461">
+ Mozilla Bug 804461
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="list" role="list">
+ <div role="listitem">item1</div>
+ </div>
+
+ <div id="strange_list" role="list">
+ <ul role="presentation">
+ <li>item1</li>
+ <li role="listitem">item2</li>
+ </ul>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_aria_menu.html b/accessible/tests/mochitest/tree/test_aria_menu.html
new file mode 100644
index 0000000000..2f1f6645db
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_aria_menu.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test accessible tree when ARIA role menuitem is used</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // Menuitem with no popup.
+ let tree =
+ { SECTION: [ // container
+ { MENUPOPUP: [ // menu
+ { MENUITEM: [
+ { LISTITEM_MARKER: [] }, // bullet
+ { TEXT_LEAF: [] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("menu", tree);
+
+ // Menuitem with explicit no popup.
+ tree =
+ { SECTION: [ // container
+ { MENUPOPUP: [ // menu
+ { MENUITEM: [
+ { LISTITEM_MARKER: [] }, // bullet
+ { TEXT_LEAF: [] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("menu_nopopup", tree);
+
+ // Menuitem with popup.
+ tree =
+ { SECTION: [ // container
+ { MENUPOPUP: [ // menu
+ { PARENT_MENUITEM: [ // menuitem with aria-haspopup="true"
+ { LISTITEM_MARKER: [] }, // bullet
+ { TEXT_LEAF: [] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("menu_popup", tree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=786566"
+ title="ARIA menuitem acting as submenu should have PARENT_MENUITEM role">
+ Mozilla Bug 786566
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="menu">
+ <ul role="menu">
+ <li role="menuitem">Normal Menu</li>
+ </ul>
+ </div>
+
+ <div id="menu_nopopup">
+ <ul role="menu">
+ <li role="menuitem" aria-haspopup="false">Menu with explicit no popup</li>
+ </ul>
+ </div>
+
+ <div id="menu_popup">
+ <ul role="menu">
+ <li role="menuitem" aria-haspopup="true">Menu with popup</li>
+ </ul>
+ </div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_aria_owns.html b/accessible/tests/mochitest/tree/test_aria_owns.html
new file mode 100644
index 0000000000..a01968521b
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_aria_owns.html
@@ -0,0 +1,197 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>@aria-owns attribute testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Tests
+ // //////////////////////////////////////////////////////////////////////////
+
+ // enableLogging("tree,verbose"); // debug stuff
+
+ var gQueue = null;
+
+ function doTest() {
+ var tree =
+ { SECTION: [ // t1_1
+ { HEADING: [ // t1_2
+ // no kids, no loop
+ ] },
+ ] };
+ testAccessibleTree("t1_1", tree);
+
+ tree =
+ { SECTION: [ // t2_1
+ { GROUPING: [ // t2_2
+ { HEADING: [ // t2_3
+ // no kids, no loop
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("t2_1", tree);
+
+ tree =
+ { SECTION: [ // t3_3
+ { GROUPING: [ // t3_1
+ { NOTE: [ // t3_2
+ { HEADING: [ // DOM child of t3_2
+ // no kids, no loop
+ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("t3_3", tree);
+
+ tree =
+ { SECTION: [ // t4_1
+ { GROUPING: [ // DOM child of t4_1, aria-owns ignored
+ // no kids, no loop
+ ] },
+ ] };
+ testAccessibleTree("t4_1", tree);
+
+ tree =
+ { SECTION: [ // t5_1
+ { GROUPING: [ // DOM child of t5_1
+ { NOTE: [ // t5_2
+ { HEADING: [ // DOM child of t5_2
+ { FORM: [ // t5_3
+ { TOOLTIP: [ // DOM child of t5_3
+ // no kids, no loop
+ ]},
+ ]},
+ ]},
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("t5_1", tree);
+
+ tree =
+ { SECTION: [ // t6_1
+ { RADIOBUTTON: [ ] },
+ { CHECKBUTTON: [ ] }, // t6_3, rearranged by aria-owns
+ { PUSHBUTTON: [ ] }, // t6_2, rearranged by aria-owns
+ ] };
+ testAccessibleTree("t6_1", tree);
+
+ tree =
+ { SECTION: [ // ariaowns_container
+ { SECTION: [ // ariaowns_self
+ { SECTION: [ // ariaowns_uncle
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("ariaowns_container", tree);
+
+ tree =
+ { TABLE: [
+ { ROW: [
+ { GRID_CELL: [
+ { TEXT_LEAF: [] },
+ ] },
+ { GRID_CELL: [
+ { TEXT_LEAF: [] },
+ ] },
+ ] },
+ { ROW: [
+ { GRID_CELL: [
+ { TEXT_LEAF: [] },
+ ] },
+ { GRID_CELL: [
+ { TEXT_LEAF: [] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("grid", tree);
+
+ tree =
+ { SECTION: [ // presentation_owner
+ // Can't own ancestor, so no children.
+ ] };
+ testAccessibleTree("presentation_owner", tree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+
+ </script>
+</head>
+
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- simple loop -->
+ <div id="t1_1" aria-owns="t1_2"></div>
+ <div id="t1_2" aria-owns="t1_1" role="heading"></div>
+
+ <!-- loop -->
+ <div id="t2_2" aria-owns="t2_3" role="group"></div>
+ <div id="t2_1" aria-owns="t2_2"></div>
+ <div id="t2_3" aria-owns="t2_1" role="heading"></div>
+
+ <!-- loop #2 -->
+ <div id="t3_1" aria-owns="t3_2" role="group"></div>
+ <div id="t3_2" role="note">
+ <div aria-owns="t3_3" role="heading"></div>
+ </div>
+ <div id="t3_3" aria-owns="t3_1"></div>
+
+ <!-- self loop -->
+ <div id="t4_1"><div aria-owns="t4_1" role="group"></div></div>
+
+ <!-- natural and aria-owns hierarchy -->
+ <div id="t5_2" role="note"><div aria-owns="t5_3" role="heading"></div></div>
+ <div id="t5_1"><div aria-owns="t5_2" role="group"></div></div>
+ <div id="t5_3" role="form"><div aria-owns="t5_1" role="tooltip"></div></div>
+
+ <!-- rearrange children -->
+ <div id="t6_1" aria-owns="t6_3 t6_2">
+ <div id="t6_2" role="button"></div>
+ <div id="t6_3" role="checkbox"></div>
+ <div role="radio"></div>
+ </div>
+
+ <div id="ariaowns_container">
+ <div id="ariaowns_self"
+ aria-owns="aria_ownscontainer ariaowns_self ariaowns_uncle"></div>
+ </div>
+ <div id="ariaowns_uncle"></div>
+
+ <!-- grid -->
+ <div aria-owns="grid-row2" role="grid" id="grid">
+ <div role="row">
+ <div role="gridcell">cell 1,1</div>
+ <div role="gridcell">cell 1,2</div>
+ </div>
+ </div>
+ <div role="row" id="grid-row2">
+ <div role="gridcell">cell 2,1</div>
+ <div role="gridcell">cell 2,2</div>
+ </div>
+
+ <!-- Owned child which is an ancestor of its owner but didn't yet exist when
+ aria-owns relocation was processed (bug 1485097). -->
+ <div id="presentation" role="presentation">
+ <div id="presentation_owner" aria-owns="presentation"></div>
+ </div>
+</body>
+
+</html>
diff --git a/accessible/tests/mochitest/tree/test_aria_presentation.html b/accessible/tests/mochitest/tree/test_aria_presentation.html
new file mode 100644
index 0000000000..5680193441
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_aria_presentation.html
@@ -0,0 +1,176 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test accessible tree when ARIA role presentation is used</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // Presentation role don't allow accessible.
+ var tree =
+ { SECTION: [ // container
+ { TEXT_LEAF: [ ] }, // child text of 'presentation' node
+ { TEXT_LEAF: [ ] }, // child text of 'none' node
+ ] };
+ testAccessibleTree("div_cnt", tree);
+
+ // Focusable element, 'presentation' and 'none' roles are ignored.
+ tree =
+ { SECTION: [ // container
+ { PUSHBUTTON: [ // button having 'presentation' role
+ { TEXT_LEAF: [ ] },
+ ] },
+ { PUSHBUTTON: [ // button having 'none' role
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] };
+ testAccessibleTree("btn_cnt", tree);
+
+ // Presentation table, no table structure is exposed.
+ tree =
+ { SECTION: [ // container
+ { TEXT_CONTAINER: [ // td generic accessible inside 'presentation' table
+ { TEXT_LEAF: [ ] }, // cell text
+ ] },
+ { TEXT_CONTAINER: [ // td generic accessible inside 'none' table
+ { TEXT_LEAF: [ ] }, // cell text
+ ] },
+ ] };
+ testAccessibleTree("tbl_cnt", tree);
+
+ // Focusable table, 'presentation' and 'none' roles are ignored.
+ tree =
+ { SECTION: [ // container
+ { TABLE: [ // table having 'presentation' role
+ { ROW: [ // tr
+ { CELL: [ // td
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] },
+ { TABLE: [ // table having 'none' role
+ { ROW: [ // tr
+ { CELL: [ // td
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("tblfocusable_cnt", tree);
+
+ // Presentation list, expose generic accesisble for list items.
+ tree =
+ { SECTION: [ // container
+ { TEXT_CONTAINER: [ // li generic accessible inside 'presentation' role
+ { TEXT_LEAF: [ ] }, // li text
+ ] },
+ { TEXT_CONTAINER: [ // li generic accessible inside 'none' role
+ { TEXT_LEAF: [ ] }, // li text
+ ] },
+ ] };
+ testAccessibleTree("list_cnt", tree);
+
+ // Has ARIA globals or referred by ARIA relationship, role='presentation'
+ // and role='none' are ignored.
+ tree =
+ { SECTION: [ // container
+ { LABEL: [ // label, has aria-owns
+ { TEXT_LEAF: [ ] },
+ { LABEL: [ // label, referenced by aria-owns
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ { LABEL: [ // label, has aria-owns
+ { TEXT_LEAF: [ ] },
+ { LABEL: [ // label, referenced by aria-owns
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("airaglobalprop_cnt", tree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=548291"
+ title="Accessible tree of ARIA image maps">
+ Bug 548291
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=666504"
+ title="Ignore role presentation on focusable elements">
+ Bug 666504
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=971212"
+ title="Implement ARIA role=none">
+ Bug 971212
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="div_cnt"><div role="presentation">t</div><div role="none">t</div></div>
+
+ <div id="btn_cnt"><button role="presentation">btn</button><button role="none">btn</button></div>
+
+ <div id="tbl_cnt">
+ <table role="presentation">
+ <tr>
+ <td>cell</td>
+ </tr>
+ </table>
+ <table role="none">
+ <tr>
+ <td>cell</td>
+ </tr>
+ </table>
+ </div>
+
+ <div id="tblfocusable_cnt">
+ <table role="presentation" tabindex="0">
+ <tr>
+ <td>cell</td>
+ </tr>
+ </table>
+ <table role="none" tabindex="0">
+ <tr>
+ <td>cell</td>
+ </tr>
+ </table>
+ </div>
+
+ <div id="list_cnt">
+ <ul role="presentation">
+ <li>item</li>
+ </ul>
+ <ul role="none">
+ <li>item</li>
+ </ul>
+ </div>
+
+ <div id="airaglobalprop_cnt"><label
+ role="presentation" aria-owns="ariaowned">has aria-owns</label><label
+ role="presentation" id="ariaowned">referred by aria-owns</label><label
+ role="none" aria-owns="ariaowned2">has aria-owns</label><label
+ role="none" id="ariaowned2">referred by aria-owns</label></div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_aria_table.html b/accessible/tests/mochitest/tree/test_aria_table.html
new file mode 100644
index 0000000000..22375faf59
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_aria_table.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>ARIA table tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // table having rowgroups
+
+ var accTree =
+ { TABLE: [
+ { GROUPING: [
+ { ROW: [
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] },
+ ] };
+
+ testAccessibleTree("table", accTree);
+
+ // tables that could contain text container accessibles but shouldn't.
+
+ accTree =
+ { TABLE: [
+ { ROW: [
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ { ROW: [
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] };
+
+ testAccessibleTree("tableWithPresentationalBlockElement", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="support ARIA table and cell roles"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1173364">
+ Bug 1173364
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="table" role="table">
+ <div role="rowgroup">
+ <div role="row">
+ <div role="cell">cell</div>
+ </div>
+ </div>
+ </div>
+
+ <div id="tableWithPresentationalBlockElement" role="table">
+ <span style="display: block;">
+ <div role="row">
+ <div role="cell">Cell 1</div>
+ <div role="cell">Cell 2</div>
+ </div>
+ </span>
+ <span style="display: block;">
+ <div role="row">
+ <span style="display: block;">
+ <div role="cell">Cell 3</div>
+ <div role="cell">Cell 4</div>
+ </span>
+ </div>
+ </span>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_brokencontext.html b/accessible/tests/mochitest/tree/test_brokencontext.html
new file mode 100644
index 0000000000..fbdefd398f
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_brokencontext.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Broken context hierarchy</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ /**
+ * Return true if TD element has a generic accessible.
+ */
+ function isTDGeneric(aID) {
+ return isAccessible(aID) && !isAccessible(aID, nsIAccessibleTableCell);
+ }
+
+ function checkIfNotAccessible(aID) {
+ ok(!isAccessible(aID), "'" + aID + "' shouldn't be accessible");
+ }
+ function checkIfTDGeneric(aID) {
+ ok(isTDGeneric(aID), "'" + aID + "' shouldn't have cell accessible");
+ }
+
+ function doTest() {
+ // //////////////////////////////////////////////////////////////////////////
+ // HTML table elements outside table context.
+
+ // HTML table role="presentation"
+ checkIfNotAccessible("tr_in_presentation_table");
+ checkIfTDGeneric("th_in_presentation_table");
+ checkIfTDGeneric("td_in_presentation_table");
+
+ // HTML table role="button"
+ var tree =
+ { PUSHBUTTON: [ // table
+ { TEXT_CONTAINER: [ // tr
+ { TEXT_CONTAINER: [ // th
+ { TEXT_LEAF: [ ] },
+ ] },
+ { TEXT_CONTAINER: [ // td
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("button_table", tree);
+
+ // //////////////////////////////////////////////////////////////////////////
+ // HTML list elements outside list context.
+
+ ok(!isAccessible("presentation_ul"),
+ "presentational ul shouldn't be accessible");
+ ok(isAccessible("item_in_presentation_ul"),
+ "li in presentational ul should have generic accessible");
+ ok(isAccessible("styleditem_in_presentation_ul"),
+ "list styled span in presentational ul should have generic accessible");
+
+ ok(!isAccessible("presentation_ol"),
+ "presentational ol shouldn't be accessible");
+ ok(isAccessible("item_in_presentation_ol"),
+ "li in presentational ol should have generic accessible");
+
+ ok(!isAccessible("presentation_dl"),
+ "presentational dl shouldn't be accessible");
+ ok(!isAccessible("dt_in_presentation_dl"),
+ "dt in presentational dl shouldn't be accessible");
+ ok(!isAccessible("dd_in_presentation_dl"),
+ "dd in presentational dl shouldn't be accessible");
+
+ tree =
+ { PUSHBUTTON: [ // ul
+ { TEXT_CONTAINER: [ // li
+ { LISTITEM_MARKER: [ ] },
+ { TEXT_LEAF: [ ] },
+ ] },
+ { TEXT_CONTAINER: [ // span styled as a list
+ { LISTITEM_MARKER: [ ] },
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] };
+ testAccessibleTree("button_ul", tree);
+
+ tree =
+ { PUSHBUTTON: [ // ol
+ { TEXT_CONTAINER: [ // li
+ { LISTITEM_MARKER: [ ] },
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] };
+ testAccessibleTree("button_ol", tree);
+
+ tree =
+ { PUSHBUTTON: [ // dl
+ { TEXT_CONTAINER: [ // dt
+ { TEXT_LEAF: [ ] },
+ ] },
+ { TEXT_CONTAINER: [ // dd
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] };
+ testAccessibleTree("button_dl", tree);
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Styled as HTML table elements, accessible is created by tag name
+
+ tree =
+ { LINK: [ // a
+ { TEXT_LEAF: [ ] },
+ ] };
+ testAccessibleTree("a_as_td", tree);
+
+ tree =
+ { HEADING: [
+ { TEXT_LEAF: [ ] },
+ ] };
+ testAccessibleTree("h1_as_td", tree);
+ testAccessibleTree("h2_as_td", tree);
+ testAccessibleTree("h3_as_td", tree);
+ testAccessibleTree("h4_as_td", tree);
+ testAccessibleTree("h5_as_td", tree);
+ testAccessibleTree("h6_as_td", tree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=706849"
+ title="Create accessible by tag name as fallback if table descendant style is used out of table context">
+ Bug 706849
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=804461"
+ title="Build the context dependent tree ">
+ Bug 804461
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=945435"
+ title="Create generic accessible for td to not jamm the cell text">
+ Bug 945435
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- HTML table elements out of table -->
+ <table role="presentation">
+ <tr id="tr_in_presentation_table">
+ <th id="th_in_presentation_table">not a header</th>
+ <td id="td_in_presentation_table">not a cell</td>
+ </tr>
+ </table>
+
+ <table role="button" id="button_table">
+ <tr id="tr_in_button_table">
+ <th id="th_in_button_table">not a header</th>
+ <td id="td_in_button_table">not a cell</td>
+ </tr>
+ </table>
+
+ <!-- HTML list elements out of list -->
+ <ul role="presentation" id="presentation_ul">
+ <li id="item_in_presentation_ul">item</li>
+ <span id="styleditem_in_presentation_ul"
+ style="display:list-item">Oranges</span>
+ </ul>
+
+ <ol role="presentation" id="presentation_ol">
+ <li id="item_in_presentation_ol">item</li>
+ </ol>
+
+ <dl role="presentation" id="presentation_dl">
+ <dt id="dt_in_presentation_dl">term</dt>
+ <dd id="dd_in_presentation_dl">definition</dd>
+ </dl>
+
+ <ul role="button" id="button_ul">
+ <li id="item_in_button_ul">item</li>
+ <span id="styleditem_in_button_ul"
+ style="display:list-item">Oranges</span>
+ </ul>
+
+ <ol role="button" id="button_ol">
+ <li id="item_in_button_ul">item</li>
+ </ol>
+
+ <dl role="button" id="button_dl">
+ <dt id="dt_in_button_dl">term</ld>
+ <dd id="dd_in_button_dl">definition</dd>
+ </dl>
+
+ <!-- styled as HTML table elements -->
+ <a id="a_as_td" style="display:table-cell;" href="http://www.google.com">Google</a>
+ <h1 id="h1_as_td" style="display: table-cell;">h1</h1>
+ <h2 id="h2_as_td" style="display: table-cell;">h2</h2>
+ <h3 id="h3_as_td" style="display: table-cell;">h3</h3>
+ <h4 id="h4_as_td" style="display: table-cell;">h4</h4>
+ <h5 id="h5_as_td" style="display: table-cell;">h5</h5>
+ <h6 id="h6_as_td" style="display: table-cell;">h6</h6>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_button.xhtml b/accessible/tests/mochitest/tree/test_button.xhtml
new file mode 100644
index 0000000000..fec453b717
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_button.xhtml
@@ -0,0 +1,83 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL button hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ function doTest()
+ {
+ //////////////////////////////////////////////////////////////////////////
+ // button
+
+ var accTree = {
+ role: ROLE_PUSHBUTTON,
+ name: "hello",
+ children: [ ]
+ };
+ testAccessibleTree("button1", accTree);
+
+ //////////////////////////////////////////////////////////////////////////
+ // toolbarbutton
+
+ accTree = {
+ role: ROLE_PUSHBUTTON,
+ name: "hello",
+ children: [ ]
+ };
+ testAccessibleTree("button2", accTree);
+
+ //////////////////////////////////////////////////////////////////////////
+ // toolbarbutton with type="checkbox"
+
+ accTree = {
+ role: ROLE_TOGGLE_BUTTON,
+ name: "hello",
+ children: [ ]
+ };
+ testAccessibleTree("button3", accTree);
+
+ SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=249292"
+ title="Ensure accessible children for toolbarbutton types 'menu'">
+ Mozilla Bug 249292
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <button id="button1" label="hello"/>
+ <toolbarbutton id="button2" label="hello"/>
+ <toolbarbutton id="button3" type="checkbox" label="hello"/>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/tree/test_canvas.html b/accessible/tests/mochitest/tree/test_canvas.html
new file mode 100644
index 0000000000..804bcc1f6e
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_canvas.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=495912
+-->
+<head>
+ <title>File Input Control tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var accTree =
+ { CANVAS: [
+ { CHECKBUTTON: [] },
+ { ENTRY: [] },
+ ] };
+
+ testAccessibleTree("canvas", accTree);
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Expose alternative content in Canvas element to ATs"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=495912">Mozilla Bug 495912</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <canvas id="canvas" tabindex="0"><input type="checkbox"><input></canvas>
+
+ <script type="text/javascript">
+ var c = document.getElementById("canvas");
+ var cxt = c.getContext("2d");
+ cxt.fillStyle = "#005500";
+ cxt.fillRect(0, 0, 150, 75);
+ </script>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_combobox.xhtml b/accessible/tests/mochitest/tree/test_combobox.xhtml
new file mode 100644
index 0000000000..8b674c9557
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_combobox.xhtml
@@ -0,0 +1,107 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Accessible XUL menulist and textbox @autocomplete hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ function doTest()
+ {
+ //////////////////////////////////////////////////////////////////////////
+ // menulist
+
+ var accTree = {
+ role: ROLE_COMBOBOX,
+ children: [
+ {
+ role: ROLE_COMBOBOX_LIST,
+ children: [
+ {
+ role: ROLE_COMBOBOX_OPTION,
+ children: []
+ },
+ {
+ role: ROLE_COMBOBOX_OPTION,
+ children: []
+ }
+ ]
+ }
+ ]
+ };
+
+ testAccessibleTree("menulist", accTree);
+
+ //////////////////////////////////////////////////////////////////////////
+ // textbox@type=autocomplete #1 (history)
+
+ accTree = {
+ // html:input
+ role: ROLE_ENTRY,
+ children: [
+ {
+ // #text
+ role: ROLE_TEXT_LEAF,
+ name: "http://mochi.test:8888/redirect-a11y.html",
+ children: []
+ }
+ ],
+ };
+
+ testAccessibleTree("autocomplete", accTree);
+
+ SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=249292"
+ title="Ensure accessible children for toolbarbutton types 'menu'">
+ Mozilla Bug 249292
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=626660"
+ title="Cache rendered text on a11y side">
+ Mozilla Bug 626660
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <menulist id="menulist">
+ <menupopup>
+ <menuitem label="item"/>
+ <menuitem label="item"/>
+ </menupopup>
+ </menulist>
+
+ <html:input is="autocomplete-input"
+ id="autocomplete"
+ value="http://mochi.test:8888/redirect-a11y.html"/>
+ </vbox>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/tree/test_cssflexbox.html b/accessible/tests/mochitest/tree/test_cssflexbox.html
new file mode 100644
index 0000000000..72fafba0a2
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_cssflexbox.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>CSS flexbox tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // Ensure that flexbox ordering and absolute positioning do not affect
+ // the accessibility tree.
+ // Note that there is no accessible for a div with display:flex style.
+ var accTree = {
+ role: ROLE_SECTION,
+ children: [
+ { // Bug 1277559. Button outside the flexed content
+ role: ROLE_PUSHBUTTON,
+ name: "Button",
+ },
+ { // Visually first button in the 3 button row
+ role: ROLE_PUSHBUTTON,
+ name: "First",
+ },
+ { // Flushed right third button in the 3 button row
+ role: ROLE_PUSHBUTTON,
+ name: "Second",
+ },
+ { // Middle button in the 3 button row
+ role: ROLE_PUSHBUTTON,
+ name: "Third",
+ }, // end bug 1277559
+ { // Bug 962558: DOM first, Order 2.
+ role: ROLE_PUSHBUTTON,
+ name: "two, tab first",
+ },
+ { // DOM order second, flex order 1
+ role: ROLE_PUSHBUTTON,
+ name: "one, tab second",
+ }, // end bug 962558
+ ],
+ };
+ testAccessibleTree("flex_elements", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="flex_elements">
+ <button type="button">Button</button>
+ <div style="position: relative; display: flex; width: 200px;">
+ <button type="button" style="order: 1">First</button>
+ <button type="button" style="order: 2; position: absolute; right: 0">Second</button>
+ <button type="button" style="order: 3">Third</button>
+ </div>
+ <div style="display: flex">
+ <button id="two" style="order: 2">two, tab first</button>
+ <button id="one" style="order: 1">one, tab second</button>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_cssoverflow.html b/accessible/tests/mochitest/tree/test_cssoverflow.html
new file mode 100644
index 0000000000..87898fef6c
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_cssoverflow.html
@@ -0,0 +1,135 @@
+<html>
+
+<head>
+ <title>CSS overflow testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <style>
+ a.link:focus {
+ overflow: scroll;
+ }
+ </style>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function focusAnchor(aID) {
+ this.linkNode = getNode(aID);
+ this.link = getAccessible(this.linkNode);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, getAccessible, this.linkNode),
+ ];
+
+ this.invoke = function focusAnchor_invoke() {
+ this.linkNode.focus();
+ };
+
+ this.check = function focusAnchor_check(aEvent) {
+ is(this.link, aEvent.accessible,
+ "Focus should be fired against new link accessible!");
+ };
+
+ this.getID = function focusAnchor_getID() {
+ return "focus a:focus{overflow:scroll} #1";
+ };
+ }
+
+ function tabAnchor(aID) {
+ this.linkNode = getNode(aID);
+ this.link = getAccessible(this.linkNode);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, getAccessible, this.linkNode),
+ ];
+
+ this.invoke = function tabAnchor_invoke() {
+ synthesizeKey("VK_TAB", { shiftKey: false });
+ };
+
+ this.check = function tabAnchor_check(aEvent) {
+ is(this.link, aEvent.accessible,
+ "Focus should be fired against new link accessible!");
+ };
+
+ this.getID = function tabAnchor_getID() {
+ return "focus a:focus{overflow:scroll} #2";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+
+ var gQueue = null;
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ function doTests() {
+ // Shift+Tab not working, and a test timeout, bug 746977
+ if (MAC) {
+ todo(false, "Shift+tab isn't working on OS X, needs to be disabled until bug 746977 is fixed!");
+ SimpleTest.finish();
+ return;
+ }
+
+ gQueue = new eventQueue();
+
+ // CSS 'overflow: scroll' property setting and unsetting causes accessible
+ // recreation (and fire show/hide events). For example, the focus and
+ // blur of HTML:a with ':focus {overflow: scroll; }' CSS style causes its
+ // accessible recreation. The focus event should be fired on new
+ // accessible.
+ gQueue.push(new focusAnchor("a"));
+ gQueue.push(new tabAnchor("a2"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=591163"
+ title="mochitest for bug 413777: focus the a:focus {overflow: scroll;} shouldn't recreate HTML a accessible">
+ Mozilla Bug 591163
+ </a><br>
+ <a target="_blank"
+ title="Rework accessible tree update code"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275">
+ Mozilla Bug 570275
+ </a><br>
+ <a target="_blank"
+ title="Text control frames should accept dynamic changes to the CSS overflow property"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=686247">
+ Mozilla Bug 686247
+ </a><br>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="eventdump"></div>
+
+ <div>
+ <a id="a" class="link" href="www">link</a>
+ </div>
+ <div>
+ <a id="a2" class="link" href="www">link2</a>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_display_contents.html b/accessible/tests/mochitest/tree/test_display_contents.html
new file mode 100644
index 0000000000..8393a35b41
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_display_contents.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+<title>CSS display:contents tests</title>
+<link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<script type="application/javascript"
+ src="../common.js"></script>
+<script type="application/javascript"
+ src="../role.js"></script>
+
+<script type="application/javascript">
+function doTest() {
+ let tree =
+ { LIST: [
+ { LISTITEM: [
+ { LISTITEM_MARKER: [] },
+ { TEXT_LEAF: [] },
+ ]},
+ { LISTITEM: [
+ { LISTITEM_MARKER: [] },
+ { TEXT_LEAF: [] },
+ ]},
+ ] };
+ testAccessibleTree("ul", tree);
+
+ tree =
+ { TABLE: [
+ { ROW: [
+ { CELL: [{ TEXT_LEAF: [] } ] },
+ { CELL: [{ TEXT_LEAF: [] } ] },
+ ]},
+ ] };
+ testAccessibleTree("tableTableContents", tree);
+ testAccessibleTree("tableTrContents", tree);
+ testAccessibleTree("tableTdContents", tree);
+
+ tree =
+ { TABLE: [
+ { GROUPING : [
+ { ROW: [
+ { CELL: [{ TEXT_LEAF: [] } ] },
+ { CELL: [{ TEXT_LEAF: [] } ] },
+ ]},
+ ]},
+ ] };
+ testAccessibleTree("tableTbodyContents", tree);
+
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addA11yLoadEvent(doTest);
+</script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <ul id="ul" style="display: contents;">
+ <li>Supermarket 1</li>
+ <li>Supermarket 2</li>
+ </ul>
+
+ <!-- The summary attribute in these tables ensures they are treated as data
+ tables. -->
+ <table id="tableTableContents" summary="summary" style="display: contents;">
+ <tr><td>a</td><td>b</td></tr>
+ </table>
+ <table id="tableTrContents" summary="table" style="display: block;">
+ <tr style="display: contents;"><td>a</td><td>b</td></tr>
+ </table>
+ <table id="tableTdContents" summary="summary">
+ <tr>
+ <td style="display: contents;">a</td>
+ <td style="display: contents;">b</td>
+ </tr>
+ </table>
+ <table id="tableTbodyContents" summary="summary" style="display: block;">
+ <tbody style="display: contents;">
+ <tr><td>a</td><td>b</td></tr>
+ </tbody>
+ </table>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_divs.html b/accessible/tests/mochitest/tree/test_divs.html
new file mode 100644
index 0000000000..24d610aef2
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_divs.html
@@ -0,0 +1,351 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+<title>div element creation tests</title>
+<link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<script type="application/javascript"
+ src="../common.js"></script>
+<script type="application/javascript"
+ src="../role.js"></script>
+<script type="application/javascript"
+ src="../attributes.js"></script>
+
+<script type="application/javascript">
+function getAccessibleDescendantFor(selector) {
+ return gAccService.getAccessibleDescendantFor(document.querySelector(selector));
+}
+
+function doTest() {
+ // All below test cases are wrapped in a div which always gets rendered.
+ // c1 through c10 are the containers, the actual test cases are inside.
+
+ // c1: Expose the div with text content
+ let tree =
+ { role: ROLE_SECTION, // outer div with ID
+ children: [
+ { role: ROLE_SECTION, // inner div
+ children: [
+ { TEXT_LEAF: [] },
+ ], // end children inner div
+ }, // end inner div
+ ], // end children outer div
+ };
+ testAccessibleTree("c1", tree);
+
+ // c2: Only the outermost and innermost divs are exposed.
+ // The middle one is skipped. This is identical to the above tree.
+ testAccessibleTree("c2", tree);
+
+ // c3: Make sure the inner div with ID is exposed, but the middle one is
+ // skipped.
+ tree =
+ { role: ROLE_SECTION, // outer div with ID
+ children: [
+ { role: ROLE_SECTION, // inner div
+ attributes: { id: "b" },
+ children: [
+ { TEXT_LEAF: [] },
+ ], // end children inner div
+ }, // end inner div
+ ], // end children outer div
+ };
+ testAccessibleTree("c3", tree);
+
+ // c4: Expose all three divs including the middle one due to its ID.
+ tree =
+ { role: ROLE_SECTION, // outer div with ID
+ children: [
+ { role: ROLE_SECTION, // middle div
+ attributes: { id: "a" },
+ children: [
+ { role: ROLE_SECTION, // inner div
+ attributes: { id: "b" },
+ children: [
+ { TEXT_LEAF: [] },
+ ], // end children inner div
+ }, // end inner div
+ ], // end children middle div
+ }, // end middle div
+ ], // end children outer div
+ };
+ testAccessibleTree("c4", tree);
+
+ // c5: Expose the inner div with class b due to its text contents.
+ tree =
+ { role: ROLE_SECTION, // outer div with ID
+ children: [
+ { role: ROLE_SECTION, // inner div with class and text
+ attributes: { class: "b" },
+ children: [
+ { TEXT_LEAF: [] },
+ ], // end children inner div
+ }, // end inner div
+ ], // end children outer div
+ };
+ testAccessibleTree("c5", tree);
+
+ // c6: Expose the outer div due to its ID, and the two inner divs due to
+ // their text contents. Skip the middle one.
+ tree =
+ { role: ROLE_SECTION, // outer div with ID
+ children: [
+ { role: ROLE_SECTION, // first inner div
+ children: [
+ { TEXT_LEAF: [] },
+ ], // end children first inner div
+ }, // end first inner div
+ { role: ROLE_SECTION, // second inner div
+ children: [
+ { TEXT_LEAF: [] },
+ ], // end children second inner div
+ }, // end second inner div
+ ], // end children outer div
+ };
+ testAccessibleTree("c6", tree);
+
+ // c7: Expose all three divs including the middle one due to it being block
+ // breaking.
+ tree =
+ { role: ROLE_SECTION, // outer div with ID
+ children: [
+ { role: ROLE_SECTION, // middle div
+ children: [
+ { TEXT_LEAF: [] }, // foo
+ { role: ROLE_SECTION, // inner div
+ children: [
+ { TEXT_LEAF: [] }, // bar
+ ], // end children inner div
+ }, // end inner div
+ { TEXT_LEAF: [] }, // baz
+ ], // end children middle div
+ }, // end middle div
+ ], // end children outer div
+ };
+ testAccessibleTree("c7", tree);
+
+ // c8: Expose all divs due to them all being text block breakers.
+ tree =
+ { role: ROLE_SECTION, // outer div with ID
+ children: [
+ { role: ROLE_SECTION, // foo div
+ children: [
+ { TEXT_LEAF: [] }, // foo
+ { role: ROLE_SECTION, // baz div
+ children: [
+ { role: ROLE_SECTION, // bar div
+ children: [
+ { TEXT_LEAF: [] }, // bar
+ ], // end children bar div
+ }, // end bar div
+ { TEXT_LEAF: [] }, // baz
+ ], // end children baz div
+ }, // end baz div
+ ], // end children foo div
+ }, // end foo div
+ ], // end children outer div
+ };
+ testAccessibleTree("c8", tree);
+
+ // c9: The same, but in a different nesting order.
+ tree =
+ { role: ROLE_SECTION, // outer div with ID
+ children: [
+ { role: ROLE_SECTION, // c div
+ children: [
+ { role: ROLE_SECTION, // b div
+ children: [
+ { role: ROLE_SECTION, // a div
+ children: [
+ { TEXT_LEAF: [] }, // a
+ ], // end children a div
+ }, // end a div
+ { TEXT_LEAF: [] }, // b
+ ], // end children b div
+ }, // end b div
+ { TEXT_LEAF: [] }, // c
+ ], // end children c div
+ }, // end foo div
+ ], // end children outer div
+ };
+ testAccessibleTree("c9", tree);
+
+ // c10: Both inner divs must be exposed so there is a break after b.
+ tree =
+ { role: ROLE_SECTION, // outer div with ID
+ children: [
+ { role: ROLE_SECTION, // first inner div
+ children: [
+ { TEXT_LEAF: [] }, // a
+ { TEXT_LEAF: [] }, // b
+ ], // end children first inner div
+ }, // end first inner div
+ { role: ROLE_SECTION, // second inner div
+ children: [
+ { TEXT_LEAF: [] }, // c
+ { TEXT_LEAF: [] }, // d
+ ], // end children second inner div
+ }, // end second inner div
+ ], // end children outer div
+ };
+ testAccessibleTree("c10", tree);
+
+ // c11: A div must be exposed if it contains a br element.
+ tree =
+ { role: ROLE_SECTION, // outer div
+ children: [
+ { role: ROLE_SECTION, // First line
+ children: [
+ { TEXT_LEAF: [] }, // text
+ ], // end children first line
+ }, // end first line
+ { role: ROLE_SECTION, // Second line
+ children: [
+ { WHITESPACE: [] }, // whitespace
+ ], // end children second line
+ }, // end second line
+ { role: ROLE_SECTION, // Third line
+ children: [
+ { TEXT_LEAF: [] }, // text
+ ], // end children third line
+ }, // end third line
+ ], // end children outer div
+ };
+ testAccessibleTree("c11", tree);
+
+ // c12: Div shouldn't be rendered if first/last child text node is invisible.
+ tree =
+ { role: ROLE_SECTION, // outer div
+ children: [
+ { role: ROLE_PARAGRAPH,
+ children: [
+ { TEXT_LEAF: [] }, // text
+ ],
+ },
+ ], // end children outer div
+ };
+ testAccessibleTree("c12", tree);
+
+ // c13: Div should be rendered if there is an inline frame after/before
+ // invisible text nodes.
+ tree =
+ { role: ROLE_SECTION, // outer div
+ children: [
+ { TEXT_LEAF: [] }, // l1
+ { role: ROLE_SECTION, // l2
+ children: [
+ { TEXT_LEAF: [] }, // l2
+ ], // end children l2
+ },
+ ], // end children outer div
+ };
+ testAccessibleTree("c13", tree);
+
+ // c14: Div should be rendered if it contains an inline-block.
+ tree =
+ { role: ROLE_SECTION, // outer div
+ children: [
+ { TEXT_LEAF: [] }, // l1
+ { role: ROLE_SECTION, // l2
+ children: [
+ { role: ROLE_PUSHBUTTON,
+ children: [
+ { TEXT_LEAF: [] },
+ ],
+ },
+ ], // end children l2
+ },
+ ], // end children outer div
+ };
+ testAccessibleTree("c14", tree);
+
+ // c15: Div should be rendered if previous sibling is text.
+ tree =
+ { role: ROLE_SECTION, // outer div
+ children: [
+ { TEXT_LEAF: [] }, // l1
+ { SECTION: [] }, // Block break
+ { TEXT_LEAF: [] }, // l2
+ ], // end children outer div
+ };
+ testAccessibleTree("c15", tree);
+
+ // Test getting descendants of unrendered nodes.
+ ok(!getAccessibleDescendantFor("#c16 > span"),
+ "Span has no accessible children");
+
+ ok(!getAccessibleDescendantFor("#c17 > span"),
+ "Span with relocated child should return null");
+
+ is(getAccessibleDescendantFor("#c12 > div").role, ROLE_PARAGRAPH,
+ "Descendant has correct role")
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+addA11yLoadEvent(doTest);
+</script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- Expose the div if it has plain text contents -->
+ <div id="c1"><div>foo</div></div>
+
+ <!-- Expose the outer and inner div, skip the middle one. -->
+ <div id="c2"><div><div>foo</div></div></div>
+
+ <!-- Expose the outer and inner divs due to the ID, but skip the middle one. -->
+ <div id="c3"><div><div id="b">foo</div></div></div>
+
+ <!-- Expose all three divs and their IDs. -->
+ <div id="c4"><div id="a"><div id="b">foo</div></div></div>
+
+ <!-- Expose outer and inner divs, due to text content, not class. -->
+ <div id="c5"><div class="a"><div class="b">foo</div></div></div>
+
+ <!-- Expose the outer and two inner divs, skip the single middle one. -->
+ <div id="c6"><div><div>foo</div><div>bar</div></div></div>
+
+ <!-- Expose all divs due to the middle one being block breaking. -->
+ <div id="c7"><div>foo<div>bar</div>baz</div></div>
+
+ <!-- Expose all divs due to them all being text block breakers. -->
+ <div id="c8"><div>foo<div><div>bar</div>baz</div></div></div>
+ <div id="c9"><div><div><div>a</div>b</div>c</div></div>
+
+ <!-- Both inner divs need to be rendered so there is a break after b. -->
+ <div id="c10"><div><b>a</b>b</div><div><b>c</b><b>d</b></div></div>
+
+ <!-- Div must be rendered if it contains a br -->
+ <div id="c11"><div>first line.</div><div><br /></div><div>third line</div></div>
+
+ <!-- Inner div shouldn't be rendered because although its first and last
+ children are text nodes, they are invisible.
+ -->
+ <div id="c12"><div> <p>Test</p> </div></div>
+
+ <!-- Inner div should be rendered because despite the first/last invisible
+ text nodes, there is also an inline frame.
+ -->
+ <div id="c13">l1<div> <span>l2 </span> </div></div>
+
+ <!-- Inner div should be rendered because it contains an inline-block. -->
+ <div id="c14">l1<div><button>l2</button></div></div>
+
+ <!-- Inner div should be rendered because previous sibling is text. -->
+ <div id="c15">l1<div></div>l2</div>
+
+ <div id="c16">hello <span></span> world</div>
+
+ <div id="c17"><div aria-owns="c"></div>hello <span><button id="c">b</button></span> world</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_dochierarchy.html b/accessible/tests/mochitest/tree/test_dochierarchy.html
new file mode 100644
index 0000000000..4822cecb84
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_dochierarchy.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test document hierarchy</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // tabDoc and testDoc are different documents depending on whether test
+ // is running in standalone mode or not.
+
+ var root = getRootAccessible();
+ var tabDoc = window.parent ?
+ getAccessible(window.parent.document, [nsIAccessibleDocument]) :
+ getAccessible(document, [nsIAccessibleDocument]);
+ var testDoc = getAccessible(document, [nsIAccessibleDocument]);
+ var iframeDoc = getAccessible("iframe").firstChild.
+ QueryInterface(nsIAccessibleDocument);
+
+ is(root.parentDocument, null,
+ "Wrong parent document of root accessible");
+ ok(root.childDocumentCount >= 1,
+ "Wrong child document count of root accessible");
+
+ var tabDocumentFound = false;
+ for (var i = 0; i < root.childDocumentCount && !tabDocumentFound; i++) {
+ tabDocumentFound = root.getChildDocumentAt(i) == tabDoc;
+ }
+ ok(tabDocumentFound,
+ "Tab document not found in children of the root accessible");
+
+ is(tabDoc.parentDocument, root,
+ "Wrong parent document of tab document");
+ is(tabDoc.childDocumentCount, 1,
+ "Wrong child document count of tab document");
+ is(tabDoc.getChildDocumentAt(0), (tabDoc == testDoc ? iframeDoc : testDoc),
+ "Wrong child document at index 0 of tab document");
+
+ if (tabDoc != testDoc) {
+ is(testDoc.parentDocument, tabDoc,
+ "Wrong parent document of test document");
+ is(testDoc.childDocumentCount, 1,
+ "Wrong child document count of test document");
+ is(testDoc.getChildDocumentAt(0), iframeDoc,
+ "Wrong child document at index 0 of test document");
+ }
+
+ is(iframeDoc.parentDocument, (tabDoc == testDoc ? tabDoc : testDoc),
+ "Wrong parent document of iframe document");
+ is(iframeDoc.childDocumentCount, 0,
+ "Wrong child document count of iframe document");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=592913"
+ title="Provide a way to quickly determine whether an accessible object is a descendant of a tab document">
+ Mozilla Bug 592913
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <iframe src="about:mozilla" id="iframe"></iframe>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_dockids.html b/accessible/tests/mochitest/tree/test_dockids.html
new file mode 100644
index 0000000000..b19a68624a
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_dockids.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test document hierarchy</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ // gA11yEventDumpToConsole = true;
+ // enableLogging("tree,verbose");
+ function doTest() {
+ var tree =
+ { DOCUMENT: [
+ { TEXT_CONTAINER: [ // head
+ { TEXT_CONTAINER: [ // link
+ { STATICTEXT: [] }, // generated content
+ { STATICTEXT: [] }, // generated content
+ ] },
+ ] },
+ { TEXT_LEAF: [ ] }, // body text
+ { ENTRY: [ ] }, // input under document element
+ { TEXT_CONTAINER: [ // link under document element
+ { TEXT_LEAF: [ ] }, // link content
+ { STATICTEXT: [ ] }, // generated content
+ { STATICTEXT: [ ] }, // generated content
+ ] },
+ { LINK: [ // anchor under document element
+ { TEXT_LEAF: [ ] }, // anchor content
+ ] },
+ ] };
+ testAccessibleTree(getNode("iframe").contentDocument, tree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=608887"
+ title="Elements appended outside the body aren't accessible">
+ Mozilla Bug 608887
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <iframe src="dockids.html" id="iframe"></iframe>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_filectrl.html b/accessible/tests/mochitest/tree/test_filectrl.html
new file mode 100644
index 0000000000..b7c540226e
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_filectrl.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=483573
+-->
+<head>
+ <title>File Input Control tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var accTree = {
+ role: ROLE_PUSHBUTTON,
+ children: [
+ { role: ROLE_TEXT_LEAF },
+ { role: ROLE_TEXT_LEAF },
+ ],
+ };
+ testAccessibleTree("filectrl", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Expose HTML5 video and audio elements' embedded controls through accessibility APIs"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=483573">Mozilla Bug 483573</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input type="file" id="filectrl" />
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_formctrl.html b/accessible/tests/mochitest/tree/test_formctrl.html
new file mode 100644
index 0000000000..3046ec2bf7
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_formctrl.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>HTML form controls tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // input@type="checkbox"
+ var accTree = {
+ role: ROLE_CHECKBUTTON,
+ children: [ ],
+ };
+
+ testAccessibleTree("checkbox", accTree);
+
+ // input@type="radio"
+ accTree = {
+ role: ROLE_RADIOBUTTON,
+ children: [ ],
+ };
+
+ testAccessibleTree("radio", accTree);
+
+ // input@type="button" and input@type="submit"
+ // button
+ accTree = {
+ role: ROLE_PUSHBUTTON,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF, // XXX Bug 567203
+ },
+ ],
+ };
+
+ testAccessibleTree("btn1", accTree);
+ testAccessibleTree("submit", accTree);
+ testAccessibleTree("btn2", accTree);
+
+ // input@type="image"
+ accTree = {
+ role: ROLE_PUSHBUTTON,
+ children: [
+ {
+ role: ROLE_STATICTEXT,
+ },
+ ],
+ };
+ testAccessibleTree("image_submit", accTree);
+
+ // input@type="range"
+ accTree = { SLIDER: [ ] };
+ testAccessibleTree("range", accTree);
+
+ // input@type="number"
+ accTree = { SPINBUTTON: [ ] };
+ testAccessibleTree("number", accTree);
+
+ // output
+ accTree = {
+ role: ROLE_STATUSBAR,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ },
+ ],
+ };
+ testAccessibleTree("output", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Fix O(n^2) access to all the children of a container"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=342045">
+ Bug 342045
+ </a>
+ <a target="_blank"
+ title="add test for role of input type='image'"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=524521">
+ Bug 524521
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=558036"
+ title="make HTML <output> accessible">
+ Bug 558036
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=559764"
+ title="make HTML5 input@type=range element accessible">
+ Bug 559764
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <input type="checkbox" id="checkbox">
+ <input type="radio" id="radio">
+ <input type="button" value="button" id="btn1">
+ <button id="btn2">button</button>
+
+ <input type="submit" id="submit">
+ <input type="image" id="image_submit">
+ <input type="range" id="range">
+ <input type="number" id="number">
+ <output id="output">1337</output>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_formctrl.xhtml b/accessible/tests/mochitest/tree/test_formctrl.xhtml
new file mode 100644
index 0000000000..d85a97171f
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_formctrl.xhtml
@@ -0,0 +1,129 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<!-- Firefox toolbar -->
+<?xml-stylesheet href="chrome://browser/content/browser.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL checkbox and radio hierarchy tests">
+
+ <!-- Firefox toolbar -->
+ <script type="application/javascript"
+ src="chrome://browser/content/browser.js"/>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ function doTest()
+ {
+ // checkbox
+ var accTree = {
+ role: ROLE_CHECKBUTTON,
+ children: [ ]
+ };
+
+ testAccessibleTree("checkbox", accTree);
+
+ // radiogroup
+ accTree = {
+ role: ROLE_RADIO_GROUP,
+ children: [
+ {
+ role: ROLE_RADIOBUTTON,
+ children: [ ]
+ },
+ {
+ role: ROLE_RADIOBUTTON,
+ children: [ ]
+ }
+ ]
+ };
+
+ testAccessibleTree("radiogroup", accTree);
+
+ // toolbar
+ accTree = {
+ role: ROLE_TOOLBAR,
+ name: "My toolbar",
+ children: [
+ {
+ role: ROLE_PUSHBUTTON,
+ name: "hello",
+ children: [ ]
+ }
+ ]
+ };
+
+ testAccessibleTree("toolbar", accTree);
+
+ // toolbar
+ accTree = {
+ role: ROLE_TOOLBAR,
+ name: "My second toolbar",
+ children: [
+ {
+ role: ROLE_PUSHBUTTON,
+ name: "hello",
+ children: [ ]
+ }
+ ]
+ };
+
+ testAccessibleTree("toolbar2", accTree);
+
+ if (!SEAMONKEY)
+ testAccessibleTree("tb_customizable", { TOOLBAR: [] });
+
+ SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=342045"
+ title="Fix O(n^2) access to all the children of a container">
+ Mozilla Bug 342045
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <checkbox id="checkbox" label="checkbox"/>
+ <radiogroup id="radiogroup">
+ <radio label="radio1"/>
+ <radio label="radio2"/>
+ </radiogroup>
+ <toolbar id="toolbar" toolbarname="My toolbar">
+ <toolbarbutton id="button1" label="hello"/>
+ </toolbar>
+ <toolbar id="toolbar2" toolbarname="2nd" aria-label="My second toolbar">
+ <toolbarbutton id="button2" label="hello"/>
+ </toolbar>
+
+ <toolbar id="tb_customizable" customizable="true"/>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/tree/test_gencontent.html b/accessible/tests/mochitest/tree/test_gencontent.html
new file mode 100644
index 0000000000..3cd5e35e9a
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_gencontent.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Generated content tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <style>
+ .gentext:before {
+ content: "START"
+ }
+ .gentext:after {
+ content: "END"
+ }
+ </style>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // :before and :after pseudo styles
+ var accTree = {
+ role: ROLE_SECTION,
+ children: [
+ {
+ role: ROLE_STATICTEXT,
+ name: "START",
+ },
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "MIDDLE",
+ },
+ {
+ role: ROLE_STATICTEXT,
+ name: "END",
+ },
+ ],
+ };
+
+ testAccessibleTree("gentext", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Clean up our tree walker"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=530081">
+ Mozilla Bug 530081
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div class="gentext" id="gentext">MIDDLE</div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_groupbox.xhtml b/accessible/tests/mochitest/tree/test_groupbox.xhtml
new file mode 100644
index 0000000000..ba873d68c8
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_groupbox.xhtml
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL groupbox hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ function doTest()
+ {
+ var accTree =
+ { GROUPING: [
+ { LABEL: [
+ { STATICTEXT: [ ] }
+ ] },
+ { CHECKBUTTON: [ ] }
+ ] };
+ testAccessibleTree("groupbox", accTree);
+
+ SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=342045"
+ title="Fix O(n^2) access to all the children of a container">
+ Mozilla Bug 342045
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <groupbox id="groupbox">
+ <label value="Some caption"/>
+ <checkbox label="some checkbox label" />
+ </groupbox>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/tree/test_html_in_mathml.html b/accessible/tests/mochitest/tree/test_html_in_mathml.html
new file mode 100644
index 0000000000..214658de4a
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_html_in_mathml.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML in MathML Tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // <label> is generally not valid in MathML and shouldn't create an HTMl
+ // label.
+ testAccessibleTree("invalidLabel", {
+ MATHML_MATH: [ {
+ TEXT: [ {
+ TEXT_LEAF: []
+ } ],
+ } ],
+ });
+
+ // HTML is valid inside <mtext>, so <label> should create an HTMl label.
+ testAccessibleTree("validLabel", {
+ MATHML_MATH: [ {
+ MATHML_TEXT: [ {
+ LABEL: [ {
+ TEXT_LEAF: []
+ } ],
+ } ],
+ } ],
+ });
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <math id="invalidLabel">
+ <label>Text</label>
+ </math>
+
+ <math id="validLabel">
+ <mtext>
+ <label>Text</label>
+ </mtext>
+ </math>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_iframe.html b/accessible/tests/mochitest/tree/test_iframe.html
new file mode 100644
index 0000000000..e0e691550b
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_iframe.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Outer document accessible tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var accTree = {
+ role: ROLE_INTERNAL_FRAME,
+ children: [
+ {
+ role: ROLE_DOCUMENT,
+ },
+ ],
+ };
+
+ testAccessibleTree("iframe", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Fix O(n^2) access to all the children of a container"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=342045">
+ Mozilla Bug 342045
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <iframe id="iframe" src="about:mozilla">
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_image.xhtml b/accessible/tests/mochitest/tree/test_image.xhtml
new file mode 100644
index 0000000000..ee7a6eabb1
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_image.xhtml
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL textbox and textarea hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ function doTest()
+ {
+ var accTree = {
+ role: ROLE_GRAPHIC,
+ children: []
+ };
+ testAccessibleTree("image", accTree);
+
+ SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1403231"
+ title="Remove the image XBL binding">
+ Mozilla Bug 1403231
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <image id="image" src="../moz.png" tooltiptext="hello"/>
+ </vbox>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/tree/test_img.html b/accessible/tests/mochitest/tree/test_img.html
new file mode 100644
index 0000000000..c2a7351a15
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_img.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML img tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ // gA11yEventDumpToConsole = true;
+ function doPreTest() {
+ waitForImageMap("imgmap", doTest);
+ }
+
+ function doTest() {
+ // image map
+ var accTree = {
+ role: ROLE_IMAGE_MAP,
+ children: [
+ {
+ role: ROLE_LINK,
+ children: [],
+ },
+ {
+ role: ROLE_LINK,
+ children: [],
+ },
+ ],
+ };
+
+ testAccessibleTree("imgmap", accTree);
+
+ // img
+ accTree = {
+ role: ROLE_GRAPHIC,
+ children: [],
+ };
+
+ testAccessibleTree("img", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doPreTest);
+ </script>
+
+</head>
+<body>
+
+ <a target="_blank"
+ title="Fix O(n^2) access to all the children of a container"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=342045">
+ Mozilla Bug 342045
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <map name="atoz_map">
+ <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b"
+ coords="17,0,30,14" alt="b" shape="rect">
+ <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a"
+ coords="0,0,13,14" alt="a" shape="rect">
+ </map>
+
+ <img id="imgmap" width="447" height="15"
+ usemap="#atoz_map"
+ src="../letters.gif">
+
+ <img id="img" src="../moz.png">
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_invalid_img.xhtml b/accessible/tests/mochitest/tree/test_invalid_img.xhtml
new file mode 100644
index 0000000000..3d02900cd8
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_invalid_img.xhtml
@@ -0,0 +1,48 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>invalid html img</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script>
+ <![CDATA[
+ function doTest() {
+ document.getElementsByTagName("img")[0].firstChild.data = "2";
+
+ var accTree = {
+ role: ROLE_GRAPHIC,
+ children: [],
+ };
+ testAccessibleTree("the_img", accTree);
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="use HyperTextAccessible for invalid img"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=852129">
+ Mozilla Bug 852129
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <img id="the_img">1</img>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_invalidationlist.html b/accessible/tests/mochitest/tree/test_invalidationlist.html
new file mode 100644
index 0000000000..bad908f3c8
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_invalidationlist.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test document hierarchy</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var tree =
+ {SECTION: [
+ { role: ROLE_PUSHBUTTON, name: "Hello" },
+ { SECTION: [
+ { TEXT: [ { role: ROLE_TEXT_LEAF, name: "Hello" } ] },
+ { role: ROLE_TEXT_LEAF, name: "World" }
+ ]}
+ ]};
+ testAccessibleTree("container", tree);
+ dumpTree("container");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=673757"
+ title="Do not process invalidation list while tree is created">
+ Mozilla Bug 673757
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="container">
+ <div role="button" aria-labelledby="a"></div>
+ <div>
+ <span id="a">Hello</span><span id="b">World</span>
+ </div>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_list.html b/accessible/tests/mochitest/tree/test_list.html
new file mode 100644
index 0000000000..46acda1516
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_list.html
@@ -0,0 +1,346 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML ul/li element tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function listItemTree(aBulletText, aName, aSubtree) {
+ var obj = {
+ role: ROLE_LISTITEM,
+ children: [
+ {
+ role: ROLE_LISTITEM_MARKER,
+ name: aBulletText,
+ },
+ {
+ role: ROLE_TEXT_LEAF,
+ name: aName,
+ },
+ ],
+ };
+
+ if (aSubtree)
+ obj.children.push(aSubtree);
+
+ return obj;
+ }
+
+ function doTest() {
+ // list1
+ var discAccTree = {
+ role: ROLE_LIST,
+ children: [
+ new listItemTree(kDiscBulletText, "Oranges"),
+ new listItemTree(kDiscBulletText, "Apples"),
+ new listItemTree(kDiscBulletText, "Bananas"),
+ ],
+ };
+
+ testAccessibleTree("list1", discAccTree);
+
+ // list2
+ var circleAccTree = {
+ role: ROLE_LIST,
+ children: [
+ new listItemTree(kCircleBulletText, "Oranges"),
+ new listItemTree(kCircleBulletText, "Apples"),
+ new listItemTree(kCircleBulletText, "Bananas"),
+ ],
+ };
+
+ testAccessibleTree("list2", circleAccTree);
+
+ // list3
+ var squareAccTree = {
+ role: ROLE_LIST,
+ children: [
+ new listItemTree(kSquareBulletText, "Oranges"),
+ new listItemTree(kSquareBulletText, "Apples"),
+ new listItemTree(kSquareBulletText, "Bananas"),
+ ],
+ };
+
+ testAccessibleTree("list3", squareAccTree);
+
+ // list4
+ var nestedAccTree = {
+ role: ROLE_LIST,
+ children: [
+ new listItemTree("1. ", "Oranges"),
+ new listItemTree("2. ", "Apples"),
+ new listItemTree("3. ", "Bananas", circleAccTree),
+ ],
+ };
+
+ testAccessibleTree("list4", nestedAccTree);
+
+ // dl list
+ var tree =
+ { DEFINITION_LIST: [ // dl
+ { TERM: [ // dt
+ { TEXT_LEAF: [] },
+ ] },
+ { DEFINITION: [ // dd
+ { TEXT_LEAF: [] },
+ ] },
+ { TERM: [ // dt
+ { TEXT_LEAF: [] },
+ ] },
+ { DEFINITION: [ // dd
+ { TEXT_LEAF: [] },
+ ] },
+ ] };
+
+ testAccessibleTree("list5", tree);
+
+ // dl list inside ordered list
+ tree =
+ { LIST: [ // ol
+ { LISTITEM: [ // li
+ { LISTITEM_MARKER: [ ] },
+ { DEFINITION_LIST: [ // dl
+ { TERM: [ // dt
+ { TEXT_LEAF: [] },
+ ] },
+ { DEFINITION: [ // dd
+ { TEXT_LEAF: [] },
+ ] },
+ ] },
+ ] },
+ ] };
+
+ testAccessibleTree("list6", tree);
+
+ // li having no display:list-item style
+ tree =
+ { LIST: [ // ul
+ { LISTITEM: [ // li
+ { TEXT_LEAF: [] },
+ ] },
+ { TEXT_LEAF: [] },
+ { LISTITEM: [ // li
+ { TEXT_LEAF: [] },
+ ] },
+ ] };
+ testAccessibleTree("list7", tree);
+
+ tree =
+ { LIST: [ // ul
+ { LISTITEM: [ // li
+ { TEXT_LEAF: [] },
+ ] },
+ { LISTITEM: [ // li
+ { TEXT_LEAF: [] },
+ ] },
+ ] };
+ testAccessibleTree("list8", tree);
+
+ // span having display:list-item style
+ testAccessibleTree("list9", discAccTree);
+
+ // dl with div grouping dt/dd
+ tree =
+ { DEFINITION_LIST: [ // dl
+ { TERM: [ // dt
+ { TEXT_LEAF: [] },
+ ] },
+ { DEFINITION: [ // dd
+ { TEXT_LEAF: [] },
+ ] },
+ { TERM: [ // dt
+ { TEXT_LEAF: [] },
+ ] },
+ { DEFINITION: [ // dd
+ { TEXT_LEAF: [] },
+ ] },
+ ] };
+
+ testAccessibleTree("list10", tree);
+
+ // list-style-image
+ testAccessibleTree("list11", discAccTree);
+
+ // list-style: none
+ tree =
+ { LIST: [ // ul
+ { LISTITEM: [ // li
+ { TEXT_LEAF: [] },
+ ] },
+ { LISTITEM: [ // li
+ { TEXT_LEAF: [] },
+ ] },
+ ] };
+ testAccessibleTree("list12", tree);
+
+ // ::marker with content
+ tree = { // ol
+ role: ROLE_LIST,
+ children: [
+ { // li
+ role: ROLE_LISTITEM,
+ children: [
+ { // ::marker content text and counter
+ role: ROLE_LISTITEM_MARKER,
+ name: "foo1",
+ },
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Oranges",
+ },
+ ],
+ },
+ { // li
+ role: ROLE_LISTITEM,
+ children: [
+ { // ::marker content text and counter
+ role: ROLE_LISTITEM_MARKER,
+ name: "foo2",
+ },
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Apples",
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree("list13", tree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Fix O(n^2) access to all the children of a container"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=342045">
+ Mozilla Bug 342045
+ </a>
+ <a target="_blank"
+ title="Wrong accessible is created for HTML:li having block display style"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=507555">
+ Mozilla Bug 507555
+ </a>
+ <a target="_blank"
+ title="Bullets of nested not ordered lists have one and the same character."
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=604587">
+ Mozilla Bug 604587
+ </a>
+ <a target="_blank"
+ title="Fix list bullets for DL list (crash [@ nsBulletFrame::GetListItemText])"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=629114">
+ Mozilla Bug 629114
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <ul id="list1">
+ <li id="l1_li1">Oranges</li>
+ <li id="l1_li2">Apples</li>
+ <li id="l1_li3">Bananas</li>
+ </ul>
+
+ <ul id="list2" style="list-style-type: circle">
+ <li id="l2_li1">Oranges</li>
+ <li id="l2_li2">Apples</li>
+ <li id="l2_li3">Bananas</li>
+ </ul>
+
+ <ul id="list3" style="list-style-type: square">
+ <li id="l3_li1">Oranges</li>
+ <li id="l3_li2">Apples</li>
+ <li id="l3_li3">Bananas</li>
+ </ul>
+
+ <ol id="list4">
+ <li id="li4">Oranges</li>
+ <li id="li5">Apples</li>
+ <li id="li6">Bananas<ul>
+ <li id="n_li4">Oranges</li>
+ <li id="n_li5">Apples</li>
+ <li id="n_li6">Bananas</li>
+ </ul>
+ </li>
+ </ol>
+
+ <dl id="list5">
+ <dt>item1</dt><dd>description</dd>
+ <dt>item2</td><dd>description</dd>
+ </dl>
+
+ <ol id="list6">
+ <li>
+ <dl id="dl">
+ <dt>item1</dt><dd>description</dd>
+ </dl>
+ </li>
+ </ol>
+
+ <!-- display style different than list-item -->
+ <ul id="list7">
+ <li id="l7_li1" style="display:inline-block;">Oranges</li>
+ <li id="l7_li2" style="display:inline-block;">Apples</li>
+ </ul>
+
+ <ul id="list8">
+ <li id="l8_li1" style="display:inline; float:right;">Oranges</li>
+ <li id="l8_li2" style="display:inline; float:right;">Apples</li>
+ </ul>
+
+ <!-- list-item display style -->
+ <ul id="list9">
+ <span id="l9_li1" style="display:list-item">Oranges</span>
+ <span id="l9_li2" style="display:list-item">Apples</span>
+ <span id="l9_li3" style="display:list-item">Bananas</span>
+ </ul>
+
+ <!-- dl with div grouping dd/dt elements (bug 1476347) -->
+ <dl id="list10">
+ <div><dt>item1</dt><dd>description</dd></div>
+ <div><dt>item2</td><dd>description</dd></div>
+ </dl>
+
+ <!-- list-style-image -->
+ <ul id="list11"
+ style="list-style-type: none; list-style-image: url('../moz.png');">
+ <li>Oranges</li>
+ <li>Apples</li>
+ <li>Bananas</li>
+ </ul>
+
+ <!-- list-style: none -->
+ <ul id="list12" style="list-style: none;">
+ <li>Oranges</li>
+ <li>Apples</li>
+ </ul>
+
+ <!-- ::marker with content -->
+ <style>
+ #list13 li {
+ counter-increment: list13counter;
+ }
+ #list13 li::marker {
+ content: 'foo' counter(list13counter);
+ }
+ </style>
+ <ol id="list13">
+ <li>Oranges</li>
+ <li>Apples</li>
+ </ol>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_map.html b/accessible/tests/mochitest/tree/test_map.html
new file mode 100644
index 0000000000..72de628f9f
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_map.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML map accessible tree tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // map used as imagemap, not accessible
+ var accTree =
+ { SECTION: [ ] };
+
+ testAccessibleTree("imagemapcontainer", accTree);
+
+ // map group. Imagemaps are inlines by default, so TEXT_CONTAINER.
+ accTree =
+ { TEXT_CONTAINER: [
+ { PARAGRAPH: [
+ { TEXT_LEAF: [ ] },
+ { LINK: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ { TEXT_LEAF: [ ] },
+ { LINK: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] };
+
+ testAccessibleTree("mapgroup", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Map used for grouping is not accessible under certain circumstances"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=627718">
+ Mozilla Bug 627718
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="imagemapcontainer">
+ <map name="atoz_map">
+ <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b"
+ coords="17,0,30,14" alt="b" shape="rect">
+ <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a"
+ coords="0,0,13,14" alt="a" shape="rect">
+ </map>
+ </div>
+
+ <img id="imgmap" width="447" height="15"
+ usemap="#atoz_map"
+ src="../letters.gif">
+
+ <map id="mapgroup" title="Navigation Bar" name="mapgroup">
+ <p>
+ [<a href="#how">Bypass navigation bar</a>]
+ [<a href="home.html">Home</a>]
+ </p>
+ </map>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_media.html b/accessible/tests/mochitest/tree/test_media.html
new file mode 100644
index 0000000000..5d9fe0ef24
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_media.html
@@ -0,0 +1,127 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=483573
+-->
+<head>
+ <title>HTML5 audio/video tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+
+ async function loadAudioSource() {
+ /**
+ * Setting the source dynamically and wait for it to load,
+ * so we can test the accessibility tree of the control in its ready and
+ * stable state.
+ *
+ * See bug 1484048 comment 25 for discussion on how it switches UI when
+ * loading a statically declared source.
+ */
+ const bufferA11yShown = waitForEvent(
+ EVENT_SHOW,
+ evt => evt.accessible.role == ROLE_TEXT_LEAF &&
+ evt.accessible.indexInParent == 2 &&
+ evt.accessible.parent.role == ROLE_STATUSBAR
+ );
+ await new Promise(resolve => {
+ let el = document.getElementById("audio");
+ el.addEventListener("canplaythrough", resolve, {once: true});
+ el.src = "../bug461281.ogg";
+ });
+ // Wait for this to be reflected in the a11y tree.
+ await bufferA11yShown;
+
+ // Give Fluent time to update the value of the scrubber asynchronously.
+ let scrubber = document.getElementById("audio")
+ .openOrClosedShadowRoot.getElementById("scrubber");
+ await SimpleTest.promiseWaitForCondition(() =>
+ scrubber.getAttribute("aria-valuetext") == "0:00 / 0:02");
+
+ doTest();
+ }
+
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // test the accessible tree
+
+ var accTree = {
+ role: ROLE_GROUPING,
+ children: [
+ { // start/stop button
+ role: ROLE_PUSHBUTTON,
+ name: "Play",
+ children: [],
+ },
+ { // buffer bar
+ role: ROLE_STATUSBAR,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Loading:",
+ },
+ {
+ role: ROLE_TEXT_LEAF,
+ name: " ",
+ },
+ {
+ role: ROLE_TEXT_LEAF,
+ // The name is the percentage buffered; e.g. "0%", "100%".
+ // We can't check it here because it might be different
+ // depending on browser caching.
+ },
+ ],
+ },
+ { // slider of progress bar
+ role: ROLE_SLIDER,
+ name: "Position",
+ value: "0:00 / 0:02",
+ children: [],
+ },
+ { // mute button
+ role: ROLE_PUSHBUTTON,
+ name: "Mute",
+ children: [],
+ },
+ { // slider of volume bar
+ role: ROLE_SLIDER,
+ children: [],
+ name: "Volume",
+ },
+ ],
+ };
+ testAccessibleTree("audio", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(loadAudioSource);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Expose HTML5 video and audio elements' embedded controls through accessibility APIs"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=483573">Mozilla Bug 483573</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <audio id="audio" controls="true"></audio>
+
+ <div id="eventDump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_select.html b/accessible/tests/mochitest/tree/test_select.html
new file mode 100644
index 0000000000..aa5be5f517
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_select.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML select control tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var accTree = {
+ role: ROLE_LISTBOX,
+ children: [
+ {
+ role: ROLE_GROUPING,
+ children: [
+ {
+ role: ROLE_STATICTEXT,
+ children: [ ],
+ },
+ {
+ role: ROLE_STATICTEXT,
+ children: [ ],
+ },
+ {
+ role: ROLE_OPTION,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ },
+ ],
+ },
+ {
+ role: ROLE_OPTION,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ role: ROLE_OPTION,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree("listbox", accTree);
+
+ accTree = {
+ role: ROLE_COMBOBOX,
+ children: [
+ {
+ role: ROLE_COMBOBOX_LIST,
+ children: [
+ {
+ role: ROLE_GROUPING,
+ children: [
+ {
+ role: ROLE_COMBOBOX_OPTION,
+ children: [ ],
+ },
+ {
+ role: ROLE_COMBOBOX_OPTION,
+ children: [ ],
+ },
+ ],
+ },
+ {
+ role: ROLE_COMBOBOX_OPTION,
+ children: [ ],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree("combobox", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="remove all the code in #ifdef COMBO_BOX_WITH_THREE_CHILDREN"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=506616">
+ Mozilla Bug 506616
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <select id="listbox" size="4">
+ <optgroup label="Colors">
+ <option>Red</option>
+ <option>Blue</option>
+ </optgroup>
+ <option>Animal</option>
+ </select>
+
+ <select id="combobox">
+ <optgroup label="Colors">
+ <option>Red</option>
+ <option>Blue</option>
+ </optgroup>
+ <option>Animal</option>
+ </select>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_svg.html b/accessible/tests/mochitest/tree/test_svg.html
new file mode 100644
index 0000000000..4a071d0f4a
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_svg.html
@@ -0,0 +1,127 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>SVG Tree Tests</title>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // svgText
+ var accTree = {
+ role: ROLE_DIAGRAM,
+ children: [
+ {
+ role: ROLE_TEXT_CONTAINER,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree("svgText", accTree);
+
+ // svg1
+ accTree = {
+ role: ROLE_DIAGRAM,
+ children: []
+ };
+ testAccessibleTree("svg1", accTree);
+
+ // svg2
+ accTree = {
+ role: ROLE_DIAGRAM,
+ children: [
+ {
+ role: ROLE_GROUPING,
+ children: []
+ }
+ ]
+ };
+ testAccessibleTree("svg2", accTree);
+
+ // svg3
+ accTree = {
+ role: ROLE_DIAGRAM,
+ children: [
+ {
+ role: ROLE_GRAPHIC,
+ children: []
+ }
+ ]
+ };
+ testAccessibleTree("svg3", accTree);
+
+ // svg4
+ accTree = {
+ role: ROLE_DIAGRAM,
+ children: [
+ {
+ role: ROLE_GROUPING,
+ children: [
+ {
+ role: ROLE_GRAPHIC,
+ children: []
+ }
+ ]
+ }
+ ]
+ };
+ testAccessibleTree("svg4", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <svg id="svgText">
+ <text>This is some text</text>
+ </svg>
+
+ <!-- no accessible objects -->
+ <svg id="svg1">
+ <g id="g1">
+ <rect width="300" height="100" id="rect1" style="fill:#00f" />
+ </g>
+ </svg>
+
+ <svg id="svg2">
+ <g id="g2">
+ <title>g</title>
+ <rect width="300" height="100" id="rect2" style="fill:#00f" />
+ </g>
+ </svg>
+
+ <svg id="svg3">
+ <g id="g3">
+ <rect width="300" height="100" id="rect3" style="fill:#00f">
+ <title>rect</title>
+ </rect>
+ </g>
+ </svg>
+
+ <svg id="svg4">
+ <g id="g4">
+ <title>g</title>
+ <rect width="300" height="100" id="rect4" style="fill:#00f">
+ <title>rect</title>
+ </rect>
+ </g>
+ </svg>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_tabbox.xhtml b/accessible/tests/mochitest/tree/test_tabbox.xhtml
new file mode 100644
index 0000000000..27e9222873
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_tabbox.xhtml
@@ -0,0 +1,108 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL tabbox hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ function doTest()
+ {
+ //////////////////////////////////////////////////////////////////////////
+ // tabbox
+
+ var accTree = {
+ role: ROLE_PAGETABLIST,
+ children: [
+ {
+ role: ROLE_PAGETAB,
+ children: [
+ {
+ role: ROLE_STATICTEXT,
+ children: [],
+ }
+ ]
+ },
+ {
+ role: ROLE_PAGETAB,
+ children: [
+ {
+ role: ROLE_STATICTEXT,
+ children: [],
+ }
+ ]
+ }
+ ]
+ };
+ testAccessibleTree("tabs", accTree);
+
+ accTree = {
+ role: ROLE_PANE,
+ children: [
+ {
+ role: ROLE_PROPERTYPAGE,
+ children: []
+ },
+ {
+ role: ROLE_PROPERTYPAGE,
+ children: []
+ }
+ ]
+ };
+ testAccessibleTree("tabpanels", accTree);
+
+ SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=540389"
+ title=" WARNING: Bad accessible tree!: [tabbrowser tab] ">
+ Mozilla Bug 540389
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=552944"
+ title="No relationship between tabs and associated property page in new tabbrowser construct">
+ Mozilla Bug 552944
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <tabbox>
+ <tabs id="tabs">
+ <tab label="tab1"/>
+ <tab label="tab2"/>
+ </tabs>
+ <tabpanels id="tabpanels">
+ <tabpanel/>
+ <tabpanel/>
+ </tabpanels>
+ </tabbox>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/tree/test_tabbrowser.xhtml b/accessible/tests/mochitest/tree/test_tabbrowser.xhtml
new file mode 100644
index 0000000000..401ea1a2b1
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_tabbrowser.xhtml
@@ -0,0 +1,261 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL tabbrowser hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../browser.js"></script>
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // invoker
+ function testTabHierarchy()
+ {
+ this.eventSeq = [
+ new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, tabDocumentAt, 0),
+ new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, tabDocumentAt, 1)
+ ];
+
+ this.invoke = function testTabHierarchy_invoke()
+ {
+ var docURIs = ["about:license", "about:mozilla"];
+ tabBrowser().loadTabs(docURIs, {
+ inBackground: false,
+ replace: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ // Flush layout, so as to guarantee that the a11y tree is constructed.
+ browserDocument().documentElement.getBoundingClientRect();
+ }
+
+ this.finalCheck = function testTabHierarchy_finalCheck(aEvent)
+ {
+ ////////////////////
+ // Tab bar
+ ////////////////////
+ var tabsAccTree = {
+ // xul:tabs
+ role: ROLE_PAGETABLIST,
+ children: [
+ // Children depend on application (UI): see below.
+ ]
+ };
+
+ // SeaMonkey and Firefox tabbrowser UIs differ.
+ if (SEAMONKEY) {
+ SimpleTest.ok(true, "Testing SeaMonkey tabbrowser UI.");
+
+ tabsAccTree.children.splice(0, 0,
+ {
+ // xul:toolbarbutton ("Open a new tab")
+ role: ROLE_PUSHBUTTON,
+ children: []
+ },
+ {
+ // xul:tab ("about:license")
+ role: ROLE_PAGETAB,
+ children: []
+ },
+ {
+ // tab ("about:mozilla")
+ role: ROLE_PAGETAB,
+ children: []
+ },
+ {
+ // xul:toolbarbutton ("List all tabs")
+ role: ROLE_PUSHBUTTON,
+ children: [
+ {
+ // xul:menupopup
+ role: ROLE_MENUPOPUP,
+ children: []
+ }
+ ]
+ },
+ {
+ // xul:toolbarbutton ("Close current tab")
+ role: ROLE_PUSHBUTTON,
+ children: []
+ }
+ );
+ } else {
+ SimpleTest.ok(true, "Testing Firefox tabbrowser UI.");
+ let newTabChildren = [];
+ if (SpecialPowers.getBoolPref("privacy.userContext.enabled")) {
+ newTabChildren = [
+ {
+ role: ROLE_MENUPOPUP,
+ children: []
+ }
+ ];
+ }
+
+ // NB: The (3) buttons are not visible, unless manually hovered,
+ // probably due to size reduction in this test.
+ tabsAccTree.children.splice(0, 0,
+ {
+ // xul:tab ("about:license")
+ role: ROLE_PAGETAB,
+ children: [
+ {
+ // xul:text, i.e. the tab label text
+ role: ROLE_TEXT_LEAF,
+ children: []
+ }
+ ]
+ },
+ {
+ // tab ("about:mozilla")
+ role: ROLE_PAGETAB,
+ children: [
+ {
+ // xul:text, i.e. the tab label text
+ role: ROLE_TEXT_LEAF,
+ children: []
+ }
+ ]
+ },
+ {
+ // xul:toolbarbutton ("Open a new tab")
+ role: ROLE_PUSHBUTTON,
+ children: newTabChildren
+ }
+ // "List all tabs" dropdown
+ // XXX: This child(?) is not present in this test.
+ // I'm not sure why (though probably expected).
+ );
+ }
+
+ testAccessibleTree(tabBrowser().tabContainer, tabsAccTree);
+
+ ////////////////////
+ // Tab contents
+ ////////////////////
+ var tabboxAccTree = {
+ // xul:tabpanels
+ role: ROLE_PANE,
+ children: [
+ {
+ // xul:notificationbox
+ role: ROLE_PROPERTYPAGE,
+ children: [
+ {
+ // xul:browser
+ role: ROLE_INTERNAL_FRAME,
+ children: [
+ {
+ // #document ("about:license")
+ role: ROLE_DOCUMENT
+ // children: [ ... ] // Ignore document content.
+ }
+ ]
+ }
+ ]
+ },
+ {
+ // notificationbox
+ role: ROLE_PROPERTYPAGE,
+ children: [
+ {
+ // browser
+ role: ROLE_INTERNAL_FRAME,
+ children: [
+ {
+ // #document ("about:mozilla")
+ role: ROLE_DOCUMENT
+ // children: [ ... ] // Ignore document content.
+ }
+ ]
+ }
+ ]
+ },
+ {
+ // notificationbox
+ role: ROLE_PROPERTYPAGE,
+ children: [
+ {
+ // browser
+ role: ROLE_INTERNAL_FRAME,
+ children: [
+ {
+ // #document ("about:newtab" preloaded)
+ role: ROLE_DOCUMENT
+ // children: [ ... ] // Ignore document content.
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ };
+
+ testAccessibleTree(tabBrowser().tabbox.tabpanels, tabboxAccTree);
+ }
+
+ this.getID = function testTabHierarchy_getID()
+ {
+ return "hierarchy of tabs";
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+ gA11yEventDumpToConsole = true;
+ //enableLogging("tree,verbose,stack");
+
+ var gQueue = null;
+ function doTest()
+ {
+ SimpleTest.requestCompleteLog();
+
+ // Load documents into tabs and wait for docLoadComplete events caused by these
+ // documents load before we start the test.
+ gQueue = new eventQueue();
+
+ gQueue.push(new testTabHierarchy());
+ gQueue.onFinish = function() { closeBrowserWindow(); }
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ openBrowserWindow(doTest);
+ ]]>
+ </script>
+
+ <vbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=540389"
+ title=" WARNING: Bad accessible tree!: [tabbrowser tab] ">
+ Mozilla Bug 540389
+ </a><br/>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=552944"
+ title="No relationship between tabs and associated property page in new tabbrowser construct">
+ Mozilla Bug 552944
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox id="eventdump"></vbox>
+ </vbox>
+
+</window>
diff --git a/accessible/tests/mochitest/tree/test_table.html b/accessible/tests/mochitest/tree/test_table.html
new file mode 100644
index 0000000000..5f34c12067
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_table.html
@@ -0,0 +1,507 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML table tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // ////////////////////////////////////////////////////////////////////////
+ // tables having captions
+
+ // Two captions, first is used, second is ignored.
+ var accTree =
+ { TABLE: [
+ { CAPTION: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "caption",
+ },
+ ] },
+ { ROW: [
+ { COLUMNHEADER: [ { TEXT_LEAF: [ ] } ] },
+ { COLUMNHEADER: [ { TEXT_LEAF: [ ] } ] },
+ ] },
+ { ROW: [
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ ] },
+ { ROW: [
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ ] },
+ { ROW: [
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ ] },
+ ] };
+
+ testAccessibleTree("table", accTree);
+
+ // One caption, empty text, caption is included.
+ accTree =
+ { TABLE: [
+ { CAPTION: [ ] },
+ { ROW: [
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ ] },
+ ] };
+
+ testAccessibleTree("table_caption_empty", accTree);
+
+ // Two captions, first has empty text, second is ignored.
+ accTree =
+ { TABLE: [
+ { CAPTION: [ ] },
+ { ROW: [
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ ] },
+ ] };
+
+ testAccessibleTree("table_caption_firstempty", accTree);
+
+ // One caption, placed in the end of table. In use.
+ accTree =
+ { TABLE: [
+ { CAPTION: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "caption",
+ },
+ ] },
+ { ROW: [
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ ] },
+ ] };
+
+ testAccessibleTree("table_caption_intheend", accTree);
+
+ // One caption, collapsed to zero width and height. In use.
+ accTree =
+ { TABLE: [
+ { CAPTION: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "caption",
+ },
+ ] },
+ { ROW: [
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ { CELL: [ { TEXT_LEAF: [ ] } ] },
+ ] },
+ ] };
+
+ testAccessibleTree("table_caption_collapsed", accTree);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // table2 (consist of one column)
+
+ accTree = {
+ role: ROLE_TABLE,
+ children: [
+ {
+ role: ROLE_ROW,
+ children: [
+ {
+ role: ROLE_COLUMNHEADER,
+ },
+ ],
+ },
+ {
+ role: ROLE_ROW,
+ children: [
+ {
+ role: ROLE_CELL,
+ },
+ ],
+ },
+ ],
+ };
+
+ testAccessibleTree("table2", accTree);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // table3 (consist of one row)
+
+ accTree = {
+ role: ROLE_TABLE,
+ children: [
+ {
+ role: ROLE_ROW,
+ children: [
+ {
+ role: ROLE_ROWHEADER,
+ },
+ {
+ role: ROLE_CELL,
+ },
+ ],
+ },
+ ],
+ };
+
+ testAccessibleTree("table3", accTree);
+
+ // ///////////////////////////////////////////////////////////////////////
+ // table4 (display: table-row)
+ accTree =
+ { TABLE: [
+ { ROW: [
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] } ],
+ };
+ testAccessibleTree("table4", accTree);
+
+ // ///////////////////////////////////////////////////////////////////////
+ // table5 (tbody with display: block should not get accessible)
+ accTree =
+ { TABLE: [
+ { ROW: [
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("table5", accTree);
+
+ // ///////////////////////////////////////////////////////////////////////
+ // log table
+ accTree =
+ { TABLE: [
+ { ROW: [
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("logtable", accTree);
+
+ // ///////////////////////////////////////////////////////////////////////
+ // display:block table
+ accTree =
+ { TABLE: [
+ { ROW: [
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("block_table", accTree);
+
+ // ///////////////////////////////////////////////////////////////////////
+ // display:inline table
+ accTree =
+ { TABLE: [
+ { ROW: [
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("inline_table1", accTree);
+
+ // ///////////////////////////////////////////////////////////////////////
+ // display:inline table
+ accTree =
+ { TABLE: [
+ { ROW: [
+ { CELL: [
+ { TABLE: [
+ { ROW: [
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("table_containing_inlinetable", accTree);
+
+ // ///////////////////////////////////////////////////////////////////////
+ // table with a cell that has display:block
+ accTree =
+ { TABLE: [
+ { ROW: [
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("table_containing_block_cell", accTree);
+
+ // ///////////////////////////////////////////////////////////////////////
+ // A table with all elements being display:block, including a row group.
+ // This makes us fall back to the ARIAGridRowAccessible.
+ // Strange example from Gmail.
+ accTree =
+ { TABLE: [
+ { ROW: [
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("table_all_display_block", accTree);
+
+ // ///////////////////////////////////////////////////////////////////////
+ // A table with a display:block tbody that has an aria role
+ // The tbody should get an accessible with the desired role.
+ accTree =
+ { TABLE: [
+ { DIALOG: [
+ { TEXT_CONTAINER: [
+ { TEXT_CONTAINER: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("table_with_block_tbody_and_role", accTree);
+
+ // ///////////////////////////////////////////////////////////////////////
+ // A table with a display:block tbody that is focusable
+ // The tbody should get a grouping accessible.
+ accTree =
+ { TABLE: [
+ { GROUPING: [
+ { ROW: [
+ { CELL: [
+ { TEXT_LEAF: [ ] },
+ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("table_with_focusable_block_tbody", accTree);
+
+ // ///////////////////////////////////////////////////////////////////////
+ // Test that the CSS position property doesn't stop th elements from
+ // reporting the proper columnheader, rowheader roles.
+ accTree =
+ { TABLE: [
+ { ROW: [
+ { COLUMNHEADER: [ { TEXT_LEAF: [ ] } ] },
+ { COLUMNHEADER: [ { TEXT_LEAF: [ ] } ] },
+ { COLUMNHEADER: [ { TEXT_LEAF: [ ] } ] },
+ { COLUMNHEADER: [ { TEXT_LEAF: [ ] } ] },
+ { COLUMNHEADER: [ { TEXT_LEAF: [ ] } ] },
+ ] },
+ { ROW: [
+ { ROWHEADER: [ { TEXT_LEAF: [ ] } ] },
+ { CELL: [ ] },
+ { ROWHEADER: [ { TEXT_LEAF: [ ] } ] },
+ { CELL: [ ] },
+ { ROWHEADER: [ { TEXT_LEAF: [ ] } ] },
+ { CELL: [ ] },
+ { ROWHEADER: [ { TEXT_LEAF: [ ] } ] },
+ { CELL: [ ] },
+ { ROWHEADER: [ { TEXT_LEAF: [ ] } ] },
+ { CELL: [ ] },
+ ] },
+ ] };
+ testAccessibleTree("table_containing_pos_styled_th", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="When a table has only one column per row and that column happens to be a column header its role is exposed wrong"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=529621">
+ Mozilla Bug 529621
+ </a>
+ <a target="_blank"
+ title="when div has display style table-row"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=727722">
+ Mozilla Bug 727722
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <table id="table">
+ <thead>
+ <tr>
+ <th>col1</th><th>col2</th>
+ </tr>
+ </thead>
+ <caption>caption</caption>
+ <tbody>
+ <tr>
+ <td>cell1</td><td>cell2</td>
+ </tr>
+ </tbody>
+ <tr>
+ <td>cell3</td><td>cell4</td>
+ </tr>
+ <caption>caption2</caption>
+ <tfoot>
+ <tr>
+ <td>cell5</td><td>cell6</td>
+ </tr>
+ </tfoot>
+ </table>
+
+ <table id="table_caption_empty">
+ <caption></caption>
+ <tr>
+ <td>cell1</td><td>cell2</td>
+ </tr>
+ </table>
+
+ <table id="table_caption_firstempty">
+ <caption></caption>
+ <tr>
+ <td>cell1</td><td>cell2</td>
+ </tr>
+ <caption>caption</caption>
+ </table>
+
+ <table id="table_caption_intheend">
+ <tr>
+ <td>cell1</td><td>cell2</td>
+ </tr>
+ <caption>caption</caption>
+ </table>
+
+ <table id="table_caption_collapsed">
+ <caption style="width: 0; height: 0">caption</caption>
+ <tr>
+ <td>cell1</td><td>cell2</td>
+ </tr>
+ </table>
+ <table id="table2">
+ <thead>
+ <tr>
+ <th>colheader</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>bla</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table id="table3">
+ <tr>
+ <th>rowheader</th>
+ <td>cell</td>
+ </tr>
+ </table>
+
+ <table id="table4">
+ <div style="display: table-row">
+ <td>cell1</td>
+ </div>
+ </table>
+
+ <table id="table5">
+ <tbody style="display:block;">
+ <tr>
+ <td>bla</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table id="logtable" role="log"><tr><td>blah</td></tr></table>
+
+ <table id="block_table" style="display: block;">
+ <tr>
+ <td>bla</td>
+ </tr>
+ </table>
+
+ <table id="inline_table1" border="1" style="display:inline">
+ <tr>
+ <td>table1 cell1</td>
+ <td>table1 cell2</td>
+ </tr>
+ </table>
+
+ <table id="table_containing_inlinetable"><tr><td>
+ <table id="inline_table2" border="1" style="display:inline">
+ <tr id="tr_in_inline_table2">
+ <td id="td_in_inline_table2">cell</td>
+ </tr>
+ </table>
+ </td></tr></table>
+
+ <table id="table_containing_block_cell">
+ <tr>
+ <td>Normal cell</td>
+ <td style="display: block;">Block cell</td>
+ </tr>
+ </table>
+ <table id="table_all_display_block" style="display:block;">
+ <tbody style="display:block;">
+ <tr style="display:block;">
+ <td style="display:block;">text</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table id="table_with_block_tbody_and_role">
+ <tbody style="display:block;" role="dialog">
+ <tr>
+ <td>text</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table id="table_with_focusable_block_tbody">
+ <tbody style="display:block;" tabindex="0">
+ <tr>
+ <td>text</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table id="table_containing_pos_styled_th">
+ <tr>
+ <th style="position: static">static colheader</th>
+ <th style="position: relative">relative colheader</th>
+ <th style="position: absolute">absolute colheader</th>
+ <th style="position: fixed">fixed colheader</th>
+ <th style="position: sticky">sticky colheader</th>
+ </tr>
+ <tr>
+ <th style="position: static">static rowheader</th>
+ <td/>
+ <th style="position: relative">relative rowheader</th>
+ <td/>
+ <th style="position: absolute">absolute rowheader</th>
+ <td/>
+ <th style="position: fixed">fixed rowheader</th>
+ <td/>
+ <th style="position: sticky">sticky rowheader</th>
+ <td/>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_table_2.html b/accessible/tests/mochitest/tree/test_table_2.html
new file mode 100644
index 0000000000..c0faece7e5
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_table_2.html
@@ -0,0 +1,242 @@
+<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en">
+<html>
+<head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+<link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+<style>
+.responsive-table {
+ width: 100%;
+ margin-bottom: 1.5em;
+}
+.responsive-table thead {
+ position: absolute;
+ clip: rect(1px 1px 1px 1px);
+ /* IE6, IE7 */
+ clip: rect(1px, 1px, 1px, 1px);
+ padding: 0;
+ border: 0;
+ height: 1px;
+ width: 1px;
+ overflow: hidden;
+}
+.responsive-table thead th {
+ background-color: #1d96b2;
+ border: 1px solid #1d96b2;
+ font-weight: normal;
+ text-align: center;
+ color: white;
+}
+.responsive-table thead th:first-of-type {
+ text-align: left;
+}
+.responsive-table tbody,
+.responsive-table tr,
+.responsive-table th,
+.responsive-table td {
+ display: block;
+ padding: 0;
+ text-align: left;
+ white-space: normal;
+}
+.responsive-table th,
+.responsive-table td {
+ padding: .5em;
+ vertical-align: middle;
+}
+.responsive-table caption {
+ margin-bottom: 1em;
+ font-size: 1em;
+ font-weight: bold;
+ text-align: center;
+}
+.responsive-table tfoot {
+ font-size: .8em;
+ font-style: italic;
+}
+.responsive-table tbody tr {
+ margin-bottom: 1em;
+ border: 2px solid #1d96b2;
+}
+.responsive-table tbody tr:last-of-type {
+ margin-bottom: 0;
+}
+.responsive-table tbody th[scope="row"] {
+ background-color: #1d96b2;
+ color: white;
+}
+.responsive-table tbody td[data-type=currency] {
+ text-align: right;
+}
+.responsive-table tbody td[data-title]:before {
+ content: attr(data-title);
+ float: left;
+ font-size: .8em;
+ color: rgba(94, 93, 82, 0.75);
+}
+.responsive-table tbody td {
+ text-align: right;
+ border-bottom: 1px solid #1d96b2;
+}
+
+.responsive-table {
+ font-size: .9em;
+}
+.responsive-table thead {
+ position: relative;
+ clip: auto;
+ height: auto;
+ width: auto;
+ overflow: auto;
+}
+.responsive-table tr {
+ display: table-row;
+}
+.responsive-table th,
+.responsive-table td {
+ display: table-cell;
+ padding: .5em;
+}
+
+.responsive-table caption {
+ font-size: 1.5em;
+}
+.responsive-table tbody {
+ display: table-row-group;
+}
+.responsive-table tbody tr {
+ display: table-row;
+ border-width: 1px;
+}
+.responsive-table tbody tr:nth-of-type(even) {
+ background-color: rgba(94, 93, 82, 0.1);
+}
+.responsive-table tbody th[scope="row"] {
+ background-color: transparent;
+ color: #5e5d52;
+ text-align: left;
+}
+.responsive-table tbody td {
+ text-align: center;
+}
+.responsive-table tbody td[data-title]:before {
+ content: none;
+}
+</style>
+
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<script type="application/javascript"
+ src="../common.js"></script>
+<script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+
+<script type="application/javascript">
+
+const COLHEADER = ROLE_COLUMNHEADER;
+const ROWHEADER = ROLE_ROWHEADER;
+const CELL = ROLE_CELL;
+const TEXT_LEAF = ROLE_TEXT_LEAF;
+
+function doTest() {
+ let accTree =
+ { TABLE: [
+ { CAPTION: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Top 10 Grossing Animated Films of All Time",
+ },
+ ] },
+ { TEXT_CONTAINER: [
+ { ROW: [
+ { role: COLHEADER, name: "Film Title" },
+ { role: COLHEADER, name: "Released" },
+ { role: COLHEADER, name: "Studio" },
+ { role: COLHEADER, name: "Worldwide Gross" },
+ { role: COLHEADER, name: "Domestic Gross" },
+ { role: COLHEADER, name: "Foreign Gross" },
+ { role: COLHEADER, name: "Budget" },
+ ] },
+ ] },
+ { ROW: [
+ { role: CELL },
+ ] },
+ { ROW: [
+ { role: ROWHEADER, name: "Toy Story 3" },
+ { CELL: [ { role: TEXT_LEAF, name: "2010" }] },
+ { CELL: [ { role: TEXT_LEAF, name: "Disney Pixar" }] },
+ { CELL: [ { role: TEXT_LEAF, name: "$1,063,171,911" }] },
+ { CELL: [ { role: TEXT_LEAF, name: "$415,004,880" }] },
+ { CELL: [ { role: TEXT_LEAF, name: "$648,167,031" }] },
+ { CELL: [ { role: TEXT_LEAF, name: "$200,000,000" }] },
+ ] },
+ { ROW: [
+ { role: ROWHEADER, name: "Shrek Forever After" },
+ { CELL: [ { role: TEXT_LEAF, name: "2010" }] },
+ { CELL: [ { role: TEXT_LEAF, name: "Dreamworks" }] },
+ { CELL: [ { role: TEXT_LEAF, name: "$752,600,867" }] },
+ { CELL: [ { role: TEXT_LEAF, name: "$238,736,787" }] },
+ { CELL: [ { role: TEXT_LEAF, name: "$513,864,080" }] },
+ { CELL: [ { role: TEXT_LEAF, name: "$165,000,000" }] },
+ ] },
+ ] };
+
+ testAccessibleTree("table", accTree);
+
+ SimpleTest.finish();
+}
+SimpleTest.waitForExplicitFinish();
+addA11yLoadEvent(doTest);
+</script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <table class="responsive-table" id="table">
+ <caption>Top 10 Grossing Animated Films of All Time</caption>
+ <thead>
+ <tr>
+ <th scope="col">Film Title</th>
+ <th scope="col">Released</th>
+ <th scope="col">Studio</th>
+ <th scope="col">Worldwide Gross</th>
+ <th scope="col">Domestic Gross</th>
+ <th scope="col">Foreign Gross</th>
+ <th scope="col">Budget</th>
+ </tr>
+ </thead>
+ <tfoot>
+ <tr>
+ <td colspan="7">Sources: <a href="http://en.wikipedia.org/wiki/List_of_highest-grossing_animated_films" rel="external">Wikipedia</a> &amp; <a href="http://www.boxofficemojo.com/genres/chart/?id=animation.htm" rel="external">Box Office Mojo</a>. Data is current as of March 12, 2014</td>
+ </tr>
+ </tfoot>
+ <tbody>
+ <tr>
+ <th scope="row">Toy Story 3</th>
+ <td data-title="Released">2010</td>
+ <td data-title="Studio">Disney Pixar</td>
+ <td data-title="Worldwide Gross" data-type="currency">$1,063,171,911</td>
+ <td data-title="Domestic Gross" data-type="currency">$415,004,880</td>
+ <td data-title="Foreign Gross" data-type="currency">$648,167,031</td>
+ <td data-title="Budget" data-type="currency">$200,000,000</td>
+ </tr>
+ <tr>
+ <th scope="row">Shrek Forever After</th>
+ <td data-title="Released">2010</td>
+ <td data-title="Studio">Dreamworks</td>
+ <td data-title="Worldwide Gross" data-type="currency">$752,600,867</td>
+ <td data-title="Domestic Gross" data-type="currency">$238,736,787</td>
+ <td data-title="Foreign Gross" data-type="currency">$513,864,080</td>
+ <td data-title="Budget" data-type="currency">$165,000,000</td>
+ </tr>
+ </tbody>
+ </table>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_table_3.html b/accessible/tests/mochitest/tree/test_table_3.html
new file mode 100644
index 0000000000..60af4d7f82
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_table_3.html
@@ -0,0 +1,244 @@
+<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en">
+<html>
+<head>
+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
+<link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+<style>
+.responsive-table {
+ width: 100%;
+ margin-bottom: 1.5em;
+}
+.responsive-table thead {
+ position: absolute;
+ clip: rect(1px 1px 1px 1px);
+ /* IE6, IE7 */
+ clip: rect(1px, 1px, 1px, 1px);
+ padding: 0;
+ border: 0;
+ height: 1px;
+ width: 1px;
+ overflow: hidden;
+}
+.responsive-table thead th {
+ background-color: #1d96b2;
+ border: 1px solid #1d96b2;
+ font-weight: normal;
+ text-align: center;
+ color: white;
+}
+.responsive-table thead th:first-of-type {
+ text-align: left;
+}
+.responsive-table tbody,
+.responsive-table tr,
+.responsive-table th,
+.responsive-table td {
+ display: block;
+ padding: 0;
+ text-align: left;
+ white-space: normal;
+}
+.responsive-table th,
+.responsive-table td {
+ padding: .5em;
+ vertical-align: middle;
+}
+.responsive-table caption {
+ margin-bottom: 1em;
+ font-size: 1em;
+ font-weight: bold;
+ text-align: center;
+}
+.responsive-table tfoot {
+ font-size: .8em;
+ font-style: italic;
+}
+.responsive-table tbody tr {
+ margin-bottom: 1em;
+ border: 2px solid #1d96b2;
+}
+.responsive-table tbody tr:last-of-type {
+ margin-bottom: 0;
+}
+.responsive-table tbody th[scope="row"] {
+ background-color: #1d96b2;
+ color: white;
+}
+.responsive-table tbody td[data-type=currency] {
+ text-align: right;
+}
+.responsive-table tbody td[data-title]:before {
+ content: attr(data-title);
+ float: left;
+ font-size: .8em;
+ color: #1d96b2;
+ font-weight: bold;
+}
+.responsive-table tbody td {
+ text-align: right;
+ border-bottom: 1px solid #1d96b2;
+}
+
+/* float everything */
+.responsive-table tbody tr {
+ float: left;
+ width: 48%;
+ margin-left: 2%;
+}
+</style>
+
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<script type="application/javascript"
+ src="../common.js"></script>
+<script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../table.js"></script>
+
+<script type="application/javascript">
+
+const COLHEADER = ROLE_COLUMNHEADER;
+const ROWHEADER = ROLE_ROWHEADER;
+const CELL = ROLE_CELL;
+const STATICTEXT = ROLE_STATICTEXT;
+const TEXT_LEAF = ROLE_TEXT_LEAF;
+const GROUPING = ROLE_GROUPING;
+
+function doTest() {
+ let accTree =
+ { TABLE: [
+ { CAPTION: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Top 10 Grossing Animated Films of All Time",
+ },
+ ] },
+ { TEXT_CONTAINER: [
+ { ROW: [
+ { COLUMNHEADER: [ { role: TEXT_LEAF, name: "Film Title" } ] },
+ { COLUMNHEADER: [ { role: TEXT_LEAF, name: "Released" } ] },
+ { COLUMNHEADER: [ { role: TEXT_LEAF, name: "Studio" } ] },
+ { COLUMNHEADER: [ { role: TEXT_LEAF, name: "Worldwide Gross" } ] },
+ { COLUMNHEADER: [ { role: TEXT_LEAF, name: "Domestic Gross" } ] },
+ { COLUMNHEADER: [ { role: TEXT_LEAF, name: "Foreign Gross" } ] },
+ { COLUMNHEADER: [ { role: TEXT_LEAF, name: "Budget" } ] },
+ ] },
+ ] },
+ { ROW: [
+ { role: CELL },
+ ] },
+ { ROW: [
+ { ROWHEADER: [ { role: TEXT_LEAF, name: "Toy Story 3" } ] },
+ { CELL: [
+ { role: STATICTEXT, name: "Released" },
+ { role: TEXT_LEAF, name: "2010" },
+ ] },
+ { CELL: [
+ { role: STATICTEXT, name: "Studio" },
+ { role: TEXT_LEAF, name: "Disney Pixar" },
+ ] },
+ { CELL: [
+ { role: STATICTEXT, name: "Worldwide Gross" },
+ { role: TEXT_LEAF, name: "$1,063,171,911" },
+ ] },
+ { CELL: [
+ { role: STATICTEXT, name: "Domestic Gross" },
+ { role: TEXT_LEAF, name: "$415,004,880" },
+ ] },
+ { CELL: [
+ { role: STATICTEXT, name: "Foreign Gross" },
+ { role: TEXT_LEAF, name: "$648,167,031" },
+ ] },
+ { CELL: [
+ { role: STATICTEXT, name: "Budget" },
+ { role: TEXT_LEAF, name: "$200,000,000" },
+ ]},
+ ] },
+ { ROW: [
+ { ROWHEADER: [ { role: TEXT_LEAF, name: "Shrek Forever After" } ] },
+ { CELL: [
+ { role: STATICTEXT, name: "Released" },
+ { role: TEXT_LEAF, name: "2010" },
+ ] },
+ { CELL: [
+ { role: STATICTEXT, name: "Studio" },
+ { role: TEXT_LEAF, name: "Dreamworks" },
+ ] },
+ { CELL: [
+ { role: STATICTEXT, name: "Worldwide Gross" },
+ { role: TEXT_LEAF, name: "$752,600,867" },
+ ] },
+ { CELL: [
+ { role: STATICTEXT, name: "Domestic Gross" },
+ { role: TEXT_LEAF, name: "$238,736,787" },
+ ] },
+ { CELL: [
+ { role: STATICTEXT, name: "Foreign Gross" },
+ { role: TEXT_LEAF, name: "$513,864,080" },
+ ] },
+ { CELL: [
+ { role: STATICTEXT, name: "Budget" },
+ { role: TEXT_LEAF, name: "$165,000,000" },
+ ] },
+ ] },
+ ] };
+
+ testAccessibleTree("table", accTree);
+
+ SimpleTest.finish();
+}
+SimpleTest.waitForExplicitFinish();
+addA11yLoadEvent(doTest);
+</script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <table class="responsive-table" id="table">
+ <caption>Top 10 Grossing Animated Films of All Time</caption>
+ <thead>
+ <tr>
+ <th scope="col">Film Title</th>
+ <th scope="col">Released</th>
+ <th scope="col">Studio</th>
+ <th scope="col">Worldwide Gross</th>
+ <th scope="col">Domestic Gross</th>
+ <th scope="col">Foreign Gross</th>
+ <th scope="col">Budget</th>
+ </tr>
+ </thead>
+ <tfoot>
+ <tr>
+ <td colspan="7">Sources: <a href="http://en.wikipedia.org/wiki/List_of_highest-grossing_animated_films" rel="external">Wikipedia</a> &amp; <a href="http://www.boxofficemojo.com/genres/chart/?id=animation.htm" rel="external">Box Office Mojo</a>. Data is current as of March 12, 2014</td>
+ </tr>
+ </tfoot>
+ <tbody>
+ <tr>
+ <th scope="row">Toy Story 3</th>
+ <td data-title="Released">2010</td>
+ <td data-title="Studio">Disney Pixar</td>
+ <td data-title="Worldwide Gross" data-type="currency">$1,063,171,911</td>
+ <td data-title="Domestic Gross" data-type="currency">$415,004,880</td>
+ <td data-title="Foreign Gross" data-type="currency">$648,167,031</td>
+ <td data-title="Budget" data-type="currency">$200,000,000</td>
+ </tr>
+ <tr>
+ <th scope="row">Shrek Forever After</th>
+ <td data-title="Released">2010</td>
+ <td data-title="Studio">Dreamworks</td>
+ <td data-title="Worldwide Gross" data-type="currency">$752,600,867</td>
+ <td data-title="Domestic Gross" data-type="currency">$238,736,787</td>
+ <td data-title="Foreign Gross" data-type="currency">$513,864,080</td>
+ <td data-title="Budget" data-type="currency">$165,000,000</td>
+ </tr>
+ </tbody>
+ </table>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_tree.xhtml b/accessible/tests/mochitest/tree/test_tree.xhtml
new file mode 100644
index 0000000000..e22b3faa9d
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_tree.xhtml
@@ -0,0 +1,182 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL tree hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Accessible tree testers
+
+ function getTreeItemAccTree(aTableRole, acolumnCount)
+ {
+ var treeItemRole;
+ switch (aTableRole) {
+ case ROLE_LIST:
+ treeItemRole = ROLE_LISTITEM;
+ break;
+ case ROLE_OUTLINE:
+ treeItemRole = ROLE_OUTLINEITEM;
+ break;
+ case ROLE_TABLE: case ROLE_TREE_TABLE:
+ treeItemRole = ROLE_ROW;
+ break;
+ }
+
+ var accTree = {
+ role: treeItemRole,
+ children: []
+ };
+
+ if (aTableRole == ROLE_TABLE || aTableRole == ROLE_TREE_TABLE) {
+ for (var idx = 0; idx < acolumnCount; idx++) {
+ var cellAccTree = {
+ role: ROLE_GRID_CELL,
+ children: []
+ };
+ accTree.children.push(cellAccTree);
+ }
+ }
+
+ return accTree;
+ }
+
+ function testAccessibleTreeFor(aTree, aRole)
+ {
+ var accTreeForColumns = {
+ role: ROLE_LIST,
+ children: []
+ };
+
+ var accTreeForTree = {
+ role: aRole,
+ children: [
+ accTreeForColumns
+ ]
+ };
+
+ var view = aTree.view;
+ var columnCount = aTree.columns.count;
+
+ for (let idx = 0; idx < columnCount; idx++)
+ accTreeForColumns.children.push({ COLUMNHEADER: [ ] });
+ if (!aTree.hasAttribute("hidecolumnpicker")) {
+ accTreeForColumns.children.push({ PUSHBUTTON: [ ] });
+ accTreeForColumns.children.push({ MENUPOPUP: [ ] });
+ }
+
+ for (let idx = 0; idx < view.rowCount; idx++)
+ accTreeForTree.children.push(getTreeItemAccTree(aRole, columnCount));
+
+ testAccessibleTree(aTree, accTreeForTree);
+ }
+
+ /**
+ * Event queue invoker object to test accessible tree for XUL tree element.
+ */
+ function treeChecker(aID, aView, aRole)
+ {
+ this.DOMNode = getNode(aID);
+
+ this.invoke = function invoke()
+ {
+ this.DOMNode.view = aView;
+ }
+ this.check = function check(aEvent)
+ {
+ testAccessibleTreeFor(this.DOMNode, aRole);
+ }
+ this.getID = function getID()
+ {
+ return "Tree testing of " + aID;
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "debug";
+ var gQueue = null;
+
+ function doTest()
+ {
+ gQueue = new eventQueue(EVENT_REORDER);
+
+ gQueue.push(new treeChecker("list", new nsTableTreeView(3), ROLE_LIST));
+ gQueue.push(new treeChecker("tree", new nsTreeTreeView(), ROLE_OUTLINE));
+ gQueue.push(new treeChecker("table", new nsTableTreeView(3), ROLE_TABLE));
+ gQueue.push(new treeChecker("treetable", new nsTreeTreeView(), ROLE_TREE_TABLE));
+
+ gQueue.invoke(); // Will call SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727"
+ title="Reorganize implementation of XUL tree accessibility">
+ Mozilla Bug 503727
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <tree id="list" flex="1" hidecolumnpicker="true">
+ <treecols>
+ <treecol id="col" flex="1" hideheader="true"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col" flex="1" primary="true" label="column"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <tree id="table" flex="1">
+ <treecols>
+ <treecol id="col1" flex="1" label="column"/>
+ <treecol id="col2" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <tree id="treetable" flex="1">
+ <treecols>
+ <treecol id="col1" flex="1" primary="true" label="column"/>
+ <treecol id="col2" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <vbox id="debug"/>
+ </vbox>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/tree/test_txtcntr.html b/accessible/tests/mochitest/tree/test_txtcntr.html
new file mode 100644
index 0000000000..4c44701a41
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_txtcntr.html
@@ -0,0 +1,234 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>HTML text containers tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ var accTree = {
+ role: ROLE_SECTION,
+ children: [
+ { // text child
+ role: ROLE_TEXT_LEAF,
+ children: [],
+ },
+ ],
+ };
+
+ testAccessibleTree("c1", accTree);
+ testAccessibleTree("c2", accTree);
+
+ accTree = {
+ role: ROLE_SECTION,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Hello1",
+ },
+ {
+ role: ROLE_WHITESPACE,
+ },
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Hello2",
+ },
+ {
+ role: ROLE_SEPARATOR,
+ },
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Hello3 ",
+ },
+ {
+ role: ROLE_PARAGRAPH,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "Hello4 ",
+ },
+ ],
+ },
+ ],
+ };
+
+ testAccessibleTree("c3", accTree);
+
+ // contentEditable div
+ accTree = {
+ role: ROLE_SECTION,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "helllo ",
+ },
+ {
+ role: ROLE_PARAGRAPH,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "blabla",
+ },
+ ],
+ },
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "hello ",
+ },
+ ],
+ };
+
+ testAccessibleTree("c4", accTree);
+
+ // blockquote
+ accTree = {
+ role: ROLE_SECTION,
+ children: [
+ { // block quote
+ role: ROLE_BLOCKQUOTE,
+ children: [
+ { // text child
+ role: ROLE_TEXT_LEAF,
+ name: "Hello",
+ children: [],
+ },
+ ],
+ },
+ ],
+ };
+
+ testAccessibleTree("c5", accTree);
+
+ // abbreviation tag
+ accTree = {
+ role: ROLE_SECTION,
+ children: [
+ { // text leaf
+ role: ROLE_TEXT_LEAF,
+ name: "This ",
+ children: [],
+ },
+ { // abbr tag
+ role: ROLE_TEXT,
+ name: "accessibility",
+ children: [
+ { // text leaf with actual text
+ role: ROLE_TEXT_LEAF,
+ name: "a11y",
+ children: [],
+ },
+ ],
+ },
+ { // text leaf
+ role: ROLE_TEXT_LEAF,
+ name: " test",
+ children: [],
+ },
+ ],
+ };
+
+ testAccessibleTree("c6", accTree);
+
+ // acronym tag
+ accTree = {
+ role: ROLE_SECTION,
+ children: [
+ { // text leaf
+ role: ROLE_TEXT_LEAF,
+ name: "This ",
+ children: [],
+ },
+ { // acronym tag
+ role: ROLE_TEXT,
+ name: "personal computer",
+ children: [
+ { // text leaf with actual text
+ role: ROLE_TEXT_LEAF,
+ name: "PC",
+ children: [],
+ },
+ ],
+ },
+ { // text leaf
+ role: ROLE_TEXT_LEAF,
+ name: " is broken",
+ children: [],
+ },
+ ],
+ };
+
+ testAccessibleTree("c7", accTree);
+
+ // only whitespace between images should be exposed
+ accTree = {
+ SECTION: [
+ { GRAPHIC: [] },
+ { TEXT_LEAF: [] },
+ { GRAPHIC: [] },
+ ],
+ };
+ testAccessibleTree("c8", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="overflowed content doesn't expose child text accessibles"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=489306">
+ Mozilla Bug 489306</a>
+ <a target="_blank"
+ title="Create child accessibles for text controls from native anonymous content"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=542824">
+ Mozilla Bug 542824</a>
+ <a target="_blank"
+ title="Update accessible tree on content insertion after layout"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=498015">
+ Mozilla Bug 498015</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="c1" style="width: 100px; height: 100px; overflow: auto;">
+ 1hellohello 2hellohello 3hellohello 4hellohello 5hellohello 6hellohello 7hellohello
+ </div>
+ <div id="c2">
+ 1hellohello 2hellohello 3hellohello 4hellohello 5hellohello 6hellohello 7hellohello
+ </div>
+ <div id="c3">
+ Hello1<br>
+ Hello2<hr>
+ Hello3
+ <p>
+ Hello4
+ </p>
+ </div>
+ <div id="c4" contentEditable="true">
+ helllo <p>blabla</p> hello
+ </div>
+ <div id="c5"><blockquote>Hello</blockquote></div>
+ <div id="c6">This <abbr title="accessibility">a11y</abbr> test</div>
+ <div id="c7">This <acronym title="personal computer">PC</acronym> is broken</div>
+
+ <!-- Whitespace between images should be exposed. Whitespace between the
+ div and img tags will be inconsistent depending on the image cache
+ state and what optimizations layout was able to apply. -->
+ <div id="c8"><img src="../moz.png"> <img src="../moz.png"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_txtctrl.html b/accessible/tests/mochitest/tree/test_txtctrl.html
new file mode 100644
index 0000000000..ee66fbf801
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_txtctrl.html
@@ -0,0 +1,171 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>HTML text controls tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // editable div
+ var accTree = {
+ role: ROLE_SECTION,
+ children: [
+ { // text child
+ role: ROLE_TEXT_LEAF,
+ children: [],
+ },
+ ],
+ };
+
+ testAccessibleTree("txc1", accTree);
+
+ // input@type="text", value
+ accTree = {
+ role: ROLE_ENTRY,
+ children: [
+ { // text child
+ role: ROLE_TEXT_LEAF,
+ children: [],
+ },
+ ],
+ };
+
+ testAccessibleTree("txc2", accTree);
+
+ // input@type="text", no value
+ accTree =
+ { ENTRY: [ ] };
+
+ testAccessibleTree("txc3", accTree);
+
+ // textarea
+ accTree = {
+ role: ROLE_ENTRY,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF, // hello1\nhello2 text
+ },
+ ],
+ };
+
+ testAccessibleTree("txc4", accTree);
+
+ // input@type="password"
+ accTree = {
+ role: ROLE_PASSWORD_TEXT,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ children: [],
+ },
+ ],
+ };
+
+ testAccessibleTree("txc5", accTree);
+
+ // input@type="tel", value
+ accTree = {
+ role: ROLE_ENTRY,
+ children: [
+ { // text child
+ role: ROLE_TEXT_LEAF,
+ children: [],
+ },
+ ],
+ };
+
+ testAccessibleTree("txc6", accTree);
+
+ // input@type="email", value
+ accTree = {
+ role: ROLE_EDITCOMBOBOX, // Because of list attribute
+ children: [
+ { // text child
+ role: ROLE_TEXT_LEAF,
+ children: [],
+ },
+ ],
+ };
+
+ testAccessibleTree("txc7", accTree);
+
+ // input@type="search", value
+ accTree = {
+ role: ROLE_EDITCOMBOBOX, // Because of list attribute
+ children: [
+ { // text child
+ role: ROLE_TEXT_LEAF,
+ children: [],
+ },
+ ],
+ };
+
+ testAccessibleTree("txc8", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="overflowed content doesn't expose child text accessibles"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=489306">
+ Mozilla Bug 489306
+ </a><br>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=542824"
+ title="Create child accessibles for text controls from native anonymous content">
+ Mozilla Bug 542824
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=625652"
+ title="Make sure accessible tree is correct when rendered text is changed">
+ Mozilla Bug 625652
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="txc1" contentEditable="true">
+ 1hellohello
+ </div>
+ <input id="txc2" value="hello">
+ <input id="txc3">
+ <textarea id="txc4">
+ hello1
+ hello2
+ </textarea>
+ <input id="txc5" type="password" value="hello">
+ <input id="txc6" type="tel" value="4167771234">
+
+ Email Address:
+ <input id="txc7" type="email" list="contacts" value="xyzzy">
+ <datalist id="contacts">
+ <option>xyzzy@plughs.com</option>
+ <option>nobody@mozilla.org</option>
+ </datalist>
+
+ </br>Search for:
+ <input id="txc8" type="search" list="searchhisty" value="Gamma">
+ <datalist id="searchhisty">
+ <option>Gamma Rays</option>
+ <option>Gamma Ray Bursts</option>
+ </datalist>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/tree/test_txtctrl.xhtml b/accessible/tests/mochitest/tree/test_txtctrl.xhtml
new file mode 100644
index 0000000000..ff3d4977f0
--- /dev/null
+++ b/accessible/tests/mochitest/tree/test_txtctrl.xhtml
@@ -0,0 +1,86 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL textbox and textarea hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ //gA11yEventDumpToConsole = true;
+ //enableLogging("tree,verbose"); // debug stuff
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ function doTest()
+ {
+ //////////////////////////////////////////////////////////////////////////
+ // search textbox
+ let accTree =
+ { GROUPING: [
+ { ENTRY: [ { TEXT_LEAF: [] } ] },
+ ] };
+ testAccessibleTree("txc_search", accTree);
+
+ //////////////////////////////////////////////////////////////////////////
+ // search textbox with search button
+
+ if (MAC) {
+ accTree =
+ { GROUPING: [
+ { ENTRY: [ { TEXT_LEAF: [] } ] },
+ ] };
+ } else {
+ accTree =
+ { GROUPING: [
+ { ENTRY: [ { TEXT_LEAF: [] } ] },
+ { PUSHBUTTON: [] },
+ ] };
+ }
+
+ testAccessibleTree("txc_search_searchbutton", accTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=542824"
+ title="Create child accessibles for text controls from native anonymous content">
+ Mozilla Bug 542824
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <box role="group" id="txc_search">
+ <search-textbox value="hello"/>
+ </box>
+ <box role="group" id="txc_search_searchbutton">
+ <search-textbox searchbutton="true" value="hello"/>
+ </box>
+ </vbox>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/tree/wnd.xhtml b/accessible/tests/mochitest/tree/wnd.xhtml
new file mode 100644
index 0000000000..3b87cb5e0d
--- /dev/null
+++ b/accessible/tests/mochitest/tree/wnd.xhtml
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Empty Window">
+
+</window>
+
diff --git a/accessible/tests/mochitest/treeupdate/a11y.toml b/accessible/tests/mochitest/treeupdate/a11y.toml
new file mode 100644
index 0000000000..b98af5388e
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/a11y.toml
@@ -0,0 +1,84 @@
+[DEFAULT]
+support-files = [
+ "!/accessible/tests/mochitest/*.js",
+ "!/accessible/tests/mochitest/letters.gif",
+ "!/accessible/tests/mochitest/moz.png"]
+
+["test_ariadialog.html"]
+
+["test_ariahidden.html"]
+
+["test_ariaowns.html"]
+
+["test_bug852150.xhtml"]
+
+["test_bug883708.xhtml"]
+
+["test_bug884251.xhtml"]
+
+["test_bug895082.html"]
+
+["test_bug1040735.html"]
+
+["test_bug1175913.html"]
+
+["test_bug1189277.html"]
+
+["test_bug1276857.html"]
+support-files = "test_bug1276857_subframe.html"
+
+["test_canvas.html"]
+
+["test_contextmenu.xhtml"]
+
+["test_cssoverflow.html"]
+
+["test_deck.xhtml"]
+
+["test_delayed_removal.html"]
+
+["test_doc.html"]
+
+["test_gencontent.html"]
+
+["test_general.html"]
+
+["test_hidden.html"]
+
+["test_imagemap.html"]
+
+["test_inert.html"]
+
+["test_inner_reorder.html"]
+
+["test_list.html"]
+
+["test_list_editabledoc.html"]
+
+["test_list_style.html"]
+
+["test_listbox.xhtml"]
+
+["test_menu.xhtml"]
+
+["test_menubutton.xhtml"]
+
+["test_optgroup.html"]
+
+["test_recreation.html"]
+
+["test_select.html"]
+
+["test_shadow_slots.html"]
+
+["test_shutdown.xhtml"]
+
+["test_table.html"]
+
+["test_textleaf.html"]
+
+["test_tooltip.xhtml"]
+
+["test_visibility.html"]
+
+["test_whitespace.html"]
diff --git a/accessible/tests/mochitest/treeupdate/test_ariadialog.html b/accessible/tests/mochitest/treeupdate/test_ariadialog.html
new file mode 100644
index 0000000000..0b7f4bbb56
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_ariadialog.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Table creation in ARIA dialog test</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function showARIADialog(aID) {
+ this.node = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.node),
+ ];
+
+ this.invoke = function showARIADialog_invoke() {
+ getNode("dialog").style.display = "block";
+ getNode("table").style.visibility = "visible";
+ getNode("a").textContent = "link";
+ getNode("input").value = "hello";
+ getNode("input").focus();
+ };
+
+ this.finalCheck = function showARIADialog_finalCheck() {
+ var tree = {
+ role: ROLE_DIALOG,
+ children: [
+ {
+ role: ROLE_PUSHBUTTON,
+ children: [ { role: ROLE_TEXT_LEAF } ],
+ },
+ {
+ role: ROLE_ENTRY,
+ },
+ ],
+ };
+ testAccessibleTree(aID, tree);
+ };
+
+ this.getID = function showARIADialog_getID() {
+ return "show ARIA dialog";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+
+ function doTest() {
+ // enableLogging("tree");
+ gQueue = new eventQueue();
+
+ // make the accessible an inaccessible
+ gQueue.push(new showARIADialog("dialog"));
+
+ gQueue.invoke(); // SimpleTest.finish() will be called in the end
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Rework accessible tree update code"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275">
+ Mozilla Bug 570275
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="dialog" role="dialog" style="display: none;">
+ <table id="table" role="presentation"
+ style="display: block; position: absolute; top: 88px; left: 312.5px; z-index: 10010; visibility: hidden;">
+ <tbody>
+ <tr>
+ <td role="presentation">
+ <div role="presentation">
+ <a id="a" role="button">text</a>
+ </div>
+ <input id="input">
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_ariahidden.html b/accessible/tests/mochitest/treeupdate/test_ariahidden.html
new file mode 100644
index 0000000000..302465b59f
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_ariahidden.html
@@ -0,0 +1,118 @@
+<html>
+
+<head>
+ <title>aria-hidden tree update tests</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function t1_setARIAHidden() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, "t1"),
+ ];
+
+ this.invoke = function t1_setARIAHidden_invoke() {
+ getNode("t1_child").setAttribute("aria-hidden", "true");
+ };
+
+ this.finalCheck = function t1_setARIAHidden_finalCheck() {
+ ok(!isAccessible("t1_child"), "No accessible for aria-hidden");
+ };
+
+ this.getID = function t1_setARIAHidden_getID() {
+ return "aria-hidden set to true";
+ };
+ }
+
+ function t1_removeARIAHidden() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, "t1"),
+ ];
+
+ this.invoke = function t1_removeARIAHidden_invoke() {
+ getNode("t1_child").removeAttribute("aria-hidden");
+ };
+
+ this.finalCheck = function t1_removeARIAHidden_finalCheck() {
+ ok(isAccessible("t1_child"), "No aria-hidden, has to be accessible");
+ };
+
+ this.getID = function t1_removeARIAHidden_getID() {
+ return "remove aria-hidden";
+ };
+ }
+
+ function t2_setARIAHidden() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, "t2"),
+ ];
+
+ this.invoke = function t2_setARIAHidden_invoke() {
+ getNode("t2_child").setAttribute("aria-hidden", "true");
+ };
+
+ this.finalCheck = function t2_setARIAHidden_finalCheck() {
+ testAccessibleTree("t2", { SECTION: []});
+ };
+
+ this.getID = function t2_setARIAHidden_getID() {
+ return "t2: set aria-hidden";
+ };
+ }
+
+ function t2_insertUnderARIAHidden() {
+ this.eventSeq = [
+ new unexpectedInvokerChecker(EVENT_REORDER, "t2"),
+ ];
+
+ this.invoke = function t2_insertUnderARIAHidden_invoke() {
+ getNode("t2_child").innerHTML = "<input>";
+ };
+
+ this.finalCheck = function t2_insertUnderARIAHidden_finalCheck() {
+ testAccessibleTree("t2", { SECTION: []});
+ };
+
+ this.getID = function t2_insertUnderARIAHidden_getID() {
+ return "t2: insert under aria-hidden";
+ };
+ }
+
+ // gA11yEventDumpToConsole = true;
+ function doTests() {
+ ok(!isAccessible("t1_child"), "No accessible for aria-hidden");
+
+ const gQueue = new eventQueue();
+ gQueue.push(new t1_removeARIAHidden());
+ gQueue.push(new t1_setARIAHidden());
+ gQueue.push(new t2_setARIAHidden());
+ gQueue.push(new t2_insertUnderARIAHidden());
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="t1"><div id="t1_child" aria-hidden="true">Hi</div><div>there</div></div>
+ <div id="t2">
+ <span id="t2_child">hoho</span>
+ </div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_ariaowns.html b/accessible/tests/mochitest/treeupdate/test_ariaowns.html
new file mode 100644
index 0000000000..60006a960c
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_ariaowns.html
@@ -0,0 +1,851 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>@aria-owns attribute testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+ // //////////////////////////////////////////////////////////////////////////
+
+ function changeARIAOwns() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t1_button")),
+ // no hide for t1_subdiv because it is contained by hidden t1_checkbox
+ new invokerChecker(EVENT_HIDE, getNode("t1_checkbox")),
+ new invokerChecker(EVENT_SHOW, getNode("t1_checkbox")),
+ new invokerChecker(EVENT_SHOW, getNode("t1_button")),
+ new invokerChecker(EVENT_SHOW, getNode("t1_subdiv")),
+ new invokerChecker(EVENT_REORDER, getNode("t1_container")),
+ ];
+
+ this.invoke = function setARIAOwns_invoke() {
+ // children are swapped by ARIA owns
+ var tree =
+ { SECTION: [
+ { CHECKBUTTON: [
+ { SECTION: [] },
+ ] },
+ { PUSHBUTTON: [ ] },
+ ] };
+ testAccessibleTree("t1_container", tree);
+
+ getNode("t1_container").
+ setAttribute("aria-owns", "t1_button t1_subdiv");
+ };
+
+ this.finalCheck = function setARIAOwns_finalCheck() {
+ // children are swapped again, button and subdiv are appended to
+ // the children.
+ var tree =
+ { SECTION: [
+ { CHECKBUTTON: [ ] }, // checkbox, native order
+ { PUSHBUTTON: [ ] }, // button, rearranged by ARIA own
+ { SECTION: [ ] }, // subdiv from the subtree, ARIA owned
+ ] };
+ testAccessibleTree("t1_container", tree);
+ };
+
+ this.getID = function setARIAOwns_getID() {
+ return "Change @aria-owns attribute";
+ };
+ }
+
+ function removeARIAOwns() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t1_button")),
+ new invokerChecker(EVENT_HIDE, getNode("t1_subdiv")),
+ new orderChecker(),
+ new asyncInvokerChecker(EVENT_SHOW, getNode("t1_button")),
+ new asyncInvokerChecker(EVENT_SHOW, getNode("t1_subdiv")),
+ new orderChecker(),
+ new invokerChecker(EVENT_REORDER, getNode("t1_container")),
+ new unexpectedInvokerChecker(EVENT_REORDER, getNode("t1_checkbox")),
+ ];
+
+ this.invoke = function removeARIAOwns_invoke() {
+ getNode("t1_container").removeAttribute("aria-owns");
+ };
+
+ this.finalCheck = function removeARIAOwns_finalCheck() {
+ // children follow the DOM order
+ var tree =
+ { SECTION: [
+ { PUSHBUTTON: [ ] },
+ { CHECKBUTTON: [
+ { SECTION: [] },
+ ] },
+ ] };
+ testAccessibleTree("t1_container", tree);
+ };
+
+ this.getID = function removeARIAOwns_getID() {
+ return "Remove @aria-owns attribute";
+ };
+ }
+
+ function setARIAOwns() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t1_button")),
+ new invokerChecker(EVENT_HIDE, getNode("t1_subdiv")),
+ new invokerChecker(EVENT_SHOW, getNode("t1_button")),
+ new invokerChecker(EVENT_SHOW, getNode("t1_subdiv")),
+ new invokerChecker(EVENT_REORDER, getNode("t1_container")),
+ ];
+
+ this.invoke = function setARIAOwns_invoke() {
+ getNode("t1_container").
+ setAttribute("aria-owns", "t1_button t1_subdiv");
+ };
+
+ this.finalCheck = function setARIAOwns_finalCheck() {
+ // children are swapped again, button and subdiv are appended to
+ // the children.
+ var tree =
+ { SECTION: [
+ { CHECKBUTTON: [ ] }, // checkbox
+ { PUSHBUTTON: [ ] }, // button, rearranged by ARIA own
+ { SECTION: [ ] }, // subdiv from the subtree, ARIA owned
+ ] };
+ testAccessibleTree("t1_container", tree);
+ };
+
+ this.getID = function setARIAOwns_getID() {
+ return "Set @aria-owns attribute";
+ };
+ }
+
+ function addIdToARIAOwns() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t1_group")),
+ new invokerChecker(EVENT_SHOW, getNode("t1_group")),
+ new invokerChecker(EVENT_REORDER, document),
+ ];
+
+ this.invoke = function addIdToARIAOwns_invoke() {
+ getNode("t1_container").
+ setAttribute("aria-owns", "t1_button t1_subdiv t1_group");
+ };
+
+ this.finalCheck = function addIdToARIAOwns_finalCheck() {
+ // children are swapped again, button and subdiv are appended to
+ // the children.
+ var tree =
+ { SECTION: [
+ { CHECKBUTTON: [ ] }, // t1_checkbox
+ { PUSHBUTTON: [ ] }, // button, t1_button
+ { SECTION: [ ] }, // subdiv from the subtree, t1_subdiv
+ { GROUPING: [ ] }, // group from outside, t1_group
+ ] };
+ testAccessibleTree("t1_container", tree);
+ };
+
+ this.getID = function addIdToARIAOwns_getID() {
+ return "Add id to @aria-owns attribute value";
+ };
+ }
+
+ function appendEl() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, getNode, "t1_child3"),
+ new invokerChecker(EVENT_REORDER, getNode("t1_container")),
+ ];
+
+ this.invoke = function appendEl_invoke() {
+ var div = document.createElement("div");
+ div.setAttribute("id", "t1_child3");
+ div.setAttribute("role", "radio");
+ getNode("t1_container").appendChild(div);
+ };
+
+ this.finalCheck = function appendEl_finalCheck() {
+ // children are invalidated, they includes aria-owns swapped kids and
+ // newly inserted child.
+ var 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("t1_container", tree);
+ };
+
+ this.getID = function appendEl_getID() {
+ return "Append child under @aria-owns element";
+ };
+ }
+
+ function removeEl() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t1_subdiv")),
+ new invokerChecker(EVENT_REORDER, getNode("t1_container")),
+ ];
+
+ this.invoke = function removeEl_invoke() {
+ // remove a container of t1_subdiv
+ getNode("t1_span").remove();
+ };
+
+ this.finalCheck = function removeEl_finalCheck() {
+ // subdiv should go away
+ var tree =
+ { SECTION: [
+ { CHECKBUTTON: [ ] }, // explicit, t1_checkbox
+ { RADIOBUTTON: [ ] }, // explicit, t1_child3
+ { PUSHBUTTON: [ ] }, // ARIA owned, t1_button
+ { GROUPING: [ ] }, // ARIA owned, t1_group
+ ] };
+ testAccessibleTree("t1_container", tree);
+ };
+
+ this.getID = function removeEl_getID() {
+ return "Remove a container of ARIA owned element";
+ };
+ }
+
+ function removeId() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t1_group")),
+ new invokerChecker(EVENT_SHOW, getNode("t1_group")),
+ new invokerChecker(EVENT_REORDER, document),
+ ];
+
+ this.invoke = function removeId_invoke() {
+ getNode("t1_group").removeAttribute("id");
+ };
+
+ this.finalCheck = function removeId_finalCheck() {
+ var tree =
+ { SECTION: [
+ { CHECKBUTTON: [ ] },
+ { RADIOBUTTON: [ ] },
+ { PUSHBUTTON: [ ] }, // ARIA owned, t1_button
+ ] };
+ testAccessibleTree("t1_container", tree);
+ };
+
+ this.getID = function removeId_getID() {
+ return "Remove ID from ARIA owned element";
+ };
+ }
+
+ function setId() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("t1_grouptmp")),
+ new invokerChecker(EVENT_SHOW, getNode("t1_grouptmp")),
+ new invokerChecker(EVENT_REORDER, document),
+ ];
+
+ this.invoke = function setId_invoke() {
+ getNode("t1_grouptmp").setAttribute("id", "t1_group");
+ };
+
+ this.finalCheck = function setId_finalCheck() {
+ var tree =
+ { SECTION: [
+ { CHECKBUTTON: [ ] },
+ { RADIOBUTTON: [ ] },
+ { PUSHBUTTON: [ ] }, // ARIA owned, t1_button
+ { GROUPING: [ ] }, // ARIA owned, t1_group, previously t1_grouptmp
+ ] };
+ testAccessibleTree("t1_container", tree);
+ };
+
+ this.getID = function setId_getID() {
+ return "Set ID that is referred by ARIA owns";
+ };
+ }
+
+ /**
+ * Remove an accessible DOM element containing an element referred by
+ * ARIA owns.
+ */
+ function removeA11eteiner() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, getNode("t2_container1")),
+ ];
+
+ this.invoke = function removeA11eteiner_invoke() {
+ var tree =
+ { SECTION: [
+ { CHECKBUTTON: [ ] }, // ARIA owned, 't2_owned'
+ ] };
+ testAccessibleTree("t2_container1", tree);
+
+ getNode("t2_container2").removeChild(getNode("t2_container3"));
+ };
+
+ this.finalCheck = function removeA11eteiner_finalCheck() {
+ var tree =
+ { SECTION: [
+ ] };
+ testAccessibleTree("t2_container1", tree);
+ };
+
+ this.getID = function removeA11eteiner_getID() {
+ return "Remove an accessible DOM element containing an element referred by ARIA owns";
+ };
+ }
+
+ /**
+ * Attempt to steal an element from other ARIA owns element. This should
+ * not be possible. The only child that will get owned into this
+ * container is a previously not aria-owned one.
+ */
+ function stealFromOtherARIAOwns() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, getNode("t3_container3")),
+ ];
+
+ this.invoke = function stealFromOtherARIAOwns_invoke() {
+ getNode("t3_container3").setAttribute("aria-owns", "t3_child t3_child2");
+ };
+
+ this.finalCheck = function stealFromOtherARIAOwns_finalCheck() {
+ var tree =
+ { SECTION: [
+ { CHECKBUTTON: [
+ ] },
+ ] };
+ testAccessibleTree("t3_container1", tree);
+
+ tree =
+ { SECTION: [
+ ] };
+ testAccessibleTree("t3_container2", tree);
+
+ tree =
+ { SECTION: [
+ { CHECKBUTTON: [
+ ] },
+ ] };
+ testAccessibleTree("t3_container3", tree);
+ };
+
+ this.getID = function stealFromOtherARIAOwns_getID() {
+ return "Steal an element from other ARIA owns element";
+ };
+ }
+
+ function appendElToRecacheChildren() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, getNode("t3_container3")),
+ ];
+
+ this.invoke = function appendElToRecacheChildren_invoke() {
+ var div = document.createElement("div");
+ div.setAttribute("role", "radio");
+ getNode("t3_container3").appendChild(div);
+ };
+
+ this.finalCheck = function appendElToRecacheChildren_finalCheck() {
+ var tree =
+ { SECTION: [
+ ] };
+ testAccessibleTree("t3_container2", tree);
+
+ tree =
+ { SECTION: [
+ { RADIOBUTTON: [ ] },
+ { CHECKBUTTON: [ ] }, // ARIA owned
+ ] };
+ testAccessibleTree("t3_container3", tree);
+ };
+
+ this.getID = function appendElToRecacheChildren_getID() {
+ return "Append a child under @aria-owns element to trigger children recache";
+ };
+ }
+
+ function showHiddenElement() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, getNode("t4_container1")),
+ ];
+
+ this.invoke = function showHiddenElement_invoke() {
+ var tree =
+ { SECTION: [
+ { RADIOBUTTON: [] },
+ ] };
+ testAccessibleTree("t4_container1", tree);
+
+ getNode("t4_child1").style.display = "block";
+ };
+
+ this.finalCheck = function showHiddenElement_finalCheck() {
+ var tree =
+ { SECTION: [
+ { CHECKBUTTON: [] },
+ { RADIOBUTTON: [] },
+ ] };
+ testAccessibleTree("t4_container1", tree);
+ };
+
+ this.getID = function showHiddenElement_getID() {
+ return "Show hidden ARIA owns referred element";
+ };
+ }
+
+ function rearrangeARIAOwns(aContainer, aAttr, aIdList, aRoleList) {
+ this.eventSeq = [];
+ for (let id of aIdList) {
+ this.eventSeq.push(new invokerChecker(EVENT_HIDE, getNode(id)));
+ }
+
+ for (let id of aIdList) {
+ this.eventSeq.push(new invokerChecker(EVENT_SHOW, getNode(id)));
+ }
+ this.eventSeq.push(new invokerChecker(EVENT_REORDER, getNode(aContainer)));
+
+ this.invoke = function rearrangeARIAOwns_invoke() {
+ getNode(aContainer).setAttribute("aria-owns", aAttr);
+ };
+
+ this.finalCheck = function rearrangeARIAOwns_finalCheck() {
+ var tree = { SECTION: [ ] };
+ for (var role of aRoleList) {
+ var ch = {};
+ ch[role] = [];
+ tree.SECTION.push(ch);
+ }
+ testAccessibleTree(aContainer, tree);
+ };
+
+ this.getID = function rearrangeARIAOwns_getID() {
+ return `Rearrange @aria-owns attribute to '${aAttr}'`;
+ };
+ }
+
+ function removeNotARIAOwnedEl(aContainer, aChild) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, aContainer),
+ ];
+
+ this.invoke = function removeNotARIAOwnedEl_invoke() {
+ var tree = {
+ SECTION: [
+ { TEXT_LEAF: [ ] },
+ { GROUPING: [ ] },
+ ],
+ };
+ testAccessibleTree(aContainer, tree);
+
+ getNode(aContainer).removeChild(getNode(aChild));
+ };
+
+ this.finalCheck = function removeNotARIAOwnedEl_finalCheck() {
+ var tree = {
+ SECTION: [
+ { GROUPING: [ ] },
+ ],
+ };
+ testAccessibleTree(aContainer, tree);
+ };
+
+ this.getID = function removeNotARIAOwnedEl_getID() {
+ return `remove not ARIA owned child`;
+ };
+ }
+
+ function setARIAOwnsOnElToRemove(aParent, aChild) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getAccessible(aParent)),
+ ];
+
+ this.invoke = function setARIAOwnsOnElToRemove_invoke() {
+ getNode(aChild).setAttribute("aria-owns", "no_id");
+ getNode(aParent).removeChild(getNode(aChild));
+ getNode(aParent).remove();
+ };
+
+ this.getID = function setARIAOwnsOnElToRemove_getID() {
+ return `set ARIA owns on an element, and then remove it, and then remove its parent`;
+ };
+ }
+
+ /**
+ * Set ARIA owns on inaccessible span element that contains
+ * accessible children. This will move children from the container for
+ * the span.
+ */
+ function test8() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, "t8_container"),
+ ];
+
+ this.invoke = function test8_invoke() {
+ var tree =
+ { SECTION: [
+ { PUSHBUTTON: [] },
+ { ENTRY: [] },
+ { ENTRY: [] },
+ { ENTRY: [] },
+ ] };
+ testAccessibleTree("t8_container", tree);
+
+ getNode(t8_container).setAttribute("aria-owns", "t8_span t8_button");
+ };
+
+ this.finalCheck = function test8_finalCheck() {
+ var tree =
+ { SECTION: [
+ { TEXT: [
+ { ENTRY: [] },
+ { ENTRY: [] },
+ { ENTRY: [] },
+ ] },
+ { PUSHBUTTON: [] },
+ ] };
+ testAccessibleTree("t8_container", tree);
+ };
+
+ this.getID = function test8_getID() {
+ return `Set ARIA owns on inaccessible span element that contains accessible children`;
+ };
+ }
+
+ function test9_prepare() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, () => {
+ return getNode("t9_container").contentDocument;
+ }),
+ ];
+
+ this.invoke = () => {
+ // The \ before the final /script avoids the script from being terminated
+ // by the html parser.
+ getNode("t9_container").src = `data:text/html,
+ <html><body></body>
+ <script>
+ let el = document.createElement('div');
+ el.id = 'container';
+ el.innerHTML = "<input id='input'>";
+ document.documentElement.appendChild(el);
+ <\/script></html>`;
+ };
+
+ this.finalCheck = () => {
+ var tree =
+ { INTERNAL_FRAME: [
+ { DOCUMENT: [
+ { SECTION: [
+ { ENTRY: [] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("t9_container", tree);
+ };
+
+ this.getID = () => {
+ return `Set ARIA owns on a document (part1)`;
+ };
+ }
+
+ function test9_setARIAOwns() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, () => {
+ let doc = getNode("t9_container").contentDocument;
+ return doc && doc.getElementById("input");
+ }),
+ ];
+
+ this.invoke = () => {
+ let doc = getNode("t9_container").contentDocument;
+ doc.body.setAttribute("aria-owns", "input");
+ };
+
+ this.finalCheck = () => {
+ var tree =
+ { INTERNAL_FRAME: [
+ { DOCUMENT: [
+ { SECTION: [] },
+ { ENTRY: [] },
+ ] },
+ ] };
+ testAccessibleTree("t9_container", tree);
+ };
+
+ this.getID = () => {
+ return `Set ARIA owns on a document (part2)`;
+ };
+ }
+
+ function test9_finish() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, () => {
+ return getNode("t9_container").contentDocument;
+ }),
+ ];
+
+ this.invoke = () => {
+ // trigger a tree update.
+ let doc = getNode("t9_container").contentDocument;
+ doc.body.appendChild(doc.createElement("p"));
+ };
+
+ this.finalCheck = () => {
+ var tree =
+ { INTERNAL_FRAME: [
+ { DOCUMENT: [
+ { PARAGRAPH: [] },
+ { SECTION: [] },
+ { ENTRY: [] },
+ ] },
+ ] };
+ testAccessibleTree("t9_container", tree);
+ };
+
+ this.getID = () => {
+ return `Set ARIA owns on a document (part3)`;
+ };
+ }
+
+ /**
+ * Put ARIA owned child back when ARIA owner removed.
+ */
+ function test10_removeARIAOwner() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getAccessible("t10_owner")),
+ ];
+
+ this.invoke = () => {
+ let tree =
+ { SECTION: [ // t10_container
+ { SECTION: [ // t10_owner
+ { ENTRY: [] }, // t10_child
+ ] },
+ ] };
+ testAccessibleTree("t10_container", tree);
+
+ getNode("t10_owner").remove();
+ };
+
+ this.getID = () => {
+ return "Put aria owned child back when aria owner removed";
+ };
+ }
+
+ function test10_finishTest() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, "t10_container"),
+ ];
+
+ this.invoke = () => {
+ // trigger a tree update.
+ getNode("t10_container").append(document.createElement("p"));
+ };
+
+ this.finalCheck = () => {
+ let tree =
+ { SECTION: [ // t10_container
+ { ENTRY: [] }, // t10_child
+ { PARAGRAPH: [] },
+ ] };
+ testAccessibleTree("t10_container", tree);
+ todo(false, "Input accessible has be moved back in the tree");
+ };
+
+ this.getID = () => {
+ return `Put aria owned child back when aria owner removed (finish test)`;
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Test
+ // //////////////////////////////////////////////////////////////////////////
+
+ // gA11yEventDumpToConsole = true;
+ // enableLogging("tree,eventTree,verbose"); // debug stuff
+
+ var gQueue = null;
+
+ async function doTest() {
+ let PromEvents = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js",
+ PromEvents);
+
+ gQueue = new eventQueue();
+ let queueFinished = new Promise(resolve => {
+ gQueue.onFinish = function() {
+ resolve();
+ return DO_NOT_FINISH_TEST;
+ };
+ });
+
+ // test1
+ gQueue.push(new changeARIAOwns());
+ gQueue.push(new removeARIAOwns());
+ gQueue.push(new setARIAOwns());
+ gQueue.push(new addIdToARIAOwns());
+ gQueue.push(new appendEl());
+ gQueue.push(new removeEl());
+ gQueue.push(new removeId());
+ gQueue.push(new setId());
+
+ // test2
+ gQueue.push(new removeA11eteiner());
+
+ // test3
+ gQueue.push(new stealFromOtherARIAOwns());
+ gQueue.push(new appendElToRecacheChildren());
+
+ // test4
+ gQueue.push(new showHiddenElement());
+
+ // test5
+ gQueue.push(new rearrangeARIAOwns(
+ "t5_container", "t5_checkbox t5_radio t5_button",
+ [ "t5_checkbox", "t5_radio", "t5_button" ],
+ [ "CHECKBUTTON", "RADIOBUTTON", "PUSHBUTTON" ]));
+ gQueue.push(new rearrangeARIAOwns(
+ "t5_container", "t5_radio t5_button t5_checkbox",
+ [ "t5_radio", "t5_button" ],
+ [ "RADIOBUTTON", "PUSHBUTTON", "CHECKBUTTON" ]));
+
+ gQueue.push(new removeNotARIAOwnedEl("t6_container", "t6_span"));
+
+ gQueue.push(new setARIAOwnsOnElToRemove("t7_parent", "t7_child"));
+
+ gQueue.push(new test8());
+ gQueue.push(new test9_prepare());
+ gQueue.push(new test9_setARIAOwns());
+ gQueue.push(new test9_finish());
+
+ gQueue.push(new test10_removeARIAOwner());
+ gQueue.push(new test10_finishTest());
+
+ gQueue.invoke();
+ await queueFinished;
+
+ let owned = document.createElement('div');
+ owned.id = 't11_child';
+ owned.textContent = 'owned';
+ let evtPromise = PromEvents.waitForEvent(EVENT_SHOW, "t11_child");
+ getNode("t11_container").append(owned);
+ let evt = await evtPromise;
+ is(evt.accessible.parent.name, "t11_owner");
+
+ // Test owning an ancestor which isn't created yet.
+ testAccessibleTree("t12_container", { SECTION: [ // t12_container
+ { SECTION: [ // t12b
+ { SECTION: [] }, // t12c
+ ] },
+ { SECTION: [] }, // t12d
+ ] });
+ // Owning t12a would create a cycle, so we expect it to do nothing.
+ // We own t12d so we get an event when aria-owns relocation is complete.
+ evtPromise = PromEvents.waitForEvent(EVENT_SHOW, "t12d");
+ getNode("t12c").setAttribute("aria-owns", "t12a t12d");
+ await evtPromise;
+ testAccessibleTree("t12_container", { SECTION: [ // t12_container
+ { SECTION: [ // t12b
+ { SECTION: [ // t12c
+ { SECTION: [] }, // t12d
+ ] },
+ ] },
+ ] });
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+
+ </script>
+</head>
+
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="t1_container" aria-owns="t1_checkbox t1_button">
+ <div role="button" id="t1_button"></div>
+ <div role="checkbox" id="t1_checkbox">
+ <span id="t1_span">
+ <div id="t1_subdiv"></div>
+ </span>
+ </div>
+ </div>
+ <div id="t1_group" role="group"></div>
+ <div id="t1_grouptmp" role="group"></div>
+
+ <div id="t2_container1" aria-owns="t2_owned"></div>
+ <div id="t2_container2">
+ <div id="t2_container3"><div id="t2_owned" role="checkbox"></div></div>
+ </div>
+
+ <div id="t3_container1" aria-owns="t3_child"></div>
+ <div id="t3_child" role="checkbox"></div>
+ <div id="t3_container2">
+ <div id="t3_child2" role="checkbox"></div>
+ </div>
+ <div id="t3_container3"></div>
+
+ <div id="t4_container1" aria-owns="t4_child1 t4_child2"></div>
+ <div id="t4_container2">
+ <div id="t4_child1" style="display:none" role="checkbox"></div>
+ <div id="t4_child2" role="radio"></div>
+ </div>
+
+ <div id="t5_container">
+ <div role="button" id="t5_button"></div>
+ <div role="checkbox" id="t5_checkbox"></div>
+ <div role="radio" id="t5_radio"></div>
+ </div>
+
+ <div id="t6_container" aria-owns="t6_fake">
+ <span id="t6_span">hey</span>
+ </div>
+ <div id="t6_fake" role="group"></div>
+
+ <div id="t7_container">
+ <div id="t7_parent">
+ <div id="t7_child"></div>
+ </div>
+ </div>
+
+ <div id="t8_container">
+ <input id="t8_button" type="button"><span id="t8_span"><input><input><input></span>
+ </div>
+
+ <iframe id="t9_container"></iframe>
+
+ <div id="t10_container">
+ <div id="t10_owner" aria-owns="t10_child"></div>
+ <input id="t10_child">
+ </div>
+
+ <div id="t11_container" aria-label="t11_container">
+ <div aria-owns="t11_child" aria-label="t11_owner"></div>
+ </div>
+
+ <div id="t12_container">
+ <span id="t12a">
+ <div id="t12b" aria-owns="t12c"></div>
+ </span>
+ <div id="t12c"></div>
+ <div id="t12d"></div>
+ </div>
+</body>
+
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_bug1040735.html b/accessible/tests/mochitest/treeupdate/test_bug1040735.html
new file mode 100644
index 0000000000..b7d0e472d0
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_bug1040735.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Adopt DOM node from anonymous subtree</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript"
+ src="../common.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ document.body.appendChild(document.getElementById("mw_a"));
+ setTimeout(function() { ok(true, "no crash and assertions"); SimpleTest.finish(); }, 0);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1040735"
+ title="Bug 1040735 - DOM node reinsertion under anonymous content may trigger a11y child adoption">
+ Bug 1040735</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <marquee>
+ <div id="mw_a" style="visibility: hidden;">
+ <div style="visibility: visible;" id="mw_inside"></div>
+ </div>
+ </marquee>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_bug1175913.html b/accessible/tests/mochitest/treeupdate/test_bug1175913.html
new file mode 100644
index 0000000000..1fe2720434
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_bug1175913.html
@@ -0,0 +1,95 @@
+<html>
+
+<head>
+ <title>Test hide/show events on event listener changes</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ function dummyListener() {}
+
+ function testAddListener() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, getNode("parent")),
+ ];
+
+ this.invoke = function testAddListener_invoke() {
+ is(getAccessible("parent", null, null, DONOTFAIL_IF_NO_ACC), null, "Check that parent is not accessible.");
+ is(getAccessible("child", null, null, DONOTFAIL_IF_NO_ACC), null, "Check that child is not accessible.");
+ getNode("parent").addEventListener("click", dummyListener);
+ };
+
+ this.finalCheck = function testAddListener_finalCheck() {
+ var tree = { TEXT: [] };
+ testAccessibleTree("parent", tree);
+ };
+
+ this.getID = function testAddListener_getID() {
+ return "Test that show event is sent when click listener is added";
+ };
+ }
+
+ function testRemoveListener() {
+ this.eventSeq = [
+ new unexpectedInvokerChecker(EVENT_HIDE, getNode("parent")),
+ ];
+
+ this.invoke = function testRemoveListener_invoke() {
+ getNode("parent").removeEventListener("click", dummyListener);
+ };
+
+ this.finalCheck = function testRemoveListener_finalCheck() {
+ ok(getAccessible("parent", null, null, DONOTFAIL_IF_NO_ACC),
+ "Parent stays accessible after click event listener is removed");
+ ok(!getAccessible("child", null, null, DONOTFAIL_IF_NO_ACC),
+ "Child stays inaccessible");
+ };
+
+ this.getID = function testRemoveListener_getID() {
+ return "Test that hide event is sent when click listener is removed";
+ };
+ }
+
+ var gQueue = null;
+ function doTest() {
+ gQueue = new eventQueue();
+ gQueue.push(new testAddListener());
+ gQueue.push(new testRemoveListener());
+ gQueue.invoke(); // SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1175913"
+ title="Crash in mozilla::a11y::DocAccessibleParent::RemoveAccessible(ProxyAccessible* aAccessible)">
+ Mozilla Bug 1175913
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <span id="parent">
+ <span id="child">
+ </span>
+ </span>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_bug1189277.html b/accessible/tests/mochitest/treeupdate/test_bug1189277.html
new file mode 100644
index 0000000000..95efe5135a
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_bug1189277.html
@@ -0,0 +1,82 @@
+<html>
+
+<head>
+ <title>Test hide/show events for HTMLListBulletAccessibles on list restyle</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function runTest() {
+ this.containerNode = getNode("outerDiv");
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode("child")),
+ new invokerChecker(EVENT_HIDE, getNode("childDoc")),
+ new invokerChecker(EVENT_SHOW, "newChildDoc"),
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.invoke = function runTest_invoke() {
+ this.containerNode.removeChild(getNode("child"));
+
+ var docContainer = getNode("docContainer");
+ var iframe = document.createElement("iframe");
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ iframe.setAttribute("src", "http://example.com");
+ iframe.setAttribute("id", "newChildDoc");
+
+ docContainer.removeChild(getNode("childDoc"));
+ docContainer.appendChild(iframe);
+ };
+
+ this.getID = function runTest_getID() {
+ return "check show events are not incorrectly coalesced";
+ };
+ }
+
+ // enableLogging("tree");
+ gA11yEventDumpToConsole = true;
+ var gQueue = null;
+ function doTest() {
+ gQueue = new eventQueue();
+ gQueue.push(new runTest());
+ gQueue.invoke(); // SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1189277"
+ title="content process crash caused by missing show event">
+ Mozilla Bug 1189277
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="outerDiv">
+ <div id="child">foo</div>
+ <div id="docContainer">
+ <iframe id="childDoc" src="about:blank">
+ </iframe>
+ </div>
+ </div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_bug1276857.html b/accessible/tests/mochitest/treeupdate/test_bug1276857.html
new file mode 100644
index 0000000000..a164247534
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_bug1276857.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>DOM mutations test</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function runTest() {
+ let iframe = document.getElementById("iframe");
+
+ // children change will recreate the table
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, () => {
+ let doc = getNode("iframe").contentDocument;
+ return doc && doc.getElementById("c1");
+ }),
+ ];
+
+ this.invoke = function runTest_invoke() {
+ var tree = {
+ SECTION: [ // c1
+ { TEXT_LEAF: [] }, // Some text
+ { TEXT_CONTAINER: [
+ { TEXT_LEAF: [] }, // something with ..
+ ] },
+ { TEXT_LEAF: [] }, // More text
+ ],
+ };
+ testAccessibleTree(iframe.contentDocument.getElementById("c1"), tree);
+
+ iframe.contentDocument.getElementById("c1_t").querySelector("span").remove();
+ };
+
+ this.finalCheck = function runTest_finalCheck() {
+ var tree = {
+ SECTION: [ // c1
+ { TEXT_LEAF: [] }, // Some text
+ { TEXT_LEAF: [] }, // More text
+ ],
+ };
+ testAccessibleTree(iframe.contentDocument.getElementById("c1"), tree);
+ };
+
+ this.getID = function runTest_getID() {
+ return "child DOM node is removed before the layout notifies the a11y about parent removal/show";
+ };
+ }
+
+ function runShadowTest() {
+ // children change will recreate the table
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, () => {
+ let doc = getNode("iframe").contentDocument;
+ return doc && doc.getElementById("c2");
+ }),
+ ];
+
+ this.invoke = function runShadowTest_invoke() {
+ var tree = {
+ SECTION: [ // c2
+ { TEXT_LEAF: [] }, // Some text
+ { TEXT_CONTAINER: [
+ { TEXT_LEAF: [] }, // something with ..
+ ] },
+ { TEXT_LEAF: [] }, // More text
+ ],
+ };
+ const iframe = document.getElementById("iframe");
+ testAccessibleTree(iframe.contentDocument.getElementById("c2"), tree);
+
+ var shadowRoot = iframe.contentDocument.getElementById("c2_c").shadowRoot;
+ shadowRoot.firstElementChild.querySelector("span").remove();
+ // bug 1487312
+ shadowRoot.firstElementChild.offsetTop;
+ shadowRoot.appendChild(document.createElement("button"));
+ };
+
+ this.finalCheck = function runShadowTest_finalCheck() {
+ var tree = {
+ SECTION: [ // c2
+ { TEXT_LEAF: [] }, // Some text
+ { TEXT_LEAF: [] }, // More text
+ { PUSHBUTTON: [] }, // The button we appended.
+ ],
+ };
+ const iframe = document.getElementById("iframe");
+ testAccessibleTree(iframe.contentDocument.getElementById("c2"), tree);
+ };
+
+ this.getID = function runShadowTest_getID() {
+ return "child DOM node is removed before the layout notifies the a11y about parent removal/show in shadow DOM";
+ };
+ }
+
+ // enableLogging("tree");
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+ function doTest() {
+ gQueue = new eventQueue();
+ gQueue.push(new runTest());
+ gQueue.push(new runShadowTest());
+ gQueue.invoke(); // will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ window.onload = () => {
+ let iframe = document.createElement("iframe");
+ iframe.id = "iframe";
+ iframe.src = "test_bug1276857_subframe.html";
+ addA11yLoadEvent(doTest, iframe.contentWindow);
+ document.body.appendChild(iframe);
+ };
+ </script>
+
+</head>
+<body>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_bug1276857_subframe.html b/accessible/tests/mochitest/treeupdate/test_bug1276857_subframe.html
new file mode 100644
index 0000000000..869c9ebe6c
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_bug1276857_subframe.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>DOM mutations test</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript" src="../role.js"></script>
+</head>
+<body>
+ <div id="c1">
+ <div id="c1_t" style="display: table" role="presentation">
+ Some text
+ <span style="display: table-cell">something with accessibles goes here</span>
+ More text
+ </div>
+ </div>
+
+ <template id="tmpl">
+ <div style="display: table" role="presentation">
+ Some text
+ <span style="display: table-cell">something with accessibles goes here</span>
+ More text
+ </div>
+ </template>
+
+ <div id="c2"><div id="c2_c" role="presentation"></div></div>
+
+ <script>
+ var gShadowRoot = document.getElementById("c2_c").attachShadow({mode: "open"});
+ var tmpl = document.getElementById("tmpl");
+ gShadowRoot.appendChild(document.importNode(tmpl.content, true));
+ </script>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_bug852150.xhtml b/accessible/tests/mochitest/treeupdate/test_bug852150.xhtml
new file mode 100644
index 0000000000..51a3c39047
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_bug852150.xhtml
@@ -0,0 +1,57 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Canvas subdom mutation</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+
+ <script>
+ <![CDATA[
+ function doTest() {
+ var the_displayNone = getNode("the_displaynone");
+ var the_table = getNode("the_table");
+ var the_row = getNode("the_row");
+ ok(isAccessible(the_table), "table should be accessible");
+ the_displayNone.appendChild(the_table);
+ ok(!isAccessible(the_table), "table in display none tree shouldn't be accessible");
+
+ setTimeout(function() {
+ document.body.removeChild(the_row);
+ // make sure no accessibles have stuck around.
+ ok(!isAccessible(the_row), "row shouldn't be accessible");
+ ok(!isAccessible(the_table), "table shouldn't be accessible");
+ ok(!isAccessible(the_displayNone), "display none things shouldn't be accessible");
+ SimpleTest.finish();
+ }, 0);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="test accessible removal when reframe root isn't accessible"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=852150">
+ Mozilla Bug 852150
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="the_displaynone" style="display: none;"></div>
+ <table id="the_table"></table>
+ <tr id="the_row"></tr>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_bug883708.xhtml b/accessible/tests/mochitest/treeupdate/test_bug883708.xhtml
new file mode 100644
index 0000000000..5d9e813f3a
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_bug883708.xhtml
@@ -0,0 +1,31 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<script>
+
+function boom() {
+ var newSpan = document.createElementNS("http://www.w3.org/1999/xhtml", "span");
+ document.getElementById("c").insertBefore(newSpan, document.getElementById("d"));
+ document.getElementById("a").style.visibility = "visible";
+ ok(true, "test didn't crash or assert");
+ SimpleTest.finish();
+}
+
+</script>
+</head>
+
+<body onload="boom();">
+ <a target="_blank"
+ title="test reparenting accessible subtree when inaccessible element becomes accessible"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=883708">
+ Mozilla Bug 883708
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+<div style="visibility: collapse;" id="a"><div style="float: right; visibility: visible;"><div id="c"><td id="d"></td></div></div></div></body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_bug884251.xhtml b/accessible/tests/mochitest/treeupdate/test_bug884251.xhtml
new file mode 100644
index 0000000000..7e3cf14fac
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_bug884251.xhtml
@@ -0,0 +1,19 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<script>
+
+function boom() {
+ document.getElementById("k").removeAttribute("href");
+ ok(true, "changing iframe contents doesn't cause assertions");
+ SimpleTest.finish();
+}
+
+</script>
+</head>
+
+<body onload="boom();">
+<iframe src="data:text/html,1"><link id="k" href="data:text/html,2" /></iframe>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_bug895082.html b/accessible/tests/mochitest/treeupdate/test_bug895082.html
new file mode 100644
index 0000000000..8332c5206e
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_bug895082.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Replace body test</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+function doTest() {
+ var y = document.getElementById("y");
+ var oldBody = document.body;
+ var newBody = document.createElement("body");
+ document.documentElement.insertBefore(newBody, oldBody);
+ setTimeout(function() {
+ document.documentElement.removeChild(oldBody);
+ newBody.appendChild(y);
+ ok(true, "we didn't assert");
+ SimpleTest.finish();
+ }, 0);
+}
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=895082"
+ title="Bug 895082 - replacing body element asserts">
+ Bug 895082</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+<div><div id="y"></div></div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_canvas.html b/accessible/tests/mochitest/treeupdate/test_canvas.html
new file mode 100644
index 0000000000..229bf4f2e3
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_canvas.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Canvas subdom mutation</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function addSubtree(aID) {
+ this.node = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.node),
+ ];
+
+ this.invoke = function addSubtree_invoke() {
+ // ensure we start with no subtree
+ testAccessibleTree("canvas", { CANVAS: [] });
+ getNode("dialog").style.display = "block";
+ };
+
+ this.finalCheck = function addSubtree_finalCheck() {
+ testAccessibleTree("dialog", { DIALOG: [] });
+ };
+
+ this.getID = function addSubtree_getID() {
+ return "show canvas subdom";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+
+ function doTest() {
+ gQueue = new eventQueue();
+
+ // make the subdom come alive!
+ gQueue.push(new addSubtree("dialog"));
+
+ gQueue.invoke(); // SimpleTest.finish() will be called in the end
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Expose content in Canvas element"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=495912">
+ Mozilla Bug 495912
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <canvas id="canvas">
+ <div id="dialog" role="dialog" style="display: none;">
+ </div>
+ </canvas>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_contextmenu.xhtml b/accessible/tests/mochitest/treeupdate/test_contextmenu.xhtml
new file mode 100644
index 0000000000..f81d77332d
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_contextmenu.xhtml
@@ -0,0 +1,315 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="menu tree and events">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ function openMenu(aID, aTree)
+ {
+ this.eventSeq = [
+ new invokerChecker(EVENT_MENUPOPUP_START, getNode(aID))
+ ];
+
+ this.invoke = function openMenu_invoke()
+ {
+ var button = getNode("button");
+ getNode(aID).openPopup(button, "after_start", 0, 0, true, false);
+ }
+
+ this.finalCheck = function openMenu_finalCheck(aEvent)
+ {
+ testAccessibleTree(aID, aTree);
+ }
+
+ this.getID = function openMenu_getID()
+ {
+ return "open menu " + prettyName(aID);
+ }
+ }
+
+ function selectNextMenuItem(aID)
+ {
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, getNode(aID))
+ ];
+
+ this.invoke = function selectMenuItem_invoke()
+ {
+ synthesizeKey("KEY_ArrowDown");
+ }
+
+ this.getID = function selectMenuItem_getID()
+ {
+ return "select menuitem " + prettyName(aID);
+ }
+ }
+
+ function openSubMenu(aSubMenuID, aItemID, aMenuID, aTree)
+ {
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, getNode(aItemID)),
+ ];
+
+ this.invoke = function openSubMenu_invoke()
+ {
+ synthesizeKey("KEY_Enter");
+ }
+
+ this.finalCheck = function openSubMenu_finalCheck(aEvent)
+ {
+ testAccessibleTree(aMenuID, aTree);
+ }
+
+ this.getID = function openSubMenu_getID()
+ {
+ return "open submenu " + prettyName(aSubMenuID) + " focusing item " + prettyName(aItemID);
+ }
+ }
+
+ function closeSubMenu(aSubMenuID, aItemID)
+ {
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, getNode(aItemID)),
+ ];
+
+ this.invoke = function closeSubMenu_invoke()
+ {
+ synthesizeKey("KEY_Escape");
+ }
+
+ this.getID = function closeSubMenu_getID()
+ {
+ return "close submenu " + prettyName(aSubMenuID) + " focusing item " + prettyName(aItemID);
+ }
+ }
+
+ function closeMenu(aID)
+ {
+ this.eventSeq = [
+ new invokerChecker(EVENT_MENUPOPUP_END, getNode(aID))
+ ];
+
+ this.invoke = function closeMenu_invoke()
+ {
+ synthesizeKey("KEY_Escape");
+ }
+
+ this.getID = function closeMenu_getID()
+ {
+ return "close menu " + prettyName(aID);
+ }
+ }
+
+ //gA11yEventDumpToConsole = true;
+ //enableLogging("tree,verbose");
+
+ var gQueue = null;
+ var gContextTree = {};
+
+ // Linux and Windows menu trees discrepancy: bug 527646.
+
+ /**
+ * Return the context menu tree before submenus were open.
+ */
+ function getMenuTree1()
+ {
+ if (LINUX || SOLARIS) {
+ let tree = {
+ role: ROLE_MENUPOPUP,
+ children: [
+ {
+ name: "item0",
+ role: ROLE_MENUITEM,
+ children: []
+ },
+ {
+ name: "item1",
+ role: ROLE_MENUITEM,
+ children: []
+ },
+ {
+ name: "item2",
+ role: ROLE_PARENT_MENUITEM,
+ children: [ ]
+ }
+ ]
+ };
+ return tree;
+ }
+
+ // Windows
+ let tree = {
+ role: ROLE_MENUPOPUP,
+ children: [
+ {
+ name: "item0",
+ role: ROLE_MENUITEM,
+ children: []
+ },
+ {
+ name: "item1",
+ role: ROLE_MENUITEM,
+ children: []
+ },
+ {
+ name: "item2",
+ role: ROLE_PARENT_MENUITEM,
+ children: [
+ {
+ name: "item2",
+ role: ROLE_MENUPOPUP,
+ children: [ ]
+ }
+ ]
+ }
+ ]
+ };
+ return tree;
+ }
+
+ /**
+ * Return context menu tree when submenu was open.
+ */
+ function getMenuTree2()
+ {
+ var tree = getMenuTree1();
+ if (LINUX || SOLARIS) {
+ let submenuTree =
+ {
+ name: "item2.0",
+ role: ROLE_PARENT_MENUITEM,
+ children: [ ]
+ };
+ tree.children[2].children.push(submenuTree);
+ return tree;
+ }
+
+ // Windows
+ let submenuTree =
+ {
+ name: "item2.0",
+ role: ROLE_PARENT_MENUITEM,
+ children: [
+ {
+ name: "item2.0",
+ role: ROLE_MENUPOPUP,
+ children: [ ]
+ }
+ ]
+ };
+
+ tree.children[2].children[0].children.push(submenuTree);
+ return tree;
+ }
+
+ /**
+ * Return context menu tree when subsub menu was open.
+ */
+ function getMenuTree3()
+ {
+ var tree = getMenuTree2();
+ var subsubmenuTree =
+ {
+ name: "item2.0.0",
+ role: ROLE_MENUITEM,
+ children: []
+ };
+
+ if (LINUX || SOLARIS)
+ tree.children[2].children[0].children.push(subsubmenuTree);
+ else
+ tree.children[2].children[0].children[0].children[0].children.push(subsubmenuTree);
+
+ return tree;
+ }
+
+
+ function doTests()
+ {
+ gQueue = new eventQueue();
+
+ // Check initial empty tree
+ testAccessibleTree("context", { MENUPOPUP: [] });
+
+ // Open context menu and check that menu item accesibles are created.
+ gQueue.push(new openMenu("context", getMenuTree1()));
+
+ // Select items and check focus event on them.
+ gQueue.push(new selectNextMenuItem("item0"));
+ gQueue.push(new selectNextMenuItem("item1"));
+ gQueue.push(new selectNextMenuItem("item2"));
+
+ // Open sub menu and check menu accessible tree and focus event.
+ gQueue.push(new openSubMenu("submenu2", "item2.0",
+ "context", getMenuTree2()));
+ gQueue.push(new openSubMenu("submenu2.0", "item2.0.0",
+ "context", getMenuTree3()));
+
+ // Close submenus and check that focus goes to parent.
+ gQueue.push(new closeSubMenu("submenu2.0", "item2.0"));
+ gQueue.push(new closeSubMenu("submenu2", "item2"));
+
+ gQueue.push(new closeMenu("context"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=630194"
+ title="Update accessible tree when opening the menu popup">
+ Mozilla Bug 630194
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=630486"
+ title="Don't force accessible creation for popup children.">
+ Mozilla Bug 630486
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+
+ <menupopup id="context">
+ <menuitem id="item0" label="item0"/>
+ <menuitem id="item1" label="item1"/>
+ <menu id="item2" label="item2">
+ <menupopup id="submenu2">
+ <menu id="item2.0" label="item2.0">
+ <menupopup id="submenu2.0">
+ <menuitem id="item2.0.0" label="item2.0.0"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ </menupopup>
+
+ <button context="context" id="button">btn</button>
+ </vbox>
+ </hbox>
+</window>
diff --git a/accessible/tests/mochitest/treeupdate/test_cssoverflow.html b/accessible/tests/mochitest/treeupdate/test_cssoverflow.html
new file mode 100644
index 0000000000..6b60fce975
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_cssoverflow.html
@@ -0,0 +1,149 @@
+<html>
+
+<head>
+ <title>Testing HTML scrollable frames (css overflow style)</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+ // //////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Change scroll range to not empty size and inserts a child into container
+ * to trigger tree update of the container. Prior to bug 677154 not empty
+ * size resulted to accessible creation for scroll area, container tree
+ * update picked up that accessible unattaching scroll area accessible
+ * subtree.
+ */
+ function changeScrollRange(aContainerID, aScrollAreaID) {
+ this.containerNode = getNode(aContainerID);
+ this.container = getAccessible(this.containerNode);
+ this.scrollAreaNode = getNode(aScrollAreaID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.container),
+ ];
+
+ this.invoke = function changeScrollRange_invoke() {
+ this.scrollAreaNode.style.width = "20px";
+ this.containerNode.appendChild(document.createElement("input"));
+ };
+
+ this.finalCheck = function changeScrollRange_finalCheck() {
+ var accTree =
+ { SECTION: [ // container
+ { SECTION: [ // scroll area
+ { ENTRY: [] }, // child content
+ ] },
+ { ENTRY: [] }, // inserted input
+ ] };
+ testAccessibleTree(this.container, accTree);
+ };
+
+ this.getID = function changeScrollRange_getID() {
+ return "change scroll range for " + prettyName(aScrollAreaID);
+ };
+ }
+
+ /**
+ * Change scrollbar styles from visible to auto to make the scroll area focusable.
+ * That causes us to create an accessible for it.
+ * Make sure the tree stays intact.
+ * The scroll area has no ID on purpose to make it inaccessible initially.
+ */
+ function makeFocusableByScrollbarStyles(aContainerID) {
+ this.container = getAccessible(aContainerID);
+ this.scrollAreaNode = getNode(aContainerID).firstChild;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, getAccessible, this.scrollAreaNode),
+ new invokerChecker(EVENT_REORDER, this.container),
+ ];
+
+ this.invoke = function makeFocusableByScrollbarStyles_invoke() {
+ var accTree =
+ { SECTION: [ // container
+ { PARAGRAPH: [ // paragraph
+ { TEXT_LEAF: [] },
+ ] },
+ ] };
+ testAccessibleTree(this.container, accTree);
+
+ this.scrollAreaNode.style.overflow = "auto";
+ };
+
+ this.finalCheck = function makeFocusableByScrollbarStyles_finalCheck() {
+ var accTree =
+ { SECTION: [ // container
+ { role: ROLE_SECTION, // focusable scroll area
+ states: STATE_FOCUSABLE,
+ children: [
+ { PARAGRAPH: [ // paragraph
+ { TEXT_LEAF: [] }, // text leaf
+ ] },
+ ],
+ }, // focusable scroll area
+ ] };
+ testAccessibleTree(this.container, accTree);
+ };
+
+ this.getID = function makeFocusableByScrollbarStyles_getID() {
+ return "make div focusable through scrollbar styles "
+ + prettyName(aContainerID);
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+ // //////////////////////////////////////////////////////////////////////////
+
+ var gQueue = null;
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new changeScrollRange("container", "scrollarea"));
+ gQueue.push(new makeFocusableByScrollbarStyles("container2"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=677154"
+ title="Detached document accessibility tree">
+ Mozilla Bug 677154</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="eventdump"></div>
+
+ <div id="container"><div id="scrollarea" style="overflow:auto;"><input></div></div>
+ <div id="container2"><div style="height: 1px;"><p>foo</p></div></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_deck.xhtml b/accessible/tests/mochitest/treeupdate/test_deck.xhtml
new file mode 100644
index 0000000000..979996a66c
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_deck.xhtml
@@ -0,0 +1,154 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Tree update on XUL deck panel switching">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function switchDeckPanel(aContainerID, aDeckID)
+ {
+ this.panelIndex = 0;
+
+ this.container = getAccessible(aContainerID);
+ this.deckNode = getNode(aDeckID);
+ this.prevPanel = getAccessible(this.deckNode.selectedPanel);
+ this.panelNode = this.deckNode.childNodes[this.panelIndex];
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.prevPanel),
+ new invokerChecker(EVENT_SHOW, this.panelNode),
+ new invokerChecker(EVENT_REORDER, this.container)
+ ];
+
+ this.invoke = function switchDeckPanel_invoke()
+ {
+ var tree =
+ { GROUPING: [ // role="group"
+ { GROUPING: [ // groupbox, a selected panel #2
+ { PUSHBUTTON: [ ] } // button
+ ] }
+ ] };
+ testAccessibleTree(this.container, tree);
+
+ this.deckNode.selectedIndex = this.panelIndex;
+ }
+
+ this.finalCheck = function switchDeckPanel_finalCheck()
+ {
+ var tree =
+ { GROUPING: [ // role="group"
+ { LABEL: [ // description, a selected panel #1
+ { TEXT_LEAF: [] } // text leaf, a description value
+ ] }
+ ] };
+ testAccessibleTree(this.container, tree);
+ }
+
+ this.getID = function switchDeckPanel_getID()
+ {
+ return "switch deck panel";
+ }
+ }
+
+ function showDeckPanel(aContainerID, aPanelID)
+ {
+ this.container = getAccessible(aContainerID);
+ this.deckNode = getNode(aPanelID);
+ var tree =
+ { GROUPING: [ // role="group"
+ { GROUPING: [ // grouping of panel 2
+ { PUSHBUTTON: [] } // push button in panel 2
+ ] }
+ ] };
+
+
+ this.unexpectedEventSeq = [
+ new invokerChecker(EVENT_REORDER, this.container)
+ ];
+
+ this.invoke = function showDeckPanel_invoke()
+ {
+ // This stops the refreh driver from doing its regular ticks, and leaves
+ // us in control. 100 is an arbitrary positive number to advance the clock
+ // it is not checked or used anywhere.
+ window.windowUtils.advanceTimeAndRefresh(100);
+
+ testAccessibleTree(this.container, tree);
+ this.deckNode.style.display = "-moz-box";
+
+ // This flushes our DOM mutations and forces any pending mutation events.
+ window.windowUtils.advanceTimeAndRefresh(100);
+ }
+
+ this.finalCheck = function showDeckPanel_finalCheck()
+ {
+ testAccessibleTree(this.container, tree);
+
+ // Return to regular refresh driver ticks.
+ window.windowUtils.restoreNormalRefresh();
+ }
+
+ this.getID = function showDeckPanel_getID()
+ {
+ return "show deck panel";
+ }
+ }
+
+ var gQueue = null;
+ function doTest()
+ {
+ gQueue = new eventQueue();
+ gQueue.push(new showDeckPanel("container", "hidden"));
+ gQueue.push(new switchDeckPanel("container", "deck"));
+ gQueue.invoke(); // will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=814836"
+ title=" xul:deck element messes up screen reader">
+ Mozilla Bug 814836
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1" id="container" role="group">
+
+ <deck id="deck" selectedIndex="1">
+ <description>This is the first page</description>
+ <groupbox>
+ <button label="This is the second page"/>
+ </groupbox>
+ <hbox id="hidden" style="display: none;"><label>This is the third page</label></hbox>
+ </deck>
+
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/treeupdate/test_delayed_removal.html b/accessible/tests/mochitest/treeupdate/test_delayed_removal.html
new file mode 100644
index 0000000000..3f421f0c5b
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_delayed_removal.html
@@ -0,0 +1,500 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Test accessible delayed removal</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <style>
+ .gentext:before {
+ content: "START"
+ }
+ .gentext:after {
+ content: "END"
+ }
+ </style>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+
+ async function hideDivFromInsideSpan() {
+ let msg = "hideDivFromInsideSpan";
+ info(msg);
+ let events = waitForOrderedEvents([
+ [EVENT_HIDE, "div1"], [EVENT_TEXT_REMOVED, "span1"],
+ [EVENT_REORDER, "span1"]
+ ], msg);
+ document.body.offsetTop; // Flush layout.
+ getNode("div1").style.display = "none";
+ await events;
+
+ testAccessibleTree("c1", { SECTION: [ { REGION: [] }, ] });
+ }
+
+ async function showDivFromInsideSpan() {
+ let msg = "showDivFromInsideSpan";
+ info(msg);
+ let events = waitForOrderedEvents(
+ [[EVENT_SHOW, "div2"], [EVENT_REORDER, "span2"]], msg);
+ document.body.offsetTop; // Flush layout.
+ getNode("div2").style.display = "block";
+ await events;
+
+ testAccessibleTree("c2",
+ { SECTION: [ { REGION: [{ SECTION: [ { TEXT_LEAF: [] } ] }] }, ] });
+ }
+
+ async function removeDivFromInsideSpan() {
+ let msg = "removeDivFromInsideSpan";
+ info(msg);
+ let events = waitForOrderedEvents([
+ [EVENT_HIDE, getNode("div3")], [EVENT_TEXT_REMOVED, "span3"],
+ [EVENT_REORDER, "span3"]
+ ], msg);
+ document.body.offsetTop; // Flush layout.
+ getNode("div3").remove();
+ await events;
+
+ testAccessibleTree("c3", { SECTION: [ { REGION: [] }, ] });
+ }
+
+ // Test to see that generated content is inserted
+ async function addCSSGeneratedContent() {
+ let msg = "addCSSGeneratedContent";
+ let c4_child = getAccessible("c4_child");
+ info(msg);
+ let events = waitForOrderedEvents([
+ [EVENT_SHOW, evt => evt.accessible == c4_child.firstChild],
+ [EVENT_SHOW, evt => evt.accessible == c4_child.lastChild],
+ [EVENT_REORDER, c4_child]], msg);
+ document.body.offsetTop; // Flush layout.
+ getNode("c4_child").classList.add('gentext');
+ await events;
+
+ testAccessibleTree("c4", { SECTION: [ // container
+ { SECTION: [ // inserted node
+ { STATICTEXT: [] }, // :before
+ { TEXT_LEAF: [] }, // primary text
+ { STATICTEXT: [] }, // :after
+ ] },
+ ] });
+ }
+
+ // Test to see that generated content gets removed
+ async function removeCSSGeneratedContent() {
+ let msg = "removeCSSGeneratedContent";
+ let c5_child = getAccessible("c5_child");
+ info(msg);
+ let events = waitForEvents([
+ [EVENT_HIDE, c5_child.firstChild],
+ [EVENT_HIDE, c5_child.lastChild],
+ [EVENT_REORDER, c5_child]], msg);
+ document.body.offsetTop; // Flush layout.
+ getNode("c5_child").classList.remove('gentext');
+ await events;
+
+ testAccessibleTree("c5",{ SECTION: [ // container
+ { SECTION: [ // inserted node
+ { TEXT_LEAF: [] }, // primary text
+ ] },
+ ] });
+ }
+
+ // Test to see that a non-accessible intermediate container gets its accessible
+ // descendants removed and inserted correctly.
+ async function intermediateNonAccessibleContainers() {
+ let msg = "intermediateNonAccessibleContainers";
+ info(msg);
+
+ testAccessibleTree("c6",{ SECTION: [
+ { SECTION: [
+ { role: ROLE_PUSHBUTTON, name: "Hello" },
+ ] },
+ ] });
+
+ let events = waitForOrderedEvents(
+ [[EVENT_HIDE, "b1"], [EVENT_SHOW, "b2"], [EVENT_REORDER, "scrollarea"]], msg);
+ document.body.offsetTop; // Flush layout.
+ getNode("scrollarea").style.overflow = "auto";
+ document.querySelector("#scrollarea > div > div:first-child").style.display = "none";
+ document.querySelector("#scrollarea > div > div:last-child").style.display = "block";
+ await events;
+
+ testAccessibleTree("c6",{ SECTION: [
+ { SECTION: [
+ { role: ROLE_PUSHBUTTON, name: "Goodbye" },
+ ] },
+ ] });
+ }
+
+ // Test to see that the button gets reparented into the new accessible container.
+ async function intermediateNonAccessibleContainerBecomesAccessible() {
+ let msg = "intermediateNonAccessibleContainerBecomesAccessible";
+ info(msg);
+
+ testAccessibleTree("c7",{ SECTION: [
+ { role: ROLE_PUSHBUTTON, name: "Hello" },
+ { TEXT_LEAF: [] }
+ ] });
+
+ let events = waitForOrderedEvents(
+ [[EVENT_HIDE, "b3"],
+ // b3 show event coalesced into its new container
+ [EVENT_SHOW, evt => evt.DOMNode.classList.contains('intermediate')],
+ [EVENT_REORDER, "c7"]], msg);
+ document.body.offsetTop; // Flush layout.
+ document.querySelector("#c7 > div").style.display = "block";
+ await events;
+
+ testAccessibleTree("c7",{ SECTION: [
+ { SECTION: [ { role: ROLE_PUSHBUTTON, name: "Hello" } ] }
+ ] });
+ }
+
+ // Test to ensure that relocated accessibles are removed when a DOM
+ // ancestor is hidden.
+ async function removeRelocatedWhenDomAncestorHidden() {
+ info("removeRelocatedWhenDomAncestorHidden");
+
+ testAccessibleTree("c8",{ SECTION: [
+ { EDITCOMBOBOX: [ // c8_owner
+ { COMBOBOX_LIST: [] }, // c8_owned
+ ]},
+ { SECTION: [] }, // c8_owned_container
+ ] });
+
+ let events = waitForOrderedEvents([
+ [EVENT_HIDE, "c8_owned_container"],
+ [EVENT_HIDE, "c8_owned"],
+ [EVENT_REORDER, "c8"],
+ ], "removeRelocatedWhenDomAncestorHidden");
+ document.body.offsetTop; // Flush layout.
+ getNode("c8_owned_container").hidden = true;
+ await events;
+
+ testAccessibleTree("c8",{ SECTION: [
+ { EDITCOMBOBOX: [] }, // c8_owner
+ ] });
+ }
+
+ // Bug 1572829
+ async function removeShadowRootHost() {
+ info("removeShadowRootHost");
+ document.body.offsetTop; // Flush layout.
+
+ let event = waitForEvent(EVENT_REORDER, "c9", "removeShadowRootHost");
+ getNode("c9").firstElementChild.attachShadow({mode: "open"});
+ getNode("c9").firstElementChild.replaceWith("");
+
+ await event;
+ }
+
+ function listItemReframe() {
+ testAccessibleTree("li",{ LISTITEM: [
+ { LISTITEM_MARKER: [] },
+ { TEXT_LEAF: [] },
+ ] });
+
+ getNode("li").style.listStylePosition = "inside";
+ document.body.offsetTop; // Flush layout.
+ window.windowUtils.advanceTimeAndRefresh(100);
+
+ testAccessibleTree("li",{ LISTITEM: [
+ { LISTITEM_MARKER: [] },
+ { TEXT_LEAF: [] },
+ ] });
+
+ window.windowUtils.restoreNormalRefresh();
+ }
+
+ // Check to see that a reframed body gets its children pruned correctly.
+ async function bodyReframe(argument) {
+ // Load sub-document in iframe.
+ let event = waitForEvent(EVENT_REORDER, "iframe", "bodyReframe");
+ getNode("iframe").src =
+ `data:text/html,<div>Hello</div><div style="display: none">World</div>`;
+ await event;
+
+ // Initial tree should have one section leaf.
+ testAccessibleTree("c10",{ SECTION: [
+ { INTERNAL_FRAME: [
+ { DOCUMENT: [
+ { SECTION: [
+ { role: ROLE_TEXT_LEAF, name: "Hello" }
+ ] }
+ ]}
+ ] }
+ ] });
+
+
+ let iframeDoc = getNode("iframe").contentWindow.document;
+
+ // Trigger coalesced reframing. Both the body node and its children
+ // will need reframing.
+ event = waitForEvent(EVENT_REORDER, iframeDoc, "bodyReframe");
+ iframeDoc.body.style.display = "inline-block";
+ iframeDoc.querySelector("div:first-child").style.display = "none";
+ iframeDoc.querySelector("div:last-child").style.display = "block";
+
+ await event;
+
+ // Only the second section should be showing
+ testAccessibleTree("c10",{ SECTION: [
+ { INTERNAL_FRAME: [
+ { DOCUMENT: [
+ { SECTION: [
+ { role: ROLE_TEXT_LEAF, name: "World" }
+ ] }
+ ]}
+ ] }
+ ] });
+ }
+
+ // Ensure that embed elements recreate their Accessible if they started
+ // without an src and then an src is set later.
+ async function embedBecomesOuterDoc() {
+ let msg = "embedBecomesOuterDoc";
+ info(msg);
+
+ testAccessibleTree("c12", { SECTION: [
+ { TEXT: [] }
+ ] });
+
+ let events = waitForOrderedEvents([
+ [EVENT_HIDE, "embed"],
+ [EVENT_SHOW, "embed"],
+ [EVENT_REORDER, "c12"],
+ ], msg);
+ getNode("embed").src = "data:text/html,";
+ await events;
+
+ testAccessibleTree("c12", { SECTION: [
+ { INTERNAL_FRAME: [
+ { DOCUMENT: [] }
+ ] }
+ ] });
+ }
+
+ // Test that we get a text removed event when removing generated content from a button
+ async function testCSSGeneratedContentRemovedFromButton() {
+ let msg = "testCSSGeneratedContentRemovedFromButton";
+ info(msg);
+
+ testAccessibleTree("c13", { SECTION: [
+ { role: ROLE_PUSHBUTTON, name: "beforego",
+ children: [{ STATICTEXT: [] }, { TEXT_LEAF: [] }] }
+ ] });
+
+ let events = waitForOrderedEvents([
+ [EVENT_HIDE, evt => evt.accessible.name == "before"],
+ [EVENT_TEXT_REMOVED, evt => evt.accessible.role == ROLE_PUSHBUTTON],
+ [EVENT_SHOW, evt => evt.DOMNode.tagName == "HR"],
+ [EVENT_REORDER, "c13"],
+ ], msg);
+ getNode("b13").click();
+ await events;
+
+ testAccessibleTree("c13", { SECTION: [
+ { role: ROLE_PUSHBUTTON, name: "go",
+ children: [{ TEXT_LEAF: [] }] },
+ { SEPARATOR: [] }
+ ] });
+ }
+
+ // Slack seems to often restyle containers and change children
+ // simultaneously, this results in an insertion queue filled with
+ // redundant insertions and unparented nodes.
+ // This test duplicates some of this.
+ async function testSlack() {
+ let msg = "testSlack";
+ info(msg);
+
+ window.windowUtils.advanceTimeAndRefresh(100);
+ let event = waitForEvent(EVENT_REORDER, "c14", "testSlack");
+
+ let keyContainer = document.querySelector("#c14 .intermediate");
+ keyContainer.style.display = "inline-block";
+ document.body.offsetTop; // Flush layout.
+
+ let one = document.querySelector("#c14 [aria-label='one']");
+ let three = document.querySelector("#c14 [aria-label='three']");
+ one.remove();
+ three.remove();
+ // insert one first
+ keyContainer.firstChild.before(one.cloneNode());
+ // insert three last
+ keyContainer.lastChild.after(three.cloneNode());
+
+ keyContainer.style.display = "flex";
+ document.body.offsetTop; // Flush layout.
+
+ window.windowUtils.restoreNormalRefresh();
+
+ await event;
+
+ is(getAccessible("c14").name, "one two three", "subtree has correct order");
+ }
+
+ // Ensure that a node is removed when visibility: hidden is set but the
+ // layout frame is reconstructed; e.g. because of position: fixed. Also
+ // ensure that visible children aren't clobbered.
+ async function visibilityHiddenWithReframe() {
+ let msg = "visibilityHiddenWithReframe";
+ info(msg);
+
+ testAccessibleTree("c15", { SECTION: [ // c15
+ { SECTION: [ // c15_inner
+ { TEXT_LEAF: [] }, // Text
+ { PARAGRAPH: [
+ { TEXT_LEAF: [] } // Para
+ ] },
+ { HEADING: [ // c15_visible
+ { TEXT_LEAF: [] } // Visible
+ ] }, // c15_visible
+ ] } // c15_inner
+ ] });
+
+ let events = waitForOrderedEvents([
+ [EVENT_HIDE, "c15_inner"],
+ [EVENT_SHOW, "c15_visible"],
+ [EVENT_REORDER, "c15"],
+ ], msg);
+ getNode("c15_inner").style = "visibility: hidden; position: fixed;";
+ await events;
+
+ testAccessibleTree("c15", { SECTION: [ // c15
+ { HEADING: [ // c15_visible
+ { TEXT_LEAF: [] } // Visible
+ ] }, // c15_visible
+ ] });
+ }
+
+ async function doTest() {
+ await hideDivFromInsideSpan();
+
+ await showDivFromInsideSpan();
+
+ await removeDivFromInsideSpan();
+
+ await addCSSGeneratedContent();
+
+ await removeCSSGeneratedContent();
+
+ await intermediateNonAccessibleContainers();
+
+ await intermediateNonAccessibleContainerBecomesAccessible();
+
+ await removeRelocatedWhenDomAncestorHidden();
+
+ await removeShadowRootHost();
+
+ listItemReframe();
+
+ await bodyReframe();
+
+ await embedBecomesOuterDoc();
+
+ await testCSSGeneratedContentRemovedFromButton();
+
+ await testSlack();
+
+ await visibilityHiddenWithReframe();
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="c1">
+ <span role="region" id="span1" aria-label="region"><div id="div1">hello</div></span>
+ </div>
+
+ <div id="c2">
+ <span role="region" id="span2" aria-label="region"><div id="div2" style="display: none">hello</div></span>
+ </div>
+
+ <div id="c3">
+ <span role="region" id="span3" aria-label="region"><div id="div3">hello</div></span>
+ </div>
+
+ <div id="c4"><div id="c4_child">text</div></div>
+
+ <div id="c5"><div id="c5_child" class="gentext">text</div></div>
+
+ <div id="c6">
+ <div id="scrollarea" style="overflow:hidden;">
+ <div><div role="none"><button id="b1">Hello</button></div><div role="none" style="display: none"><button id="b2">Goodbye</button></div></div>
+ </div>
+ </div>
+
+ <div id="c7">
+ <div style="display: inline;" class="intermediate">
+ <button id="b3">Hello</button>
+ </div>
+ </div>
+
+ <div id="c8">
+ <div id="c8_owner" role="combobox" aria-owns="c8_owned"></div>
+ <div id="c8_owned_container">
+ <div id="c8_owned" role="listbox"></div>
+ </div>
+ </div>
+
+ <div id="c9">
+ <div><dir>a</dir></div>
+ </div>
+
+ <div id="c11">
+ <ul>
+ <li id="li">Test</li>
+ </ul>
+ </div>
+
+ <div id="c12"><embed id="embed"></embed></div>
+
+ <div id="c10">
+ <iframe id="iframe"></iframe>
+ </div>
+
+ <div id="c13">
+ <style>
+ .before::before { content: 'before' }
+ </style>
+ <button id="b13" class="before" onclick="this.className = ''; this.insertAdjacentElement('afterend', document.createElement('hr'))">go</button>
+ </div>
+
+ <div role="heading" id="c14" data-qa="virtual-list-item">
+ <div class="intermediate">
+ <div role="img" aria-label="one"></div> two <div role="img"
+ aria-label="three"></div>
+ </div>
+ </div>
+
+ <div id="c15"><div id="c15_inner">
+ Text
+ <p>Para</p>
+ <h1 id="c15_visible" style="visibility: visible;">Visible</h1>
+ </div></div>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_doc.html b/accessible/tests/mochitest/treeupdate/test_doc.html
new file mode 100644
index 0000000000..6bb2863df4
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_doc.html
@@ -0,0 +1,415 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Test document root content mutations</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../states.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Helpers
+
+ function getDocNode(aID) {
+ return getNode(aID).contentDocument;
+ }
+ function getDocChildNode(aID) {
+ return getDocNode(aID).body.firstChild;
+ }
+
+ function rootContentReplaced(aID, aTextName, aRootContentRole) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, getDocChildNode, aID),
+ new invokerChecker(EVENT_REORDER, getDocNode, aID),
+ ];
+
+ this.finalCheck = function rootContentReplaced_finalCheck() {
+ var tree = {
+ role: aRootContentRole || ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: aTextName,
+ },
+ ],
+ };
+ testAccessibleTree(getDocNode(aID), tree);
+ };
+ }
+
+ function rootContentRemoved(aID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, null),
+ new invokerChecker(EVENT_REORDER, getDocNode, aID),
+ ];
+
+ this.preinvoke = function rootContentRemoved_preinvoke() {
+ // Set up target for hide event before we invoke.
+ var text = getAccessible(getAccessible(getDocNode(aID)).firstChild);
+ this.eventSeq[0].target = text;
+ };
+
+ this.finalCheck = function rootContentRemoved_finalCheck() {
+ var tree = {
+ role: ROLE_DOCUMENT,
+ children: [ ],
+ };
+ testAccessibleTree(getDocNode(aID), tree);
+ };
+ }
+
+ function rootContentInserted(aID, aTextName) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, getDocChildNode, aID),
+ new invokerChecker(EVENT_REORDER, getDocNode, aID),
+ ];
+
+ this.finalCheck = function rootContentInserted_finalCheck() {
+ var tree = {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: aTextName,
+ },
+ ],
+ };
+ testAccessibleTree(getDocNode(aID), tree);
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function writeIFrameDoc(aID) {
+ this.__proto__ = new rootContentReplaced(aID, "hello");
+
+ this.invoke = function writeIFrameDoc_invoke() {
+ var docNode = getDocNode(aID);
+
+ // We can't use open/write/close outside of iframe document because of
+ // security error.
+ var script = docNode.createElement("script");
+ script.textContent = "document.open(); document.write('hello'); document.close();";
+ docNode.body.appendChild(script);
+ };
+
+ this.getID = function writeIFrameDoc_getID() {
+ return "write document";
+ };
+ }
+
+ /**
+ * Replace HTML element.
+ */
+ function replaceIFrameHTMLElm(aID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, getDocChildNode, aID),
+ new invokerChecker(EVENT_REORDER, getDocNode, aID),
+ ];
+
+ this.invoke = function replaceIFrameHTMLElm_invoke() {
+ var docNode = getDocNode(aID);
+ var newHTMLNode = docNode.createElement("html");
+ newHTMLNode.innerHTML = `<body><p>New Wave</p></body`;
+ docNode.replaceChild(newHTMLNode, docNode.documentElement);
+ };
+
+ this.finalCheck = function replaceIFrameHTMLElm_finalCheck() {
+ var tree = {
+ role: ROLE_DOCUMENT,
+ children: [
+ {
+ role: ROLE_PARAGRAPH,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "New Wave",
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(getDocNode(aID), tree);
+ };
+
+ this.getID = function replaceIFrameHTMLElm_getID() {
+ return "replace HTML element";
+ };
+ }
+
+ /**
+ * Replace HTML body on new body having ARIA role.
+ */
+ function replaceIFrameBody(aID) {
+ this.__proto__ = new rootContentReplaced(aID, "New Hello");
+
+ this.invoke = function replaceIFrameBody_invoke() {
+ var docNode = getDocNode(aID);
+ var newBodyNode = docNode.createElement("body");
+ var newTextNode = docNode.createTextNode("New Hello");
+ newBodyNode.appendChild(newTextNode);
+ docNode.documentElement.replaceChild(newBodyNode, docNode.body);
+ };
+
+ this.getID = function replaceIFrameBody_getID() {
+ return "replace body";
+ };
+ }
+
+ /**
+ * Replace HTML body on new body having ARIA role.
+ */
+ function replaceIFrameBodyOnARIARoleBody(aID) {
+ this.__proto__ = new rootContentReplaced(aID, "New Hello",
+ ROLE_APPLICATION);
+
+ this.invoke = function replaceIFrameBodyOnARIARoleBody_invoke() {
+ var docNode = getDocNode(aID);
+ var newBodyNode = docNode.createElement("body");
+ var newTextNode = docNode.createTextNode("New Hello");
+ newBodyNode.appendChild(newTextNode);
+ newBodyNode.setAttribute("role", "application");
+ docNode.documentElement.replaceChild(newBodyNode, docNode.body);
+ };
+
+ this.getID = function replaceIFrameBodyOnARIARoleBody_getID() {
+ return "replace body on body having ARIA role";
+ };
+ }
+
+ /**
+ * Open/close document pair.
+ */
+ function openIFrameDoc(aID) {
+ this.__proto__ = new rootContentRemoved(aID);
+
+ this.invoke = function openIFrameDoc_invoke() {
+ this.preinvoke();
+
+ // Open document.
+ var docNode = getDocNode(aID);
+ var script = docNode.createElement("script");
+ script.textContent = "function closeMe() { document.write('Works?'); document.close(); } window.closeMe = closeMe; document.open();";
+ docNode.body.appendChild(script);
+ };
+
+ this.getID = function openIFrameDoc_getID() {
+ return "open document";
+ };
+ }
+
+ function closeIFrameDoc(aID) {
+ this.__proto__ = new rootContentInserted(aID, "Works?");
+
+ this.invoke = function closeIFrameDoc_invoke() {
+ // Write and close document.
+ getDocNode(aID).write("Works?"); getDocNode(aID).close();
+ };
+
+ this.getID = function closeIFrameDoc_getID() {
+ return "close document";
+ };
+ }
+
+ /**
+ * Remove/insert HTML element pair.
+ */
+ function removeHTMLFromIFrameDoc(aID) {
+ this.__proto__ = new rootContentRemoved(aID);
+
+ this.invoke = function removeHTMLFromIFrameDoc_invoke() {
+ this.preinvoke();
+
+ // Remove HTML element.
+ var docNode = getDocNode(aID);
+ docNode.firstChild.remove();
+ };
+
+ this.getID = function removeHTMLFromIFrameDoc_getID() {
+ return "remove HTML element";
+ };
+ }
+
+ function insertHTMLToIFrameDoc(aID) {
+ this.__proto__ = new rootContentInserted(aID, "Haha");
+
+ this.invoke = function insertHTMLToIFrameDoc_invoke() {
+ // Insert HTML element.
+ var docNode = getDocNode(aID);
+ var html = docNode.createElement("html");
+ var body = docNode.createElement("body");
+ var text = docNode.createTextNode("Haha");
+ body.appendChild(text);
+ html.appendChild(body);
+ docNode.appendChild(html);
+ };
+
+ this.getID = function insertHTMLToIFrameDoc_getID() {
+ return "insert HTML element document";
+ };
+ }
+
+ /**
+ * Remove/insert HTML body pair.
+ */
+ function removeBodyFromIFrameDoc(aID) {
+ this.__proto__ = new rootContentRemoved(aID);
+
+ this.invoke = function removeBodyFromIFrameDoc_invoke() {
+ this.preinvoke();
+
+ // Remove body element.
+ var docNode = getDocNode(aID);
+ docNode.documentElement.removeChild(docNode.body);
+ };
+
+ this.getID = function removeBodyFromIFrameDoc_getID() {
+ return "remove body element";
+ };
+ }
+
+ function insertElmUnderDocElmWhileBodyMissed(aID) {
+ this.docNode = null;
+ this.inputNode = null;
+
+ function getInputNode() { return this.inputNode; }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, getInputNode.bind(this)),
+ new invokerChecker(EVENT_REORDER, getDocNode, aID),
+ ];
+
+ this.invoke = function invoke() {
+ this.docNode = getDocNode(aID);
+ this.inputNode = this.docNode.createElement("input");
+ this.docNode.documentElement.appendChild(this.inputNode);
+ };
+
+ this.finalCheck = function finalCheck() {
+ var tree =
+ { DOCUMENT: [
+ { ENTRY: [ ] },
+ ] };
+ testAccessibleTree(this.docNode, tree);
+
+ // Remove aftermath of this test before next test starts.
+ this.docNode.documentElement.removeChild(this.inputNode);
+ };
+
+ this.getID = function getID() {
+ return "Insert element under document element while body is missed.";
+ };
+ }
+
+ function insertBodyToIFrameDoc(aID) {
+ this.__proto__ = new rootContentInserted(aID, "Yo ho ho i butylka roma!");
+
+ this.invoke = function insertBodyToIFrameDoc_invoke() {
+ // Insert body element.
+ var docNode = getDocNode(aID);
+ var body = docNode.createElement("body");
+ var text = docNode.createTextNode("Yo ho ho i butylka roma!");
+ body.appendChild(text);
+ docNode.documentElement.appendChild(body);
+ };
+
+ this.getID = function insertBodyToIFrameDoc_getID() {
+ return "insert body element";
+ };
+ }
+
+ function changeSrc(aID) {
+ this.containerNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.invoke = function changeSrc_invoke() {
+ this.containerNode.src = "data:text/html,<html><input></html>";
+ };
+
+ this.finalCheck = function changeSrc_finalCheck() {
+ var tree =
+ { INTERNAL_FRAME: [
+ { DOCUMENT: [
+ { ENTRY: [ ] },
+ ] },
+ ] };
+ testAccessibleTree(this.containerNode, tree);
+ };
+
+ this.getID = function changeSrc_getID() {
+ return "change src on iframe";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpToConsole = true;
+ // enableLogging('tree,verbose');
+
+ var gQueue = null;
+
+ function doTest() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new writeIFrameDoc("iframe"));
+ gQueue.push(new replaceIFrameHTMLElm("iframe"));
+ gQueue.push(new replaceIFrameBody("iframe"));
+ gQueue.push(new openIFrameDoc("iframe"));
+ gQueue.push(new closeIFrameDoc("iframe"));
+ gQueue.push(new removeHTMLFromIFrameDoc("iframe"));
+ gQueue.push(new insertHTMLToIFrameDoc("iframe"));
+ gQueue.push(new removeBodyFromIFrameDoc("iframe"));
+ gQueue.push(new insertElmUnderDocElmWhileBodyMissed("iframe"));
+ gQueue.push(new insertBodyToIFrameDoc("iframe"));
+ gQueue.push(new changeSrc("iframe"));
+ gQueue.push(new replaceIFrameBodyOnARIARoleBody("iframe"));
+
+ gQueue.invoke(); // SimpleTest.finish() will be called in the end
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Update accessible tree when root element is changed"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=606082">Mozilla Bug 606082</a>
+ <a target="_blank"
+ title="Elements inserted outside the body aren't accessible"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=608887">Mozilla Bug 608887</a>
+ <a target="_blank"
+ title="Reorder event for document must be fired after document initial tree creation"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=669263">Mozilla Bug 669263</a>
+ <a target="_blank"
+ title="Changing the HTML body doesn't pick up ARIA role"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=818407">Mozilla Bug 818407</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <iframe id="iframe"></iframe>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_gencontent.html b/accessible/tests/mochitest/treeupdate/test_gencontent.html
new file mode 100644
index 0000000000..9a0c107133
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_gencontent.html
@@ -0,0 +1,187 @@
+<html>
+
+<head>
+ <title>Elements with CSS generated content</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <style>
+ .gentext:before {
+ content: "START"
+ }
+ .gentext:after {
+ content: "END"
+ }
+ </style>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+ // //////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Insert new node with CSS generated content style applied to container.
+ */
+ function insertNodeHavingGenContent(aContainerID) {
+ this.containerNode = getNode(aContainerID);
+ this.container = getAccessible(this.containerNode);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, getFirstChild, this.container),
+ new invokerChecker(EVENT_REORDER, this.container),
+ ];
+
+ this.invoke = function insertNodeHavingGenContent_invoke() {
+ var node = document.createElement("div");
+ node.textContent = "text";
+ node.setAttribute("class", "gentext");
+ this.containerNode.appendChild(node);
+ };
+
+ this.finalCheck = function insertNodeHavingGenContent_finalCheck() {
+ var accTree =
+ { SECTION: [ // container
+ { SECTION: [ // inserted node
+ { STATICTEXT: [] }, // :before
+ { TEXT_LEAF: [] }, // primary text
+ { STATICTEXT: [] }, // :after
+ ] },
+ ] };
+ testAccessibleTree(this.container, accTree);
+ };
+
+ this.getID = function insertNodeHavingGenContent_getID() {
+ return "insert node having generated content to " + prettyName(aContainerID);
+ };
+ }
+
+ /**
+ * Add CSS generated content to the given node contained by container node.
+ */
+ function addGenContent(aContainerID, aNodeID) {
+ this.container = getAccessible(aContainerID);
+ this.nodeAcc = getAccessible(aNodeID);
+ this.node = getNode(aNodeID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, getFirstChild, this.nodeAcc),
+ new invokerChecker(EVENT_SHOW, getLastChild, this.nodeAcc),
+ new invokerChecker(EVENT_REORDER, this.nodeAcc),
+ ];
+
+ this.invoke = function addGenContent_invoke() {
+ this.node.classList.add("gentext");
+ };
+
+ this.finalCheck = function insertNodeHavingGenContent_finalCheck() {
+ var accTree =
+ { SECTION: [ // container
+ { SECTION: [ // inserted node
+ { STATICTEXT: [] }, // :before
+ { TEXT_LEAF: [] }, // primary text
+ { STATICTEXT: [] }, // :after
+ ] },
+ ] };
+ testAccessibleTree(this.container, accTree);
+ };
+
+ this.getID = function addGenContent_getID() {
+ return "add generated content to" + prettyName(aNodeID);
+ };
+ }
+
+ /**
+ * Remove CSS generated content from the given node contained by container node.
+ */
+ function removeGenContent(aContainerID, aNodeID) {
+ this.container = getAccessible(aContainerID);
+ this.nodeAcc = getAccessible(aNodeID);
+ this.node = getNode(aNodeID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.nodeAcc.lastChild),
+ new invokerChecker(EVENT_HIDE, this.nodeAcc.firstChild),
+ new invokerChecker(EVENT_REORDER, this.nodeAcc),
+ ];
+
+ this.invoke = function removeGenContent_invoke() {
+ this.node.classList.remove("gentext");
+ };
+
+ this.finalCheck = function removeGenContent_finalCheck() {
+ var accTree =
+ { SECTION: [ // container
+ { SECTION: [ // inserted node
+ { TEXT_LEAF: [] }, // primary text
+ ] },
+ ] };
+ testAccessibleTree(this.container, accTree);
+ };
+
+ this.getID = function addGenContent_getID() {
+ return "remove generated content from" + prettyName(aNodeID);
+ };
+ }
+ /**
+ * Target getters.
+ */
+ function getFirstChild(aAcc) {
+ try { return aAcc.firstChild; } catch (e) { return null; }
+ }
+
+ function getLastChild(aAcc) {
+ try { return aAcc.lastChild; } catch (e) { return null; }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+ // //////////////////////////////////////////////////////////////////////////
+
+ var gQueue = null;
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new insertNodeHavingGenContent("container1"));
+ gQueue.push(new addGenContent("container2", "container2_child"));
+ gQueue.push(new removeGenContent("container3", "container3_child"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=646350"
+ title="Add a test for dynamic chnages of CSS generated content">
+ Mozilla Bug 646350</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="eventdump"></div>
+
+ <div id="container1"></div>
+ <div id="container2"><div id="container2_child">text</div></div>
+ <div id="container3"><div id="container3_child" class="gentext">text</div></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_general.html b/accessible/tests/mochitest/treeupdate/test_general.html
new file mode 100644
index 0000000000..8129cae98a
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_general.html
@@ -0,0 +1,174 @@
+<html>
+
+<head>
+ <title>Testing the tree updates</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+ // //////////////////////////////////////////////////////////////////////////
+
+ function prependAppend(aContainer) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, aContainer),
+ ];
+
+ this.invoke = function prependAppend_invoke() {
+ var checkbox = document.createElement("input");
+ checkbox.setAttribute("type", "checkbox");
+ getNode(aContainer).insertBefore(checkbox, getNode(aContainer).firstChild);
+
+ var button = document.createElement("input");
+ button.setAttribute("type", "button");
+ getNode(aContainer).appendChild(button);
+ };
+
+ this.finalCheck = function prependAppend_finalCheck() {
+ var accTree =
+ { SECTION: [ // container
+ { CHECKBUTTON: [ ] },
+ { ENTRY: [ ] },
+ { PUSHBUTTON: [ ] },
+ ] };
+ testAccessibleTree(aContainer, accTree);
+ };
+
+ this.getID = function prependAppend_getID() {
+ return "prepends a child and appends a child";
+ };
+ }
+
+ function removeRemove(aContainer) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, aContainer),
+ ];
+
+ this.invoke = function removeRemove_invoke() {
+ getNode(aContainer).firstChild.remove();
+ };
+
+ this.finalCheck = function removeRemove_finalCheck() {
+ var accTree =
+ { SECTION: [ // container
+ { PUSHBUTTON: [ ] },
+ ] };
+ testAccessibleTree(aContainer, accTree);
+ };
+
+ this.getID = function removeRemove_getID() {
+ return "remove first and second children";
+ };
+ }
+
+ function insertInaccessibleAccessibleSiblings() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, "c3"),
+ ];
+
+ this.invoke = function insertInaccessibleAccessibleSiblings_invoke() {
+ getNode("c3").appendChild(document.createElement("span"));
+ getNode("c3").appendChild(document.createElement("input"));
+ };
+
+ this.finalCheck = function insertInaccessibleAccessibleSiblings_finalCheck() {
+ var accTree =
+ { SECTION: [ // container
+ { PUSHBUTTON: [
+ { TEXT_LEAF: [] },
+ ] },
+ { ENTRY: [ ] },
+ ] };
+ testAccessibleTree("c3", accTree);
+ };
+
+ this.getID = function insertInaccessibleAccessibleSiblings_getID() {
+ return "insert inaccessible and then accessible siblings";
+ };
+ }
+
+ // Test for bug 1500416.
+ function displayContentsInsertion() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, "c4"),
+ ];
+
+ this.invoke = function displayContentsInsertion_invoke() {
+ document.body.offsetTop; // Flush layout.
+
+ let list = document.createElement("ul");
+ list.style.display = "contents";
+ list.appendChild(document.createElement("li"));
+ list.firstChild.appendChild(document.createTextNode("Text"));
+ getNode("c4").appendChild(list);
+ };
+
+ this.finalCheck = function displayContentsInsertion_finalCheck() {
+ var accTree =
+ { SECTION: [ // container
+ { LIST: [
+ { LISTITEM: [
+ { LISTITEM_MARKER: [] },
+ { TEXT_LEAF: [] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree("c4", accTree);
+ };
+
+ this.getID = function displayContentsInsertion_getID() {
+ return "insert accessible display: contents element.";
+ };
+ }
+
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Do tests
+ // //////////////////////////////////////////////////////////////////////////
+
+ var gQueue = null;
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ function doTests() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new prependAppend("c1"));
+ gQueue.push(new removeRemove("c2"));
+ gQueue.push(new insertInaccessibleAccessibleSiblings());
+ gQueue.push(new displayContentsInsertion());
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTests);
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="c1"><input></div>
+ <div id="c2"><span><input type="checkbox"><input></span><input type="button"></div>
+
+ <div id="c3"><input type="button" value="button"></div>
+ <div id="c4"></div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_hidden.html b/accessible/tests/mochitest/treeupdate/test_hidden.html
new file mode 100644
index 0000000000..e687fc97c9
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_hidden.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>@hidden attribute testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+ // //////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Set @hidden attribute
+ */
+ function setHiddenAttr(aContainerID, aChildID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, getNode(aContainerID)),
+ ];
+
+ this.invoke = function setHiddenAttr_invoke() {
+ var tree =
+ { SECTION: [
+ { ENTRY: [
+ ] },
+ ] };
+ testAccessibleTree(aContainerID, tree);
+
+ getNode(aChildID).setAttribute("hidden", "true");
+ };
+
+ this.finalCheck = function setHiddenAttr_finalCheck() {
+ var tree =
+ { SECTION: [
+ ] };
+ testAccessibleTree(aContainerID, tree);
+ };
+
+ this.getID = function setHiddenAttr_getID() {
+ return "Set @hidden attribute on input and test accessible tree for div";
+ };
+ }
+
+ /**
+ * Remove @hidden attribute
+ */
+ function removeHiddenAttr(aContainerID, aChildID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, getNode(aContainerID)),
+ ];
+
+ this.invoke = function removeHiddenAttr_invoke() {
+ var tree =
+ { SECTION: [
+ ] };
+ testAccessibleTree(aContainerID, tree);
+
+ getNode(aChildID).removeAttribute("hidden");
+ };
+
+ this.finalCheck = function removeHiddenAttr_finalCheck() {
+ var tree =
+ { SECTION: [
+ { ENTRY: [
+ ] },
+ ] };
+ testAccessibleTree(aContainerID, tree);
+ };
+
+ this.getID = function removeHiddenAttr_getID() {
+ return "Remove @hidden attribute on input and test accessible tree for div";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Test
+ // //////////////////////////////////////////////////////////////////////////
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+
+ function doTest() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new setHiddenAttr("container", "child"));
+ gQueue.push(new removeHiddenAttr("container", "child"));
+
+ gQueue.invoke(); // SimpleTest.finish() will be called in the end
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+
+ </script>
+
+</head>
+
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="container"><input id="child"></div>
+
+ <div id="eventdump"></div>
+
+</body>
+
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_imagemap.html b/accessible/tests/mochitest/treeupdate/test_imagemap.html
new file mode 100644
index 0000000000..7befef6905
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_imagemap.html
@@ -0,0 +1,402 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML img map accessible tree update tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+ function insertArea(aImageMapID, aMapID) {
+ this.imageMap = getAccessible(aImageMapID);
+ this.mapNode = getNode(aMapID);
+
+ function getInsertedArea(aThisObj) {
+ return aThisObj.imageMap.firstChild;
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, getInsertedArea, this),
+ new invokerChecker(EVENT_REORDER, this.imageMap),
+ ];
+
+ this.invoke = function insertArea_invoke() {
+ var areaElm = document.createElement("area");
+ 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");
+
+ this.mapNode.insertBefore(areaElm, this.mapNode.firstChild);
+ };
+
+ this.finalCheck = function insertArea_finalCheck() {
+ var accTree =
+ { IMAGE_MAP: [
+ {
+ role: ROLE_LINK,
+ name: "a",
+ children: [ ],
+ },
+ {
+ role: ROLE_LINK,
+ name: "b",
+ children: [ ],
+ },
+ ] };
+ testAccessibleTree(this.imageMap, accTree);
+ };
+
+ this.getID = function insertArea_getID() {
+ return "insert area element";
+ };
+ }
+
+ function appendArea(aImageMapID, aMapID) {
+ this.imageMap = getAccessible(aImageMapID);
+ this.mapNode = getNode(aMapID);
+
+ function getAppendedArea(aThisObj) {
+ return aThisObj.imageMap.lastChild;
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, getAppendedArea, this),
+ new invokerChecker(EVENT_REORDER, this.imageMap),
+ ];
+
+ this.invoke = function appendArea_invoke() {
+ var areaElm = document.createElement("area");
+ 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");
+
+ this.mapNode.appendChild(areaElm);
+ };
+
+ this.finalCheck = function appendArea_finalCheck() {
+ var accTree =
+ { IMAGE_MAP: [
+ {
+ role: ROLE_LINK,
+ name: "a",
+ children: [ ],
+ },
+ {
+ role: ROLE_LINK,
+ name: "b",
+ children: [ ],
+ },
+ {
+ role: ROLE_LINK,
+ name: "c",
+ children: [ ],
+ },
+ ] };
+ testAccessibleTree(this.imageMap, accTree);
+ };
+
+ this.getID = function appendArea_getID() {
+ return "append area element";
+ };
+ }
+
+ function removeArea(aImageMapID, aMapID) {
+ this.imageMap = getAccessible(aImageMapID);
+ this.area = null;
+ this.mapNode = getNode(aMapID);
+
+ function getRemovedArea(aThisObj) {
+ return aThisObj.area;
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getRemovedArea, this),
+ new invokerChecker(EVENT_REORDER, this.imageMap),
+ ];
+
+ this.invoke = function removeArea_invoke() {
+ this.area = this.imageMap.firstChild;
+ this.mapNode.removeChild(this.mapNode.firstElementChild);
+ };
+
+ this.finalCheck = function removeArea_finalCheck() {
+ var accTree =
+ { IMAGE_MAP: [
+ {
+ role: ROLE_LINK,
+ name: "b",
+ children: [ ],
+ },
+ {
+ role: ROLE_LINK,
+ name: "c",
+ children: [ ],
+ },
+ ] };
+ testAccessibleTree(this.imageMap, accTree);
+ };
+
+ this.getID = function removeArea_getID() {
+ return "remove area element";
+ };
+ }
+
+ function removeNameOnMap(aImageMapContainerID, aImageMapID, aMapID) {
+ this.container = getAccessible(aImageMapContainerID);
+ this.containerNode = this.container.DOMNode;
+ this.imageMap = getAccessible(aImageMapID);
+ this.imgNode = this.imageMap.DOMNode;
+ this.mapNode = getNode(aMapID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.imageMap),
+ new invokerChecker(EVENT_SHOW, this.imgNode),
+ new invokerChecker(EVENT_REORDER, this.container),
+ ];
+
+ this.invoke = function removeNameOnMap_invoke() {
+ this.mapNode.removeAttribute("name");
+ };
+
+ this.finalCheck = function removeNameOnMap_finalCheck() {
+ var accTree =
+ { SECTION: [
+ { GRAPHIC: [ ] },
+ ] };
+ testAccessibleTree(this.container, accTree);
+ };
+
+ this.getID = function removeNameOnMap_getID() {
+ return "remove @name on map element";
+ };
+ }
+
+ function restoreNameOnMap(aImageMapContainerID, aImageMapID, aMapID) {
+ this.container = getAccessible(aImageMapContainerID);
+ this.containerNode = this.container.DOMNode;
+ this.imageMap = null;
+ this.imgNode = getNode(aImageMapID);
+ this.mapNode = getNode(aMapID);
+
+ function getImageMap(aThisObj) {
+ return aThisObj.imageMap;
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getImageMap, this),
+ new invokerChecker(EVENT_SHOW, this.imgNode),
+ new invokerChecker(EVENT_REORDER, this.container),
+ ];
+
+ this.invoke = function restoreNameOnMap_invoke() {
+ this.imageMap = getAccessible(aImageMapID);
+ this.mapNode.setAttribute("name", "atoz_map");
+
+ // XXXhack: force repainting of the image (see bug 745788 for details).
+ waveOverImageMap(aImageMapID);
+ };
+
+ this.finalCheck = function removeNameOnMap_finalCheck() {
+ var accTree =
+ { SECTION: [
+ { IMAGE_MAP: [
+ { LINK: [ ] },
+ { LINK: [ ] },
+ ] },
+ ] };
+ testAccessibleTree(this.container, accTree);
+ };
+
+ this.getID = function removeNameOnMap_getID() {
+ return "restore @name on map element";
+ };
+ }
+
+ function removeMap(aImageMapContainerID, aImageMapID, aMapID) {
+ this.container = getAccessible(aImageMapContainerID);
+ this.containerNode = this.container.DOMNode;
+ this.imageMap = null;
+ this.imgNode = getNode(aImageMapID);
+ this.mapNode = getNode(aMapID);
+
+ function getImageMap(aThisObj) {
+ return aThisObj.imageMap;
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getImageMap, this),
+ new invokerChecker(EVENT_SHOW, this.imgNode),
+ new invokerChecker(EVENT_REORDER, this.container),
+ ];
+
+ this.invoke = function removeMap_invoke() {
+ this.imageMap = getAccessible(aImageMapID);
+ this.mapNode.remove();
+ };
+
+ this.finalCheck = function removeMap_finalCheck() {
+ var accTree =
+ { SECTION: [
+ { GRAPHIC: [ ] },
+ ] };
+ testAccessibleTree(this.container, accTree);
+ };
+
+ this.getID = function removeMap_getID() {
+ return "remove map element";
+ };
+ }
+
+ function insertMap(aImageMapContainerID, aImageID) {
+ this.container = getAccessible(aImageMapContainerID);
+ this.containerNode = this.container.DOMNode;
+ this.image = null;
+ this.imgMapNode = getNode(aImageID);
+
+ function getImage(aThisObj) {
+ return aThisObj.image;
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getImage, this),
+ new invokerChecker(EVENT_SHOW, this.imgMapNode),
+ new invokerChecker(EVENT_REORDER, this.container),
+ ];
+
+ this.invoke = function insertMap_invoke() {
+ this.image = getAccessible(aImageID);
+
+ var map = document.createElement("map");
+ map.setAttribute("name", "atoz_map");
+ map.setAttribute("id", "map");
+
+ var area = document.createElement("area");
+ area.setAttribute("href",
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ "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);
+
+ this.containerNode.appendChild(map);
+ };
+
+ this.finalCheck = function insertMap_finalCheck() {
+ var accTree =
+ { SECTION: [
+ { IMAGE_MAP: [
+ { LINK: [ ] },
+ ] },
+ ] };
+ testAccessibleTree(this.container, accTree);
+ };
+
+ this.getID = function insertMap_getID() {
+ return "insert map element";
+ };
+ }
+
+ function hideImageMap(aContainerID, aImageID) {
+ this.container = getAccessible(aContainerID);
+ this.imageMap = null;
+ this.imageMapNode = getNode(aImageID);
+
+ function getImageMap(aThisObj) {
+ return aThisObj.imageMap;
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getImageMap, this),
+ new invokerChecker(EVENT_REORDER, aContainerID),
+ ];
+
+ this.invoke = function hideImageMap_invoke() {
+ this.imageMap = getAccessible(this.imageMapNode);
+ this.imageMapNode.style.display = "none";
+ };
+
+ this.finalCheck = function hideImageMap_finalCheck() {
+ var accTree =
+ { SECTION: [ ] };
+ testAccessibleTree(this.container, accTree);
+ };
+
+ this.getID = function hideImageMap_getID() {
+ return "display:none image";
+ };
+ }
+
+ // gA11yEventDumpToConsole = true; // debug stuff
+ function doPreTest() {
+ waitForImageMap("imgmap", doTest);
+ }
+
+ var gQueue = null;
+ function doTest() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new insertArea("imgmap", "map"));
+ gQueue.push(new appendArea("imgmap", "map"));
+ gQueue.push(new removeArea("imgmap", "map"));
+ gQueue.push(new removeNameOnMap("container", "imgmap", "map"));
+ gQueue.push(new restoreNameOnMap("container", "imgmap", "map"));
+ gQueue.push(new removeMap("container", "imgmap", "map"));
+ gQueue.push(new insertMap("container", "imgmap"));
+ gQueue.push(new hideImageMap("container", "imgmap"));
+
+ // enableLogging("tree"); // debug stuff
+ // gQueue.onFinish = function() { disableLogging("tree"); }
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doPreTest);
+ </script>
+
+</head>
+<body>
+
+ <a target="_blank"
+ title="Image map accessible tree is not updated when image map is changed"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=732389">
+ Mozilla Bug 732389
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <map name="atoz_map" id="map">
+ <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b"
+ coords="17,0,30,14" alt="b" shape="rect">
+ </map>
+
+ <div id="container">
+ <img id="imgmap" width="447" height="15"
+ usemap="#atoz_map"
+ src="../letters.gif"><!--
+ Important: no whitespace between the <img> and the </div>, so we
+ don't end up with textframes there, because those would be reflected
+ in our accessible tree in some cases.
+ --></div>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_inert.html b/accessible/tests/mochitest/treeupdate/test_inert.html
new file mode 100644
index 0000000000..dd9a43bc2f
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_inert.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Inert tree update test</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+ <script src="../common.js"></script>
+ <script src="../promisified-events.js"></script>
+ <script src="../role.js"></script>
+
+ <script>
+ async function doTest() {
+ const inertContainer = getAccessible("inertContainer");
+ const inertTree = { SECTION: [ // inertContainer
+ { PARAGRAPH: [{ TEXT_LEAF: [] }] }, // before
+ { PARAGRAPH: [{ TEXT_LEAF: [] }] }, // after
+ ]};
+ testAccessibleTree(inertContainer, inertTree);
+
+ info("Unsetting inert");
+ let reordered = waitForEvent(EVENT_REORDER, inertContainer);
+ const inertNode = getNode("inert");
+ inertNode.inert = false;
+ await reordered;
+ testAccessibleTree(inertContainer, { SECTION: [ // inertContainer
+ { PARAGRAPH: [{ TEXT_LEAF: [] }] }, // before
+ { SECTION: [ // inert
+ { TEXT_LEAF: [] },
+ { PUSHBUTTON: [] },
+ ] },
+ { PARAGRAPH: [{ TEXT_LEAF: [] }] }, // after
+ ]});
+
+ info("Setting inert");
+ reordered = waitForEvent(EVENT_REORDER, inertContainer);
+ inertNode.inert = true;
+ await reordered;
+ testAccessibleTree(inertContainer, inertTree);
+
+ const noDialogTree = { SECTION: [ // dialogContainer
+ { TEXT_LEAF: [] }
+ ] };
+ testAccessibleTree("dialogContainer", noDialogTree);
+
+ info("Showing modal dialog");
+ reordered = waitForEvent(EVENT_REORDER, document);
+ const dialogNode = getNode("dialog");
+ dialogNode.showModal();
+ await reordered;
+ // The dialog makes everything else in the document inert.
+ testAccessibleTree(document, { DOCUMENT: [
+ { DIALOG: [
+ { PUSHBUTTON: [] }
+ ] }
+ ] });
+
+ info("Closing dialog");
+ reordered = waitForEvent(EVENT_REORDER, document);
+ dialogNode.close();
+ await reordered;
+ testAccessibleTree("dialogContainer", noDialogTree);
+
+ const fullscreenTree = { SECTION: [ // fullscreen
+ { PUSHBUTTON: [] }
+ ] };
+ const notFullscreenTree = { SECTION: [ // fullscreenContainer
+ { SECTION: [
+ { PUSHBUTTON: [] } // requestFullscreen
+ ] },
+ fullscreenTree,
+ ] };
+ testAccessibleTree("fullscreenContainer", notFullscreenTree);
+
+ info("Requesting fullscreen");
+ reordered = waitForEvent(EVENT_REORDER, document);
+ // Fullscreen must be requested by a user input event.
+ synthesizeMouseAtCenter(getNode("requestFullscreen"), {});
+ await reordered;
+ // Fullscreen makes everything else in the document inert.
+ testAccessibleTree(document, { DOCUMENT: [
+ fullscreenTree
+ ] });
+
+ info("Exiting fullscreen");
+ reordered = waitForEvent(EVENT_REORDER, document);
+ document.exitFullscreen();
+ await reordered;
+ testAccessibleTree("fullscreenContainer", notFullscreenTree);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="inertContainer">
+ <p>before</p>
+ <div id="inert" inert>inert <button></button></div>
+ <p>after</p>
+ </div>
+
+ <div id="dialogContainer">
+ dialogContainer
+ <dialog id="dialog"><button></button></dialog>
+ </div>
+
+ <div id="fullscreenContainer">
+ <div>
+ <button id="requestFullscreen"
+ onclick="document.getElementById('fullscreen').requestFullscreen();">
+ </button>
+ </div>
+ <div id="fullscreen"><button></button></div>
+ </div>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_inner_reorder.html b/accessible/tests/mochitest/treeupdate/test_inner_reorder.html
new file mode 100644
index 0000000000..b4411833d7
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_inner_reorder.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Test accessible delayed removal</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+
+ async function testInnerReorder() {
+ window.windowUtils.advanceTimeAndRefresh(100);
+
+ let events = waitForOrderedEvents([
+ [EVENT_HIDE, "c1.1.1"],
+ [EVENT_SHOW, "c1.1.1"],
+ [EVENT_INNER_REORDER, "c1.1"],
+ [EVENT_REORDER, "c1"],
+ ], "events yay");
+
+ let child = getNode("c1.1.1");
+ child.remove();
+ getNode("c1").appendChild(child);
+
+ window.windowUtils.restoreNormalRefresh();
+
+ await events;
+ }
+
+ async function testInnerReorderEntry() {
+ window.windowUtils.advanceTimeAndRefresh(100);
+
+ let events = waitForOrderedEvents([
+ [EVENT_HIDE, e => e.accessible.name == "hello"],
+ [EVENT_HIDE, "c2.2"],
+ [EVENT_INNER_REORDER, "c2.1"],
+ [EVENT_REORDER, "c2"],
+ [EVENT_TEXT_VALUE_CHANGE, "c2.1"],
+ ], "events yay");
+
+ getNode("c2.1.1").remove();
+ getNode("c2.2").remove();
+
+ window.windowUtils.restoreNormalRefresh();
+
+ await events;
+ }
+
+ async function testInnerReorderAriaOwns() {
+ let events = waitForOrderedEvents([
+ [EVENT_HIDE, "c3.1.1"],
+ [EVENT_SHOW, "c3.1.1"],
+ [EVENT_INNER_REORDER, "c3.1"],
+ [EVENT_REORDER, "c3"],
+ ], "events yay");
+
+ getNode("c3").setAttribute("aria-owns", "c3.1.1");
+
+ await events;
+
+ events = waitForOrderedEvents([
+ [EVENT_HIDE, "c3.1.1"],
+ [EVENT_SHOW, "c3.1.1"],
+ [EVENT_INNER_REORDER, "c3.1"],
+ [EVENT_REORDER, "c3"],
+ ], "events yay");
+
+ getNode("c3").removeAttribute("aria-owns");
+
+ await events;
+ }
+
+ async function testInnerContainerRemoved() {
+ window.windowUtils.advanceTimeAndRefresh(100);
+
+ let events = waitForOrderedEvents([
+ [EVENT_HIDE, "c4.1"],
+ [EVENT_SHOW, "c4.1.1"],
+ [EVENT_REORDER, "c4"],
+ ], "events yay");
+
+ let child = getNode("c4.1.1");
+ child.remove();
+ getNode("c1").appendChild(child);
+ getNode("c4.1").remove();
+
+ window.windowUtils.restoreNormalRefresh();
+
+ await events;
+ }
+
+
+ async function doTest() {
+ await testInnerReorder();
+
+ await testInnerReorderEntry();
+
+ await testInnerReorderAriaOwns();
+
+ await testInnerContainerRemoved();
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="c1">
+ <div id="c1.1"><div id="c1.1.1">hello</div></div>
+ </div>
+
+ <div id="c2">
+ <div role="textbox" contenteditable="true" id="c2.1">
+ <span id="c2.1.1">hello</span>
+ </div>
+ <input type="submit" id="c2.2">
+ </div>
+
+ <div id="c3">
+ <div id="c3.1"><div id="c3.1.1"></div></div>
+ </div>
+
+ <div id="c4">
+ <div id="c4.1"><div id="c4.1.1">hello</div></div>
+ </div>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_list.html b/accessible/tests/mochitest/treeupdate/test_list.html
new file mode 100644
index 0000000000..06c308e422
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_list.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Test HTML li and listitem bullet accessible cache</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Helpers
+
+ function testLiAccessibleTree() {
+ // Test accessible tree.
+ var accTree = {
+ role: ROLE_LISTITEM,
+ children: [
+ {
+ role: ROLE_LISTITEM_MARKER,
+ children: [],
+ },
+ {
+ role: ROLE_TEXT_LEAF,
+ children: [],
+ },
+ ],
+ };
+
+ testAccessibleTree("li", accTree);
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Sequence item processors
+
+ function hideProcessor() {
+ this.liNode = getNode("li");
+ this.li = getAccessible(this.liNode);
+ this.bullet = this.li.firstChild;
+
+ this.process = function hideProcessor_process() {
+ this.liNode.style.display = "none";
+ };
+
+ this.onProcessed = function hideProcessor_onProcessed() {
+ window.setTimeout(
+ function(aLiAcc, aLiNode, aBulletAcc) {
+ testDefunctAccessible(aLiAcc, aLiNode);
+ testDefunctAccessible(aBulletAcc);
+
+ gSequence.processNext();
+ },
+ 0, this.li, this.liNode, this.bullet
+ );
+ };
+ }
+
+ function showProcessor() {
+ this.liNode = getNode("li");
+
+ this.process = function showProcessor_process() {
+ this.liNode.style.display = "list-item";
+ };
+
+ this.onProcessed = function showProcessor_onProcessed() {
+ testLiAccessibleTree();
+ gSequence.processNext();
+ };
+ }
+
+ function textReplaceProcessor() {
+ this.liNode = getNode("li");
+
+ this.process = function textReplaceProcessor_process() {
+ this.liNode.textContent = "hey";
+ };
+
+ this.onProcessed = function textReplaceProcessor_onProcessed() {
+ var tree = {
+ LISTITEM: [
+ { LISTITEM_MARKER: [] },
+ { TEXT_LEAF: [] },
+ ],
+ };
+ testAccessibleTree(this.liNode, tree);
+ SimpleTest.finish();
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpToConsole = true;
+
+ var gSequence = null;
+ function doTest() {
+ testLiAccessibleTree();
+
+ gSequence = new sequence();
+
+ gSequence.append(new hideProcessor(), EVENT_HIDE, getAccessible("li"),
+ "hide HTML li");
+ gSequence.append(new showProcessor(), EVENT_SHOW, getNode("li"),
+ "show HTML li");
+ gSequence.append(new textReplaceProcessor(), EVENT_REORDER, getNode("li"),
+ "change text of HTML li");
+
+ gSequence.processNext(); // SimpleTest.finish() will be called in the end
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="setParent shouldn't be virtual"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=496783">Mozilla Bug 496783</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <ul>
+ <li id="li">item1</li>
+ </ul>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_list_editabledoc.html b/accessible/tests/mochitest/treeupdate/test_list_editabledoc.html
new file mode 100644
index 0000000000..59b9dc9c53
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_list_editabledoc.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Test HTML li and listitem bullet accessible insertion into editable document</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function addLi(aID) {
+ this.listNode = getNode(aID);
+ this.liNode = document.createElement("li");
+ this.liNode.textContent = "item";
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, getAccessible, this.liNode),
+ new invokerChecker(EVENT_REORDER, this.listNode),
+ ];
+
+ this.invoke = function addLi_invoke() {
+ this.listNode.appendChild(this.liNode);
+ };
+
+ this.finalCheck = function addLi_finalCheck() {
+ var tree = {
+ role: ROLE_LIST,
+ children: [
+ {
+ role: ROLE_LISTITEM,
+ children: [
+ {
+ role: ROLE_LISTITEM_MARKER,
+ name: "1. ",
+ children: [],
+ },
+ {
+ role: ROLE_TEXT_LEAF,
+ children: [],
+ },
+ ],
+ },
+ ],
+ };
+ testAccessibleTree(aID, tree);
+ };
+
+ this.getID = function addLi_getID() {
+ return "add li";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+
+ var gQueue = null;
+
+ function doTest() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new addLi("list"));
+
+ gQueue.invoke(); // SimpleTest.finish() will be called in the end
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body contentEditable="true">
+
+ <a target="_blank"
+ title="Wrong list bullet text of accessible for the first numbered HTML:li in CKEditor"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=557795">Mozilla Bug 557795</a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <ol id="list">
+ </ol>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_list_style.html b/accessible/tests/mochitest/treeupdate/test_list_style.html
new file mode 100644
index 0000000000..1d9e1c00ca
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_list_style.html
@@ -0,0 +1,181 @@
+<html>
+
+<head>
+ <title>Test hide/show events for HTMLListBulletAccessibles on list restyle</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../name.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ /**
+ * Change list style type to none.
+ */
+ async function hideBullet() {
+ info("Hide bullet by setting style to none");
+
+ let liAcc = getAccessible("list_element");
+ let bullet = liAcc.firstChild;
+
+ let events = waitForEvents([
+ [EVENT_HIDE, bullet],
+ [EVENT_REORDER, liAcc]
+ ]);
+
+ getNode("list").style.setProperty("list-style-type", "none");
+
+ await events;
+
+ is(liAcc.name, "list element",
+ "Check that first child of LI is not a bullet.");
+
+ dumpTree("list");
+ }
+
+ /**
+ * Change list style type to circles.
+ */
+ async function showBullet() {
+ info("Show bullet by setting style to circle");
+ let liAcc = getAccessible("list_element");
+
+ let events = waitForEvents([
+ [EVENT_SHOW, evt => evt.accessible.parent == liAcc],
+ [EVENT_REORDER, liAcc]
+ ]);
+
+ getNode("list").style.setProperty("list-style-type", "circle");
+
+ await events;
+
+ is(liAcc.name, "◦ list element",
+ "Check that first child of LI is a circle bullet.");
+
+ dumpTree("list");
+ }
+
+ /**
+ * Change list style position.
+ */
+ async function changeBulletPosition() {
+ info("Change list style position");
+ let liAcc = getAccessible("list_element");
+
+ let events = waitForEvents([
+ [EVENT_HIDE, evt => evt.accessible.role == ROLE_LISTITEM_MARKER],
+ [EVENT_SHOW, evt => evt.accessible.role == ROLE_LISTITEM_MARKER],
+ [EVENT_REORDER, liAcc]
+ ]);
+
+ getNode("list").style.setProperty("list-style-position", "inside");
+
+ await events;
+
+ is(liAcc.name, "◦ list element",
+ "Check that first child of LI is a circle bullet.");
+ }
+
+ async function changeBulletPositionAndType() {
+ let events = waitForEvents([
+ [EVENT_HIDE, evt => evt.accessible.role == ROLE_LISTITEM_MARKER],
+ [EVENT_REORDER, evt => evt.accessible.role == ROLE_LISTITEM]
+ ]);
+
+ let list = getNode("inside-marker-list");
+
+ // Bug 1513447 - This changes the list type to "none" and the
+ // position implicitly to "outside".
+ list.style.setProperty("list-style", "none");
+ list.offsetLeft
+ list.style.setProperty("list-style-type", "telugu");
+
+ await events;
+ }
+
+ async function doTest() {
+
+ testAccessibleTree("list", { LIST: [ // ol
+ { LISTITEM: [ // li
+ { LISTITEM_MARKER: [ ] },
+ { TEXT_LEAF: [] },
+ ] },
+ ] } );
+
+ await hideBullet();
+
+ testAccessibleTree("list", { LIST: [ // ol
+ { LISTITEM: [ // li
+ { TEXT_LEAF: [] },
+ ] },
+ ] } );
+
+ await showBullet();
+
+ testAccessibleTree("list", { LIST: [ // ol
+ { LISTITEM: [ // li
+ { LISTITEM_MARKER: [ ] },
+ { TEXT_LEAF: [] },
+ ] },
+ ] } );
+
+ await changeBulletPosition();
+
+ testAccessibleTree("list", { LIST: [ // ol
+ { LISTITEM: [ // li
+ { LISTITEM_MARKER: [ ] },
+ { TEXT_LEAF: [] },
+ ] },
+ ] } );
+
+ testAccessibleTree("unmarked-list", { LIST: [ // ol
+ { LISTITEM: [ // li
+ { TEXT_LEAF: [] },
+ ] },
+ ] } );
+
+ await changeBulletPositionAndType();
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1100602"
+ title="[e10s] crash in mozilla::a11y::ProxyAccessible::Shutdown()">
+ Mozilla Bug 1100602
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <ol id="list" style="list-style-type: circle;">
+ <li id="list_element">list element</li>
+ </ol>
+
+ <ol id="unmarked-list" style="list-style: none;">
+ <li>list element</li>
+ </ol>
+
+ <ol id="inside-marker-list" style="list-style-position: inside;">
+ <li>list element</li>
+ </ol>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_listbox.xhtml b/accessible/tests/mochitest/treeupdate/test_listbox.xhtml
new file mode 100644
index 0000000000..629c4b0915
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_listbox.xhtml
@@ -0,0 +1,181 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL listbox hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ function insertListitem(aListboxID)
+ {
+ this.listboxNode = getNode(aListboxID);
+
+ this.listitemNode = document.createXULElement("richlistitem");
+ var label = document.createXULElement("label");
+ label.setAttribute("value", "item1");
+ this.listitemNode.appendChild(label);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, this.listitemNode),
+ new invokerChecker(EVENT_REORDER, this.listboxNode)
+ ];
+
+ this.invoke = function insertListitem_invoke()
+ {
+ this.listboxNode.insertBefore(this.listitemNode,
+ this.listboxNode.firstChild);
+ }
+
+ this.finalCheck = function insertListitem_finalCheck()
+ {
+ var tree =
+ { LISTBOX: [
+ {
+ role: ROLE_RICH_OPTION,
+ name: "item1"
+ },
+ {
+ role: ROLE_RICH_OPTION,
+ name: "item2"
+ },
+ {
+ role: ROLE_RICH_OPTION,
+ name: "item3"
+ },
+ {
+ role: ROLE_RICH_OPTION,
+ name: "item4"
+ }
+ ] };
+ testAccessibleTree(this.listboxNode, tree);
+ }
+
+ this.getID = function insertListitem_getID()
+ {
+ return "insert listitem ";
+ }
+ }
+
+ function removeListitem(aListboxID)
+ {
+ this.listboxNode = getNode(aListboxID);
+ this.listitemNode = null;
+ this.listitem;
+
+ function getListitem(aThisObj)
+ {
+ return aThisObj.listitem;
+ }
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getListitem, this),
+ new invokerChecker(EVENT_REORDER, this.listboxNode)
+ ];
+
+ this.invoke = function removeListitem_invoke()
+ {
+ this.listitemNode = this.listboxNode.firstChild;
+ this.listitem = getAccessible(this.listitemNode);
+
+ this.listboxNode.removeChild(this.listitemNode);
+ }
+
+ this.finalCheck = function removeListitem_finalCheck()
+ {
+ var tree =
+ { LISTBOX: [
+ {
+ role: ROLE_RICH_OPTION,
+ name: "item2"
+ },
+ {
+ role: ROLE_RICH_OPTION,
+ name: "item3"
+ },
+ {
+ role: ROLE_RICH_OPTION,
+ name: "item4"
+ }
+ ] };
+ testAccessibleTree(this.listboxNode, tree);
+ }
+
+ this.getID = function removeListitem_getID()
+ {
+ return "remove listitem ";
+ }
+ }
+
+ //gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+ function doTest()
+ {
+ var tree =
+ { LISTBOX: [
+ {
+ role: ROLE_RICH_OPTION,
+ name: "item2"
+ },
+ {
+ role: ROLE_RICH_OPTION,
+ name: "item3"
+ },
+ {
+ role: ROLE_RICH_OPTION,
+ name: "item4"
+ }
+ ] };
+ testAccessibleTree("listbox", tree);
+
+ gQueue = new eventQueue();
+ gQueue.push(new insertListitem("listbox"));
+ gQueue.push(new removeListitem("listbox"));
+ gQueue.invoke(); // Will call SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=656225"
+ title="XUL listbox accessible tree doesn't get updated">
+ Mozilla Bug 656225
+ </a>
+ <br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <richlistbox id="listbox">
+ <richlistitem><label value="item2"/></richlistitem>
+ <richlistitem><label value="item3"/></richlistitem>
+ <richlistitem><label value="item4"/></richlistitem>
+ </richlistbox>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/treeupdate/test_menu.xhtml b/accessible/tests/mochitest/treeupdate/test_menu.xhtml
new file mode 100644
index 0000000000..44042cc9e7
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_menu.xhtml
@@ -0,0 +1,127 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL menu hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ ////////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function openMenu(aID)
+ {
+ this.menuNode = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_FOCUS, this.menuNode)
+ ];
+
+ this.invoke = function openMenu_invoke()
+ {
+ var tree;
+ if (LINUX || SOLARIS) {
+ tree =
+ { PARENT_MENUITEM: [ ] };
+
+ } else {
+ tree =
+ { PARENT_MENUITEM: [
+ { MENUPOPUP: [ ] }
+ ] };
+ }
+ testAccessibleTree(aID, tree);
+
+ // Show menu.
+ this.menuNode.open = true;
+ }
+
+ this.finalCheck = function openMenu_finalCheck()
+ {
+ var tree;
+ if (LINUX || SOLARIS) {
+ tree =
+ { PARENT_MENUITEM: [
+ { MENUITEM: [ ] },
+ { MENUITEM: [ ] }
+ ] };
+
+ } else {
+ tree =
+ { PARENT_MENUITEM: [
+ { MENUPOPUP: [
+ { MENUITEM: [ ] },
+ { MENUITEM: [ ] }
+ ] }
+ ] };
+ }
+ testAccessibleTree(aID, tree);
+ }
+
+ this.getID = function openMenu_getID()
+ {
+ return "open menu " + prettyName(aID);
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ var gQueue = null;
+ function doTest()
+ {
+ gQueue = new eventQueue();
+ gQueue.push(new openMenu("menu"));
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=249292"
+ title="Ensure accessible children for toolbarbutton types 'menu'">
+ Mozilla Bug 249292
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=630486"
+ title="Don't force accessible creation for popup children.">
+ Mozilla Bug 630486
+ </a>
+ <br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <menubar>
+ <menu id="menu" label="menu">
+ <menupopup>
+ <menuitem label="menuitem"/>
+ <menuitem label="menuitem"/>
+ </menupopup>
+ </menu>
+ </menubar>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/treeupdate/test_menubutton.xhtml b/accessible/tests/mochitest/treeupdate/test_menubutton.xhtml
new file mode 100644
index 0000000000..d3eeb29f78
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_menubutton.xhtml
@@ -0,0 +1,141 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL button hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function openMenu(aButtonID, aIsTree)
+ {
+ var menuItemRole = aIsTree ? ROLE_CHECK_MENU_ITEM : ROLE_MENUITEM;
+ this.button = getAccessible(aButtonID);
+ this.menupopup = aIsTree ? this.button.nextSibling : this.button.firstChild;
+
+ var checker = new invokerChecker(EVENT_REORDER, this.menupopup);
+ this.__proto__ = new synthClick(aButtonID, checker);
+
+ let testButton = popup => {
+ var children = [];
+ if (!aIsTree) {
+ children.push(popup);
+ }
+ var tree = { PUSHBUTTON: children };
+ testAccessibleTree(this.button, tree);
+ testAccessibleTree(this.menupop, popup);
+ }
+
+ this.invoke = function openMenu_invoke()
+ {
+ testButton({ MENUPOPUP: [ ] });
+ this.__proto__.invoke();
+ }
+
+ this.finalCheck = function openMenu_finalCheck()
+ {
+ testButton({ MENUPOPUP: [
+ { role: menuItemRole, children: [ ] },
+ { role: menuItemRole, children: [ ] }
+ ] });
+
+ synthesizeKey("KEY_Escape");
+ }
+
+ this.getID = function openMenu_getID()
+ {
+ return "open menu of the button " + prettyName(aButtonID);
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Do test
+
+ gA11yEventDumpToConsole = true; // debug stuff
+
+ var gQueue = null;
+
+ function doTest()
+ {
+ gQueue = new eventQueue();
+
+ gQueue.push(new openMenu("button1"));
+ gQueue.push(new openMenu("button3"));
+
+ var columnPickerBtn = getAccessible("tree").firstChild.lastChild.previousSibling;
+ gQueue.push(new openMenu(columnPickerBtn, true));
+ gQueue.invoke(); // SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=249292"
+ title="Ensure accessible children for toolbarbutton types 'menu'">
+ Bug 249292
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=630486"
+ title="Don't force accessible creation for popup children">
+ Bug 630486
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=722265"
+ title="Column header selection popup no longer exposed to accessibility APIs">
+ Bug 722265
+ </a>
+ <br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <button id="button1" type="menu" label="button">
+ <menupopup>
+ <menuitem label="menuitem"/>
+ <menuitem label="menuitem"/>
+ </menupopup>
+ </button>
+
+ <toolbarbutton id="button3" type="menu" label="toolbarbutton">
+ <menupopup>
+ <menuitem label="menuitem"/>
+ <menuitem label="menuitem"/>
+ </menupopup>
+ </toolbarbutton>
+
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col" flex="1" primary="true" label="column"/>
+ <treecol id="col2" flex="1" label="another column"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/treeupdate/test_optgroup.html b/accessible/tests/mochitest/treeupdate/test_optgroup.html
new file mode 100644
index 0000000000..943b73be43
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_optgroup.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Add and remove optgroup test</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ function addOptGroup(aID) {
+ this.selectNode = getNode(aID);
+ this.select = getAccessible(this.selectNode);
+ this.selectList = this.select.firstChild;
+
+ this.invoke = function addOptGroup_invoke() {
+ var optGroup = document.createElement("optgroup");
+ for (let i = 0; i < 2; i++) {
+ var opt = document.createElement("option");
+ opt.value = i;
+ opt.text = "Option: Value " + i;
+
+ optGroup.appendChild(opt);
+ }
+
+ this.selectNode.add(optGroup, null);
+ var option = document.createElement("option");
+ this.selectNode.add(option, null);
+ };
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.selectList),
+ ];
+
+ this.finalCheck = function addOptGroup_finalCheck() {
+ var tree =
+ { COMBOBOX: [
+ { COMBOBOX_LIST: [
+ { GROUPING: [
+ { COMBOBOX_OPTION: [ ] },
+ { COMBOBOX_OPTION: [ ] },
+ ]},
+ { COMBOBOX_OPTION: [] },
+ ] },
+ ] };
+ testAccessibleTree(this.select, tree);
+ };
+
+ this.getID = function addOptGroup_getID() {
+ return "test optgroup's insertion into a select";
+ };
+ }
+
+ function removeOptGroup(aID) {
+ this.selectNode = getNode(aID);
+ this.select = getAccessible(this.selectNode);
+ this.selectList = this.select.firstChild;
+
+ this.invoke = function removeOptGroup_invoke() {
+ this.option1Node = this.selectNode.firstChild.firstChild;
+ this.selectNode.firstChild.remove();
+ };
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.selectList),
+ ];
+
+ this.finalCheck = function removeOptGroup_finalCheck() {
+ var tree =
+ { COMBOBOX: [
+ { COMBOBOX_LIST: [
+ { COMBOBOX_OPTION: [] },
+ ] },
+ ] };
+ testAccessibleTree(this.select, tree);
+ is(isAccessible(this.option1Node), false, "removed option shouldn't be accessible anymore!");
+ };
+
+ this.getID = function removeOptGroup_getID() {
+ return "test optgroup's removal from a select";
+ };
+ }
+
+ // gA11yEventDumpToConsole = true;
+
+ function doTest() {
+ const gQueue = new eventQueue();
+
+ gQueue.push(new addOptGroup("select"));
+ gQueue.push(new removeOptGroup("select"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=616452"
+ title="Bug 616452 - Dynamically inserted select options aren't reflected in accessible tree">
+ Bug 616452</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <select id="select"></select>
+
+ <div id="debug"/>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_recreation.html b/accessible/tests/mochitest/treeupdate/test_recreation.html
new file mode 100644
index 0000000000..d403b0890e
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_recreation.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Test accessible recreation</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+
+ async function doTest() {
+ let events, msg;
+
+ msg = "Assign a 'button' role to a span";
+ events = waitForOrderedEvents(
+ [[EVENT_HIDE], [EVENT_SHOW, "span"], [EVENT_REORDER, "container"]], msg);
+ document.getElementById("span").setAttribute("role", "button");
+ await events;
+
+ msg = "Remove the 'button' role from a span";
+ events = waitForOrderedEvents(
+ [[EVENT_HIDE, "span"], [EVENT_SHOW], [EVENT_REORDER, "container"]], msg);
+ document.getElementById("span").removeAttribute("role");
+ await events;
+
+ msg = "Assign a 'button' role to a div";
+ events = waitForOrderedEvents(
+ [[EVENT_HIDE, "div1"], [EVENT_SHOW, "div1"], [EVENT_REORDER, "container"]], msg);
+ document.getElementById("div1").setAttribute("role", "button");
+ await events;
+
+ msg = "Change a password field to a text field";
+ events = waitForOrderedEvents(
+ [[EVENT_HIDE, "password"], [EVENT_SHOW, "password"], [EVENT_REORDER, "container"]], msg);
+ document.getElementById("password").setAttribute("type", "text");
+ await events;
+
+ msg = "Change a text field to a password field";
+ events = waitForOrderedEvents(
+ [[EVENT_HIDE, "text"], [EVENT_SHOW, "text"], [EVENT_REORDER, "container"]], msg);
+ document.getElementById("text").setAttribute("type", "password");
+ await events;
+
+ msg = "Add aria-label to a span";
+ ok(!isAccessible("span2"), "span2 has no accessible");
+ events = waitForOrderedEvents(
+ [[EVENT_SHOW, "span2"], [EVENT_REORDER, "container"]], msg);
+ document.getElementById("span2").setAttribute("aria-label", "label");
+ await events;
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Rework accessible tree update code"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275">
+ Mozilla Bug 570275
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="container">
+ <span id="span">span</span>
+ <div id="div1">div</div>
+ <a id="anchor">anchor</a>
+ <div id="div3" role="listbox">list</div>
+ <input type="password" id="password"/>
+ <input type="text" id="text"/>
+ <span id="span2"></span>
+ </div>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_select.html b/accessible/tests/mochitest/treeupdate/test_select.html
new file mode 100644
index 0000000000..eabb64f80f
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_select.html
@@ -0,0 +1,191 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>HTML select options test</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ function addOptions(aID) {
+ this.selectNode = getNode(aID);
+ this.select = getAccessible(this.selectNode);
+ this.selectList = this.select.firstChild;
+
+ this.invoke = function addOptions_invoke() {
+ for (let i = 0; i < 2; i++) {
+ var opt = document.createElement("option");
+ opt.value = i;
+ opt.text = "Option: Value " + i;
+
+ this.selectNode.add(opt, null);
+ }
+ };
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.selectList),
+ ];
+
+ this.finalCheck = function addOptions_finalCheck() {
+ var tree =
+ { COMBOBOX: [
+ { COMBOBOX_LIST: [
+ { COMBOBOX_OPTION: [ ] },
+ { COMBOBOX_OPTION: [ ] },
+ ] },
+ ] };
+ testAccessibleTree(this.select, tree);
+ };
+
+ this.getID = function addOptions_getID() {
+ return "test elements insertion into a select";
+ };
+ }
+
+ function removeOptions(aID) {
+ this.selectNode = getNode(aID);
+ this.select = getAccessible(this.selectNode);
+ this.selectList = this.select.firstChild;
+
+ this.invoke = function removeOptions_invoke() {
+ while (this.selectNode.length)
+ this.selectNode.remove(0);
+ };
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.selectList),
+ ];
+
+ this.finalCheck = function removeOptions_finalCheck() {
+ var tree =
+ { COMBOBOX: [
+ { COMBOBOX_LIST: [] },
+ ] };
+ testAccessibleTree(this.select, tree);
+ };
+
+ this.getID = function removeptions_getID() {
+ return "test elements removal from a select";
+ };
+ }
+
+ /**
+ * Setting role=option on option makes the accessible recreate.
+ */
+ function setRoleOnOption() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, "s2_o"),
+ new invokerChecker(EVENT_SHOW, "s2_o"),
+ ];
+
+ this.invoke = function setRoleOnOption_setRole() {
+ getNode("s2_o").setAttribute("role", "option");
+ };
+
+ this.finalCheck = function() {
+ var tree =
+ { COMBOBOX: [
+ { COMBOBOX_LIST: [
+ { COMBOBOX_OPTION: [ ] },
+ ] },
+ ] };
+ testAccessibleTree("s2", tree);
+ };
+
+ this.getID = function removeptions_getID() {
+ return "setting role=option on select option";
+ };
+ }
+
+ /**
+ * Setting multiple on select makes the accessible recreate.
+ */
+ function setMultipleOnSelect() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, "s3"),
+ new invokerChecker(EVENT_SHOW, "s3"),
+ ];
+
+ this.invoke = function setRoleOnOption_setRole() {
+ getNode("s3").multiple = true;
+ };
+
+ this.finalCheck = function() {
+ var tree =
+ { LISTBOX: [
+ { OPTION: [ ] },
+ ] };
+ testAccessibleTree("s3", tree);
+ };
+
+ this.getID = function removeptions_getID() {
+ return "setting multiple on select element";
+ };
+ }
+
+
+ /**
+ * Removing size on select makes the accessible recreate.
+ */
+ function removeSizeFromSelect() {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, "s4"),
+ new invokerChecker(EVENT_SHOW, "s4"),
+ ];
+
+ this.invoke = function setRoleOnOption_setRole() {
+ getNode("s4").removeAttribute("size");
+ };
+
+ this.finalCheck = function() {
+ var tree =
+ { COMBOBOX: [
+ { COMBOBOX_LIST: [
+ { COMBOBOX_OPTION: [ ] },
+ ] },
+ ] };
+ testAccessibleTree("s4", tree);
+ };
+
+ this.getID = function removeptions_getID() {
+ return "removing size from select element";
+ };
+ }
+
+ function doTest() {
+ const gQueue = new eventQueue();
+
+ gQueue.push(new addOptions("select"));
+ gQueue.push(new removeOptions("select"));
+ gQueue.push(new setRoleOnOption());
+ gQueue.push(new setMultipleOnSelect());
+ gQueue.push(new removeSizeFromSelect());
+
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <select id="select"></select>
+ <select id="s2"><option id="s2_o"></option></select>
+ <select id="s3"><option></option></select>
+ <select id="s4" size="3"><option></option></select>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_shadow_slots.html b/accessible/tests/mochitest/treeupdate/test_shadow_slots.html
new file mode 100644
index 0000000000..27baef0e4a
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_shadow_slots.html
@@ -0,0 +1,554 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Test shadow roots with slots</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../promisified-events.js"></script>
+
+ <script type="application/javascript">
+ async function _dynamicShadowTest(name, mutationFunc, expectedTree, reorder_targets) {
+ info(name);
+
+ let container = getNode(name);
+ let host = container.querySelector('.host');
+
+ document.body.offsetTop;
+ let event = reorder_targets ?
+ waitForEvents(reorder_targets.map(target => [EVENT_REORDER, target, name])) :
+ waitForEvent(EVENT_REORDER, host, name);
+
+ mutationFunc(container, host);
+
+ await event;
+
+ testAccessibleTree(container, expectedTree);
+
+ return true;
+ }
+
+ async function attachFlatShadow() {
+ await _dynamicShadowTest("attachFlatShadow",
+ (container, host) => {
+ host.attachShadow({ mode: "open" })
+ .appendChild(container.querySelector('.shadowtree').content.cloneNode(true));
+ }, { SECTION: [{ SECTION: [{ name: "red"} ] }] });
+ }
+
+ async function attachOneDeepShadow() {
+ await _dynamicShadowTest("attachOneDeepShadow",
+ (container, host) => {
+ host.attachShadow({ mode: "open" })
+ .appendChild(container.querySelector('.shadowtree').content.cloneNode(true));
+ }, { SECTION: [{ SECTION: [{ SECTION: [{ name: "red"} ] }] }] });
+ }
+
+ async function changeSlotFlat() {
+ await _dynamicShadowTest("changeSlotFlat",
+ (container, host) => {
+ container.querySelector('.red').removeAttribute('slot');
+ container.querySelector('.green').setAttribute('slot', 'myslot');
+ }, { SECTION: [{ SECTION: [{ name: "green"} ] }] });
+ }
+
+ async function changeSlotOneDeep() {
+ await _dynamicShadowTest("changeSlotOneDeep",
+ (container, host) => {
+ container.querySelector('.red').removeAttribute('slot');
+ container.querySelector('.green').setAttribute('slot', 'myslot');
+ }, { SECTION: [{ SECTION: [{ SECTION: [{ name: "green"} ] }] }] }, ["shadowdiv"]);
+ }
+
+ // Nested roots and slots
+ async function changeSlotNested() {
+ await _dynamicShadowTest("changeSlotNested",
+ (container, host) => {
+ testAccessibleTree(getNode("changeSlotNested"),
+ { SECTION: [{ SECTION: [{ SECTION: [{ name: "red"} ] }] }] });
+ container.querySelector('.red').removeAttribute('slot');
+ container.querySelector('.green').setAttribute('slot', 'myslot');
+ }, { SECTION: [{ SECTION: [{ SECTION: [{ name: "green"} ] }] }] }, ["shadowdiv"]);
+ }
+
+ async function changeSlotSingleChild() {
+ await _dynamicShadowTest("changeSlotSingleChild",
+ (container, host) => {
+ container.querySelector('.red').setAttribute('slot', 'invalid');
+ }, { SECTION: [{ SECTION: [] }] });
+ }
+
+ async function changeSlotNoShadow() {
+ await _dynamicShadowTest("changeSlotNoShadow",
+ (container, host) => {
+ // Make sure changing the slot attribute doesn't remove an element
+ // even when it remains in the flat tree.
+ container.querySelector('.red').setAttribute('slot', 'invalid');
+ // We need a reorder to know when we're done here, so remove another
+ // child.
+ container.querySelector('.green').remove();
+ }, { SECTION: [{ SECTION: [{ name: "red"} ] }] });
+ }
+
+ // Dynamic mutations to both shadow root and shadow host subtrees
+ // testing/web-platform/tests/css/css-scoping/shadow-assign-dynamic-001.html
+ async function assignSlotDynamic() {
+ await _dynamicShadowTest("assignSlotDynamic",
+ (container, host) => {
+ host.shadowRoot.appendChild(container.querySelector('.shadowtree').content.cloneNode(true));
+ host.appendChild(container.querySelector('.lighttree').content.cloneNode(true));
+ }, { SECTION: [{ SECTION: [{ name: "slot1"}, { name: "slot2" } ] }] });
+ }
+
+ // testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-001.html
+ async function shadowFallbackDynamic_1() {
+ await _dynamicShadowTest("shadowFallbackDynamic_1",
+ (container, host) => {
+ host.firstElementChild.remove();
+ }, { SECTION: [{ SECTION: [{ name: "green"} ] }] });
+ }
+
+ // testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-002.html
+ async function shadowFallbackDynamic_2() {
+ await _dynamicShadowTest("shadowFallbackDynamic_2",
+ (container, host) => {
+ host.firstElementChild.removeAttribute("slot");
+ }, { SECTION: [{ SECTION: [{ name: "green"} ] }] });
+ }
+
+ // testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-003.html
+ async function shadowFallbackDynamic_3() {
+ await _dynamicShadowTest("shadowFallbackDynamic_3",
+ (container, host) => {
+ host.appendChild(container.querySelector(".lighttree").content.cloneNode(true));
+ }, { SECTION: [{ SECTION: [{ name: "green"} ] }] });
+ }
+
+ // testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-004.html
+ async function shadowFallbackDynamic_4() {
+ await _dynamicShadowTest("shadowFallbackDynamic_4",
+ (container, host) => {
+ host.shadowRoot.insertBefore(
+ container.querySelector(".moreshadowtree").
+ content.cloneNode(true), host.shadowRoot.firstChild);
+ }, { SECTION: [{ SECTION: [{ name: "slotparent2", children: [{ name: "green"} ] }, { name: "slotparent1" } ] }] });
+ }
+
+ // testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-004.html
+ // This tests a case when the the slotted element would remain in the same accessible container
+ async function shadowFallbackDynamic_4_1() {
+ await _dynamicShadowTest("shadowFallbackDynamic_4_1",
+ (container, host) => {
+ host.shadowRoot.insertBefore(
+ container.querySelector(".moreshadowtree").
+ content.cloneNode(true), host.shadowRoot.firstChild);
+ }, { SECTION: [{ SECTION: [ { name: "green"}, { SEPARATOR: [] } ] }] });
+ }
+
+ // testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-005.html
+ async function shadowFallbackDynamic_5() {
+ await _dynamicShadowTest("shadowFallbackDynamic_5",
+ (container, host) => {
+ host.firstElementChild.setAttribute("slot", "myotherslot");
+ }, { SECTION: [{ SECTION: [{ name: "green"} ] }] });
+ }
+
+ // testing/web-platform/tests/css/css-scoping/shadow-reassign-dynamic-002.html
+ async function shadowReassignDynamic_2() {
+ await _dynamicShadowTest("shadowReassignDynamic_2",
+ (container, host) => {
+ host.shadowRoot.querySelector("slot").setAttribute("name", "myslot");
+ }, { SECTION: [{ SECTION: [{ name: "green"} ] }] });
+ }
+
+ // testing/web-platform/tests/css/css-scoping/shadow-reassign-dynamic-003.html
+ async function shadowReassignDynamic_3() {
+ await _dynamicShadowTest("shadowReassignDynamic_3",
+ (container, host) => {
+ testAccessibleTree(container, { SECTION: [{ SECTION: [{ name: "green"}, { name: "red", children: [ { PUSHBUTTON: [] }]} ] }] });
+ host.shadowRoot.querySelector("slot[name]").removeAttribute("name");
+
+ }, { SECTION: [{ SECTION: [{ name: "green", children: [ { PUSHBUTTON: [] }]}, { name: "red"} ] }] },
+ [evt => evt.accessible.name == "green", evt => evt.accessible.name == "red"]);
+ }
+
+ // testing/web-platform/tests/css/css-scoping/shadow-reassign-dynamic-004.html
+ async function shadowReassignDynamic_4() {
+ await _dynamicShadowTest("shadowReassignDynamic_4",
+ (container, host) => {
+ host.shadowRoot.getElementById("slot").remove();
+ }, { SECTION: [{ SECTION: [{ name: "green"} ] }] });
+ }
+
+ function shadowProcessInvalidation() {
+ testAccessibleTree("shadowProcessInvalidation",
+ { SECTION: [{
+ SECTION: [{
+ SECTION: [{ TEXT_LEAF: { name: "Hello "} },
+ { TEXT: [{ TEXT_LEAF: { name: "World"} }] },
+ { PUSHBUTTON: { name: "World"} }]
+ }]
+ }]
+ });
+ }
+
+ async function justAttachShadow() {
+ await _dynamicShadowTest("justAttachShadow",
+ (container, host) => {
+ host.attachShadow({ mode: "open" });
+ }, { SECTION: [{ SECTION: [] }] });
+ }
+
+ async function doTest() {
+ await attachFlatShadow();
+
+ await attachOneDeepShadow();
+
+ await changeSlotFlat();
+
+ await changeSlotOneDeep();
+
+ await changeSlotNested();
+
+ await changeSlotSingleChild();
+
+ await changeSlotNoShadow();
+
+ await assignSlotDynamic();
+
+ await shadowFallbackDynamic_1();
+
+ await shadowFallbackDynamic_2();
+
+ await shadowFallbackDynamic_3();
+
+ await shadowFallbackDynamic_4();
+
+ await shadowFallbackDynamic_4_1();
+
+ await shadowFallbackDynamic_5();
+
+ await shadowReassignDynamic_2();
+
+ await shadowReassignDynamic_3();
+
+ await shadowReassignDynamic_4();
+
+ shadowProcessInvalidation();
+
+ await justAttachShadow();
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+ <div id="attachFlatShadow">
+ <template class="shadowtree">
+ <style>::slotted(div) { width: 100px; height: 100px }</style>
+ <slot name="myslot">FAIL</slot>
+ </template>
+ <section class="host">
+ <div style="background: green" aria-label="green"></div>
+ <div style="background: red" aria-label="red" slot="myslot"></div>
+ </section>
+ </div>
+
+ <div id="attachOneDeepShadow">
+ <template class="shadowtree">
+ <style>::slotted(div) { width: 100px; height: 100px }</style>
+ <div id="shadowdiv">
+ <slot name="myslot">FAIL</slot>
+ </div>
+ </template>
+ <section class="host">
+ <div style="background: green" aria-label="green"></div>
+ <div style="background: red" aria-label="red" slot="myslot"></div>
+ </section>
+ </div>
+
+ <div id="changeSlotFlat">
+ <template class="shadowtree">
+ <style>::slotted(div) { width: 100px; height: 100px }</style>
+ <slot name="myslot">FAIL</slot>
+ </template>
+ <section class="host">
+ <div class="green" style="background: green" aria-label="green"></div>
+ <div class="red" style="background: red" aria-label="red" slot="myslot"></div>
+ </section>
+ <script>
+ document.querySelector("#changeSlotFlat > .host")
+ .attachShadow({ mode: "open" })
+ .appendChild(document.querySelector("#changeSlotFlat > .shadowtree").content.cloneNode(true));
+ </script>
+ </div>
+
+ <div id="changeSlotOneDeep">
+ <template class="shadowtree">
+ <style>::slotted(div) { width: 100px; height: 100px }</style>
+ <div id="shadowdiv">
+ <slot name="myslot">FAIL</slot>
+ </div>
+ </template>
+ <section class="host">
+ <div class="green" style="background: green" aria-label="green"></div>
+ <div class="red" style="background: red" aria-label="red" slot="myslot"></div>
+ </section>
+ <script>
+ document.querySelector("#changeSlotOneDeep > .host")
+ .attachShadow({ mode: "open" })
+ .appendChild(document.querySelector("#changeSlotOneDeep > .shadowtree").content.cloneNode(true));
+ </script>
+ </div>
+
+ <div id="changeSlotNested">
+ <template class="shadowtree outer">
+ <div id="shadowdiv">
+ <slot name="myslot">FAIL</slot>
+ </div>
+ </template>
+ <template class="shadowtree inner">
+ <style>::slotted(div) { width: 100px; height: 100px }</style>
+ <slot>FAIL</slot>
+ </template>
+ <section class="host">
+ <div class="green" style="background: green" aria-label="green"></div>
+ <div class="red" style="background: red" aria-label="red" slot="myslot"></div>
+ </section>
+ <script>
+ (function foo() {
+ let outerShadow =
+ document.querySelector("#changeSlotNested > .host").
+ attachShadow({ mode: "open" });
+ outerShadow.appendChild(
+ document.querySelector("#changeSlotNested > .shadowtree.outer").
+ content.cloneNode(true));
+ let innerShadow =
+ outerShadow.querySelector("#shadowdiv").
+ attachShadow({ mode: "open" });
+ innerShadow.appendChild(
+ document.querySelector("#changeSlotNested > .shadowtree.inner").
+ content.cloneNode(true));
+ })();
+ </script>
+ </div>
+
+ <div id="changeSlotSingleChild">
+ <template class="shadowtree">
+ <style>::slotted(div) { width: 100px; height: 100px }</style>
+ <slot></slot>
+ </template>
+ <section class="host">
+ <div class="red" style="background: red" aria-label="red"></div>
+ </section>
+ <script>
+ document.querySelector("#changeSlotSingleChild > .host")
+ .attachShadow({ mode: "open" })
+ .appendChild(document.querySelector("#changeSlotSingleChild > .shadowtree").content.cloneNode(true));
+ </script>
+ </div>
+
+ <div id="changeSlotNoShadow">
+ <section class="host">
+ <div class="red" style="background: red; width: 100px; height: 100px;" aria-label="red"></div>
+ <div class="green" style="background: green; width: 100px; height: 100px;" aria-label="green"></div>
+ </section>
+ </div>
+
+ <div id="assignSlotDynamic">
+ <template class="shadowtree">
+ <style>::slotted(div) { width: 50px; height: 100px }</style>
+ <slot name="slot1">FAIL</slot>
+ <slot name="slot2">FAIL</slot>
+ </template>
+ <template class="lighttree">
+ <div aria-label="slot1" slot="slot1"></div>
+ <div aria-label="slot2" slot="slot2"></div>
+ </template>
+ <section class="host"></section>
+ <script>
+ document.querySelector("#assignSlotDynamic > .host").attachShadow({ mode: "open" });
+ </script>
+ </div>
+
+ <div id="shadowFallbackDynamic_1">
+ <template class="shadowtree">
+ <slot name="myslot">
+ <div aria-label="green" style="width: 100px; height: 100px; background: green"></div>
+ </slot>
+ </template>
+ <section class="host"><span slot="myslot">FAIL</span></section>
+ <script>
+ document.querySelector("#shadowFallbackDynamic_1 > .host")
+ .attachShadow({ mode: "open" })
+ .appendChild(document.querySelector("#shadowFallbackDynamic_1 > .shadowtree").content.cloneNode(true));
+ </script>
+ </div>
+
+ <div id="shadowFallbackDynamic_2">
+ <template class="shadowtree">
+ <slot name="myslot">
+ <div aria-label="green" style="width: 100px; height: 100px; background: green"></div>
+ </slot>
+ </template>
+ <section class="host"><span slot="myslot">FAIL</span></section>
+ <script>
+ document.querySelector("#shadowFallbackDynamic_2 > .host")
+ .attachShadow({ mode: "open" })
+ .appendChild(document.querySelector("#shadowFallbackDynamic_2 > .shadowtree").content.cloneNode(true));
+ </script>
+ </div>
+
+ <div id="shadowFallbackDynamic_3">
+ <template class="shadowtree">
+ <slot name="myslot">FAIL</slot>
+ </template>
+ <template class="lighttree">
+ <div aria-label="green" slot="myslot" style="width: 100px; height: 100px; background: green"></div>
+ </template>
+ <section class="host"></section>
+ <script>
+ document.querySelector("#shadowFallbackDynamic_3 > .host")
+ .attachShadow({ mode: "open" })
+ .appendChild(document.querySelector("#shadowFallbackDynamic_3 > .shadowtree").content.cloneNode(true));
+ </script>
+ </div>
+
+ <div id="shadowFallbackDynamic_4">
+ <template class="shadowtree">
+ <div aria-label="slotparent1"><slot name="myslot"></slot></div>
+ </template>
+ <template class="moreshadowtree">
+ <div aria-label="slotparent2"><slot name="myslot">FAIL</slot></div>
+ </template>
+ <section class="host">
+ <div slot="myslot" aria-label="green" style="width: 100px; height: 100px; background: green"></div>
+ </section>
+ <script>
+ document.querySelector("#shadowFallbackDynamic_4 > .host")
+ .attachShadow({ mode: "open" })
+ .appendChild(document.querySelector("#shadowFallbackDynamic_4 > .shadowtree").content.cloneNode(true));
+ </script>
+ </div>
+
+ <div id="shadowFallbackDynamic_4_1">
+ <template class="shadowtree">
+ <hr>
+ <slot name="myslot"></slot>
+ </template>
+ <template class="moreshadowtree">
+ <slot name="myslot">FAIL</slot>
+ </template>
+ <section class="host">
+ <div slot="myslot" aria-label="green" style="width: 100px; height: 100px; background: green"></div>
+ </section>
+ <script>
+ document.querySelector("#shadowFallbackDynamic_4_1 > .host")
+ .attachShadow({ mode: "open" })
+ .appendChild(document.querySelector("#shadowFallbackDynamic_4_1 > .shadowtree").content.cloneNode(true));
+ </script>
+ </div>
+
+ <div id="shadowFallbackDynamic_5">
+ <template class="shadowtree">
+ <slot name="myslot"></slot>
+ <slot name="myotherslot">FAIL</slot>
+ </template>
+ <section class="host">
+ <div slot="myslot" aria-label="green" style="width: 100px; height: 100px; background: green"></div>
+ </section>
+ <script>
+ document.querySelector("#shadowFallbackDynamic_5 > .host")
+ .attachShadow({ mode: "open" })
+ .appendChild(document.querySelector("#shadowFallbackDynamic_5 > .shadowtree").content.cloneNode(true));
+ </script>
+ </div>
+
+ <div id="shadowReassignDynamic_2">
+ <template class="shadowtree">
+ <style>::slotted(div) { width: 100px; height: 100px }</style>
+ <slot>FAIL</slot>
+ </template>
+ <section class="host">
+ <div slot="myslot" aria-label="green" style="width: 100px; height: 100px; background: green"></div>
+ </section>
+ <script>
+ document.querySelector("#shadowReassignDynamic_2 > .host")
+ .attachShadow({ mode: "open" })
+ .appendChild(document.querySelector("#shadowReassignDynamic_2 > .shadowtree").content.cloneNode(true));
+ </script>
+ </div>
+
+ <div id="shadowReassignDynamic_3">
+ <template class="shadowtree">
+ <div aria-label="green"><slot name="nomatch"></slot></div>
+ <div aria-label="red"><slot></slot></div>
+ </template>
+ <section class="host">
+ <div role="button"></div>
+ </section>
+ <script>
+ document.querySelector("#shadowReassignDynamic_3 > .host")
+ .attachShadow({ mode: "open" })
+ .appendChild(document.querySelector("#shadowReassignDynamic_3 > .shadowtree").content.cloneNode(true));
+ </script>
+ </div>
+
+ <div id="shadowReassignDynamic_4">
+ <template class="shadowtree">
+ <style>::slotted(div),div { width: 100px; height: 100px }</style>
+ <slot id="slot"></slot>
+ <slot>
+ <div aria-label="red" style="background: red"></div>
+ </slot>
+ </template>
+ <section class="host">
+ <div aria-label="green" style="background: green"></div>
+ </section>
+ <script>
+ document.querySelector("#shadowReassignDynamic_4 > .host")
+ .attachShadow({ mode: "open" })
+ .appendChild(document.querySelector("#shadowReassignDynamic_4 > .shadowtree").content.cloneNode(true));
+ </script>
+ </div>
+
+ <div id="shadowProcessInvalidation">
+ <template class="shadowtree">
+ <div id="shadowdiv">
+ <slot></slot>
+ </div>
+ </template>
+ <section class="host">Hello <span id="c">World</span><button aria-labelledby="c"></button></section>
+ <script>
+ document.querySelector("#shadowProcessInvalidation > .host")
+ .attachShadow({ mode: "open" })
+ .appendChild(document.querySelector("#shadowProcessInvalidation > .shadowtree").content.cloneNode(true));
+ </script>
+ </div>
+
+ <div id="justAttachShadow">
+ <section class="host">
+ <button></button>
+ </section>
+ </div>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_shutdown.xhtml b/accessible/tests/mochitest/treeupdate/test_shutdown.xhtml
new file mode 100644
index 0000000000..ad8aebf812
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_shutdown.xhtml
@@ -0,0 +1,131 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL tree hierarchy tests">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../treeview.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+ <script type="application/javascript"
+ src="../events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function setXULTreeView(aTreeID, aTreeView)
+ {
+ this.treeNode = getNode(aTreeID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.treeNode)
+ ];
+
+ this.invoke = function loadXULTree_invoke()
+ {
+ this.treeNode.view = aTreeView;
+ };
+
+ this.getID = function loadXULTree_getID()
+ {
+ return "Load XUL tree " + prettyName(aTreeID);
+ };
+ }
+
+ function removeTree(aID)
+ {
+ this.tree = getAccessible(aID);
+ this.lastItem = null;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, document)
+ ];
+
+ this.invoke = function invoke()
+ {
+ this.lastItem = getAccessible(aID).lastChild;
+ this.lastCell = this.lastItem.lastChild;
+ getNode(aID).remove();
+ };
+
+ this.check = function check(aEvent)
+ {
+ testIsDefunct(this.tree, aID);
+ testIsDefunct(this.lastItem, "last item of " + aID);
+ if (this.lastCell) {
+ testIsDefunct(this.lastCell, "last item cell of " + aID);
+ }
+ };
+
+ this.getID = function getID()
+ {
+ return "Remove tree from DOM";
+ };
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "debug";
+ var gQueue = null;
+
+ function doTest()
+ {
+ gQueue = new eventQueue();
+
+ gQueue.push(new setXULTreeView("tree", new nsTreeTreeView()));
+ gQueue.push(new removeTree("tree"));
+
+ gQueue.push(new setXULTreeView("treetable", new nsTreeTreeView()));
+ gQueue.push(new removeTree("treetable"));
+
+ gQueue.invoke(); // Will call SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727"
+ title="Reorganize implementation of XUL tree accessibility">
+ Bug 503727
+ </a><br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <tree id="tree" flex="1">
+ <treecols>
+ <treecol id="col" flex="1" primary="true" label="column"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+
+ <tree id="treetable" flex="1">
+ <treecols>
+ <treecol id="col1" flex="1" primary="true" label="column"/>
+ <treecol id="col2" flex="1" label="column 2"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ </vbox>
+ </hbox>
+
+</window>
diff --git a/accessible/tests/mochitest/treeupdate/test_table.html b/accessible/tests/mochitest/treeupdate/test_table.html
new file mode 100644
index 0000000000..50fac91757
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_table.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Table update tests</title>
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ function appendCaption(aTableID) {
+ this.invoke = function appendCaption_invoke() {
+ // append a caption, it should appear as a first element in the
+ // accessible tree.
+ var caption = document.createElement("caption");
+ caption.textContent = "table caption";
+ getNode(aTableID).appendChild(caption);
+ };
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, aTableID),
+ ];
+
+ this.finalCheck = function appendCaption_finalCheck() {
+ var tree =
+ { TABLE: [
+ { CAPTION: [
+ { TEXT_LEAF: [] },
+ ] },
+ { ROW: [
+ { CELL: [ {TEXT_LEAF: [] }]},
+ { CELL: [ {TEXT_LEAF: [] }]},
+ ] },
+ ] };
+ testAccessibleTree(aTableID, tree);
+ };
+
+ this.getID = function appendCaption_getID() {
+ return "append caption";
+ };
+ }
+
+ function doTest() {
+ const gQueue = new eventQueue();
+ gQueue.push(new appendCaption("table"));
+ gQueue.invoke(); // Will call SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <table id="table">
+ <tr>
+ <td>cell1</td>
+ <td>cell2</td>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_textleaf.html b/accessible/tests/mochitest/treeupdate/test_textleaf.html
new file mode 100644
index 0000000000..f0181dd754
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_textleaf.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Test accessible recreation</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ function textLeafUpdate(aID, aIsTextLeafLinkable) {
+ this.node = getNode(aID);
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.node.parentNode),
+ ];
+
+ this.finalCheck = function textLeafUpdate_finalCheck() {
+ var textLeaf = getAccessible(this.node).firstChild;
+ is(textLeaf.actionCount, (aIsTextLeafLinkable ? 1 : 0),
+ "Wrong action numbers!");
+ };
+ }
+
+ function setOnClickAttr(aID) {
+ var node = getNode(aID);
+ node.setAttribute("onclick", "alert(3);");
+ var textLeaf = getAccessible(node).firstChild;
+ is(textLeaf.actionCount, 1, "setOnClickAttr: wrong action numbers!");
+ }
+
+ function removeOnClickAttr(aID) {
+ var node = getNode(aID);
+ node.removeAttribute("onclick");
+ var textLeaf = getAccessible(node).firstChild;
+ is(textLeaf.actionCount, 0,
+ "removeOnClickAttr: wrong action numbers!");
+ }
+
+ function setOnClickNRoleAttrs(aID) {
+ this.__proto__ = new textLeafUpdate(aID, true);
+
+ this.invoke = function setOnClickAttr_invoke() {
+ this.node.setAttribute("role", "link");
+ this.node.setAttribute("onclick", "alert(3);");
+ };
+
+ this.getID = function setOnClickAttr_getID() {
+ return "make " + prettyName(aID) + " linkable";
+ };
+ }
+
+ function removeTextData(aID, aRole) {
+ this.containerNode = getNode(aID);
+ this.textNode = this.containerNode.firstChild;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.invoke = function removeTextData_invoke() {
+ var tree = {
+ role: aRole,
+ children: [
+ {
+ role: ROLE_TEXT_LEAF,
+ name: "text",
+ },
+ ],
+ };
+ testAccessibleTree(this.containerNode, tree);
+
+ this.textNode.data = "";
+ };
+
+ this.finalCheck = function removeTextData_finalCheck() {
+ var tree = {
+ role: aRole,
+ children: [],
+ };
+ testAccessibleTree(this.containerNode, tree);
+ };
+
+ this.getID = function removeTextData_finalCheck() {
+ return "remove text data of text node inside '" + aID + "'.";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+
+ function doTest() {
+ // adds onclick on element, text leaf should inherit its action
+ setOnClickAttr("div");
+ // remove onclick attribute, text leaf shouldn't have any action
+ removeOnClickAttr("div");
+
+ // Call rest of event tests.
+ gQueue = new eventQueue();
+
+ // set onclick attribute making span accessible, it's inserted into tree
+ // and adopts text leaf accessible, text leaf should have an action
+ gQueue.push(new setOnClickNRoleAttrs("span"));
+
+ // text data removal of text node should remove its text accessible
+ gQueue.push(new removeTextData("p", ROLE_PARAGRAPH));
+ gQueue.push(new removeTextData("pre", ROLE_TEXT_CONTAINER));
+
+ gQueue.invoke(); // SimpleTest.finish() will be called in the end
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Clean up the code of accessible initialization and binding to the tree"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=545465">
+ Mozilla Bug 545465
+ </a>
+ <a target="_blank"
+ title="Make sure accessible tree is correct when rendered text is changed"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=625652">
+ Mozilla Bug 625652
+ </a>
+ <a target="_blank"
+ title="Remove text accesible getting no text inside a preformatted area"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=706335">
+ Mozilla Bug 706335
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <div id="container">
+ <div id="div">div</div>
+ <span id="span">span</span>
+ </div>
+
+ <p id="p">text</p>
+ <pre id="pre">text</pre>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_tooltip.xhtml b/accessible/tests/mochitest/treeupdate/test_tooltip.xhtml
new file mode 100644
index 0000000000..dba83b2267
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_tooltip.xhtml
@@ -0,0 +1,75 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL tooltip test">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../promisified-events.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ async function doTest() {
+ let tooltip = document.getElementById("tooltip");
+
+ testAccessibleTree("tooltip-container", { GROUPING: [
+ ] });
+
+ let shown = waitForEvent(EVENT_SHOW, tooltip);
+ tooltip.openPopup();
+ await shown;
+
+ testAccessibleTree("tooltip-container",
+ { GROUPING: [
+ { TOOLTIP: [] },
+ { STATICTEXT: [] },
+ ] });
+
+ let hidden = waitForEvent(EVENT_HIDE, tooltip);
+ tooltip.hidePopup();
+ await hidden;
+
+ testAccessibleTree("tooltip-container", { GROUPING: [] });
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1652211"
+ title="Added anonymous tooltip to mochitest docs messes with text">
+ Bug 1652211
+ </a>
+ <br/>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1" role="group" id="tooltip-container">
+ <tooltip id="tooltip">
+ <description class="tooltip-label" value="hello world"/>
+ </tooltip>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/treeupdate/test_visibility.html b/accessible/tests/mochitest/treeupdate/test_visibility.html
new file mode 100644
index 0000000000..4107832b3e
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_visibility.html
@@ -0,0 +1,411 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Style visibility tree update test</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ /**
+ * Hide parent while child stays visible.
+ */
+ function test1(aContainerID, aParentID, aChildID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode(aParentID)),
+ new invokerChecker(EVENT_SHOW, getNode(aChildID)),
+ new invokerChecker(EVENT_REORDER, getNode(aContainerID)),
+ ];
+
+ this.invoke = function invoke() {
+ var tree =
+ { SECTION: [
+ { SECTION: [
+ { SECTION: [
+ { TEXT_LEAF: [] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree(aContainerID, tree);
+
+ getNode(aParentID).style.visibility = "hidden";
+ };
+
+ this.finalCheck = function finalCheck() {
+ var tree =
+ { SECTION: [
+ { SECTION: [
+ { TEXT_LEAF: [] },
+ ] },
+ ] };
+ testAccessibleTree(aContainerID, tree);
+ };
+
+ this.getID = function getID() {
+ return "hide parent while child stays visible";
+ };
+ }
+
+ /**
+ * Hide grand parent while its children stay visible.
+ */
+ function test2(aContainerID, aGrandParentID, aChildID, aChild2ID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode(aGrandParentID)),
+ new invokerChecker(EVENT_SHOW, getNode(aChildID)),
+ new invokerChecker(EVENT_SHOW, getNode(aChild2ID)),
+ new invokerChecker(EVENT_REORDER, getNode(aContainerID)),
+ ];
+
+ this.invoke = function invoke() {
+ var tree =
+ { SECTION: [ // container
+ { SECTION: [ // grand parent
+ { SECTION: [
+ { SECTION: [ // child
+ { TEXT_LEAF: [] },
+ ] },
+ { SECTION: [ // child2
+ { TEXT_LEAF: [] },
+ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree(aContainerID, tree);
+
+ getNode(aGrandParentID).style.visibility = "hidden";
+ };
+
+ this.finalCheck = function finalCheck() {
+ var tree =
+ { SECTION: [ // container
+ { SECTION: [ // child
+ { TEXT_LEAF: [] },
+ ] },
+ { SECTION: [ // child2
+ { TEXT_LEAF: [] },
+ ] },
+ ] };
+ testAccessibleTree(aContainerID, tree);
+ };
+
+ this.getID = function getID() {
+ return "hide grand parent while its children stay visible";
+ };
+ }
+
+ /**
+ * Change container style, hide parents while their children stay visible.
+ */
+ function test3(aContainerID, aParentID, aParent2ID, aChildID, aChild2ID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode(aParentID)),
+ new invokerChecker(EVENT_HIDE, getNode(aParent2ID)),
+ new invokerChecker(EVENT_SHOW, getNode(aChildID)),
+ new invokerChecker(EVENT_SHOW, getNode(aChild2ID)),
+ new invokerChecker(EVENT_REORDER, getNode(aContainerID)),
+ ];
+
+ this.invoke = function invoke() {
+ var tree =
+ { SECTION: [ // container
+ { SECTION: [ // parent
+ { SECTION: [ // child
+ { TEXT_LEAF: [] },
+ ] },
+ ] },
+ { SECTION: [ // parent2
+ { SECTION: [ // child2
+ { TEXT_LEAF: [] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree(aContainerID, tree);
+
+ getNode(aContainerID).style.color = "red";
+ getNode(aParentID).style.visibility = "hidden";
+ getNode(aParent2ID).style.visibility = "hidden";
+ };
+
+ this.finalCheck = function finalCheck() {
+ var tree =
+ { SECTION: [ // container
+ { SECTION: [ // child
+ { TEXT_LEAF: [] },
+ ] },
+ { SECTION: [ // child2
+ { TEXT_LEAF: [] },
+ ] },
+ ] };
+ testAccessibleTree(aContainerID, tree);
+ };
+
+ this.getID = function getID() {
+ return "change container style, hide parents while their children stay visible";
+ };
+ }
+
+ /**
+ * Change container style and make visible child inside the table.
+ */
+ function test4(aContainerID, aChildID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_SHOW, getNode(aChildID)),
+ new invokerChecker(EVENT_REORDER, getNode(aChildID).parentNode),
+ ];
+
+ this.invoke = function invoke() {
+ var tree =
+ { SECTION: [
+ { TABLE: [
+ { ROW: [
+ { CELL: [ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree(aContainerID, tree);
+
+ getNode(aContainerID).style.color = "red";
+ getNode(aChildID).style.visibility = "visible";
+ };
+
+ this.finalCheck = function finalCheck() {
+ var tree =
+ { SECTION: [
+ { TABLE: [
+ { ROW: [
+ { CELL: [
+ { SECTION: [
+ { TEXT_LEAF: [] },
+ ] },
+ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree(aContainerID, tree);
+ };
+
+ this.getID = function getID() {
+ return "change container style, make visible child insdie the table";
+ };
+ }
+
+ /**
+ * Hide subcontainer while child inside the table stays visible.
+ */
+ function test5(aContainerID, aSubContainerID, aChildID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode(aSubContainerID)),
+ new invokerChecker(EVENT_SHOW, getNode(aChildID)),
+ new invokerChecker(EVENT_REORDER, getNode(aContainerID)),
+ ];
+
+ this.invoke = function invoke() {
+ var tree =
+ { SECTION: [ // container
+ { SECTION: [ // subcontainer
+ { TABLE: [
+ { ROW: [
+ { CELL: [
+ { SECTION: [ // child
+ { TEXT_LEAF: [] },
+ ] },
+ ] },
+ ] },
+ ] },
+ ] },
+ ] };
+ testAccessibleTree(aContainerID, tree);
+
+ getNode(aSubContainerID).style.visibility = "hidden";
+ };
+
+ this.finalCheck = function finalCheck() {
+ var tree =
+ { SECTION: [ // container
+ { SECTION: [ // child
+ { TEXT_LEAF: [] },
+ ] },
+ ] };
+ testAccessibleTree(aContainerID, tree);
+ };
+
+ this.getID = function getID() {
+ return "hide subcontainer while child inside the table stays visible";
+ };
+ }
+
+ /**
+ * Hide subcontainer while its child and child inside the nested table stays visible.
+ */
+ function test6(aContainerID, aSubContainerID, aChildID, aChild2ID) {
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, getNode(aSubContainerID)),
+ new invokerChecker(EVENT_SHOW, getNode(aChildID)),
+ new invokerChecker(EVENT_SHOW, getNode(aChild2ID)),
+ new invokerChecker(EVENT_REORDER, getNode(aContainerID)),
+ ];
+
+ this.invoke = function invoke() {
+ var tree =
+ { SECTION: [ // container
+ { SECTION: [ // subcontainer
+ { TABLE: [
+ { ROW: [
+ { CELL: [
+ { TABLE: [ // nested table
+ { ROW: [
+ { CELL: [
+ { SECTION: [ // child
+ { TEXT_LEAF: [] } ]} ]} ]} ]} ]} ]} ]},
+ { SECTION: [ // child2
+ { TEXT_LEAF: [] } ]} ]} ]};
+
+ testAccessibleTree(aContainerID, tree);
+
+ // invoke
+ getNode(aSubContainerID).style.visibility = "hidden";
+ };
+
+ this.finalCheck = function finalCheck() {
+ var tree =
+ { SECTION: [ // container
+ { SECTION: [ // child
+ { TEXT_LEAF: [] } ]},
+ { SECTION: [ // child2
+ { TEXT_LEAF: [] } ]} ]};
+
+ testAccessibleTree(aContainerID, tree);
+ };
+
+ this.getID = function getID() {
+ return "hide subcontainer while its child and child inside the nested table stays visible";
+ };
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+
+ function doTest() {
+ gQueue = new eventQueue();
+
+ gQueue.push(new test1("t1_container", "t1_parent", "t1_child"));
+ gQueue.push(new test2("t2_container", "t2_grandparent", "t2_child", "t2_child2"));
+ gQueue.push(new test3("t3_container", "t3_parent", "t3_parent2", "t3_child", "t3_child2"));
+ gQueue.push(new test4("t4_container", "t4_child"));
+ gQueue.push(new test5("t5_container", "t5_subcontainer", "t5_child"));
+ gQueue.push(new test6("t6_container", "t6_subcontainer", "t6_child", "t6_child2"));
+
+ gQueue.invoke(); // SimpleTest.finish() will be called in the end
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Develop a way to handle visibility style"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=606125">
+ Mozilla Bug 606125
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- hide parent while child stays visible -->
+ <div id="t1_container">
+ <div id="t1_parent">
+ <div id="t1_child" style="visibility: visible">text</div>
+ </div>
+ </div>
+
+ <!-- hide grandparent while its children stay visible -->
+ <div id="t2_container">
+ <div id="t2_grandparent">
+ <div id="t2_parent">
+ <div id="t2_child" style="visibility: visible">text</div>
+ <div id="t2_child2" style="visibility: visible">text</div>
+ </div>
+ </div>
+ </div>
+
+ <!-- change container style, hide parents while their children stay visible -->
+ <div id="t3_container">
+ <div id="t3_parent">
+ <div id="t3_child" style="visibility: visible">text</div>
+ </div>
+ <div id="t3_parent2">
+ <div id="t3_child2" style="visibility: visible">text</div>
+ </div>
+ </div>
+
+ <!-- change container style, show child inside the table -->
+ <div id="t4_container">
+ <table>
+ <tr>
+ <td>
+ <div id="t4_child" style="visibility: hidden;">text</div>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ <!-- hide subcontainer while child inside the table stays visible -->
+ <div id="t5_container">
+ <div id="t5_subcontainer">
+ <table>
+ <tr>
+ <td>
+ <div id="t5_child" style="visibility: visible;">text</div>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+
+ <!-- hide subcontainer while its child and child inside the nested table stays visible -->
+ <div id="t6_container">
+ <div id="t6_subcontainer">
+ <table>
+ <tr>
+ <td>
+ <table>
+ <tr>
+ <td>
+ <div id="t6_child" style="visibility: visible;">text</div>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ <div id="t6_child2" style="visibility: visible">text</div>
+ </div>
+ </div>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeupdate/test_whitespace.html b/accessible/tests/mochitest/treeupdate/test_whitespace.html
new file mode 100644
index 0000000000..7d2062387f
--- /dev/null
+++ b/accessible/tests/mochitest/treeupdate/test_whitespace.html
@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Whitespace text accessible creation/destruction</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../role.js"></script>
+ <script type="application/javascript"
+ src="../events.js"></script>
+
+ <script type="application/javascript">
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Invokers
+
+ /**
+ * Middle image accessible removal results in text accessible removal.
+ *
+ * Before:
+ * DOM: whitespace img1 whitespace img2 whitespace img3 whitespace,
+ * a11y: img1 whitespace img2 whitespace img3
+ * After:
+ * DOM: whitespace img1 whitespace whitespace img3 whitespace,
+ * a11y: img1 whitespace img3
+ */
+ function removeImg() {
+ this.containerNode = getNode("container1");
+ this.imgNode = getNode("img1");
+ this.img = getAccessible(this.imgNode);
+ this.text = this.img.nextSibling;
+
+ this.eventSeq = [
+ new invokerChecker(EVENT_HIDE, this.img),
+ new invokerChecker(EVENT_HIDE, this.text),
+ new invokerChecker(EVENT_REORDER, this.containerNode),
+ ];
+
+ this.finalCheck = function textLeafUpdate_finalCheck() {
+ var tree =
+ { SECTION: [
+ { GRAPHIC: [] },
+ { TEXT_LEAF: [] },
+ { GRAPHIC: [] },
+ ] };
+
+ testAccessibleTree(this.containerNode, tree);
+ };
+
+ this.invoke = function setOnClickAttr_invoke() {
+ var tree =
+ { SECTION: [
+ { GRAPHIC: [] },
+ { TEXT_LEAF: [] },
+ { GRAPHIC: [] },
+ { TEXT_LEAF: [] },
+ { GRAPHIC: [] },
+ ] };
+
+ testAccessibleTree(this.containerNode, tree);
+
+ this.containerNode.removeChild(this.imgNode);
+ };
+
+ this.getID = function setOnClickAttr_getID() {
+ return "remove middle img";
+ };
+ }
+
+ /**
+ * Append image making the whitespace visible and thus accessible.
+ * Note: images and whitespaces are on different leves of accessible trees,
+ * so that image container accessible update doesn't update the tree
+ * of whitespace container.
+ *
+ * Before:
+ * DOM: whitespace emptylink whitespace linkwithimg whitespace
+ * a11y: emptylink linkwithimg
+ * After:
+ * DOM: whitespace linkwithimg whitespace linkwithimg whitespace
+ * a11y: linkwithimg whitespace linkwithimg
+ */
+ function insertImg() {
+ this.containerNode = getNode("container2");
+ this.topNode = this.containerNode.parentNode;
+ this.textNode = this.containerNode.nextSibling;
+ this.imgNode = document.createElement("img");
+ this.imgNode.setAttribute("src", "../moz.png");
+
+ this.eventSeq = [
+ new asyncInvokerChecker(EVENT_SHOW, getAccessible, this.textNode),
+ new asyncInvokerChecker(EVENT_SHOW, getAccessible, this.imgNode),
+ new orderChecker(),
+ new invokerChecker(EVENT_REORDER, this.topNode),
+ ];
+
+ this.invoke = function insertImg_invoke() {
+ var tree =
+ { SECTION: [
+ { LINK: [] },
+ { LINK: [
+ { GRAPHIC: [] },
+ ] },
+ ] };
+
+ testAccessibleTree(this.topNode, tree);
+
+ this.containerNode.appendChild(this.imgNode);
+ };
+
+ this.finalCheck = function insertImg_finalCheck() {
+ var tree =
+ { SECTION: [
+ { LINK: [
+ { GRAPHIC: [ ] },
+ ] },
+ { TEXT_LEAF: [ ] },
+ { LINK: [
+ { GRAPHIC: [ ] },
+ ] },
+ ] };
+
+ testAccessibleTree(this.topNode, tree);
+ };
+
+ this.getID = function appendImg_getID() {
+ return "insert img into internal container";
+ };
+ }
+
+ function dontCreateMapWhiteSpace() {
+ const tree = { SECTION: [ { role: ROLE_TEXT_LEAF, name: "x" } ] };
+ testAccessibleTree("container3", tree);
+
+ getNode("c3_inner").style.textAlign = "center";
+ document.body.offsetTop; // Flush layout.
+ window.windowUtils.advanceTimeAndRefresh(100);
+
+ testAccessibleTree("container3", tree);
+ window.windowUtils.restoreNormalRefresh();
+ }
+
+ // //////////////////////////////////////////////////////////////////////////
+ // Test
+
+ // gA11yEventDumpID = "eventdump"; // debug stuff
+ // gA11yEventDumpToConsole = true;
+
+ var gQueue = null;
+
+ function doTest() {
+ dontCreateMapWhiteSpace();
+
+ gQueue = new eventQueue();
+
+ gQueue.push(new removeImg());
+ gQueue.push(new insertImg());
+
+ gQueue.invoke(); // SimpleTest.finish() will be called in the end
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+<body>
+
+ <a target="_blank"
+ title="Make sure accessible tree is correct when rendered text is changed"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=625652">
+ Mozilla Bug 625652
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test">
+ </pre>
+
+ <!-- Whitespace between the div and img tags will be inconsistent depending
+ on the image cache state and what optimizations layout was able to
+ apply. -->
+ <div id="container1"><img src="../moz.png"> <img id="img1" src="../moz.png"> <img src="../moz.png"></div>
+ <div><a id="container2" href=""></a> <a href=""><img src="../moz.png"></a></div>
+
+ <div id="container3">
+ <div id="c3_inner" role="presentation">
+ x<map> </map>
+ </div>
+ </div>
+
+ <div id="eventdump"></div>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/treeview.css b/accessible/tests/mochitest/treeview.css
new file mode 100644
index 0000000000..462f560d6f
--- /dev/null
+++ b/accessible/tests/mochitest/treeview.css
@@ -0,0 +1,15 @@
+treechildren::-moz-tree-checkbox(checked) {
+ list-style-image: url("chrome://global/skin/icons/check.svg");
+}
+
+treechildren::-moz-tree-image(cyclerState1) {
+ list-style-type: disc;
+}
+
+treechildren::-moz-tree-image(cyclerState2) {
+ list-style-type: circle;
+}
+
+treechildren::-moz-tree-image(cyclerState3) {
+ list-style-type: square;
+}
diff --git a/accessible/tests/mochitest/treeview.js b/accessible/tests/mochitest/treeview.js
new file mode 100644
index 0000000000..5dc3b595d8
--- /dev/null
+++ b/accessible/tests/mochitest/treeview.js
@@ -0,0 +1,273 @@
+/* import-globals-from common.js */
+/* import-globals-from events.js */
+
+/**
+ * Helper method to start a single XUL tree test.
+ */
+function loadXULTreeAndDoTest(aDoTestFunc, aTreeID, aTreeView) {
+ var doTestFunc = aDoTestFunc ? aDoTestFunc : gXULTreeLoadContext.doTestFunc;
+ var treeID = aTreeID ? aTreeID : gXULTreeLoadContext.treeID;
+ var treeView = aTreeView ? aTreeView : gXULTreeLoadContext.treeView;
+
+ let treeNode = getNode(treeID);
+
+ gXULTreeLoadContext.queue = new eventQueue();
+ gXULTreeLoadContext.queue.push({
+ treeNode,
+
+ eventSeq: [new invokerChecker(EVENT_REORDER, treeNode)],
+
+ invoke() {
+ this.treeNode.view = treeView;
+ },
+
+ getID() {
+ return "Load XUL tree " + prettyName(treeID);
+ },
+ });
+ gXULTreeLoadContext.queue.onFinish = function () {
+ SimpleTest.executeSoon(doTestFunc);
+ return DO_NOT_FINISH_TEST;
+ };
+ gXULTreeLoadContext.queue.invoke();
+}
+
+/**
+ * Analogy of addA11yLoadEvent, nice helper to load XUL tree and start the test.
+ */
+function addA11yXULTreeLoadEvent(aDoTestFunc, aTreeID, aTreeView) {
+ gXULTreeLoadContext.doTestFunc = aDoTestFunc;
+ gXULTreeLoadContext.treeID = aTreeID;
+ gXULTreeLoadContext.treeView = aTreeView;
+
+ addA11yLoadEvent(loadXULTreeAndDoTest);
+}
+
+function nsTableTreeView(aRowCount) {
+ this.__proto__ = new nsTreeView();
+
+ for (var idx = 0; idx < aRowCount; idx++) {
+ this.mData.push(new treeItem("row" + String(idx) + "_"));
+ }
+}
+
+function nsTreeTreeView() {
+ this.__proto__ = new nsTreeView();
+
+ this.mData = [
+ new treeItem("row1"),
+ new treeItem("row2_", true, [
+ new treeItem("row2.1_"),
+ new treeItem("row2.2_"),
+ ]),
+ new treeItem("row3_", false, [
+ new treeItem("row3.1_"),
+ new treeItem("row3.2_"),
+ ]),
+ new treeItem("row4"),
+ ];
+}
+
+function nsTreeView() {
+ this.mTree = null;
+ this.mData = [];
+}
+
+nsTreeView.prototype = {
+ // ////////////////////////////////////////////////////////////////////////////
+ // nsITreeView implementation
+
+ get rowCount() {
+ return this.getRowCountIntl(this.mData);
+ },
+ setTree: function setTree(aTree) {
+ this.mTree = aTree;
+ },
+ getCellText: function getCellText(aRow, aCol) {
+ var data = this.getDataForIndex(aRow);
+ if (aCol.id in data.colsText) {
+ return data.colsText[aCol.id];
+ }
+
+ return data.text + aCol.id;
+ },
+ getCellValue: function getCellValue(aRow, aCol) {
+ var data = this.getDataForIndex(aRow);
+ return data.value;
+ },
+ getRowProperties: function getRowProperties(aIndex) {
+ return "";
+ },
+ getCellProperties: function getCellProperties(aIndex, aCol) {
+ if (!aCol.cycler) {
+ return "";
+ }
+
+ var data = this.getDataForIndex(aIndex);
+ return this.mCyclerStates[data.cyclerState];
+ },
+ getColumnProperties: function getColumnProperties(aCol) {
+ return "";
+ },
+ getParentIndex: function getParentIndex(aRowIndex) {
+ var info = this.getInfoByIndex(aRowIndex);
+ return info.parentIndex;
+ },
+ hasNextSibling: function hasNextSibling(aRowIndex, aAfterIndex) {},
+ getLevel: function getLevel(aIndex) {
+ var info = this.getInfoByIndex(aIndex);
+ return info.level;
+ },
+ getImageSrc: function getImageSrc(aRow, aCol) {},
+ isContainer: function isContainer(aIndex) {
+ var data = this.getDataForIndex(aIndex);
+ return data.open != undefined;
+ },
+ isContainerOpen: function isContainerOpen(aIndex) {
+ var data = this.getDataForIndex(aIndex);
+ return data.open;
+ },
+ isContainerEmpty: function isContainerEmpty(aIndex) {
+ var data = this.getDataForIndex(aIndex);
+ return data.open == undefined;
+ },
+ isSeparator: function isSeparator(aIndex) {},
+ isSorted: function isSorted() {},
+ toggleOpenState: function toggleOpenState(aIndex) {
+ var data = this.getDataForIndex(aIndex);
+
+ data.open = !data.open;
+ var rowCount = this.getRowCountIntl(data.children);
+
+ if (data.open) {
+ this.mTree.rowCountChanged(aIndex + 1, rowCount);
+ } else {
+ this.mTree.rowCountChanged(aIndex + 1, -rowCount);
+ }
+ },
+ selectionChanged: function selectionChanged() {},
+ cycleHeader: function cycleHeader(aCol) {},
+ cycleCell: function cycleCell(aRow, aCol) {
+ var data = this.getDataForIndex(aRow);
+ data.cyclerState = (data.cyclerState + 1) % 3;
+
+ this.mTree.invalidateCell(aRow, aCol);
+ },
+ isEditable: function isEditable(aRow, aCol) {
+ return true;
+ },
+ setCellText: function setCellText(aRow, aCol, aValue) {
+ var data = this.getDataForIndex(aRow);
+ data.colsText[aCol.id] = aValue;
+ },
+ setCellValue: function setCellValue(aRow, aCol, aValue) {
+ var data = this.getDataForIndex(aRow);
+ data.value = aValue;
+
+ this.mTree.invalidateCell(aRow, aCol);
+ },
+
+ // ////////////////////////////////////////////////////////////////////////////
+ // public implementation
+
+ appendItem: function appendItem(aText) {
+ this.mData.push(new treeItem(aText));
+ },
+
+ // ////////////////////////////////////////////////////////////////////////////
+ // private implementation
+
+ getDataForIndex: function getDataForIndex(aRowIndex) {
+ return this.getInfoByIndex(aRowIndex).data;
+ },
+
+ getInfoByIndex: function getInfoByIndex(aRowIndex) {
+ var info = {
+ data: null,
+ parentIndex: -1,
+ level: 0,
+ index: -1,
+ };
+
+ this.getInfoByIndexIntl(aRowIndex, info, this.mData, 0);
+ return info;
+ },
+
+ getRowCountIntl: function getRowCountIntl(aChildren) {
+ var rowCount = 0;
+ for (var childIdx = 0; childIdx < aChildren.length; childIdx++) {
+ rowCount++;
+
+ var data = aChildren[childIdx];
+ if (data.open) {
+ rowCount += this.getRowCountIntl(data.children);
+ }
+ }
+
+ return rowCount;
+ },
+
+ getInfoByIndexIntl: function getInfoByIndexIntl(
+ aRowIdx,
+ aInfo,
+ aChildren,
+ aLevel
+ ) {
+ var rowIdx = aRowIdx;
+ for (var childIdx = 0; childIdx < aChildren.length; childIdx++) {
+ var data = aChildren[childIdx];
+
+ aInfo.index++;
+
+ if (rowIdx == 0) {
+ aInfo.data = data;
+ aInfo.level = aLevel;
+ return -1;
+ }
+
+ if (data.open) {
+ var parentIdx = aInfo.index;
+ rowIdx = this.getInfoByIndexIntl(
+ rowIdx - 1,
+ aInfo,
+ data.children,
+ aLevel + 1
+ );
+
+ if (rowIdx == -1) {
+ if (aInfo.parentIndex == -1) {
+ aInfo.parentIndex = parentIdx;
+ }
+ return 0;
+ }
+ } else {
+ rowIdx--;
+ }
+ }
+
+ return rowIdx;
+ },
+
+ mCyclerStates: ["cyclerState1", "cyclerState2", "cyclerState3"],
+};
+
+function treeItem(aText, aOpen, aChildren) {
+ this.text = aText;
+ this.colsText = {};
+ this.open = aOpen;
+ this.value = "true";
+ this.cyclerState = 0;
+ if (aChildren) {
+ this.children = aChildren;
+ }
+}
+
+/**
+ * Used in conjunction with loadXULTreeAndDoTest and addA11yXULTreeLoadEvent.
+ */
+var gXULTreeLoadContext = {
+ doTestFunc: null,
+ treeID: null,
+ treeView: null,
+ queue: null,
+};
diff --git a/accessible/tests/mochitest/value.js b/accessible/tests/mochitest/value.js
new file mode 100644
index 0000000000..379403ecaf
--- /dev/null
+++ b/accessible/tests/mochitest/value.js
@@ -0,0 +1,52 @@
+/* import-globals-from common.js */
+
+// //////////////////////////////////////////////////////////////////////////////
+// Public methods
+
+/**
+ * Tests nsIAccessibleValue interface.
+ *
+ * @param aAccOrElmOrId [in] identifier of accessible
+ * @param aValue [in] accessible value (nsIAccessible::value)
+ * @param aCurrValue [in] current value (nsIAccessibleValue::currentValue)
+ * @param aMinValue [in] minimum value (nsIAccessibleValue::minimumValue)
+ * @param aMaxValue [in] maximumn value (nsIAccessibleValue::maximumValue)
+ * @param aMinIncr [in] minimum increment value
+ * (nsIAccessibleValue::minimumIncrement)
+ */
+function testValue(
+ aAccOrElmOrId,
+ aValue,
+ aCurrValue,
+ aMinValue,
+ aMaxValue,
+ aMinIncr
+) {
+ var acc = getAccessible(aAccOrElmOrId, [nsIAccessibleValue]);
+ if (!acc) {
+ return;
+ }
+
+ is(acc.value, aValue, "Wrong value of " + prettyName(aAccOrElmOrId));
+
+ is(
+ acc.currentValue,
+ aCurrValue,
+ "Wrong current value of " + prettyName(aAccOrElmOrId)
+ );
+ is(
+ acc.minimumValue,
+ aMinValue,
+ "Wrong minimum value of " + prettyName(aAccOrElmOrId)
+ );
+ is(
+ acc.maximumValue,
+ aMaxValue,
+ "Wrong maximum value of " + prettyName(aAccOrElmOrId)
+ );
+ is(
+ acc.minimumIncrement,
+ aMinIncr,
+ "Wrong minimum increment value of " + prettyName(aAccOrElmOrId)
+ );
+}
diff --git a/accessible/tests/mochitest/value/a11y.toml b/accessible/tests/mochitest/value/a11y.toml
new file mode 100644
index 0000000000..c64d23d8cb
--- /dev/null
+++ b/accessible/tests/mochitest/value/a11y.toml
@@ -0,0 +1,16 @@
+[DEFAULT]
+support-files = "!/accessible/tests/mochitest/*.js"
+
+["test_ariavalue.html"]
+
+["test_datetime.html"]
+
+["test_general.html"]
+
+["test_meter.html"]
+
+["test_number.html"]
+
+["test_progress.html"]
+
+["test_range.html"]
diff --git a/accessible/tests/mochitest/value/test_ariavalue.html b/accessible/tests/mochitest/value/test_ariavalue.html
new file mode 100644
index 0000000000..bdb62a866d
--- /dev/null
+++ b/accessible/tests/mochitest/value/test_ariavalue.html
@@ -0,0 +1,85 @@
+<html>
+
+<head>
+ <title>nsIAccessible value testing for implicit aria-value* attributes</title>
+
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript" src="../common.js"></script>
+ <script type="application/javascript" src="../value.js"></script>
+
+ <script src="chrome://mochikit/content/chrome-harness.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ for (const role of ["slider", "scrollbar"]) {
+ testValue(`${role}_default`, "50", 50, 0, 100, 0);
+ testValue(`${role}_min1max50`, "25.5", 25.5, 1, 50, 0);
+ testValue(`${role}_max200`, "100", 100, 0, 200, 0);
+ testValue(`${role}_min10`, "55", 55, 10, 100, 0);
+ testValue(`${role}_vt`, "juice", 50, 0, 100, 0);
+ testValue(`${role}_vn`, "6", 6, 0, 100, 0);
+ testValue(`${role}_vtvn`, "juice", 6, 0, 100, 0);
+ }
+
+ testValue("spinbutton_default", "", 0, 0, 0, 0);
+ testValue("spinbutton_min1max50", "", 0, 1, 50, 0);
+ testValue("spinbutton_max200", "", 0, 0, 200, 0);
+ testValue("spinbutton_min10", "", 0, 10, 0, 0);
+ testValue("spinbutton_vt", "juice", 0, 0, 0, 0);
+ testValue("spinbutton_vn", "6", 6, 0, 0, 0);
+ testValue("spinbutton_vtvn", "juice", 6, 0, 0, 0);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1357071"
+ title="Add support for implicit values for aria-value* attributes for scrollbar and slider roles">
+ Bug 1357071
+ </a>
+
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+
+ <!-- ARIA sliders -->
+ <div id="slider_default" role="slider">vanilla slider</div>
+ <div id="slider_min1max50" role="slider" aria-valuemin="1" aria-valuemax="50">banana slider</div>
+ <div id="slider_max200" role="slider" aria-valuemax="200">cherry slider</div>
+ <div id="slider_min10" role="slider" aria-valuemin="10">strawberry slider</div>
+ <div id="slider_vt" role="slider" aria-valuetext="juice">orange slider</div>
+ <div id="slider_vn" role="slider" aria-valuenow="6">chocolate slider</div>
+ <div id="slider_vtvn" role="slider" aria-valuetext="juice" aria-valuenow="6">apple slider</div>
+
+ <!-- ARIA scrollbars -->
+ <div id="scrollbar_default" role="scrollbar">vanilla scrollbar</div>
+ <div id="scrollbar_min1max50" role="scrollbar" aria-valuemin="1" aria-valuemax="50">banana scrollbar</div>
+ <div id="scrollbar_max200" role="scrollbar" aria-valuemax="200">cherry scrollbar</div>
+ <div id="scrollbar_min10" role="scrollbar" aria-valuemin="10">strawberry scrollbar</div>
+ <div id="scrollbar_vt" role="scrollbar" aria-valuetext="juice">orange scrollbar</div>
+ <div id="scrollbar_vn" role="scrollbar" aria-valuenow="6">chocolate scrollbar</div>
+ <div id="scrollbar_vtvn" role="scrollbar" aria-valuetext="juice" aria-valuenow="6">apple scrollbar</div>
+
+ <!-- ARIA spinbuttons -->
+ <div id="spinbutton_default" role="spinbutton">vanilla spinbutton</div>
+ <div id="spinbutton_min1max50" role="spinbutton" aria-valuemin="1" aria-valuemax="50">banana spinbutton</div>
+ <div id="spinbutton_max200" role="spinbutton" aria-valuemax="200">cherry spinbutton</div>
+ <div id="spinbutton_min10" role="spinbutton" aria-valuemin="10">strawberry spinbutton</div>
+ <div id="spinbutton_vt" role="spinbutton" aria-valuetext="juice">orange spinbutton</div>
+ <div id="spinbutton_vn" role="spinbutton" aria-valuenow="6">chocolate spinbutton</div>
+ <div id="spinbutton_vtvn" role="spinbutton" aria-valuetext="juice" aria-valuenow="6">apple spinbutton</div>
+</body>
+
+</html>
diff --git a/accessible/tests/mochitest/value/test_datetime.html b/accessible/tests/mochitest/value/test_datetime.html
new file mode 100644
index 0000000000..e03356ac1a
--- /dev/null
+++ b/accessible/tests/mochitest/value/test_datetime.html
@@ -0,0 +1,76 @@
+<!doctype html>
+<html>
+<head>
+ <title>nsIAccessible value testing for datetime-local input element</title>
+ <link rel="stylesheet"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script src="../common.js"></script>
+ <script src="../promisified-events.js"></script>
+
+ <script>
+ async function doTest() {
+ const dateTimeNode = getNode("datetime");
+ const dateTime = getAccessible(dateTimeNode);
+ // We assume en-US for testing. However, the OS date format might
+ // override the en-US date format.
+ const monthFirst = dateTime.getChildAt(0).name == "Month";
+ const month = dateTime.getChildAt(monthFirst ? 0 : 2);
+ const day = dateTime.getChildAt(monthFirst ? 2 : 0);
+ const year = dateTime.getChildAt(4);
+ const hour = dateTime.getChildAt(6);
+ const minute = dateTime.getChildAt(8);
+ const amPm = dateTime.getChildAt(10);
+
+ // We don't use testValue() because it also checks numeric value, but
+ // we don't support numeric value here because it isn't useful.
+ function assertIsClear() {
+ is(year.value, "");
+ is(month.value, "");
+ is(day.value, "");
+ is(hour.value, "");
+ is(minute.value, "");
+ // Unlike the numeric fields, amPm is a textbox. Since textboxes take
+ // their a11y value from their text content and "--" is set as the text
+ // content, the a11y value is "--".
+ is(amPm.value, "--");
+ }
+
+ info("Checking that input is initially clear");
+ assertIsClear();
+
+ // The container doesn't notify of value changes, so we wait for a value
+ // change on one of the fields to know when it's updated.
+ info("Setting value");
+ let changed = waitForEvent(EVENT_TEXT_VALUE_CHANGE, month);
+ dateTimeNode.value = "2000-01-02T03:04";
+ await changed;
+ is(year.value, "2000");
+ is(month.value, "01");
+ is(day.value, "02");
+ is(hour.value, "03");
+ is(minute.value, "04");
+ // Again, the OS date format might override, so we might get "am" instead
+ // of "AM".
+ is(amPm.value.toLowerCase(), "am");
+
+ info("Clearing value");
+ changed = waitForEvent(EVENT_TEXT_VALUE_CHANGE, month);
+ dateTimeNode.value = "";
+ await changed;
+ assertIsClear();
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+</head>
+
+<body>
+ <input type="datetime-local" id="datetime">
+</body>
+</html>
diff --git a/accessible/tests/mochitest/value/test_general.html b/accessible/tests/mochitest/value/test_general.html
new file mode 100644
index 0000000000..862c5254c0
--- /dev/null
+++ b/accessible/tests/mochitest/value/test_general.html
@@ -0,0 +1,159 @@
+<html>
+
+<head>
+ <title>nsIAccessible value testing</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <style type="text/css">
+ .offscreen {
+ position: absolute;
+ left: -5000px;
+ top: -5000px;
+ height: 100px;
+ width: 100px;
+ }
+ </style>
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+
+ <script src="chrome://mochikit/content/chrome-harness.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ function testValue(aID, aValue) {
+ var acc = getAccessible(aID);
+ if (!acc)
+ return;
+ is(acc.value, aValue, "Wrong value for " + aID + "!");
+ }
+
+ var href = getRootDirectory(window.location.href) + "foo";
+
+ // roles that can't live as HTMLLinkAccessibles
+ testValue("aria_menuitem_link", "");
+ testValue("aria_button_link", "");
+ testValue("aria_checkbox_link", "");
+ testValue("aria_application_link", "");
+ testValue("aria_main_link", "");
+ testValue("aria_navigation_link", "");
+
+ // roles that can live as HTMLLinkAccessibles
+ testValue("aria_link_link", href);
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA textboxes
+
+ testValue("aria_textbox1", "helo");
+ // Textbox containing list.
+ testValue("aria_textbox2", "1. test");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // ARIA comboboxes
+
+ // aria-activedescendant defines a current item the value is computed from
+ testValue("aria_combobox1", kDiscBulletText + "Zoom");
+
+ // aria-selected defines a selected item the value is computed from,
+ // list control is pointed by aria-owns relation.
+ testValue("aria_combobox2", kDiscBulletText + "Zoom");
+
+ // aria-selected defines a selected item the value is computed from,
+ // list control is a child of combobox.
+ testValue("aria_combobox3", kDiscBulletText + "2");
+
+ // ////////////////////////////////////////////////////////////////////////
+ // HTML controls
+ testValue("combobox1", "item1");
+ testValue("combobox2", "item2");
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=494807"
+ title="Do not expose a11y info specific to hyperlinks when role is overridden using ARIA">
+ Bug 494807
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=819273"
+ title="ARIA combobox should have accessible value">
+ Bug 819273
+ </a>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=887250"
+ title="ARIA textbox role doesn't expose value">
+ Bug 887250
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+
+ <a id="aria_menuitem_link" role="menuitem" href="foo">menuitem</a>
+ <a id="aria_button_link" role="button" href="foo">button</a>
+ <a id="aria_checkbox_link" role="checkbox" href="foo">checkbox</a>
+
+ <!-- landmark links -->
+ <a id="aria_application_link" role="application" href="foo">app</a>
+ <a id="aria_main_link" role="main" href="foo">main</a>
+ <a id="aria_navigation_link" role="navigation" href="foo">nav</a>
+
+ <!-- strange edge case: please don't do this in the wild -->
+ <a id="aria_link_link" role="link" href="foo">link</a>
+
+ <div id="aria_textbox1" role="textbox">helo</div>
+ <div id="aria_textbox2" contenteditable role="textbox">
+ <ol><li>test</li></ol>
+ </div>
+
+ <div id="aria_combobox1" role="combobox"
+ aria-owns="aria_combobox1_owned_listbox"
+ aria-activedescendant="aria_combobox1_selected_option">
+ </div>
+ <ul role="listbox" id="aria_combobox1_owned_listbox">
+ <li role="option">Zebra</li>
+ <li role="option" id="aria_combobox1_selected_option">Zoom</li>
+ </ul>
+
+ <div id="aria_combobox2" role="combobox"
+ aria-owns="aria_combobox2_owned_listbox">
+ </div>
+ <ul role="listbox" id="aria_combobox2_owned_listbox">
+ <li role="option">Zebra</li>
+ <li role="option" aria-selected="true">Zoom</li>
+ </ul>
+
+ <div id="aria_combobox3" role="combobox">
+ <div role="textbox"></div>
+ <ul role="listbox">
+ <li role="option">1</li>
+ <li role="option" aria-selected="true">2</li>
+ <li role="option">3</li>
+ </ul>
+ </div>
+
+ <select id="combobox1">
+ <option id="cb1_item1">item1</option>
+ <option id="cb1_item2">item2</option>
+ </select>
+ <select id="combobox2">
+ <option id="cb2_item1">item1</option>
+ <option id="cb2_item2" selected="true">item2</option>
+ </select>
+
+</body>
+</html>
diff --git a/accessible/tests/mochitest/value/test_meter.html b/accessible/tests/mochitest/value/test_meter.html
new file mode 100644
index 0000000000..7ae1ed3543
--- /dev/null
+++ b/accessible/tests/mochitest/value/test_meter.html
@@ -0,0 +1,82 @@
+<html>
+
+<head>
+ <title>nsIAccessible value testing for meter element</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../value.js"></script>
+
+ <script src="chrome://mochikit/content/chrome-harness.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // HTML5 meter element tests
+ testValue("nothing", "0", 0, 0, 1, 0);
+ testValue("minOnly", "20", 20, 20, 20, 0);
+ testValue("maxOnly", "0", 0, 0, 20, 0);
+ testValue("valOnly", "1", 1, 0, 1, 0);
+ testValue("regular", "15", 15, 10, 30, 0);
+ testValue("noMin", "10", 10, 0, 100, 0);
+ testValue("noMax", "5", 5, 5, 5, 0);
+ testValue("noVal", "10", 10, 10, 20, 0);
+ testValue("invalidValue", "20", 20, 10, 20, 0);
+ testValue("invalidMax", "10", 10, 10, 10, 0);
+ testValue("invalidValueMax", "20", 20, 20, 20, 0);
+
+ testValue("plainText", "Hello world", 0, 0, 1, 0);
+ testValue("regularText", "You scored 15 out of 30", 15, 10, 30, 0);
+ testValue("invalidText", "Something isnt right here", 20, 20, 20, 0);
+
+ testValue("valueText", "value", 0, 0, 1, 0);
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1460378"
+ title="HTML <meter> not spoken by screen readers">
+ Mozilla Bug 559773
+ </a><br />
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+
+ <meter id="nothing"></meter>
+
+ <meter id="minOnly" min="20"></meter>
+ <meter id="maxOnly" max="20"></meter>
+ <meter id="valOnly" value="20"></meter>
+
+ <meter id="regular" min="10" value="15" max="30"></meter>
+
+ <meter id="noMin" value="10" max="100"></meter>
+ <meter id="noMax" min="5" value="10"></meter>
+ <meter id="noVal" min="10" max="20"></meter>
+
+ <meter id="invalidValue" min="10" value="30" max="20"></meter>
+ <meter id="invalidMax" min="10" value="15" max="2"></meter>
+ <meter id="invalidValueMax" min="20" value="17" max="10"></meter>
+
+ <meter id="plainText">Hello world</meter>
+ <meter id="regularText" min="10" value="15" max="30">You scored 15 out of 30</meter>
+ <meter id="invalidText" min="20" value="17" max="10">Something isnt right here</meter>
+
+ <meter id="valueText" aria-valuetext="value">valuetext should take precedence over internal text</meter>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/value/test_number.html b/accessible/tests/mochitest/value/test_number.html
new file mode 100644
index 0000000000..59024c3ef3
--- /dev/null
+++ b/accessible/tests/mochitest/value/test_number.html
@@ -0,0 +1,56 @@
+<html>
+
+<head>
+ <title>nsIAccessible value testing for input@type=range element</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../value.js"></script>
+
+ <script src="chrome://mochikit/content/chrome-harness.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // HTML5 number element tests
+ testValue("number", "", 0, 0, 0, 1);
+ testValue("number_value", "1", 1, 0, 0, 1);
+ testValue("number_step", "", 0, 0, 0, 1);
+ testValue("number_min42", "", 0, 42, 0, 1);
+ testValue("number_max42", "", 0, 0, 42, 1);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=559761"
+ title="make HTML5 input@type=number element accessible">
+ Bug 559761
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+
+ <!-- HTML5 input@type=number element -->
+ <input type="number" id="number">
+ <input type="number" id="number_value" value="1">
+ <input type="number" id="number_step" step="1">
+ <input type="number" id="number_min42" min="42">
+ <input type="number" id="number_max42" max="42">
+</body>
+</html>
diff --git a/accessible/tests/mochitest/value/test_progress.html b/accessible/tests/mochitest/value/test_progress.html
new file mode 100644
index 0000000000..03ddb02196
--- /dev/null
+++ b/accessible/tests/mochitest/value/test_progress.html
@@ -0,0 +1,61 @@
+<html>
+
+<head>
+ <title>nsIAccessible value testing for progress element</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../value.js"></script>
+
+ <script src="chrome://mochikit/content/chrome-harness.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // HTML5 progress element tests
+ testValue("pr_indeterminate", "", 0, 0, 1, 0);
+ testValue("pr_zero", "0%", 0, 0, 1, 0);
+ testValue("pr_zeropointfive", "50%", 0.5, 0, 1, 0);
+ testValue("pr_one", "100%", 1, 0, 1, 0);
+ testValue("pr_42", "100%", 42, 0, 1, 0);
+ testValue("pr_21", "50%", 21, 0, 42, 0);
+ testValue("pr_valuetext", "value", 0, 0, 1, 0);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=559773"
+ title="make HTML5 progress element accessible">
+ Mozilla Bug 559773
+ </a><br />
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+
+ <!-- HTML5 progress element -->
+ <progress id="pr_indeterminate">this will be read by legacy browsers</progress>
+ <progress id="pr_zero" value="0">this will be read by legacy browsers</progress>
+ <progress id="pr_zeropointfive" value="0.5">this will be read by legacy browsers</progress>
+ <progress id="pr_one" value="1">this will be read by legacy browsers</progress>
+ <progress id="pr_42" value="42">this will be read by legacy browsers</progress>
+ <progress id="pr_21" value="21" max="42">this will be read by legacy browsers</progress>
+ <!-- aria-valuetext should work due to implicit progressbar role (bug 1475376) -->
+ <progress id="pr_valuetext" aria-valuetext="value">this will be read by legacy browsers</progress>
+</body>
+</html>
diff --git a/accessible/tests/mochitest/value/test_range.html b/accessible/tests/mochitest/value/test_range.html
new file mode 100644
index 0000000000..55cb0b1767
--- /dev/null
+++ b/accessible/tests/mochitest/value/test_range.html
@@ -0,0 +1,59 @@
+<html>
+
+<head>
+ <title>nsIAccessible value testing for input@type=range element</title>
+
+ <link rel="stylesheet" type="text/css"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+ <script type="application/javascript"
+ src="../common.js"></script>
+ <script type="application/javascript"
+ src="../value.js"></script>
+
+ <script src="chrome://mochikit/content/chrome-harness.js"></script>
+
+ <script type="application/javascript">
+ function doTest() {
+ // HTML5 progress element tests
+ testValue("range", "50", 50, 0, 100, 1);
+ testValue("range_value", "1", 1, 0, 100, 1);
+ testValue("range_step", "50", 50, 0, 100, 1);
+ testValue("range_min42", "71", 71, 42, 100, 1);
+ testValue("range_max42", "21", 21, 0, 42, 1);
+ testValue("range_valuetext", "value", 50, 0, 100, 1);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ </script>
+
+</head>
+
+<body>
+
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=559764"
+ title="make HTML5 input@type=range element accessible">
+ Bug 559764
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+
+ <!-- HTML5 input@type=range element -->
+ <input type="range" id="range">
+ <input type="range" id="range_value" value="1">
+ <input type="range" id="range_step" step="1">
+ <input type="range" id="range_min42" min="42">
+ <input type="range" id="range_max42" max="42">
+ <!-- aria-valuetext should work due to implicit slider role (bug 1475376) -->
+ <input type="range" id="range_valuetext" aria-valuetext="value">
+</body>
+</html>
diff --git a/accessible/windows/ia2/ia2Accessible.cpp b/accessible/windows/ia2/ia2Accessible.cpp
new file mode 100644
index 0000000000..2092730932
--- /dev/null
+++ b/accessible/windows/ia2/ia2Accessible.cpp
@@ -0,0 +1,563 @@
+/* -*- 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 "Accessible2_i.c"
+#include "Accessible2_2_i.c"
+#include "AccessibleRole.h"
+#include "AccessibleStates.h"
+
+#include "AccAttributes.h"
+#include "Compatibility.h"
+#include "ia2AccessibleRelation.h"
+#include "IUnknownImpl.h"
+#include "nsCoreUtils.h"
+#include "nsIAccessibleTypes.h"
+#include "mozilla/a11y/PDocAccessible.h"
+#include "Relation.h"
+#include "TextRange-inl.h"
+#include "nsAccessibilityService.h"
+
+#include "mozilla/PresShell.h"
+#include "nsISimpleEnumerator.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// ia2Accessible
+////////////////////////////////////////////////////////////////////////////////
+
+STDMETHODIMP
+ia2Accessible::QueryInterface(REFIID iid, void** ppv) {
+ if (!ppv) return E_INVALIDARG;
+
+ *ppv = nullptr;
+
+ // NOTE: If any new versions of IAccessible2 are added here, they should
+ // also be added to the IA2 Handler in
+ // /accessible/ipc/win/handler/AccessibleHandler.cpp
+
+ if (IID_IAccessible2_2 == iid) {
+ *ppv = static_cast<IAccessible2_2*>(this);
+ } else if (IID_IAccessible2 == iid) {
+ *ppv = static_cast<IAccessible2*>(this);
+ }
+
+ if (*ppv) {
+ (reinterpret_cast<IUnknown*>(*ppv))->AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+AccessibleWrap* ia2Accessible::LocalAcc() {
+ return static_cast<MsaaAccessible*>(this)->LocalAcc();
+}
+
+Accessible* ia2Accessible::Acc() {
+ return static_cast<MsaaAccessible*>(this)->Acc();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// IAccessible2
+
+STDMETHODIMP
+ia2Accessible::get_nRelations(long* aNRelations) {
+ if (!aNRelations) return E_INVALIDARG;
+ *aNRelations = 0;
+
+ Accessible* acc = Acc();
+ if (!acc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ for (uint32_t idx = 0; idx < ArrayLength(sRelationTypePairs); idx++) {
+ if (sRelationTypePairs[idx].second == IA2_RELATION_NULL) continue;
+
+ Relation rel = acc->RelationByType(sRelationTypePairs[idx].first);
+ if (rel.Next()) (*aNRelations)++;
+ }
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2Accessible::get_relation(long aRelationIndex,
+ IAccessibleRelation** aRelation) {
+ if (!aRelation || aRelationIndex < 0) return E_INVALIDARG;
+ *aRelation = nullptr;
+
+ Accessible* acc = Acc();
+ if (!acc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ long relIdx = 0;
+ for (uint32_t idx = 0; idx < ArrayLength(sRelationTypePairs); idx++) {
+ if (sRelationTypePairs[idx].second == IA2_RELATION_NULL) continue;
+
+ RelationType relationType = sRelationTypePairs[idx].first;
+ Relation rel = acc->RelationByType(relationType);
+ RefPtr<ia2AccessibleRelation> ia2Relation =
+ new ia2AccessibleRelation(relationType, &rel);
+ if (ia2Relation->HasTargets()) {
+ if (relIdx == aRelationIndex) {
+ ia2Relation.forget(aRelation);
+ return S_OK;
+ }
+
+ relIdx++;
+ }
+ }
+
+ return E_INVALIDARG;
+}
+
+STDMETHODIMP
+ia2Accessible::get_relations(long aMaxRelations,
+ IAccessibleRelation** aRelation,
+ long* aNRelations) {
+ if (!aRelation || !aNRelations || aMaxRelations <= 0) return E_INVALIDARG;
+ *aNRelations = 0;
+
+ Accessible* acc = Acc();
+ if (!acc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ for (uint32_t idx = 0;
+ idx < ArrayLength(sRelationTypePairs) && *aNRelations < aMaxRelations;
+ idx++) {
+ if (sRelationTypePairs[idx].second == IA2_RELATION_NULL) continue;
+
+ RelationType relationType = sRelationTypePairs[idx].first;
+ Relation rel = acc->RelationByType(relationType);
+ RefPtr<ia2AccessibleRelation> ia2Rel =
+ new ia2AccessibleRelation(relationType, &rel);
+ if (ia2Rel->HasTargets()) {
+ ia2Rel.forget(aRelation + (*aNRelations));
+ (*aNRelations)++;
+ }
+ }
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2Accessible::role(long* aRole) {
+ if (!aRole) return E_INVALIDARG;
+ *aRole = 0;
+
+ Accessible* acc = Acc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+#define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
+ msaaRole, ia2Role, androidClass, nameRule) \
+ case roles::_geckoRole: \
+ *aRole = ia2Role; \
+ break;
+
+ a11y::role geckoRole;
+ geckoRole = acc->Role();
+ switch (geckoRole) {
+#include "RoleMap.h"
+ default:
+ MOZ_CRASH("Unknown role.");
+ }
+
+#undef ROLE
+
+ // Special case, if there is a ROLE_ROW inside of a ROLE_TREE_TABLE, then call
+ // the IA2 role a ROLE_OUTLINEITEM.
+ if (geckoRole == roles::ROW) {
+ Accessible* xpParent = acc->Parent();
+ if (xpParent && xpParent->Role() == roles::TREE_TABLE)
+ *aRole = ROLE_SYSTEM_OUTLINEITEM;
+ }
+
+ return S_OK;
+}
+
+// XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294.
+MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP
+ia2Accessible::scrollTo(enum IA2ScrollType aScrollType) {
+ Accessible* acc = Acc();
+ if (!acc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ acc->ScrollTo(aScrollType);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2Accessible::scrollToPoint(enum IA2CoordinateType aCoordType, long aX,
+ long aY) {
+ Accessible* acc = Acc();
+ if (!acc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ uint32_t geckoCoordType =
+ (aCoordType == IA2_COORDTYPE_SCREEN_RELATIVE)
+ ? nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE
+ : nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE;
+
+ acc->ScrollToPoint(geckoCoordType, aX, aY);
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2Accessible::get_groupPosition(long* aGroupLevel, long* aSimilarItemsInGroup,
+ long* aPositionInGroup) {
+ if (!aGroupLevel || !aSimilarItemsInGroup || !aPositionInGroup)
+ return E_INVALIDARG;
+
+ *aGroupLevel = 0;
+ *aSimilarItemsInGroup = 0;
+ *aPositionInGroup = 0;
+
+ Accessible* acc = Acc();
+ if (!acc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ GroupPos groupPos = acc->GroupPosition();
+
+ // Group information for accessibles having level only (like html headings
+ // elements) isn't exposed by this method. AT should look for 'level' object
+ // attribute.
+ if (!groupPos.setSize && !groupPos.posInSet) return S_FALSE;
+
+ *aGroupLevel = groupPos.level;
+ *aSimilarItemsInGroup = groupPos.setSize;
+ *aPositionInGroup = groupPos.posInSet;
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2Accessible::get_states(AccessibleStates* aStates) {
+ if (!aStates) return E_INVALIDARG;
+ *aStates = 0;
+
+ // XXX: bug 344674 should come with better approach that we have here.
+
+ Accessible* acc = Acc();
+ if (!acc) {
+ *aStates = IA2_STATE_DEFUNCT;
+ return S_OK;
+ }
+
+ uint64_t state;
+ state = acc->State();
+
+ if (state & states::INVALID) *aStates |= IA2_STATE_INVALID_ENTRY;
+ if (state & states::REQUIRED) *aStates |= IA2_STATE_REQUIRED;
+
+ // The following IA2 states are not supported by Gecko
+ // IA2_STATE_ARMED
+ // IA2_STATE_MANAGES_DESCENDANTS
+ // IA2_STATE_ICONIFIED
+ // IA2_STATE_INVALID // This is not a state, it is the absence of a state
+
+ if (state & states::ACTIVE) *aStates |= IA2_STATE_ACTIVE;
+ if (state & states::DEFUNCT) *aStates |= IA2_STATE_DEFUNCT;
+ if (state & states::EDITABLE) *aStates |= IA2_STATE_EDITABLE;
+ if (state & states::HORIZONTAL) *aStates |= IA2_STATE_HORIZONTAL;
+ if (state & states::MODAL) *aStates |= IA2_STATE_MODAL;
+ if (state & states::MULTI_LINE) *aStates |= IA2_STATE_MULTI_LINE;
+ if (state & states::OPAQUE1) *aStates |= IA2_STATE_OPAQUE;
+ if (state & states::SELECTABLE_TEXT) *aStates |= IA2_STATE_SELECTABLE_TEXT;
+ if (state & states::SINGLE_LINE) *aStates |= IA2_STATE_SINGLE_LINE;
+ if (state & states::STALE) *aStates |= IA2_STATE_STALE;
+ if (state & states::SUPPORTS_AUTOCOMPLETION)
+ *aStates |= IA2_STATE_SUPPORTS_AUTOCOMPLETION;
+ if (state & states::TRANSIENT) *aStates |= IA2_STATE_TRANSIENT;
+ if (state & states::VERTICAL) *aStates |= IA2_STATE_VERTICAL;
+ if (state & states::CHECKED) *aStates |= IA2_STATE_CHECKABLE;
+ if (state & states::PINNED) *aStates |= IA2_STATE_PINNED;
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2Accessible::get_extendedRole(BSTR* aExtendedRole) {
+ if (!aExtendedRole) return E_INVALIDARG;
+
+ *aExtendedRole = nullptr;
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP
+ia2Accessible::get_localizedExtendedRole(BSTR* aLocalizedExtendedRole) {
+ if (!aLocalizedExtendedRole) return E_INVALIDARG;
+
+ *aLocalizedExtendedRole = nullptr;
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP
+ia2Accessible::get_nExtendedStates(long* aNExtendedStates) {
+ if (!aNExtendedStates) return E_INVALIDARG;
+
+ *aNExtendedStates = 0;
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP
+ia2Accessible::get_extendedStates(long aMaxExtendedStates,
+ BSTR** aExtendedStates,
+ long* aNExtendedStates) {
+ if (!aExtendedStates || !aNExtendedStates) return E_INVALIDARG;
+
+ *aExtendedStates = nullptr;
+ *aNExtendedStates = 0;
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP
+ia2Accessible::get_localizedExtendedStates(long aMaxLocalizedExtendedStates,
+ BSTR** aLocalizedExtendedStates,
+ long* aNLocalizedExtendedStates) {
+ if (!aLocalizedExtendedStates || !aNLocalizedExtendedStates)
+ return E_INVALIDARG;
+
+ *aLocalizedExtendedStates = nullptr;
+ *aNLocalizedExtendedStates = 0;
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP
+ia2Accessible::get_uniqueID(long* aUniqueID) {
+ if (!aUniqueID) return E_INVALIDARG;
+
+ Accessible* acc = Acc();
+ *aUniqueID = MsaaAccessible::GetChildIDFor(acc);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2Accessible::get_windowHandle(HWND* aWindowHandle) {
+ if (!aWindowHandle) return E_INVALIDARG;
+ *aWindowHandle = 0;
+
+ Accessible* acc = Acc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ *aWindowHandle = MsaaAccessible::GetHWNDFor(acc);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2Accessible::get_indexInParent(long* aIndexInParent) {
+ if (!aIndexInParent) return E_INVALIDARG;
+ *aIndexInParent = -1;
+
+ Accessible* acc = Acc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ *aIndexInParent = acc->IndexInParent();
+
+ if (*aIndexInParent == -1) return S_FALSE;
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2Accessible::get_locale(IA2Locale* aLocale) {
+ if (!aLocale) return E_INVALIDARG;
+
+ // Language codes consist of a primary code and a possibly empty series of
+ // subcodes: language-code = primary-code ( "-" subcode )*
+ // Two-letter primary codes are reserved for [ISO639] language abbreviations.
+ // Any two-letter subcode is understood to be a [ISO3166] country code.
+
+ Accessible* acc = Acc();
+ if (!acc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ nsAutoString lang;
+ acc->Language(lang);
+
+ // If primary code consists from two letters then expose it as language.
+ int32_t offset = lang.FindChar('-', 0);
+ if (offset == -1) {
+ if (lang.Length() == 2) {
+ aLocale->language = ::SysAllocString(lang.get());
+ return S_OK;
+ }
+ } else if (offset == 2) {
+ aLocale->language = ::SysAllocStringLen(lang.get(), 2);
+
+ // If the first subcode consists from two letters then expose it as
+ // country.
+ offset = lang.FindChar('-', 3);
+ if (offset == -1) {
+ if (lang.Length() == 5) {
+ aLocale->country = ::SysAllocString(lang.get() + 3);
+ return S_OK;
+ }
+ } else if (offset == 5) {
+ aLocale->country = ::SysAllocStringLen(lang.get() + 3, 2);
+ }
+ }
+
+ // Expose as a string if primary code or subcode cannot point to language or
+ // country abbreviations or if there are more than one subcode.
+ aLocale->variant = ::SysAllocString(lang.get());
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2Accessible::get_attributes(BSTR* aAttributes) {
+ if (!aAttributes) return E_INVALIDARG;
+ *aAttributes = nullptr;
+
+ Accessible* acc = Acc();
+ if (!acc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ // The format is name:value;name:value; with \ for escaping these
+ // characters ":;=,\".
+ RefPtr<AccAttributes> attributes = acc->Attributes();
+ return ConvertToIA2Attributes(attributes, aAttributes);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// IAccessible2_2
+
+STDMETHODIMP
+ia2Accessible::get_attribute(BSTR name, VARIANT* aAttribute) {
+ if (!aAttribute) return E_INVALIDARG;
+
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP
+ia2Accessible::get_accessibleWithCaret(IUnknown** aAccessible,
+ long* aCaretOffset) {
+ if (!aAccessible || !aCaretOffset) return E_INVALIDARG;
+
+ *aAccessible = nullptr;
+ *aCaretOffset = -1;
+
+ if (!Acc()) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+ AccessibleWrap* acc = LocalAcc();
+ if (!acc) {
+ return E_NOTIMPL; // XXX Not supported for RemoteAccessible yet.
+ }
+
+ int32_t caretOffset = -1;
+ LocalAccessible* accWithCaret =
+ SelectionMgr()->AccessibleWithCaret(&caretOffset);
+ if (!accWithCaret || acc->Document() != accWithCaret->Document())
+ return S_FALSE;
+
+ LocalAccessible* child = accWithCaret;
+ while (!child->IsDoc() && child != acc) child = child->LocalParent();
+
+ if (child != acc) return S_FALSE;
+
+ RefPtr<IAccessible2> ia2WithCaret;
+ accWithCaret->GetNativeInterface(getter_AddRefs(ia2WithCaret));
+ ia2WithCaret.forget(aAccessible);
+ *aCaretOffset = caretOffset;
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2Accessible::get_relationTargetsOfType(BSTR aType, long aMaxTargets,
+ IUnknown*** aTargets,
+ long* aNTargets) {
+ if (!aTargets || !aNTargets || aMaxTargets < 0) return E_INVALIDARG;
+ *aNTargets = 0;
+
+ Maybe<RelationType> relationType;
+ for (uint32_t idx = 0; idx < ArrayLength(sRelationTypePairs); idx++) {
+ if (wcscmp(aType, sRelationTypePairs[idx].second) == 0) {
+ relationType.emplace(sRelationTypePairs[idx].first);
+ break;
+ }
+ }
+ if (!relationType) return E_INVALIDARG;
+
+ Accessible* acc = Acc();
+ if (!acc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ nsTArray<Accessible*> targets;
+ Relation rel = acc->RelationByType(*relationType);
+ Accessible* target = nullptr;
+ while (
+ (target = rel.Next()) &&
+ (aMaxTargets == 0 || static_cast<long>(targets.Length()) < aMaxTargets)) {
+ targets.AppendElement(target);
+ }
+
+ *aNTargets = targets.Length();
+ *aTargets =
+ static_cast<IUnknown**>(::CoTaskMemAlloc(sizeof(IUnknown*) * *aNTargets));
+ if (!*aTargets) return E_OUTOFMEMORY;
+
+ for (int32_t i = 0; i < *aNTargets; i++) {
+ (*aTargets)[i] = MsaaAccessible::NativeAccessible(targets[i]);
+ }
+
+ return S_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Helpers
+
+static inline void EscapeAttributeChars(nsString& aStr) {
+ int32_t offset = 0;
+ static const char16_t kCharsToEscape[] = u":;=,\\";
+ while ((offset = aStr.FindCharInSet(kCharsToEscape, offset)) != kNotFound) {
+ aStr.Insert('\\', offset);
+ offset += 2;
+ }
+}
+
+HRESULT
+ia2Accessible::ConvertToIA2Attributes(AccAttributes* aAttributes,
+ BSTR* aIA2Attributes) {
+ *aIA2Attributes = nullptr;
+
+ // The format is name:value;name:value; with \ for escaping these
+ // characters ":;=,\".
+
+ if (!aAttributes) return S_FALSE;
+
+ nsAutoString strAttrs;
+
+ for (auto iter : *aAttributes) {
+ nsAutoString name;
+ iter.NameAsString(name);
+ EscapeAttributeChars(name);
+
+ nsAutoString value;
+ iter.ValueAsString(value);
+ EscapeAttributeChars(value);
+
+ strAttrs.Append(name);
+ strAttrs.Append(':');
+ strAttrs.Append(value);
+ strAttrs.Append(';');
+ }
+
+ if (strAttrs.IsEmpty()) return S_FALSE;
+
+ *aIA2Attributes = ::SysAllocStringLen(strAttrs.get(), strAttrs.Length());
+ return *aIA2Attributes ? S_OK : E_OUTOFMEMORY;
+}
diff --git a/accessible/windows/ia2/ia2Accessible.h b/accessible/windows/ia2/ia2Accessible.h
new file mode 100644
index 0000000000..ba65d9fcf9
--- /dev/null
+++ b/accessible/windows/ia2/ia2Accessible.h
@@ -0,0 +1,120 @@
+/* -*- 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_ia2Accessible_h_
+#define mozilla_a11y_ia2Accessible_h_
+
+#include "nsISupports.h"
+#include "nsTArray.h"
+
+#include "Accessible2_2.h"
+
+namespace mozilla {
+namespace a11y {
+class Accessible;
+class AccAttributes;
+class AccessibleWrap;
+
+class ia2Accessible : public IAccessible2_2 {
+ public:
+ // IUnknown
+ STDMETHODIMP QueryInterface(REFIID, void**);
+
+ // IAccessible2
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_nRelations(
+ /* [retval][out] */ long* nRelations);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_relation(
+ /* [in] */ long relationIndex,
+ /* [retval][out] */ IAccessibleRelation** relation);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_relations(
+ /* [in] */ long maxRelations,
+ /* [length_is][size_is][out] */ IAccessibleRelation** relation,
+ /* [retval][out] */ long* nRelations);
+
+ virtual HRESULT STDMETHODCALLTYPE role(
+ /* [retval][out] */ long* role);
+
+ virtual HRESULT STDMETHODCALLTYPE scrollTo(
+ /* [in] */ enum IA2ScrollType scrollType);
+
+ virtual HRESULT STDMETHODCALLTYPE scrollToPoint(
+ /* [in] */ enum IA2CoordinateType coordinateType,
+ /* [in] */ long x,
+ /* [in] */ long y);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_groupPosition(
+ /* [out] */ long* groupLevel,
+ /* [out] */ long* similarItemsInGroup,
+ /* [retval][out] */ long* positionInGroup);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_states(
+ /* [retval][out] */ AccessibleStates* states);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_extendedRole(
+ /* [retval][out] */ BSTR* extendedRole);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_localizedExtendedRole(
+ /* [retval][out] */ BSTR* localizedExtendedRole);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_nExtendedStates(
+ /* [retval][out] */ long* nExtendedStates);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_extendedStates(
+ /* [in] */ long maxExtendedStates,
+ /* [length_is][length_is][size_is][size_is][out] */ BSTR** extendedStates,
+ /* [retval][out] */ long* nExtendedStates);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_localizedExtendedStates(
+ /* [in] */ long maxLocalizedExtendedStates,
+ /* [length_is][length_is][size_is][size_is][out] */
+ BSTR** localizedExtendedStates,
+ /* [retval][out] */ long* nLocalizedExtendedStates);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_uniqueID(
+ /* [retval][out] */ long* uniqueID);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_windowHandle(
+ /* [retval][out] */ HWND* windowHandle);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_indexInParent(
+ /* [retval][out] */ long* indexInParent);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_locale(
+ /* [retval][out] */ IA2Locale* locale);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_attributes(
+ /* [retval][out] */ BSTR* attributes);
+
+ // IAccessible2_2
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_attribute(
+ /* [in] */ BSTR name,
+ /* [out, retval] */ VARIANT* attribute);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_accessibleWithCaret(
+ /* [out] */ IUnknown** accessible,
+ /* [out, retval] */ long* caretOffset);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_relationTargetsOfType(
+ /* [in] */ BSTR type,
+ /* [in] */ long maxTargets,
+ /* [out, size_is(,*nTargets)] */ IUnknown*** targets,
+ /* [out, retval] */ long* nTargets);
+
+ // Helper method
+ static HRESULT ConvertToIA2Attributes(AccAttributes* aAttributes,
+ BSTR* aIA2Attributes);
+
+ private:
+ AccessibleWrap* LocalAcc();
+ Accessible* Acc();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/ia2/ia2AccessibleAction.cpp b/accessible/windows/ia2/ia2AccessibleAction.cpp
new file mode 100644
index 0000000000..d89bd79ce1
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleAction.cpp
@@ -0,0 +1,152 @@
+/* -*- 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 "ia2AccessibleAction.h"
+
+#include "AccessibleAction_i.c"
+
+#include "AccessibleWrap.h"
+#include "IUnknownImpl.h"
+
+using namespace mozilla::a11y;
+
+Accessible* ia2AccessibleAction::Acc() {
+ return static_cast<MsaaAccessible*>(this)->Acc();
+}
+
+// IUnknown
+
+STDMETHODIMP
+ia2AccessibleAction::QueryInterface(REFIID iid, void** ppv) {
+ if (!ppv) return E_INVALIDARG;
+
+ *ppv = nullptr;
+
+ if (IID_IAccessibleAction == iid) {
+ *ppv = static_cast<IAccessibleAction*>(this);
+ (reinterpret_cast<IUnknown*>(*ppv))->AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+// IAccessibleAction
+
+STDMETHODIMP
+ia2AccessibleAction::nActions(long* aActionCount) {
+ if (!aActionCount) return E_INVALIDARG;
+
+ *aActionCount = 0;
+
+ Accessible* acc = Acc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ *aActionCount = acc->ActionCount();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleAction::doAction(long aActionIndex) {
+ Accessible* acc = Acc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ uint8_t index = static_cast<uint8_t>(aActionIndex);
+ return acc->DoAction(index) ? S_OK : E_INVALIDARG;
+}
+
+STDMETHODIMP
+ia2AccessibleAction::get_description(long aActionIndex, BSTR* aDescription) {
+ if (!aDescription) return E_INVALIDARG;
+ *aDescription = nullptr;
+
+ Accessible* acc = Acc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ nsAutoString description;
+ uint8_t index = static_cast<uint8_t>(aActionIndex);
+ acc->ActionDescriptionAt(index, description);
+ if (description.IsEmpty()) return S_FALSE;
+
+ *aDescription = ::SysAllocStringLen(description.get(), description.Length());
+ return *aDescription ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleAction::get_keyBinding(long aActionIndex, long aNumMaxBinding,
+ BSTR** aKeyBinding, long* aNumBinding) {
+ if (!aKeyBinding) return E_INVALIDARG;
+ *aKeyBinding = nullptr;
+
+ if (!aNumBinding) return E_INVALIDARG;
+ *aNumBinding = 0;
+
+ if (aActionIndex != 0 || aNumMaxBinding < 1) return E_INVALIDARG;
+
+ Accessible* acc = Acc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ // Expose KeyboardShortcut if it's not exposed via MSAA accKeyboardShortcut.
+ LocalAccessible* localAcc = acc->AsLocal();
+ if (!localAcc) {
+ // RemoteAccessibles can't have a KeyboardShortcut.
+ return S_FALSE;
+ }
+
+ KeyBinding keyBinding = acc->AccessKey();
+ if (keyBinding.IsEmpty()) {
+ // In this case, KeyboardShortcut will be exposed via MSAA
+ // accKeyboardShortcut.
+ return S_FALSE;
+ }
+
+ // MSAA accKeyboardShortcut will expose AccessKey.
+ keyBinding = localAcc->KeyboardShortcut();
+ if (keyBinding.IsEmpty()) return S_FALSE;
+
+ nsAutoString keyStr;
+ keyBinding.ToString(keyStr);
+
+ *aKeyBinding = static_cast<BSTR*>(::CoTaskMemAlloc(sizeof(BSTR*)));
+ if (!*aKeyBinding) return E_OUTOFMEMORY;
+
+ *(aKeyBinding[0]) = ::SysAllocStringLen(keyStr.get(), keyStr.Length());
+ if (!*(aKeyBinding[0])) {
+ ::CoTaskMemFree(*aKeyBinding);
+ return E_OUTOFMEMORY;
+ }
+
+ *aNumBinding = 1;
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleAction::get_name(long aActionIndex, BSTR* aName) {
+ if (!aName) return E_INVALIDARG;
+
+ *aName = nullptr;
+
+ Accessible* acc = Acc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ nsAutoString name;
+ uint8_t index = static_cast<uint8_t>(aActionIndex);
+ acc->ActionNameAt(index, name);
+ if (name.IsEmpty()) return E_INVALIDARG;
+
+ *aName = ::SysAllocStringLen(name.get(), name.Length());
+ return *aName ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleAction::get_localizedName(long aActionIndex,
+ BSTR* aLocalizedName) {
+ if (!aLocalizedName) return E_INVALIDARG;
+
+ *aLocalizedName = nullptr;
+ return E_NOTIMPL;
+}
diff --git a/accessible/windows/ia2/ia2AccessibleAction.h b/accessible/windows/ia2/ia2AccessibleAction.h
new file mode 100644
index 0000000000..ae3749a039
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleAction.h
@@ -0,0 +1,85 @@
+/* -*- 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 _ACCESSIBLE_ACTION_H
+#define _ACCESSIBLE_ACTION_H
+
+#include "nsISupports.h"
+
+#include "mozilla/a11y/Accessible.h"
+#include "AccessibleAction.h"
+
+namespace mozilla {
+namespace a11y {
+
+class ia2AccessibleAction : public IAccessibleAction {
+ public:
+ // IUnknown
+ STDMETHODIMP QueryInterface(REFIID, void**);
+
+ // IAccessibleAction
+ virtual HRESULT STDMETHODCALLTYPE nActions(
+ /* [retval][out] */ long* nActions);
+
+ virtual HRESULT STDMETHODCALLTYPE doAction(
+ /* [in] */ long actionIndex);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_description(
+ /* [in] */ long actionIndex,
+ /* [retval][out] */ BSTR* description);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_keyBinding(
+ /* [in] */ long actionIndex,
+ /* [in] */ long nMaxBinding,
+ /* [length_is][length_is][size_is][size_is][out] */ BSTR** keyBinding,
+ /* [retval][out] */ long* nBinding);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_name(
+ /* [in] */ long actionIndex,
+ /* [retval][out] */ BSTR* name);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_localizedName(
+ /* [in] */ long actionIndex,
+ /* [retval][out] */ BSTR* localizedName);
+
+ private:
+ Accessible* Acc();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#define FORWARD_IACCESSIBLEACTION(Class) \
+ virtual HRESULT STDMETHODCALLTYPE nActions(long* nActions) { \
+ return Class::nActions(nActions); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE doAction(long actionIndex) { \
+ return Class::doAction(actionIndex); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_description(long actionIndex, \
+ BSTR* description) { \
+ return Class::get_description(actionIndex, description); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_keyBinding( \
+ long actionIndex, long nMaxBinding, BSTR** keyBinding, long* nBinding) { \
+ return Class::get_keyBinding(actionIndex, nMaxBinding, keyBinding, \
+ nBinding); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_name(long actionIndex, BSTR* name) { \
+ return Class::get_name(actionIndex, name); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_localizedName(long actionIndex, \
+ BSTR* localizedName) { \
+ return Class::get_localizedName(actionIndex, localizedName); \
+ }
+
+#endif
diff --git a/accessible/windows/ia2/ia2AccessibleApplication.cpp b/accessible/windows/ia2/ia2AccessibleApplication.cpp
new file mode 100644
index 0000000000..7844e97074
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleApplication.cpp
@@ -0,0 +1,94 @@
+/* -*- 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/. */
+
+#include "ia2AccessibleApplication.h"
+
+#include "AccessibleApplication_i.c"
+#include "ApplicationAccessibleWrap.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+ApplicationAccessible* ia2AccessibleApplication::AppAcc() {
+ return static_cast<ApplicationAccessible*>(LocalAcc());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// IUnknown
+
+IMPL_IUNKNOWN_QUERY_HEAD(ia2AccessibleApplication)
+IMPL_IUNKNOWN_QUERY_IFACE(IAccessibleApplication)
+IMPL_IUNKNOWN_QUERY_TAIL_INHERITED(MsaaAccessible)
+
+////////////////////////////////////////////////////////////////////////////////
+// IAccessibleApplication
+
+STDMETHODIMP
+ia2AccessibleApplication::get_appName(BSTR* aName) {
+ if (!aName) return E_INVALIDARG;
+
+ *aName = nullptr;
+
+ ApplicationAccessible* appAcc = AppAcc();
+ if (!appAcc) return CO_E_OBJNOTCONNECTED;
+
+ nsAutoString name;
+ appAcc->AppName(name);
+ if (name.IsEmpty()) return S_FALSE;
+
+ *aName = ::SysAllocStringLen(name.get(), name.Length());
+ return *aName ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleApplication::get_appVersion(BSTR* aVersion) {
+ if (!aVersion) return E_INVALIDARG;
+
+ *aVersion = nullptr;
+
+ ApplicationAccessible* appAcc = AppAcc();
+ if (!appAcc) return CO_E_OBJNOTCONNECTED;
+
+ nsAutoString version;
+ appAcc->AppVersion(version);
+ if (version.IsEmpty()) return S_FALSE;
+
+ *aVersion = ::SysAllocStringLen(version.get(), version.Length());
+ return *aVersion ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleApplication::get_toolkitName(BSTR* aName) {
+ if (!aName) return E_INVALIDARG;
+
+ ApplicationAccessible* appAcc = AppAcc();
+ if (!appAcc) return CO_E_OBJNOTCONNECTED;
+
+ nsAutoString name;
+ appAcc->PlatformName(name);
+ if (name.IsEmpty()) return S_FALSE;
+
+ *aName = ::SysAllocStringLen(name.get(), name.Length());
+ return *aName ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleApplication::get_toolkitVersion(BSTR* aVersion) {
+ if (!aVersion) return E_INVALIDARG;
+
+ *aVersion = nullptr;
+
+ ApplicationAccessible* appAcc = AppAcc();
+ if (!appAcc) return CO_E_OBJNOTCONNECTED;
+
+ nsAutoString version;
+ appAcc->PlatformVersion(version);
+ if (version.IsEmpty()) return S_FALSE;
+
+ *aVersion = ::SysAllocStringLen(version.get(), version.Length());
+ return *aVersion ? S_OK : E_OUTOFMEMORY;
+}
diff --git a/accessible/windows/ia2/ia2AccessibleApplication.h b/accessible/windows/ia2/ia2AccessibleApplication.h
new file mode 100644
index 0000000000..283dc38471
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleApplication.h
@@ -0,0 +1,49 @@
+/* -*- 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 IA2_ACCESSIBLE_APPLICATION_H_
+#define IA2_ACCESSIBLE_APPLICATION_H_
+
+#include "AccessibleApplication.h"
+#include "IUnknownImpl.h"
+#include "MsaaAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+class ApplicationAccessible;
+
+class ia2AccessibleApplication : public IAccessibleApplication,
+ public MsaaAccessible {
+ public:
+ // IUnknown
+ DECL_IUNKNOWN_INHERITED
+ IMPL_IUNKNOWN_REFCOUNTING_INHERITED(MsaaAccessible)
+
+ // IAccessibleApplication
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_appName(
+ /* [retval][out] */ BSTR* name);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_appVersion(
+ /* [retval][out] */ BSTR* version);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_toolkitName(
+ /* [retval][out] */ BSTR* name);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_toolkitVersion(
+ /* [retval][out] */ BSTR* version);
+
+ protected:
+ using MsaaAccessible::MsaaAccessible;
+
+ private:
+ ApplicationAccessible* AppAcc();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/ia2/ia2AccessibleComponent.cpp b/accessible/windows/ia2/ia2AccessibleComponent.cpp
new file mode 100644
index 0000000000..9c22a66cad
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleComponent.cpp
@@ -0,0 +1,106 @@
+/* -*- 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 "ia2AccessibleComponent.h"
+
+#include "AccessibleComponent_i.c"
+
+#include "AccessibleWrap.h"
+#include "States.h"
+#include "IUnknownImpl.h"
+
+#include "nsIFrame.h"
+
+using namespace mozilla::a11y;
+
+AccessibleWrap* ia2AccessibleComponent::LocalAcc() {
+ return static_cast<MsaaAccessible*>(this)->LocalAcc();
+}
+
+// IUnknown
+
+STDMETHODIMP
+ia2AccessibleComponent::QueryInterface(REFIID iid, void** ppv) {
+ if (!ppv) return E_INVALIDARG;
+
+ *ppv = nullptr;
+
+ if (IID_IAccessibleComponent == iid) {
+ *ppv = static_cast<IAccessibleComponent*>(this);
+ (reinterpret_cast<IUnknown*>(*ppv))->AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+// IAccessibleComponent
+
+STDMETHODIMP
+ia2AccessibleComponent::get_locationInParent(long* aX, long* aY) {
+ if (!aX || !aY) return E_INVALIDARG;
+
+ *aX = 0;
+ *aY = 0;
+
+ AccessibleWrap* acc = LocalAcc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ // If the object is not on any screen the returned position is (0,0).
+ uint64_t state = acc->State();
+ if (state & states::INVISIBLE) return S_OK;
+
+ LayoutDeviceIntRect rect = acc->Bounds();
+
+ // The coordinates of the returned position are relative to this object's
+ // parent or relative to the screen on which this object is rendered if it
+ // has no parent.
+ if (!acc->LocalParent()) {
+ *aX = rect.X();
+ *aY = rect.Y();
+ return S_OK;
+ }
+
+ // The coordinates of the bounding box are given relative to the parent's
+ // coordinate system.
+ LayoutDeviceIntRect parentRect = acc->LocalParent()->Bounds();
+ *aX = rect.X() - parentRect.X();
+ *aY = rect.Y() - parentRect.Y();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleComponent::get_foreground(IA2Color* aForeground) {
+ if (!aForeground) return E_INVALIDARG;
+
+ *aForeground = 0;
+
+ AccessibleWrap* acc = LocalAcc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ nsIFrame* frame = acc->GetFrame();
+ if (frame) *aForeground = frame->StyleText()->mColor.ToColor();
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleComponent::get_background(IA2Color* aBackground) {
+ if (!aBackground) return E_INVALIDARG;
+
+ *aBackground = 0;
+
+ AccessibleWrap* acc = LocalAcc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ nsIFrame* frame = acc->GetFrame();
+ if (frame) {
+ *aBackground = frame->StyleBackground()->BackgroundColor(frame);
+ }
+
+ return S_OK;
+}
diff --git a/accessible/windows/ia2/ia2AccessibleComponent.h b/accessible/windows/ia2/ia2AccessibleComponent.h
new file mode 100644
index 0000000000..507bbbd628
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleComponent.h
@@ -0,0 +1,40 @@
+/* -*- 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 IA2_ACCESSIBLE_COMPONENT_H_
+#define IA2_ACCESSIBLE_COMPONENT_H_
+
+#include "AccessibleComponent.h"
+
+namespace mozilla {
+namespace a11y {
+class AccessibleWrap;
+
+class ia2AccessibleComponent : public IAccessibleComponent {
+ public:
+ // IUnknown
+ STDMETHODIMP QueryInterface(REFIID, void**);
+
+ // IAccessibleComponent
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_locationInParent(
+ /* [out] */ long* x,
+ /* [retval][out] */ long* y);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_foreground(
+ /* [retval][out] */ IA2Color* foreground);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_background(
+ /* [retval][out] */ IA2Color* background);
+
+ private:
+ AccessibleWrap* LocalAcc();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/ia2/ia2AccessibleEditableText.cpp b/accessible/windows/ia2/ia2AccessibleEditableText.cpp
new file mode 100644
index 0000000000..4e96736396
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleEditableText.cpp
@@ -0,0 +1,106 @@
+/* -*- 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 "ia2AccessibleEditableText.h"
+#include "ia2AccessibleHypertext.h"
+
+#include "AccessibleEditableText_i.c"
+#include "mozilla/a11y/HyperTextAccessibleBase.h"
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+using namespace mozilla::a11y;
+
+HyperTextAccessibleBase* ia2AccessibleEditableText::TextAcc() {
+ auto hyp = static_cast<ia2AccessibleHypertext*>(this);
+ Accessible* acc = hyp->Acc();
+ return acc ? acc->AsHyperTextBase() : nullptr;
+}
+
+// IAccessibleEditableText
+
+STDMETHODIMP
+ia2AccessibleEditableText::copyText(long aStartOffset, long aEndOffset) {
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) return CO_E_OBJNOTCONNECTED;
+
+ if (!textAcc->IsValidRange(aStartOffset, aEndOffset)) return E_INVALIDARG;
+
+ textAcc->CopyText(aStartOffset, aEndOffset);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleEditableText::deleteText(long aStartOffset, long aEndOffset) {
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) return CO_E_OBJNOTCONNECTED;
+
+ if (!textAcc->IsValidRange(aStartOffset, aEndOffset)) return E_INVALIDARG;
+
+ textAcc->DeleteText(aStartOffset, aEndOffset);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleEditableText::insertText(long aOffset, BSTR* aText) {
+ uint32_t length = ::SysStringLen(*aText);
+ nsAutoString text(*aText, length);
+
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) return CO_E_OBJNOTCONNECTED;
+
+ if (!textAcc->IsValidOffset(aOffset)) return E_INVALIDARG;
+
+ textAcc->InsertText(text, aOffset);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleEditableText::cutText(long aStartOffset, long aEndOffset) {
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) return CO_E_OBJNOTCONNECTED;
+
+ if (!textAcc->IsValidRange(aStartOffset, aEndOffset)) return E_INVALIDARG;
+
+ textAcc->CutText(aStartOffset, aEndOffset);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleEditableText::pasteText(long aOffset) {
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) return CO_E_OBJNOTCONNECTED;
+
+ if (!textAcc->IsValidOffset(aOffset)) return E_INVALIDARG;
+
+ textAcc->PasteText(aOffset);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleEditableText::replaceText(long aStartOffset, long aEndOffset,
+ BSTR* aText) {
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) return CO_E_OBJNOTCONNECTED;
+
+ if (!textAcc->IsValidRange(aStartOffset, aEndOffset)) return E_INVALIDARG;
+
+ textAcc->DeleteText(aStartOffset, aEndOffset);
+
+ uint32_t length = ::SysStringLen(*aText);
+ nsAutoString text(*aText, length);
+ textAcc->InsertText(text, aStartOffset);
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleEditableText::setAttributes(long aStartOffset, long aEndOffset,
+ BSTR* aAttributes) {
+ return E_NOTIMPL;
+}
diff --git a/accessible/windows/ia2/ia2AccessibleEditableText.h b/accessible/windows/ia2/ia2AccessibleEditableText.h
new file mode 100644
index 0000000000..0b018485d9
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleEditableText.h
@@ -0,0 +1,59 @@
+/* -*- 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 _ACCESSIBLE_EDITABLETEXT_H
+#define _ACCESSIBLE_EDITABLETEXT_H
+
+#include "nsISupports.h"
+
+#include "AccessibleEditableText.h"
+
+namespace mozilla {
+namespace a11y {
+class HyperTextAccessibleBase;
+
+class ia2AccessibleEditableText : public IAccessibleEditableText {
+ public:
+ // IAccessibleEditableText
+ virtual HRESULT STDMETHODCALLTYPE copyText(
+ /* [in] */ long startOffset,
+ /* [in] */ long endOffset);
+
+ virtual HRESULT STDMETHODCALLTYPE deleteText(
+ /* [in] */ long startOffset,
+ /* [in] */ long endOffset);
+
+ virtual HRESULT STDMETHODCALLTYPE insertText(
+ /* [in] */ long offset,
+ /* [in] */ BSTR* text);
+
+ virtual HRESULT STDMETHODCALLTYPE cutText(
+ /* [in] */ long startOffset,
+ /* [in] */ long endOffset);
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual HRESULT STDMETHODCALLTYPE pasteText(
+ /* [in] */ long offset);
+
+ virtual HRESULT STDMETHODCALLTYPE replaceText(
+ /* [in] */ long startOffset,
+ /* [in] */ long endOffset,
+ /* [in] */ BSTR* text);
+
+ virtual HRESULT STDMETHODCALLTYPE setAttributes(
+ /* [in] */ long startOffset,
+ /* [in] */ long endOffset,
+ /* [in] */ BSTR* attributes);
+
+ private:
+ HyperTextAccessibleBase* TextAcc();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/ia2/ia2AccessibleHyperlink.cpp b/accessible/windows/ia2/ia2AccessibleHyperlink.cpp
new file mode 100644
index 0000000000..9d71cfb411
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleHyperlink.cpp
@@ -0,0 +1,164 @@
+/* -*- 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 "AccessibleHyperlink.h"
+#include "AccessibleHyperlink_i.c"
+
+#include "AccessibleWrap.h"
+#include "IUnknownImpl.h"
+#include "nsIURI.h"
+
+using namespace mozilla::a11y;
+
+Accessible* ia2AccessibleHyperlink::Acc() {
+ return static_cast<MsaaAccessible*>(this)->Acc();
+}
+
+AccessibleWrap* ia2AccessibleHyperlink::LocalAcc() {
+ return static_cast<MsaaAccessible*>(this)->LocalAcc();
+}
+
+// IUnknown
+
+STDMETHODIMP
+ia2AccessibleHyperlink::QueryInterface(REFIID iid, void** ppv) {
+ if (!ppv) return E_INVALIDARG;
+
+ *ppv = nullptr;
+
+ if (IID_IAccessibleHyperlink == iid) {
+ Accessible* acc = Acc();
+ if (!acc || !acc->IsLink()) {
+ return E_NOINTERFACE;
+ }
+
+ *ppv = static_cast<IAccessibleHyperlink*>(this);
+ (reinterpret_cast<IUnknown*>(*ppv))->AddRef();
+ return S_OK;
+ }
+
+ return ia2AccessibleAction::QueryInterface(iid, ppv);
+}
+
+// IAccessibleHyperlink
+
+STDMETHODIMP
+ia2AccessibleHyperlink::get_anchor(long aIndex, VARIANT* aAnchor) {
+ if (!aAnchor) return E_INVALIDARG;
+
+ VariantInit(aAnchor);
+
+ Accessible* thisObj = Acc();
+ if (!thisObj) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ if (aIndex < 0 || aIndex >= static_cast<long>(thisObj->AnchorCount()))
+ return E_INVALIDARG;
+
+ if (!thisObj->IsLink()) return S_FALSE;
+
+ Accessible* anchor = thisObj->AnchorAt(aIndex);
+ if (!anchor) return S_FALSE;
+
+ RefPtr<IAccessible> result = MsaaAccessible::GetFrom(anchor);
+ result.forget(&aAnchor->punkVal);
+ aAnchor->vt = VT_UNKNOWN;
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleHyperlink::get_anchorTarget(long aIndex, VARIANT* aAnchorTarget) {
+ if (!aAnchorTarget) {
+ return E_INVALIDARG;
+ }
+
+ VariantInit(aAnchorTarget);
+
+ Accessible* thisObj = Acc();
+ if (!thisObj) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+ nsAutoCString uriStr;
+
+ if (aIndex < 0 || aIndex >= static_cast<long>(thisObj->AnchorCount())) {
+ return E_INVALIDARG;
+ }
+
+ if (!thisObj->IsLink()) {
+ return S_FALSE;
+ }
+
+ nsCOMPtr<nsIURI> uri = thisObj->AnchorURIAt(aIndex);
+ if (!uri) {
+ return S_FALSE;
+ }
+
+ nsresult rv = uri->GetSpec(uriStr);
+ if (NS_FAILED(rv)) {
+ return GetHRESULT(rv);
+ }
+
+ nsAutoString stringURI;
+ AppendUTF8toUTF16(uriStr, stringURI);
+
+ aAnchorTarget->vt = VT_BSTR;
+ aAnchorTarget->bstrVal =
+ ::SysAllocStringLen(stringURI.get(), stringURI.Length());
+ return aAnchorTarget->bstrVal ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleHyperlink::get_startIndex(long* aIndex) {
+ if (!aIndex) return E_INVALIDARG;
+
+ *aIndex = 0;
+
+ Accessible* thisObj = Acc();
+ if (!thisObj) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ if (!thisObj->IsLink()) return S_FALSE;
+
+ *aIndex = thisObj->StartOffset();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleHyperlink::get_endIndex(long* aIndex) {
+ if (!aIndex) return E_INVALIDARG;
+
+ *aIndex = 0;
+
+ Accessible* thisObj = Acc();
+ if (!thisObj) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ if (!thisObj->IsLink()) return S_FALSE;
+
+ *aIndex = thisObj->EndOffset();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleHyperlink::get_valid(boolean* aValid) {
+ if (!aValid) return E_INVALIDARG;
+
+ *aValid = false;
+
+ Accessible* thisObj = Acc();
+ if (!thisObj) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ if (!thisObj->IsLink()) return S_FALSE;
+
+ *aValid = thisObj->IsLinkValid();
+ return S_OK;
+}
diff --git a/accessible/windows/ia2/ia2AccessibleHyperlink.h b/accessible/windows/ia2/ia2AccessibleHyperlink.h
new file mode 100644
index 0000000000..e7a2c5a0e2
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleHyperlink.h
@@ -0,0 +1,55 @@
+/* -*- 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 _ACCESSIBLE_HYPERLINK_H
+#define _ACCESSIBLE_HYPERLINK_H
+
+#include "nsISupports.h"
+
+#include "ia2AccessibleAction.h"
+#include "AccessibleHyperlink.h"
+
+namespace mozilla {
+namespace a11y {
+class Accessible;
+class AccessibleWrap;
+
+class ia2AccessibleHyperlink : public ia2AccessibleAction,
+ public IAccessibleHyperlink {
+ public:
+ // IUnknown
+ STDMETHODIMP QueryInterface(REFIID, void**);
+
+ // IAccessibleAction
+ FORWARD_IACCESSIBLEACTION(ia2AccessibleAction)
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_anchor(
+ /* [in] */ long index,
+ /* [retval][out] */ VARIANT* anchor);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_anchorTarget(
+ /* [in] */ long index,
+ /* [retval][out] */ VARIANT* anchorTarget);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_startIndex(
+ /* [retval][out] */ long* index);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_endIndex(
+ /* [retval][out] */ long* index);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_valid(
+ /* [retval][out] */ boolean* valid);
+
+ private:
+ Accessible* Acc();
+ AccessibleWrap* LocalAcc();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/ia2/ia2AccessibleHypertext.cpp b/accessible/windows/ia2/ia2AccessibleHypertext.cpp
new file mode 100644
index 0000000000..cc8c0cd321
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleHypertext.cpp
@@ -0,0 +1,142 @@
+/* -*- 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 "ia2AccessibleHypertext.h"
+
+#include "AccessibleHypertext_i.c"
+#include "AccessibleHypertext2_i.c"
+
+#include "mozilla/a11y/HyperTextAccessibleBase.h"
+
+#include "IUnknownImpl.h"
+
+using namespace mozilla::a11y;
+
+HyperTextAccessibleBase* ia2AccessibleHypertext::TextAcc() {
+ Accessible* acc = Acc();
+ return acc ? acc->AsHyperTextBase() : nullptr;
+}
+
+// IUnknown
+STDMETHODIMP
+ia2AccessibleHypertext::QueryInterface(REFIID aIID, void** aInstancePtr) {
+ if (!aInstancePtr) return E_FAIL;
+
+ *aInstancePtr = nullptr;
+
+ Accessible* acc = Acc();
+ if (acc && acc->IsTextRole()) {
+ if (aIID == IID_IAccessibleText) {
+ *aInstancePtr =
+ static_cast<IAccessibleText*>(static_cast<ia2AccessibleText*>(this));
+ } else if (aIID == IID_IAccessibleHypertext) {
+ *aInstancePtr = static_cast<IAccessibleHypertext*>(this);
+ } else if (aIID == IID_IAccessibleHypertext2) {
+ *aInstancePtr = static_cast<IAccessibleHypertext2*>(this);
+ } else if (aIID == IID_IAccessibleEditableText) {
+ *aInstancePtr = static_cast<IAccessibleEditableText*>(this);
+ } else if (aIID == IID_IAccessibleTextSelectionContainer) {
+ *aInstancePtr = static_cast<IAccessibleTextSelectionContainer*>(this);
+ }
+
+ if (*aInstancePtr) {
+ AddRef();
+ return S_OK;
+ }
+ }
+
+ return MsaaAccessible::QueryInterface(aIID, aInstancePtr);
+}
+
+// IAccessibleHypertext
+
+STDMETHODIMP
+ia2AccessibleHypertext::get_nHyperlinks(long* aHyperlinkCount) {
+ if (!aHyperlinkCount) return E_INVALIDARG;
+
+ *aHyperlinkCount = 0;
+
+ HyperTextAccessibleBase* hyperText = TextAcc();
+ if (!hyperText) return CO_E_OBJNOTCONNECTED;
+
+ *aHyperlinkCount = hyperText->LinkCount();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleHypertext::get_hyperlink(long aLinkIndex,
+ IAccessibleHyperlink** aHyperlink) {
+ if (!aHyperlink) return E_INVALIDARG;
+
+ *aHyperlink = nullptr;
+
+ HyperTextAccessibleBase* hyperText = TextAcc();
+ if (!hyperText) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ Accessible* hyperLink = hyperText->LinkAt(aLinkIndex);
+
+ if (!hyperLink) return E_FAIL;
+
+ RefPtr<IAccessibleHyperlink> result = MsaaAccessible::GetFrom(hyperLink);
+ result.forget(aHyperlink);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleHypertext::get_hyperlinkIndex(long aCharIndex,
+ long* aHyperlinkIndex) {
+ if (!aHyperlinkIndex) return E_INVALIDARG;
+
+ *aHyperlinkIndex = 0;
+
+ HyperTextAccessibleBase* hyperAcc = TextAcc();
+ if (!hyperAcc) return CO_E_OBJNOTCONNECTED;
+
+ *aHyperlinkIndex = hyperAcc->LinkIndexAtOffset(aCharIndex);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleHypertext::get_hyperlinks(IAccessibleHyperlink*** aHyperlinks,
+ long* aNHyperlinks) {
+ if (!aHyperlinks || !aNHyperlinks) {
+ return E_INVALIDARG;
+ }
+
+ *aHyperlinks = nullptr;
+ *aNHyperlinks = 0;
+
+ HyperTextAccessibleBase* hyperText = TextAcc();
+ if (!hyperText) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ uint32_t count = hyperText->LinkCount();
+ *aNHyperlinks = count;
+
+ if (count == 0) {
+ *aHyperlinks = nullptr;
+ return S_FALSE;
+ }
+
+ *aHyperlinks = static_cast<IAccessibleHyperlink**>(
+ ::CoTaskMemAlloc(sizeof(IAccessibleHyperlink*) * count));
+ if (!*aHyperlinks) {
+ return E_OUTOFMEMORY;
+ }
+
+ for (uint32_t i = 0; i < count; ++i) {
+ Accessible* hyperLink = hyperText->LinkAt(i);
+ MOZ_ASSERT(hyperLink);
+ RefPtr<IAccessibleHyperlink> iaHyper = MsaaAccessible::GetFrom(hyperLink);
+ iaHyper.forget(&(*aHyperlinks)[i]);
+ }
+
+ return S_OK;
+}
diff --git a/accessible/windows/ia2/ia2AccessibleHypertext.h b/accessible/windows/ia2/ia2AccessibleHypertext.h
new file mode 100644
index 0000000000..7e99169e2c
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleHypertext.h
@@ -0,0 +1,70 @@
+/* -*- 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 _ACCESSIBLE_HYPERTEXT_H
+#define _ACCESSIBLE_HYPERTEXT_H
+
+#include "nsISupports.h"
+
+#include "ia2AccessibleEditableText.h"
+#include "ia2AccessibleText.h"
+#include "ia2AccessibleTextSelectionContainer.h"
+#include "AccessibleHypertext2.h"
+#include "IUnknownImpl.h"
+#include "MsaaAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+class HyperTextAccessibleBase;
+
+class ia2AccessibleHypertext : public ia2AccessibleText,
+ public IAccessibleHypertext2,
+ public ia2AccessibleEditableText,
+ public ia2AccessibleTextSelectionContainer,
+ public MsaaAccessible {
+ public:
+ // IUnknown
+ DECL_IUNKNOWN_INHERITED
+ IMPL_IUNKNOWN_REFCOUNTING_INHERITED(MsaaAccessible)
+
+ // IAccessible2
+ // We indirectly inherit IAccessible2, which has a get_attributes method,
+ // but IAccessibleText also has a get_attributes method with a different
+ // signature. We want both.
+ using MsaaAccessible::get_attributes;
+
+ // IAccessibleText
+ FORWARD_IACCESSIBLETEXT(ia2AccessibleText)
+
+ // IAccessibleHypertext
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_nHyperlinks(
+ /* [retval][out] */ long* hyperlinkCount);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_hyperlink(
+ /* [in] */ long index,
+ /* [retval][out] */ IAccessibleHyperlink** hyperlink);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_hyperlinkIndex(
+ /* [in] */ long charIndex,
+ /* [retval][out] */ long* hyperlinkIndex);
+
+ // IAccessibleHypertext2
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_hyperlinks(
+ /* [out, size_is(,*nHyperlinks)] */ IAccessibleHyperlink*** hyperlinks,
+ /* [out, retval] */ long* nHyperlinks);
+
+ protected:
+ using MsaaAccessible::MsaaAccessible;
+
+ private:
+ HyperTextAccessibleBase* TextAcc();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/ia2/ia2AccessibleImage.cpp b/accessible/windows/ia2/ia2AccessibleImage.cpp
new file mode 100644
index 0000000000..529269b566
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleImage.cpp
@@ -0,0 +1,81 @@
+/* -*- 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 "ia2AccessibleImage.h"
+
+#include "AccessibleImage_i.c"
+
+#include "ImageAccessible.h"
+#include "IUnknownImpl.h"
+#include "nsIAccessibleTypes.h"
+
+#include "nsString.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+// IUnknown
+IMPL_IUNKNOWN_QUERY_HEAD(ia2AccessibleImage)
+IMPL_IUNKNOWN_QUERY_IFACE(IAccessibleImage)
+IMPL_IUNKNOWN_QUERY_TAIL_INHERITED(MsaaAccessible)
+
+// IAccessibleImage
+
+STDMETHODIMP
+ia2AccessibleImage::get_description(BSTR* aDescription) {
+ if (!aDescription) return E_INVALIDARG;
+
+ *aDescription = nullptr;
+
+ Accessible* acc = Acc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ nsAutoString description;
+ acc->Name(description);
+ if (description.IsEmpty()) return S_FALSE;
+
+ *aDescription = ::SysAllocStringLen(description.get(), description.Length());
+ return *aDescription ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleImage::get_imagePosition(enum IA2CoordinateType aCoordType,
+ long* aX, long* aY) {
+ if (!aX || !aY) return E_INVALIDARG;
+
+ *aX = 0;
+ *aY = 0;
+
+ Accessible* acc = Acc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ uint32_t geckoCoordType =
+ (aCoordType == IA2_COORDTYPE_SCREEN_RELATIVE)
+ ? nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE
+ : nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE;
+
+ LayoutDeviceIntPoint pos = acc->Position(geckoCoordType);
+ *aX = pos.x;
+ *aY = pos.y;
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleImage::get_imageSize(long* aHeight, long* aWidth) {
+ if (!aHeight || !aWidth) return E_INVALIDARG;
+
+ *aHeight = 0;
+ *aWidth = 0;
+
+ Accessible* acc = Acc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ LayoutDeviceIntSize size = acc->Size();
+ *aHeight = size.width;
+ *aWidth = size.height;
+ return S_OK;
+}
diff --git a/accessible/windows/ia2/ia2AccessibleImage.h b/accessible/windows/ia2/ia2AccessibleImage.h
new file mode 100644
index 0000000000..6302130b3a
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleImage.h
@@ -0,0 +1,51 @@
+/* -*- 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 _ACCESSIBLE_IMAGE_H
+#define _ACCESSIBLE_IMAGE_H
+
+#include "AccessibleImage.h"
+#include "IUnknownImpl.h"
+#include "MsaaAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+class ImageAccessible;
+
+class ia2AccessibleImage : public IAccessibleImage, public MsaaAccessible {
+ public:
+ // IUnknown
+ DECL_IUNKNOWN_INHERITED
+ IMPL_IUNKNOWN_REFCOUNTING_INHERITED(MsaaAccessible)
+
+ // IAccessibleAction
+ // We indirectly inherit IAccessibleAction, which has a get_description
+ // method, but IAccessibleImage also has a get_description method with a
+ // different signature. We want both.
+ using MsaaAccessible::get_description;
+
+ // IAccessibleImage
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_description(
+ /* [retval][out] */ BSTR* description);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_imagePosition(
+ /* [in] */ enum IA2CoordinateType coordinateType,
+ /* [out] */ long* x,
+ /* [retval][out] */ long* y);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_imageSize(
+ /* [out] */ long* height,
+ /* [retval][out] */ long* width);
+
+ protected:
+ using MsaaAccessible::MsaaAccessible;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/ia2/ia2AccessibleRelation.cpp b/accessible/windows/ia2/ia2AccessibleRelation.cpp
new file mode 100644
index 0000000000..007ca63aa6
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleRelation.cpp
@@ -0,0 +1,94 @@
+/* -*- 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 "ia2AccessibleRelation.h"
+
+#include "Relation.h"
+#include "nsID.h"
+
+#include "AccessibleRelation_i.c"
+
+using namespace mozilla::a11y;
+
+ia2AccessibleRelation::ia2AccessibleRelation(RelationType aType, Relation* aRel)
+ : mType(aType) {
+ Accessible* target = nullptr;
+ while ((target = aRel->Next())) {
+ mTargets.AppendElement(
+ already_AddRefed(MsaaAccessible::NativeAccessible(target)));
+ }
+}
+
+// IUnknown
+
+IMPL_IUNKNOWN_QUERY_HEAD(ia2AccessibleRelation)
+IMPL_IUNKNOWN_QUERY_IFACE(IAccessibleRelation)
+IMPL_IUNKNOWN_QUERY_IFACE(IUnknown)
+IMPL_IUNKNOWN_QUERY_TAIL
+
+// IAccessibleRelation
+
+STDMETHODIMP
+ia2AccessibleRelation::get_relationType(BSTR* aRelationType) {
+ if (!aRelationType) return E_INVALIDARG;
+
+ *aRelationType = nullptr;
+
+#define RELATIONTYPE(geckoType, geckoTypeName, atkType, msaaType, ia2Type) \
+ case RelationType::geckoType: \
+ *aRelationType = ::SysAllocString(ia2Type); \
+ break;
+
+ switch (mType) {
+#include "RelationTypeMap.h"
+ }
+
+ return *aRelationType ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleRelation::get_localizedRelationType(BSTR* aLocalizedRelationType) {
+ if (!aLocalizedRelationType) return E_INVALIDARG;
+
+ *aLocalizedRelationType = nullptr;
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP
+ia2AccessibleRelation::get_nTargets(long* aNTargets) {
+ if (!aNTargets) return E_INVALIDARG;
+
+ *aNTargets = mTargets.Length();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleRelation::get_target(long aTargetIndex, IUnknown** aTarget) {
+ if (aTargetIndex < 0 || (uint32_t)aTargetIndex >= mTargets.Length() ||
+ !aTarget)
+ return E_INVALIDARG;
+
+ RefPtr<IUnknown> target = mTargets[aTargetIndex];
+ target.forget(aTarget);
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleRelation::get_targets(long aMaxTargets, IUnknown** aTargets,
+ long* aNTargets) {
+ if (!aNTargets || !aTargets) return E_INVALIDARG;
+
+ *aNTargets = 0;
+ long maxTargets = mTargets.Length();
+ if (maxTargets > aMaxTargets) maxTargets = aMaxTargets;
+
+ for (long idx = 0; idx < maxTargets; idx++) get_target(idx, aTargets + idx);
+
+ *aNTargets = maxTargets;
+ return S_OK;
+}
diff --git a/accessible/windows/ia2/ia2AccessibleRelation.h b/accessible/windows/ia2/ia2AccessibleRelation.h
new file mode 100644
index 0000000000..969abc9eef
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleRelation.h
@@ -0,0 +1,80 @@
+/* -*- 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 _NS_ACCESSIBLE_RELATION_WRAP_H
+#define _NS_ACCESSIBLE_RELATION_WRAP_H
+
+#include "MsaaAccessible.h"
+#include "IUnknownImpl.h"
+
+#include <utility>
+#include "nsTArray.h"
+
+#include "mozilla/a11y/RelationType.h"
+#include "mozilla/a11y/Accessible.h"
+#include "AccessibleRelation.h"
+
+namespace mozilla {
+namespace a11y {
+
+class ia2AccessibleRelation final : public IAccessibleRelation {
+ public:
+ ia2AccessibleRelation(RelationType aType, Relation* aRel);
+
+ // IUnknown
+ DECL_IUNKNOWN
+
+ // IAccessibleRelation
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_relationType(
+ /* [retval][out] */ BSTR* relationType);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_localizedRelationType(
+ /* [retval][out] */ BSTR* localizedRelationType);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_nTargets(
+ /* [retval][out] */ long* nTargets);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_target(
+ /* [in] */ long targetIndex,
+ /* [retval][out] */ IUnknown** target);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_targets(
+ /* [in] */ long maxTargets,
+ /* [length_is][size_is][out] */ IUnknown** target,
+ /* [retval][out] */ long* nTargets);
+
+ inline bool HasTargets() const { return mTargets.Length(); }
+
+ private:
+ ia2AccessibleRelation();
+ ia2AccessibleRelation(const ia2AccessibleRelation&);
+ ia2AccessibleRelation& operator=(const ia2AccessibleRelation&);
+
+ RelationType mType;
+ nsTArray<RefPtr<IUnknown>> mTargets;
+};
+
+/**
+ * Gecko to IAccessible2 relation types map.
+ */
+
+const WCHAR* const IA2_RELATION_NULL = L"";
+
+#define RELATIONTYPE(geckoType, name, atkType, msaaType, ia2Type) \
+ std::pair<RelationType, const WCHAR* const>(RelationType::geckoType, ia2Type),
+
+static const std::pair<RelationType, const WCHAR* const> sRelationTypePairs[] =
+ {
+#include "RelationTypeMap.h"
+};
+
+#undef RELATIONTYPE
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/ia2/ia2AccessibleTable.cpp b/accessible/windows/ia2/ia2AccessibleTable.cpp
new file mode 100644
index 0000000000..50bdc79967
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleTable.cpp
@@ -0,0 +1,534 @@
+/* -*- 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 "ia2AccessibleTable.h"
+
+#include "Accessible2.h"
+#include "AccessibleTable_i.c"
+#include "AccessibleTable2_i.c"
+
+#include "IUnknownImpl.h"
+#include "mozilla/a11y/Accessible.h"
+#include "mozilla/a11y/TableAccessible.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "Statistics.h"
+
+using namespace mozilla::a11y;
+
+TableAccessible* ia2AccessibleTable::TableAcc() {
+ Accessible* acc = Acc();
+ return acc ? acc->AsTable() : nullptr;
+}
+
+// IUnknown
+
+STDMETHODIMP
+ia2AccessibleTable::QueryInterface(REFIID iid, void** ppv) {
+ if (!ppv) return E_INVALIDARG;
+
+ *ppv = nullptr;
+
+ if (IID_IAccessibleTable == iid) {
+ statistics::IAccessibleTableUsed();
+ *ppv = static_cast<IAccessibleTable*>(this);
+ (reinterpret_cast<IUnknown*>(*ppv))->AddRef();
+ return S_OK;
+ }
+
+ if (IID_IAccessibleTable2 == iid) {
+ *ppv = static_cast<IAccessibleTable2*>(this);
+ (reinterpret_cast<IUnknown*>(*ppv))->AddRef();
+ return S_OK;
+ }
+
+ return ia2AccessibleHypertext::QueryInterface(iid, ppv);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// IAccessibleTable
+
+STDMETHODIMP
+ia2AccessibleTable::get_accessibleAt(long aRowIdx, long aColIdx,
+ IUnknown** aAccessible) {
+ return get_cellAt(aRowIdx, aColIdx, aAccessible);
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_caption(IUnknown** aAccessible) {
+ if (!aAccessible) return E_INVALIDARG;
+
+ *aAccessible = nullptr;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ Accessible* caption = table->Caption();
+ if (!caption) return S_FALSE;
+
+ RefPtr<IAccessible> result = MsaaAccessible::GetFrom(caption);
+ result.forget(aAccessible);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_childIndex(long aRowIdx, long aColIdx,
+ long* aChildIdx) {
+ if (!aChildIdx) return E_INVALIDARG;
+
+ *aChildIdx = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ if (aRowIdx < 0 || aColIdx < 0 ||
+ static_cast<uint32_t>(aRowIdx) >= table->RowCount() ||
+ static_cast<uint32_t>(aColIdx) >= table->ColCount())
+ return E_INVALIDARG;
+
+ *aChildIdx = table->CellIndexAt(aRowIdx, aColIdx);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_columnDescription(long aColIdx, BSTR* aDescription) {
+ if (!aDescription) return E_INVALIDARG;
+
+ *aDescription = nullptr;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ if (aColIdx < 0 || static_cast<uint32_t>(aColIdx) >= table->ColCount())
+ return E_INVALIDARG;
+
+ nsAutoString descr;
+ table->ColDescription(aColIdx, descr);
+ if (descr.IsEmpty()) return S_FALSE;
+
+ *aDescription = ::SysAllocStringLen(descr.get(), descr.Length());
+ return *aDescription ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_columnExtentAt(long aRowIdx, long aColIdx,
+ long* aSpan) {
+ if (!aSpan) return E_INVALIDARG;
+
+ *aSpan = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ if (aRowIdx < 0 || aColIdx < 0 ||
+ static_cast<uint32_t>(aRowIdx) >= table->RowCount() ||
+ static_cast<uint32_t>(aColIdx) >= table->ColCount())
+ return E_INVALIDARG;
+
+ *aSpan = table->ColExtentAt(aRowIdx, aColIdx);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_columnHeader(IAccessibleTable** aAccessibleTable,
+ long* aStartingRowIndex) {
+ if (!aAccessibleTable || !aStartingRowIndex) return E_INVALIDARG;
+
+ *aAccessibleTable = nullptr;
+ *aStartingRowIndex = -1;
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_columnIndex(long aCellIdx, long* aColIdx) {
+ if (!aColIdx) return E_INVALIDARG;
+
+ *aColIdx = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ if (aCellIdx < 0) {
+ return E_INVALIDARG;
+ }
+
+ long colIdx = table->ColIndexAt(aCellIdx);
+ if (colIdx == -1) { // Indicates an error.
+ return E_INVALIDARG;
+ }
+
+ *aColIdx = colIdx;
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_nColumns(long* aColCount) {
+ if (!aColCount) return E_INVALIDARG;
+
+ *aColCount = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ *aColCount = table->ColCount();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_nRows(long* aRowCount) {
+ if (!aRowCount) return E_INVALIDARG;
+
+ *aRowCount = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ *aRowCount = table->RowCount();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_nSelectedChildren(long* aChildCount) {
+ return get_nSelectedCells(aChildCount);
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_nSelectedColumns(long* aColCount) {
+ if (!aColCount) return E_INVALIDARG;
+
+ *aColCount = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ *aColCount = table->SelectedColCount();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_nSelectedRows(long* aRowCount) {
+ if (!aRowCount) return E_INVALIDARG;
+
+ *aRowCount = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ *aRowCount = table->SelectedRowCount();
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_rowDescription(long aRowIdx, BSTR* aDescription) {
+ if (!aDescription) return E_INVALIDARG;
+
+ *aDescription = nullptr;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ if (aRowIdx < 0 || static_cast<uint32_t>(aRowIdx) >= table->RowCount())
+ return E_INVALIDARG;
+
+ nsAutoString descr;
+ table->RowDescription(aRowIdx, descr);
+ if (descr.IsEmpty()) return S_FALSE;
+
+ *aDescription = ::SysAllocStringLen(descr.get(), descr.Length());
+ return *aDescription ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_rowExtentAt(long aRowIdx, long aColIdx, long* aSpan) {
+ if (!aSpan) return E_INVALIDARG;
+
+ *aSpan = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ if (aRowIdx < 0 || aColIdx < 0 ||
+ static_cast<uint32_t>(aRowIdx) >= table->RowCount() ||
+ static_cast<uint32_t>(aColIdx) >= table->ColCount())
+ return E_INVALIDARG;
+
+ *aSpan = table->RowExtentAt(aRowIdx, aColIdx);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_rowHeader(IAccessibleTable** aAccessibleTable,
+ long* aStartingColumnIndex) {
+ if (!aAccessibleTable || !aStartingColumnIndex) return E_INVALIDARG;
+
+ *aAccessibleTable = nullptr;
+ *aStartingColumnIndex = -1;
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_rowIndex(long aCellIdx, long* aRowIdx) {
+ if (!aRowIdx) return E_INVALIDARG;
+
+ *aRowIdx = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ if (aCellIdx < 0) {
+ return E_INVALIDARG;
+ }
+
+ long rowIdx = table->RowIndexAt(aCellIdx);
+ if (rowIdx == -1) { // Indicates an error.
+ return E_INVALIDARG;
+ }
+
+ *aRowIdx = rowIdx;
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_selectedChildren(long aMaxChildren, long** aChildren,
+ long* aNChildren) {
+ if (!aChildren || !aNChildren) return E_INVALIDARG;
+
+ *aChildren = nullptr;
+ *aNChildren = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ AutoTArray<uint32_t, 30> cellIndices;
+ table->SelectedCellIndices(&cellIndices);
+
+ uint32_t maxCells = cellIndices.Length();
+ if (maxCells == 0) return S_FALSE;
+
+ *aChildren = static_cast<LONG*>(moz_xmalloc(sizeof(LONG) * maxCells));
+ *aNChildren = maxCells;
+ for (uint32_t i = 0; i < maxCells; i++) (*aChildren)[i] = cellIndices[i];
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_selectedColumns(long aMaxColumns, long** aColumns,
+ long* aNColumns) {
+ return get_selectedColumns(aColumns, aNColumns);
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_selectedRows(long aMaxRows, long** aRows,
+ long* aNRows) {
+ return get_selectedRows(aRows, aNRows);
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_summary(IUnknown** aAccessible) {
+ if (!aAccessible) return E_INVALIDARG;
+
+ // 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 Table::summary to get a summary as a string which is not mapped
+ // directly to IAccessible2.
+
+ *aAccessible = nullptr;
+ return S_FALSE;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_isColumnSelected(long aColIdx, boolean* aIsSelected) {
+ if (!aIsSelected) return E_INVALIDARG;
+
+ *aIsSelected = false;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ if (aColIdx < 0 || static_cast<uint32_t>(aColIdx) >= table->ColCount())
+ return E_INVALIDARG;
+
+ *aIsSelected = table->IsColSelected(aColIdx);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_isRowSelected(long aRowIdx, boolean* aIsSelected) {
+ if (!aIsSelected) return E_INVALIDARG;
+
+ *aIsSelected = false;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ if (aRowIdx < 0 || static_cast<uint32_t>(aRowIdx) >= table->RowCount())
+ return E_INVALIDARG;
+
+ *aIsSelected = table->IsRowSelected(aRowIdx);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_isSelected(long aRowIdx, long aColIdx,
+ boolean* aIsSelected) {
+ if (!aIsSelected) return E_INVALIDARG;
+
+ *aIsSelected = false;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ if (aRowIdx < 0 || aColIdx < 0 ||
+ static_cast<uint32_t>(aColIdx) >= table->ColCount() ||
+ static_cast<uint32_t>(aRowIdx) >= table->RowCount())
+ return E_INVALIDARG;
+
+ *aIsSelected = table->IsCellSelected(aRowIdx, aColIdx);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::selectRow(long aRowIdx) { return E_NOTIMPL; }
+
+STDMETHODIMP
+ia2AccessibleTable::selectColumn(long aColIdx) { return E_NOTIMPL; }
+
+STDMETHODIMP
+ia2AccessibleTable::unselectRow(long aRowIdx) { return E_NOTIMPL; }
+
+STDMETHODIMP
+ia2AccessibleTable::unselectColumn(long aColIdx) { return E_NOTIMPL; }
+
+STDMETHODIMP
+ia2AccessibleTable::get_rowColumnExtentsAtIndex(long aCellIdx, long* aRowIdx,
+ long* aColIdx,
+ long* aRowExtents,
+ long* aColExtents,
+ boolean* aIsSelected) {
+ if (!aRowIdx || !aColIdx || !aRowExtents || !aColExtents || !aIsSelected)
+ return E_INVALIDARG;
+
+ *aRowIdx = 0;
+ *aColIdx = 0;
+ *aRowExtents = 0;
+ *aColExtents = 0;
+ *aIsSelected = false;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ if (aCellIdx < 0) {
+ return E_INVALIDARG;
+ }
+
+ int32_t colIdx = 0, rowIdx = 0;
+ table->RowAndColIndicesAt(aCellIdx, &rowIdx, &colIdx);
+ if (rowIdx == -1 || colIdx == -1) { // Indicates an error.
+ return E_INVALIDARG;
+ }
+
+ *aRowIdx = rowIdx;
+ *aColIdx = colIdx;
+ *aRowExtents = table->RowExtentAt(rowIdx, colIdx);
+ *aColExtents = table->ColExtentAt(rowIdx, colIdx);
+ *aIsSelected = table->IsCellSelected(rowIdx, colIdx);
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_modelChange(IA2TableModelChange* aModelChange) {
+ return E_NOTIMPL;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// IAccessibleTable2
+
+STDMETHODIMP
+ia2AccessibleTable::get_cellAt(long aRowIdx, long aColIdx, IUnknown** aCell) {
+ if (!aCell) return E_INVALIDARG;
+
+ *aCell = nullptr;
+
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ Accessible* cell = table->CellAt(aRowIdx, aColIdx);
+ if (!cell) return E_INVALIDARG;
+
+ RefPtr<IAccessible> result = MsaaAccessible::GetFrom(cell);
+ result.forget(aCell);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_nSelectedCells(long* aCellCount) {
+ if (!aCellCount) return E_INVALIDARG;
+
+ *aCellCount = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ *aCellCount = table->SelectedCellCount();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_selectedCells(IUnknown*** aCells,
+ long* aNSelectedCells) {
+ if (!aCells || !aNSelectedCells) return E_INVALIDARG;
+
+ *aCells = nullptr;
+ *aNSelectedCells = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ AutoTArray<Accessible*, 30> cells;
+ table->SelectedCells(&cells);
+ if (cells.IsEmpty()) return S_FALSE;
+
+ *aCells = static_cast<IUnknown**>(
+ ::CoTaskMemAlloc(sizeof(IUnknown*) * cells.Length()));
+ if (!*aCells) return E_OUTOFMEMORY;
+
+ for (uint32_t i = 0; i < cells.Length(); i++) {
+ RefPtr<IAccessible> cell = MsaaAccessible::GetFrom(cells[i]);
+ cell.forget(&(*aCells)[i]);
+ }
+
+ *aNSelectedCells = cells.Length();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_selectedColumns(long** aColumns, long* aNColumns) {
+ if (!aColumns || !aNColumns) return E_INVALIDARG;
+
+ *aColumns = nullptr;
+ *aNColumns = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ AutoTArray<uint32_t, 30> colIndices;
+ table->SelectedColIndices(&colIndices);
+
+ uint32_t maxCols = colIndices.Length();
+ if (maxCols == 0) return S_FALSE;
+
+ *aColumns = static_cast<LONG*>(moz_xmalloc(sizeof(LONG) * maxCols));
+ *aNColumns = maxCols;
+ for (uint32_t i = 0; i < maxCols; i++) (*aColumns)[i] = colIndices[i];
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTable::get_selectedRows(long** aRows, long* aNRows) {
+ if (!aRows || !aNRows) return E_INVALIDARG;
+
+ *aRows = nullptr;
+ *aNRows = 0;
+ TableAccessible* table = TableAcc();
+ if (!table) return CO_E_OBJNOTCONNECTED;
+
+ AutoTArray<uint32_t, 30> rowIndices;
+ table->SelectedRowIndices(&rowIndices);
+
+ uint32_t maxRows = rowIndices.Length();
+ if (maxRows == 0) return S_FALSE;
+
+ *aRows = static_cast<LONG*>(moz_xmalloc(sizeof(LONG) * maxRows));
+ *aNRows = maxRows;
+ for (uint32_t i = 0; i < maxRows; i++) (*aRows)[i] = rowIndices[i];
+
+ return S_OK;
+}
diff --git a/accessible/windows/ia2/ia2AccessibleTable.h b/accessible/windows/ia2/ia2AccessibleTable.h
new file mode 100644
index 0000000000..622187c379
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleTable.h
@@ -0,0 +1,178 @@
+/* -*- 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 _ACCESSIBLE_TABLE_H
+#define _ACCESSIBLE_TABLE_H
+
+#include "AccessibleTable.h"
+#include "AccessibleTable2.h"
+#include "ia2AccessibleHypertext.h"
+#include "IUnknownImpl.h"
+
+namespace mozilla {
+namespace a11y {
+
+class TableAccessible;
+
+class ia2AccessibleTable : public IAccessibleTable,
+ public IAccessibleTable2,
+ public ia2AccessibleHypertext {
+ public:
+ // IUnknown
+ DECL_IUNKNOWN_INHERITED
+ IMPL_IUNKNOWN_REFCOUNTING_INHERITED(ia2AccessibleHypertext)
+
+ // IAccessibleTable
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_accessibleAt(
+ /* [in] */ long row,
+ /* [in] */ long column,
+ /* [retval][out] */ IUnknown** accessible);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_caption(
+ /* [retval][out] */ IUnknown** accessible);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_childIndex(
+ /* [in] */ long rowIndex,
+ /* [in] */ long columnIndex,
+ /* [retval][out] */ long* childIndex);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_columnDescription(
+ /* [in] */ long column,
+ /* [retval][out] */ BSTR* description);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_columnExtentAt(
+ /* [in] */ long row,
+ /* [in] */ long column,
+ /* [retval][out] */ long* nColumnsSpanned);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_columnHeader(
+ /* [out] */ IAccessibleTable** accessibleTable,
+ /* [retval][out] */ long* startingRowIndex);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_columnIndex(
+ /* [in] */ long childIndex,
+ /* [retval][out] */ long* columnIndex);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_nColumns(
+ /* [retval][out] */ long* columnCount);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_nRows(
+ /* [retval][out] */ long* rowCount);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_nSelectedChildren(
+ /* [retval][out] */ long* childCount);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_nSelectedColumns(
+ /* [retval][out] */ long* columnCount);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_nSelectedRows(
+ /* [retval][out] */ long* rowCount);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_rowDescription(
+ /* [in] */ long row,
+ /* [retval][out] */ BSTR* description);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_rowExtentAt(
+ /* [in] */ long row,
+ /* [in] */ long column,
+ /* [retval][out] */ long* nRowsSpanned);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_rowHeader(
+ /* [out] */ IAccessibleTable** accessibleTable,
+ /* [retval][out] */ long* startingColumnIndex);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_rowIndex(
+ /* [in] */ long childIndex,
+ /* [retval][out] */ long* rowIndex);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_selectedChildren(
+ /* [in] */ long maxChildren,
+ /* [length_is][length_is][size_is][size_is][out] */ long** children,
+ /* [retval][out] */ long* nChildren);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_selectedColumns(
+ /* [in] */ long maxColumns,
+ /* [length_is][length_is][size_is][size_is][out] */ long** columns,
+ /* [retval][out] */ long* nColumns);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_selectedRows(
+ /* [in] */ long maxRows,
+ /* [length_is][length_is][size_is][size_is][out] */ long** rows,
+ /* [retval][out] */ long* nRows);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_summary(
+ /* [retval][out] */ IUnknown** accessible);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_isColumnSelected(
+ /* [in] */ long column,
+ /* [retval][out] */ boolean* isSelected);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_isRowSelected(
+ /* [in] */ long row,
+ /* [retval][out] */ boolean* isSelected);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_isSelected(
+ /* [in] */ long row,
+ /* [in] */ long column,
+ /* [retval][out] */ boolean* isSelected);
+
+ virtual HRESULT STDMETHODCALLTYPE selectRow(
+ /* [in] */ long row);
+
+ virtual HRESULT STDMETHODCALLTYPE selectColumn(
+ /* [in] */ long column);
+
+ virtual HRESULT STDMETHODCALLTYPE unselectRow(
+ /* [in] */ long row);
+
+ virtual HRESULT STDMETHODCALLTYPE unselectColumn(
+ /* [in] */ long column);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_rowColumnExtentsAtIndex(
+ /* [in] */ long index,
+ /* [out] */ long* row,
+ /* [out] */ long* column,
+ /* [out] */ long* rowExtents,
+ /* [out] */ long* columnExtents,
+ /* [retval][out] */ boolean* isSelected);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_modelChange(
+ /* [retval][out] */ IA2TableModelChange* modelChange);
+
+ // IAccessibleTable2
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_cellAt(
+ /* [in] */ long row,
+ /* [in] */ long column,
+ /* [out, retval] */ IUnknown** cell);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_nSelectedCells(
+ /* [out, retval] */ long* cellCount);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_selectedCells(
+ /* [out, size_is(,*nSelectedCells,)] */ IUnknown*** cells,
+ /* [out, retval] */ long* nSelectedCells);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_selectedColumns(
+ /* [out, size_is(,*nColumns)] */ long** selectedColumns,
+ /* [out, retval] */ long* nColumns);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_selectedRows(
+ /* [out, size_is(,*nRows)] */ long** selectedRows,
+ /* [out, retval] */ long* nRows);
+
+ protected:
+ using ia2AccessibleHypertext::ia2AccessibleHypertext;
+
+ private:
+ TableAccessible* TableAcc();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/ia2/ia2AccessibleTableCell.cpp b/accessible/windows/ia2/ia2AccessibleTableCell.cpp
new file mode 100644
index 0000000000..0204983a08
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleTableCell.cpp
@@ -0,0 +1,186 @@
+/* -*- 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 "ia2AccessibleTableCell.h"
+
+#include "AccessibleTable2_i.c"
+#include "AccessibleTableCell_i.c"
+
+#include "IUnknownImpl.h"
+#include "mozilla/a11y/Accessible.h"
+#include "mozilla/a11y/TableAccessible.h"
+#include "mozilla/a11y/TableCellAccessible.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+using namespace mozilla::a11y;
+
+TableCellAccessible* ia2AccessibleTableCell::CellAcc() {
+ Accessible* acc = Acc();
+ return acc ? acc->AsTableCell() : nullptr;
+}
+
+// IUnknown
+IMPL_IUNKNOWN_QUERY_HEAD(ia2AccessibleTableCell)
+IMPL_IUNKNOWN_QUERY_IFACE(IAccessibleTableCell)
+IMPL_IUNKNOWN_QUERY_TAIL_INHERITED(ia2AccessibleHypertext)
+
+////////////////////////////////////////////////////////////////////////////////
+// IAccessibleTableCell
+
+STDMETHODIMP
+ia2AccessibleTableCell::get_table(IUnknown** aTable) {
+ if (!aTable) return E_INVALIDARG;
+
+ *aTable = nullptr;
+ TableCellAccessible* tableCell = CellAcc();
+ if (!tableCell) return CO_E_OBJNOTCONNECTED;
+
+ TableAccessible* table = tableCell->Table();
+ if (!table) return E_FAIL;
+
+ Accessible* tableAcc = table->AsAccessible();
+ RefPtr<IAccessible> result = MsaaAccessible::GetFrom(tableAcc);
+ result.forget(aTable);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTableCell::get_columnExtent(long* aSpan) {
+ if (!aSpan) return E_INVALIDARG;
+
+ *aSpan = 0;
+ TableCellAccessible* tableCell = CellAcc();
+ if (!tableCell) return CO_E_OBJNOTCONNECTED;
+
+ *aSpan = tableCell->ColExtent();
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTableCell::get_columnHeaderCells(IUnknown*** aCellAccessibles,
+ long* aNColumnHeaderCells) {
+ if (!aCellAccessibles || !aNColumnHeaderCells) return E_INVALIDARG;
+
+ *aCellAccessibles = nullptr;
+ *aNColumnHeaderCells = 0;
+ TableCellAccessible* tableCell = CellAcc();
+ if (!tableCell) return CO_E_OBJNOTCONNECTED;
+
+ AutoTArray<Accessible*, 10> cells;
+ tableCell->ColHeaderCells(&cells);
+
+ *aNColumnHeaderCells = cells.Length();
+ *aCellAccessibles = static_cast<IUnknown**>(
+ ::CoTaskMemAlloc(sizeof(IUnknown*) * cells.Length()));
+
+ if (!*aCellAccessibles) return E_OUTOFMEMORY;
+
+ for (uint32_t i = 0; i < cells.Length(); i++) {
+ RefPtr<IAccessible> iaCell = MsaaAccessible::GetFrom(cells[i]);
+ iaCell.forget(&(*aCellAccessibles)[i]);
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTableCell::get_columnIndex(long* aColIdx) {
+ if (!aColIdx) return E_INVALIDARG;
+
+ *aColIdx = -1;
+ TableCellAccessible* tableCell = CellAcc();
+ if (!tableCell) return CO_E_OBJNOTCONNECTED;
+
+ *aColIdx = tableCell->ColIdx();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTableCell::get_rowExtent(long* aSpan) {
+ if (!aSpan) return E_INVALIDARG;
+
+ *aSpan = 0;
+ TableCellAccessible* tableCell = CellAcc();
+ if (!tableCell) return CO_E_OBJNOTCONNECTED;
+
+ *aSpan = tableCell->RowExtent();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTableCell::get_rowHeaderCells(IUnknown*** aCellAccessibles,
+ long* aNRowHeaderCells) {
+ if (!aCellAccessibles || !aNRowHeaderCells) return E_INVALIDARG;
+
+ *aCellAccessibles = nullptr;
+ *aNRowHeaderCells = 0;
+ TableCellAccessible* tableCell = CellAcc();
+ if (!tableCell) return CO_E_OBJNOTCONNECTED;
+
+ AutoTArray<Accessible*, 10> cells;
+ tableCell->RowHeaderCells(&cells);
+
+ *aNRowHeaderCells = cells.Length();
+ *aCellAccessibles = static_cast<IUnknown**>(
+ ::CoTaskMemAlloc(sizeof(IUnknown*) * cells.Length()));
+ if (!*aCellAccessibles) return E_OUTOFMEMORY;
+
+ for (uint32_t i = 0; i < cells.Length(); i++) {
+ RefPtr<IAccessible> iaCell = MsaaAccessible::GetFrom(cells[i]);
+ iaCell.forget(&(*aCellAccessibles)[i]);
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTableCell::get_rowIndex(long* aRowIdx) {
+ if (!aRowIdx) return E_INVALIDARG;
+
+ *aRowIdx = -1;
+ TableCellAccessible* tableCell = CellAcc();
+ if (!tableCell) return CO_E_OBJNOTCONNECTED;
+
+ *aRowIdx = tableCell->RowIdx();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTableCell::get_rowColumnExtents(long* aRowIdx, long* aColIdx,
+ long* aRowExtents,
+ long* aColExtents,
+ boolean* aIsSelected) {
+ if (!aRowIdx || !aColIdx || !aRowExtents || !aColExtents || !aIsSelected)
+ return E_INVALIDARG;
+
+ *aRowIdx = *aColIdx = *aRowExtents = *aColExtents = 0;
+ *aIsSelected = false;
+ TableCellAccessible* tableCell = CellAcc();
+ if (!tableCell) return CO_E_OBJNOTCONNECTED;
+
+ *aRowIdx = tableCell->RowIdx();
+ *aColIdx = tableCell->ColIdx();
+ *aRowExtents = tableCell->RowExtent();
+ *aColExtents = tableCell->ColExtent();
+ *aIsSelected = tableCell->Selected();
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTableCell::get_isSelected(boolean* aIsSelected) {
+ if (!aIsSelected) return E_INVALIDARG;
+
+ *aIsSelected = false;
+ TableCellAccessible* tableCell = CellAcc();
+ if (!tableCell) return CO_E_OBJNOTCONNECTED;
+
+ *aIsSelected = tableCell->Selected();
+ return S_OK;
+}
diff --git a/accessible/windows/ia2/ia2AccessibleTableCell.h b/accessible/windows/ia2/ia2AccessibleTableCell.h
new file mode 100644
index 0000000000..04e978cb96
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleTableCell.h
@@ -0,0 +1,71 @@
+/* -*- 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 _ACCESSIBLE_TABLECELL_H
+#define _ACCESSIBLE_TABLECELL_H
+
+#include "AccessibleTableCell.h"
+#include "ia2AccessibleHypertext.h"
+#include "IUnknownImpl.h"
+
+namespace mozilla {
+namespace a11y {
+class TableCellAccessible;
+
+class ia2AccessibleTableCell : public IAccessibleTableCell,
+ public ia2AccessibleHypertext {
+ public:
+ // IUnknown
+ DECL_IUNKNOWN_INHERITED
+ IMPL_IUNKNOWN_REFCOUNTING_INHERITED(ia2AccessibleHypertext)
+
+ // IAccessibleTableCell
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_table(
+ /* [out, retval] */ IUnknown** table);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_columnExtent(
+ /* [out, retval] */ long* nColumnsSpanned);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_columnHeaderCells(
+ /* [out, size_is(,*nColumnHeaderCells,)] */ IUnknown*** cellAccessibles,
+ /* [out, retval] */ long* nColumnHeaderCells);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_columnIndex(
+ /* [out, retval] */ long* columnIndex);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_rowExtent(
+ /* [out, retval] */ long* nRowsSpanned);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_rowHeaderCells(
+ /* [out, size_is(,*nRowHeaderCells,)] */ IUnknown*** cellAccessibles,
+ /* [out, retval] */ long* nRowHeaderCells);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_rowIndex(
+ /* [out, retval] */ long* rowIndex);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_rowColumnExtents(
+ /* [out] */ long* row,
+ /* [out] */ long* column,
+ /* [out] */ long* rowExtents,
+ /* [out] */ long* columnExtents,
+ /* [out, retval] */ boolean* isSelected);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_isSelected(
+ /* [out, retval] */ boolean* isSelected);
+
+ protected:
+ using ia2AccessibleHypertext::ia2AccessibleHypertext;
+
+ private:
+ TableCellAccessible* CellAcc();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/ia2/ia2AccessibleText.cpp b/accessible/windows/ia2/ia2AccessibleText.cpp
new file mode 100644
index 0000000000..29589fb2df
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleText.cpp
@@ -0,0 +1,466 @@
+/* -*- 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 "ia2Accessible.h"
+#include "ia2AccessibleHypertext.h"
+#include "ia2AccessibleText.h"
+
+#include "AccessibleText_i.c"
+
+#include "mozilla/a11y/HyperTextAccessibleBase.h"
+#include "mozilla/ClearOnShutdown.h"
+
+using namespace mozilla::a11y;
+
+HyperTextAccessibleBase* ia2AccessibleText::sLastTextChangeAcc = nullptr;
+mozilla::StaticAutoPtr<nsString> ia2AccessibleText::sLastTextChangeString;
+uint32_t ia2AccessibleText::sLastTextChangeStart = 0;
+uint32_t ia2AccessibleText::sLastTextChangeEnd = 0;
+bool ia2AccessibleText::sLastTextChangeWasInsert = false;
+
+HyperTextAccessibleBase* ia2AccessibleText::TextAcc() {
+ auto hyp = static_cast<ia2AccessibleHypertext*>(this);
+ Accessible* acc = hyp->Acc();
+ return acc ? acc->AsHyperTextBase() : nullptr;
+}
+
+// IAccessibleText
+
+STDMETHODIMP
+ia2AccessibleText::addSelection(long aStartOffset, long aEndOffset) {
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ return textAcc->AddToSelection(aStartOffset, aEndOffset) ? S_OK
+ : E_INVALIDARG;
+}
+
+STDMETHODIMP
+ia2AccessibleText::get_attributes(long aOffset, long* aStartOffset,
+ long* aEndOffset, BSTR* aTextAttributes) {
+ if (!aStartOffset || !aEndOffset || !aTextAttributes) return E_INVALIDARG;
+
+ *aStartOffset = 0;
+ *aEndOffset = 0;
+ *aTextAttributes = nullptr;
+
+ int32_t startOffset = 0, endOffset = 0;
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ RefPtr<AccAttributes> attributes =
+ textAcc->TextAttributes(true, aOffset, &startOffset, &endOffset);
+
+ HRESULT hr =
+ ia2Accessible::ConvertToIA2Attributes(attributes, aTextAttributes);
+ if (FAILED(hr)) return hr;
+
+ *aStartOffset = startOffset;
+ *aEndOffset = endOffset;
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleText::get_caretOffset(long* aOffset) {
+ if (!aOffset) return E_INVALIDARG;
+
+ *aOffset = -1;
+
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ *aOffset = textAcc->CaretOffset();
+
+ return *aOffset != -1 ? S_OK : S_FALSE;
+}
+
+STDMETHODIMP
+ia2AccessibleText::get_characterExtents(long aOffset,
+ enum IA2CoordinateType aCoordType,
+ long* aX, long* aY, long* aWidth,
+ long* aHeight) {
+ if (!aX || !aY || !aWidth || !aHeight) return E_INVALIDARG;
+ *aX = *aY = *aWidth = *aHeight = 0;
+
+ uint32_t geckoCoordType =
+ (aCoordType == IA2_COORDTYPE_SCREEN_RELATIVE)
+ ? nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE
+ : nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE;
+ LayoutDeviceIntRect rect;
+ auto textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ rect = textAcc->CharBounds(aOffset, geckoCoordType);
+
+ // Can't use GetRect() because of long vs. int32_t mismatch
+ *aX = rect.X();
+ *aY = rect.Y();
+ *aWidth = rect.Width();
+ *aHeight = rect.Height();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleText::get_nSelections(long* aNSelections) {
+ if (!aNSelections) return E_INVALIDARG;
+ *aNSelections = 0;
+
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ *aNSelections = textAcc->SelectionCount();
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleText::get_offsetAtPoint(long aX, long aY,
+ enum IA2CoordinateType aCoordType,
+ long* aOffset) {
+ if (!aOffset) return E_INVALIDARG;
+ *aOffset = 0;
+
+ uint32_t geckoCoordType =
+ (aCoordType == IA2_COORDTYPE_SCREEN_RELATIVE)
+ ? nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE
+ : nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE;
+
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ *aOffset = textAcc->OffsetAtPoint(aX, aY, geckoCoordType);
+
+ return *aOffset == -1 ? S_FALSE : S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleText::get_selection(long aSelectionIndex, long* aStartOffset,
+ long* aEndOffset) {
+ if (!aStartOffset || !aEndOffset) return E_INVALIDARG;
+ *aStartOffset = *aEndOffset = 0;
+
+ int32_t startOffset = 0, endOffset = 0;
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ if (!textAcc->SelectionBoundsAt(aSelectionIndex, &startOffset, &endOffset)) {
+ return E_INVALIDARG;
+ }
+
+ *aStartOffset = startOffset;
+ *aEndOffset = endOffset;
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleText::get_text(long aStartOffset, long aEndOffset, BSTR* aText) {
+ if (!aText) return E_INVALIDARG;
+
+ *aText = nullptr;
+
+ nsAutoString text;
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ if (!textAcc->IsValidRange(aStartOffset, aEndOffset)) {
+ return E_INVALIDARG;
+ }
+
+ textAcc->TextSubstring(aStartOffset, aEndOffset, text);
+
+ if (text.IsEmpty()) return S_FALSE;
+
+ *aText = ::SysAllocStringLen(text.get(), text.Length());
+ return *aText ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleText::get_textBeforeOffset(long aOffset,
+ enum IA2TextBoundaryType aBoundaryType,
+ long* aStartOffset, long* aEndOffset,
+ BSTR* aText) {
+ if (!aStartOffset || !aEndOffset || !aText) return E_INVALIDARG;
+
+ *aStartOffset = *aEndOffset = 0;
+ *aText = nullptr;
+
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ if (!textAcc->IsValidOffset(aOffset)) return E_INVALIDARG;
+
+ nsAutoString text;
+ int32_t startOffset = 0, endOffset = 0;
+
+ if (aBoundaryType == IA2_TEXT_BOUNDARY_ALL) {
+ startOffset = 0;
+ endOffset = textAcc->CharacterCount();
+ textAcc->TextSubstring(startOffset, endOffset, text);
+ } else {
+ AccessibleTextBoundary boundaryType = GetGeckoTextBoundary(aBoundaryType);
+ if (boundaryType == -1) return S_FALSE;
+
+ textAcc->TextBeforeOffset(aOffset, boundaryType, &startOffset, &endOffset,
+ text);
+ }
+
+ *aStartOffset = startOffset;
+ *aEndOffset = endOffset;
+
+ if (text.IsEmpty()) return S_FALSE;
+
+ *aText = ::SysAllocStringLen(text.get(), text.Length());
+ return *aText ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleText::get_textAfterOffset(long aOffset,
+ enum IA2TextBoundaryType aBoundaryType,
+ long* aStartOffset, long* aEndOffset,
+ BSTR* aText) {
+ if (!aStartOffset || !aEndOffset || !aText) return E_INVALIDARG;
+
+ *aStartOffset = 0;
+ *aEndOffset = 0;
+ *aText = nullptr;
+
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ if (!textAcc->IsValidOffset(aOffset)) return E_INVALIDARG;
+
+ nsAutoString text;
+ int32_t startOffset = 0, endOffset = 0;
+
+ if (aBoundaryType == IA2_TEXT_BOUNDARY_ALL) {
+ startOffset = 0;
+ endOffset = textAcc->CharacterCount();
+ textAcc->TextSubstring(startOffset, endOffset, text);
+ } else {
+ AccessibleTextBoundary boundaryType = GetGeckoTextBoundary(aBoundaryType);
+ if (boundaryType == -1) return S_FALSE;
+ textAcc->TextAfterOffset(aOffset, boundaryType, &startOffset, &endOffset,
+ text);
+ }
+
+ *aStartOffset = startOffset;
+ *aEndOffset = endOffset;
+
+ if (text.IsEmpty()) return S_FALSE;
+
+ *aText = ::SysAllocStringLen(text.get(), text.Length());
+ return *aText ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleText::get_textAtOffset(long aOffset,
+ enum IA2TextBoundaryType aBoundaryType,
+ long* aStartOffset, long* aEndOffset,
+ BSTR* aText) {
+ if (!aStartOffset || !aEndOffset || !aText) return E_INVALIDARG;
+
+ *aStartOffset = *aEndOffset = 0;
+ *aText = nullptr;
+
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) return CO_E_OBJNOTCONNECTED;
+
+ if (!textAcc->IsValidOffset(aOffset)) return E_INVALIDARG;
+
+ nsAutoString text;
+ int32_t startOffset = 0, endOffset = 0;
+ if (aBoundaryType == IA2_TEXT_BOUNDARY_ALL) {
+ startOffset = 0;
+ endOffset = textAcc->CharacterCount();
+ textAcc->TextSubstring(startOffset, endOffset, text);
+ } else {
+ AccessibleTextBoundary boundaryType = GetGeckoTextBoundary(aBoundaryType);
+ if (boundaryType == -1) return S_FALSE;
+ textAcc->TextAtOffset(aOffset, boundaryType, &startOffset, &endOffset,
+ text);
+ }
+
+ *aStartOffset = startOffset;
+ *aEndOffset = endOffset;
+
+ if (text.IsEmpty()) return S_FALSE;
+
+ *aText = ::SysAllocStringLen(text.get(), text.Length());
+ return *aText ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+ia2AccessibleText::removeSelection(long aSelectionIndex) {
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ return textAcc->RemoveFromSelection(aSelectionIndex) ? S_OK : E_INVALIDARG;
+}
+
+STDMETHODIMP
+ia2AccessibleText::setCaretOffset(long aOffset) {
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ if (!textAcc->IsValidOffset(aOffset)) return E_INVALIDARG;
+
+ textAcc->SetCaretOffset(aOffset);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleText::setSelection(long aSelectionIndex, long aStartOffset,
+ long aEndOffset) {
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ return textAcc->SetSelectionBoundsAt(aSelectionIndex, aStartOffset,
+ aEndOffset)
+ ? S_OK
+ : E_INVALIDARG;
+}
+
+STDMETHODIMP
+ia2AccessibleText::get_nCharacters(long* aNCharacters) {
+ if (!aNCharacters) return E_INVALIDARG;
+ *aNCharacters = 0;
+
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) return CO_E_OBJNOTCONNECTED;
+
+ *aNCharacters = textAcc->CharacterCount();
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleText::scrollSubstringTo(long aStartIndex, long aEndIndex,
+ enum IA2ScrollType aScrollType) {
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ if (!textAcc->IsValidRange(aStartIndex, aEndIndex)) return E_INVALIDARG;
+
+ textAcc->ScrollSubstringTo(aStartIndex, aEndIndex, aScrollType);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleText::scrollSubstringToPoint(long aStartIndex, long aEndIndex,
+ enum IA2CoordinateType aCoordType,
+ long aX, long aY) {
+ HyperTextAccessibleBase* textAcc = TextAcc();
+ if (!textAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+ if (!textAcc->IsValidRange(aStartIndex, aEndIndex)) {
+ return E_INVALIDARG;
+ }
+ uint32_t geckoCoordType =
+ (aCoordType == IA2_COORDTYPE_SCREEN_RELATIVE)
+ ? nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE
+ : nsIAccessibleCoordinateType::COORDTYPE_PARENT_RELATIVE;
+ textAcc->ScrollSubstringToPoint(aStartIndex, aEndIndex, geckoCoordType, aX,
+ aY);
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleText::get_newText(IA2TextSegment* aNewText) {
+ return GetModifiedText(true, aNewText);
+}
+
+STDMETHODIMP
+ia2AccessibleText::get_oldText(IA2TextSegment* aOldText) {
+ return GetModifiedText(false, aOldText);
+}
+
+// ia2AccessibleText
+
+HRESULT
+ia2AccessibleText::GetModifiedText(bool aGetInsertedText,
+ IA2TextSegment* aText) {
+ if (!aText) return E_INVALIDARG;
+
+ if (!sLastTextChangeAcc) return S_OK;
+
+ if (aGetInsertedText != sLastTextChangeWasInsert) return S_OK;
+
+ if (sLastTextChangeAcc != TextAcc()) return S_OK;
+
+ aText->start = sLastTextChangeStart;
+ aText->end = sLastTextChangeEnd;
+
+ if (sLastTextChangeString->IsEmpty()) return S_FALSE;
+
+ aText->text = ::SysAllocStringLen(sLastTextChangeString->get(),
+ sLastTextChangeString->Length());
+ return aText->text ? S_OK : E_OUTOFMEMORY;
+}
+
+AccessibleTextBoundary ia2AccessibleText::GetGeckoTextBoundary(
+ enum IA2TextBoundaryType aBoundaryType) {
+ switch (aBoundaryType) {
+ case IA2_TEXT_BOUNDARY_CHAR:
+ return nsIAccessibleText::BOUNDARY_CHAR;
+ case IA2_TEXT_BOUNDARY_WORD:
+ return nsIAccessibleText::BOUNDARY_WORD_START;
+ case IA2_TEXT_BOUNDARY_LINE:
+ return nsIAccessibleText::BOUNDARY_LINE_START;
+ case IA2_TEXT_BOUNDARY_PARAGRAPH:
+ return nsIAccessibleText::BOUNDARY_PARAGRAPH;
+ // case IA2_TEXT_BOUNDARY_SENTENCE:
+ // XXX: not implemented
+ default:
+ return -1;
+ }
+}
+
+void ia2AccessibleText::InitTextChangeData() {
+ ClearOnShutdown(&sLastTextChangeString);
+}
+
+void ia2AccessibleText::UpdateTextChangeData(HyperTextAccessibleBase* aAcc,
+ bool aInsert,
+ const nsAString& aStr,
+ int32_t aStart, uint32_t aLen) {
+ if (!sLastTextChangeString) sLastTextChangeString = new nsString();
+
+ sLastTextChangeAcc = aAcc;
+ sLastTextChangeStart = aStart;
+ sLastTextChangeEnd = aStart + aLen;
+ sLastTextChangeWasInsert = aInsert;
+ *sLastTextChangeString = aStr;
+}
diff --git a/accessible/windows/ia2/ia2AccessibleText.h b/accessible/windows/ia2/ia2AccessibleText.h
new file mode 100644
index 0000000000..92c255d44a
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleText.h
@@ -0,0 +1,246 @@
+/* -*- 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 _ACCESSIBLE_TEXT_H
+#define _ACCESSIBLE_TEXT_H
+
+#include <utility>
+#include "AccessibleText.h"
+#include "nsIAccessibleText.h"
+
+namespace mozilla {
+template <class T>
+class StaticAutoPtr;
+template <class T>
+class StaticRefPtr;
+
+namespace a11y {
+class HyperTextAccessibleBase;
+
+class ia2AccessibleText : public IAccessibleText {
+ public:
+ // IAccessibleText
+ virtual HRESULT STDMETHODCALLTYPE addSelection(
+ /* [in] */ long startOffset,
+ /* [in] */ long endOffset);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_attributes(
+ /* [in] */ long offset,
+ /* [out] */ long* startOffset,
+ /* [out] */ long* endOffset,
+ /* [retval][out] */ BSTR* textAttributes);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_caretOffset(
+ /* [retval][out] */ long* offset);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_characterExtents(
+ /* [in] */ long offset,
+ /* [in] */ enum IA2CoordinateType coordType,
+ /* [out] */ long* x,
+ /* [out] */ long* y,
+ /* [out] */ long* width,
+ /* [retval][out] */ long* height);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_nSelections(
+ /* [retval][out] */ long* nSelections);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_offsetAtPoint(
+ /* [in] */ long x,
+ /* [in] */ long y,
+ /* [in] */ enum IA2CoordinateType coordType,
+ /* [retval][out] */ long* offset);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_selection(
+ /* [in] */ long selectionIndex,
+ /* [out] */ long* startOffset,
+ /* [retval][out] */ long* endOffset);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_text(
+ /* [in] */ long startOffset,
+ /* [in] */ long endOffset,
+ /* [retval][out] */ BSTR* text);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_textBeforeOffset(
+ /* [in] */ long offset,
+ /* [in] */ enum IA2TextBoundaryType boundaryType,
+ /* [out] */ long* startOffset,
+ /* [out] */ long* endOffset,
+ /* [retval][out] */ BSTR* text);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_textAfterOffset(
+ /* [in] */ long offset,
+ /* [in] */ enum IA2TextBoundaryType boundaryType,
+ /* [out] */ long* startOffset,
+ /* [out] */ long* endOffset,
+ /* [retval][out] */ BSTR* text);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_textAtOffset(
+ /* [in] */ long offset,
+ /* [in] */ enum IA2TextBoundaryType boundaryType,
+ /* [out] */ long* startOffset,
+ /* [out] */ long* endOffset,
+ /* [retval][out] */ BSTR* text);
+
+ virtual HRESULT STDMETHODCALLTYPE removeSelection(
+ /* [in] */ long selectionIndex);
+
+ virtual HRESULT STDMETHODCALLTYPE setCaretOffset(
+ /* [in] */ long offset);
+
+ virtual HRESULT STDMETHODCALLTYPE setSelection(
+ /* [in] */ long selectionIndex,
+ /* [in] */ long startOffset,
+ /* [in] */ long endOffset);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_nCharacters(
+ /* [retval][out] */ long* nCharacters);
+
+ virtual HRESULT STDMETHODCALLTYPE scrollSubstringTo(
+ /* [in] */ long startIndex,
+ /* [in] */ long endIndex,
+ /* [in] */ enum IA2ScrollType scrollType);
+
+ virtual HRESULT STDMETHODCALLTYPE scrollSubstringToPoint(
+ /* [in] */ long startIndex,
+ /* [in] */ long endIndex,
+ /* [in] */ enum IA2CoordinateType coordinateType,
+ /* [in] */ long x,
+ /* [in] */ long y);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_newText(
+ /* [retval][out] */ IA2TextSegment* newText);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_oldText(
+ /* [retval][out] */ IA2TextSegment* oldText);
+
+ static void InitTextChangeData();
+ static void UpdateTextChangeData(HyperTextAccessibleBase* aAcc, bool aInsert,
+ const nsAString& aStr, int32_t aStart,
+ uint32_t aLen);
+
+ protected:
+ // This can't be a RefPtr because RemoteAccessibles aren't ref counted. It
+ // can't be an id because this is global and ids are only unique within the
+ // document. Since this is only used for comparison, we use a raw pointer.
+ // This should *never* be dereferenced, only used for comparison!
+ static HyperTextAccessibleBase* sLastTextChangeAcc;
+ static StaticAutoPtr<nsString> sLastTextChangeString;
+ static bool sLastTextChangeWasInsert;
+ static uint32_t sLastTextChangeStart;
+ static uint32_t sLastTextChangeEnd;
+
+ private:
+ HRESULT GetModifiedText(bool aGetInsertedText, IA2TextSegment* aNewText);
+ AccessibleTextBoundary GetGeckoTextBoundary(
+ enum IA2TextBoundaryType coordinateType);
+ HyperTextAccessibleBase* TextAcc();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#define FORWARD_IACCESSIBLETEXT(Class) \
+ virtual HRESULT STDMETHODCALLTYPE addSelection(long startOffset, \
+ long endOffset) { \
+ return Class::addSelection(startOffset, endOffset); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_attributes( \
+ long offset, long* startOffset, long* endOffset, BSTR* textAttributes) { \
+ return Class::get_attributes(offset, startOffset, endOffset, \
+ textAttributes); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_caretOffset(long* offset) { \
+ return Class::get_caretOffset(offset); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_characterExtents( \
+ long offset, enum IA2CoordinateType coordType, long* x, long* y, \
+ long* width, long* height) { \
+ return Class::get_characterExtents(offset, coordType, x, y, width, \
+ height); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_nSelections(long* nSelections) { \
+ return Class::get_nSelections(nSelections); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_offsetAtPoint( \
+ long x, long y, enum IA2CoordinateType coordType, long* offset) { \
+ return Class::get_offsetAtPoint(x, y, coordType, offset); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_selection( \
+ long selectionIndex, long* startOffset, long* endOffset) { \
+ return Class::get_selection(selectionIndex, startOffset, endOffset); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_text(long startOffset, long endOffset, \
+ BSTR* text) { \
+ return Class::get_text(startOffset, endOffset, text); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_textBeforeOffset( \
+ long offset, enum IA2TextBoundaryType boundaryType, long* startOffset, \
+ long* endOffset, BSTR* text) { \
+ return Class::get_textBeforeOffset(offset, boundaryType, startOffset, \
+ endOffset, text); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_textAfterOffset( \
+ long offset, enum IA2TextBoundaryType boundaryType, long* startOffset, \
+ long* endOffset, BSTR* text) { \
+ return Class::get_textAfterOffset(offset, boundaryType, startOffset, \
+ endOffset, text); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_textAtOffset( \
+ long offset, enum IA2TextBoundaryType boundaryType, long* startOffset, \
+ long* endOffset, BSTR* text) { \
+ return Class::get_textAtOffset(offset, boundaryType, startOffset, \
+ endOffset, text); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE removeSelection(long selectionIndex) { \
+ return Class::removeSelection(selectionIndex); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE setCaretOffset(long offset) { \
+ return Class::setCaretOffset(offset); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE setSelection( \
+ long selectionIndex, long startOffset, long endOffset) { \
+ return Class::setSelection(selectionIndex, startOffset, endOffset); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_nCharacters(long* nCharacters) { \
+ return Class::get_nCharacters(nCharacters); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE scrollSubstringTo( \
+ long startIndex, long endIndex, enum IA2ScrollType scrollType) { \
+ return Class::scrollSubstringTo(startIndex, endIndex, scrollType); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE scrollSubstringToPoint( \
+ long startIndex, long endIndex, enum IA2CoordinateType coordinateType, \
+ long x, long y) { \
+ return Class::scrollSubstringToPoint(startIndex, endIndex, coordinateType, \
+ x, y); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_newText(IA2TextSegment* newText) { \
+ return Class::get_newText(newText); \
+ } \
+ \
+ virtual HRESULT STDMETHODCALLTYPE get_oldText(IA2TextSegment* oldText) { \
+ return Class::get_oldText(oldText); \
+ }
+
+#endif
diff --git a/accessible/windows/ia2/ia2AccessibleTextSelectionContainer.cpp b/accessible/windows/ia2/ia2AccessibleTextSelectionContainer.cpp
new file mode 100644
index 0000000000..0360cc2028
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleTextSelectionContainer.cpp
@@ -0,0 +1,126 @@
+/* -*- 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 "ia2AccessibleTextSelectionContainer.h"
+
+#include "AccessibleTextSelectionContainer_i.c"
+#include "ia2AccessibleHypertext.h"
+#include "mozilla/a11y/HyperTextAccessibleBase.h"
+#include "TextRange.h"
+#include "TextLeafRange.h"
+
+using namespace mozilla::a11y;
+
+// IAccessibleTextSelectionContainer
+
+STDMETHODIMP
+ia2AccessibleTextSelectionContainer::get_selections(
+ IA2TextSelection** selections, long* nSelections) {
+ if (!selections || !nSelections) {
+ return E_INVALIDARG;
+ }
+ *nSelections = 0;
+ HyperTextAccessibleBase* text = TextAcc();
+ if (!text) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+ AutoTArray<TextRange, 1> ranges;
+ text->CroppedSelectionRanges(ranges);
+ *nSelections = ranges.Length();
+ *selections = static_cast<IA2TextSelection*>(
+ ::CoTaskMemAlloc(sizeof(IA2TextSelection) * *nSelections));
+ if (!*selections) {
+ return E_OUTOFMEMORY;
+ }
+ for (uint32_t idx = 0; idx < static_cast<uint32_t>(*nSelections); idx++) {
+ RefPtr<IAccessibleText> startObj =
+ GetIATextFrom(ranges[idx].StartContainer());
+ startObj.forget(&(*selections)[idx].startObj);
+ (*selections)[idx].startOffset = ranges[idx].StartOffset();
+ RefPtr<IAccessibleText> endObj = GetIATextFrom(ranges[idx].EndContainer());
+ endObj.forget(&(*selections)[idx].endObj);
+ (*selections)[idx].endOffset = ranges[idx].EndOffset();
+ // XXX Expose this properly somehow.
+ (*selections)[idx].startIsActive = true;
+ }
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleTextSelectionContainer::setSelections(
+ long nSelections, IA2TextSelection* selections) {
+ if (nSelections < 0 || (nSelections > 0 && !selections)) {
+ return E_INVALIDARG;
+ }
+ HyperTextAccessibleBase* text = TextAcc();
+ if (!text) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+ // Build and validate new selection ranges.
+ AutoTArray<TextLeafRange, 1> newRanges;
+ newRanges.SetCapacity(nSelections);
+ for (long r = 0; r < nSelections; ++r) {
+ TextLeafRange range(GetTextLeafPointFrom(selections[r].startObj,
+ selections[r].startOffset, false),
+ GetTextLeafPointFrom(selections[r].endObj,
+ selections[r].endOffset, true));
+ if (!range) {
+ return E_INVALIDARG;
+ }
+ newRanges.AppendElement(range);
+ }
+ // Get the number of existing selections. We use SelectionRanges rather than
+ // SelectionCount because SelectionCount is restricted to this Accessible,
+ // whereas we want all selections within the control/document.
+ AutoTArray<TextRange, 1> oldRanges;
+ text->SelectionRanges(&oldRanges);
+ // Set the new selections.
+ for (long r = 0; r < nSelections; ++r) {
+ newRanges[r].SetSelection(r);
+ }
+ // Remove any remaining old selections if there were more than nSelections.
+ for (long r = nSelections; r < static_cast<long>(oldRanges.Length()); ++r) {
+ text->RemoveFromSelection(r);
+ }
+ return S_OK;
+}
+
+// ia2AccessibleTextSelectionContainer
+
+HyperTextAccessibleBase* ia2AccessibleTextSelectionContainer::TextAcc() {
+ auto hyp = static_cast<ia2AccessibleHypertext*>(this);
+ Accessible* acc = hyp->Acc();
+ return acc ? acc->AsHyperTextBase() : nullptr;
+}
+
+/* static */
+RefPtr<IAccessibleText> ia2AccessibleTextSelectionContainer::GetIATextFrom(
+ Accessible* aAcc) {
+ MsaaAccessible* msaa = MsaaAccessible::GetFrom(aAcc);
+ MOZ_ASSERT(msaa);
+ RefPtr<IAccessibleText> text;
+ msaa->QueryInterface(IID_IAccessibleText, getter_AddRefs(text));
+ MOZ_ASSERT(text);
+ return text;
+}
+
+/* static */
+TextLeafPoint ia2AccessibleTextSelectionContainer::GetTextLeafPointFrom(
+ IAccessibleText* aText, long aOffset, bool aDescendToEnd) {
+ if (!aText) {
+ return TextLeafPoint();
+ }
+ Accessible* acc = MsaaAccessible::GetAccessibleFrom(aText);
+ if (!acc) {
+ return TextLeafPoint();
+ }
+ HyperTextAccessibleBase* hyp = acc->AsHyperTextBase();
+ if (!hyp) {
+ return TextLeafPoint();
+ }
+ return hyp->ToTextLeafPoint(aOffset, aDescendToEnd);
+}
diff --git a/accessible/windows/ia2/ia2AccessibleTextSelectionContainer.h b/accessible/windows/ia2/ia2AccessibleTextSelectionContainer.h
new file mode 100644
index 0000000000..77ab40c1ed
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleTextSelectionContainer.h
@@ -0,0 +1,43 @@
+/* -*- 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 ACCESSIBLE_WINDOWS_IA2_ACCESSIBLETEXTSELECTIONCONTAINER_H_
+#define ACCESSIBLE_WINDOWS_IA2_ACCESSIBLETEXTSELECTIONCONTAINER_H_
+
+#include "AccessibleTextSelectionContainer.h"
+#include "mozilla/Attributes.h"
+
+template <class T>
+class RefPtr;
+
+namespace mozilla::a11y {
+class Accessible;
+class HyperTextAccessibleBase;
+class TextLeafPoint;
+
+class ia2AccessibleTextSelectionContainer
+ : public IAccessibleTextSelectionContainer {
+ public:
+ // IAccessibleTextSelectionContainer
+ /* [propget] */ HRESULT STDMETHODCALLTYPE get_selections(
+ /* [out, size_is(,*nSelections)] */ IA2TextSelection** selections,
+ /* [out, retval] */ long* nSelections) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ HRESULT STDMETHODCALLTYPE setSelections(
+ /* [in] */ long nSelections,
+ /* [in, size_is(nSelections)] */ IA2TextSelection* selections) override;
+
+ private:
+ HyperTextAccessibleBase* TextAcc();
+ static RefPtr<IAccessibleText> GetIATextFrom(Accessible* aAcc);
+ static TextLeafPoint GetTextLeafPointFrom(IAccessibleText* aText,
+ long aOffset, bool aDescendToEnd);
+};
+
+} // namespace mozilla::a11y
+
+#endif
diff --git a/accessible/windows/ia2/ia2AccessibleValue.cpp b/accessible/windows/ia2/ia2AccessibleValue.cpp
new file mode 100644
index 0000000000..b0b9a729e3
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleValue.cpp
@@ -0,0 +1,125 @@
+/* -*- 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 "ia2AccessibleValue.h"
+
+#include "AccessibleValue_i.c"
+
+#include "AccessibleWrap.h"
+#include "LocalAccessible-inl.h"
+#include "IUnknownImpl.h"
+
+#include "mozilla/FloatingPoint.h"
+
+using namespace mozilla::a11y;
+
+AccessibleWrap* ia2AccessibleValue::LocalAcc() {
+ return static_cast<MsaaAccessible*>(this)->LocalAcc();
+}
+
+Accessible* ia2AccessibleValue::Acc() {
+ return static_cast<MsaaAccessible*>(this)->Acc();
+}
+
+// IUnknown
+
+STDMETHODIMP
+ia2AccessibleValue::QueryInterface(REFIID iid, void** ppv) {
+ if (!ppv) return E_INVALIDARG;
+
+ *ppv = nullptr;
+
+ if (IID_IAccessibleValue == iid) {
+ Accessible* valueAcc = Acc();
+ if (valueAcc && valueAcc->HasNumericValue()) {
+ RefPtr<IAccessibleValue> result = this;
+ result.forget(ppv);
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+ }
+
+ return E_NOINTERFACE;
+}
+
+// IAccessibleValue
+
+STDMETHODIMP
+ia2AccessibleValue::get_currentValue(VARIANT* aCurrentValue) {
+ if (!aCurrentValue) return E_INVALIDARG;
+
+ VariantInit(aCurrentValue);
+
+ Accessible* valueAcc = Acc();
+ if (!valueAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+ double currentValue;
+
+ currentValue = valueAcc->CurValue();
+
+ if (std::isnan(currentValue)) return S_FALSE;
+
+ aCurrentValue->vt = VT_R8;
+ aCurrentValue->dblVal = currentValue;
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleValue::setCurrentValue(VARIANT aValue) {
+ if (aValue.vt != VT_R8) return E_INVALIDARG;
+
+ Accessible* valueAcc = Acc();
+ if (!valueAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ return valueAcc->SetCurValue(aValue.dblVal) ? S_OK : E_FAIL;
+}
+
+STDMETHODIMP
+ia2AccessibleValue::get_maximumValue(VARIANT* aMaximumValue) {
+ if (!aMaximumValue) return E_INVALIDARG;
+
+ VariantInit(aMaximumValue);
+
+ Accessible* valueAcc = Acc();
+ if (!valueAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+ double maximumValue;
+
+ maximumValue = valueAcc->MaxValue();
+
+ if (std::isnan(maximumValue)) return S_FALSE;
+
+ aMaximumValue->vt = VT_R8;
+ aMaximumValue->dblVal = maximumValue;
+ return S_OK;
+}
+
+STDMETHODIMP
+ia2AccessibleValue::get_minimumValue(VARIANT* aMinimumValue) {
+ if (!aMinimumValue) return E_INVALIDARG;
+
+ VariantInit(aMinimumValue);
+
+ Accessible* valueAcc = Acc();
+ if (!valueAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+ double minimumValue;
+
+ minimumValue = valueAcc->MinValue();
+
+ if (std::isnan(minimumValue)) return S_FALSE;
+
+ aMinimumValue->vt = VT_R8;
+ aMinimumValue->dblVal = minimumValue;
+ return S_OK;
+}
diff --git a/accessible/windows/ia2/ia2AccessibleValue.h b/accessible/windows/ia2/ia2AccessibleValue.h
new file mode 100644
index 0000000000..9238b827b5
--- /dev/null
+++ b/accessible/windows/ia2/ia2AccessibleValue.h
@@ -0,0 +1,44 @@
+/* -*- 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 _ACCESSIBLE_VALUE_H
+#define _ACCESSIBLE_VALUE_H
+
+#include "mozilla/a11y/Accessible.h"
+#include "AccessibleValue.h"
+
+namespace mozilla {
+namespace a11y {
+class AccessibleWrap;
+
+class ia2AccessibleValue : public IAccessibleValue {
+ public:
+ // IUnknown
+ STDMETHODIMP QueryInterface(REFIID, void**);
+
+ // IAccessibleValue
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_currentValue(
+ /* [retval][out] */ VARIANT* currentValue);
+
+ virtual HRESULT STDMETHODCALLTYPE setCurrentValue(
+ /* [in] */ VARIANT value);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_maximumValue(
+ /* [retval][out] */ VARIANT* maximumValue);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_minimumValue(
+ /* [retval][out] */ VARIANT* minimumValue);
+
+ private:
+ Accessible* Acc();
+ AccessibleWrap* LocalAcc();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/ia2/moz.build b/accessible/windows/ia2/moz.build
new file mode 100644
index 0000000000..53f930fd5a
--- /dev/null
+++ b/accessible/windows/ia2/moz.build
@@ -0,0 +1,61 @@
+# -*- 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 += [
+ "ia2Accessible.h",
+ "ia2AccessibleAction.h",
+ "ia2AccessibleComponent.h",
+ "ia2AccessibleEditableText.h",
+ "ia2AccessibleHyperlink.h",
+ "ia2AccessibleHypertext.h",
+ "ia2AccessibleText.h",
+ "ia2AccessibleTextSelectionContainer.h",
+ "ia2AccessibleValue.h",
+]
+
+UNIFIED_SOURCES += [
+ "ia2Accessible.cpp",
+ "ia2AccessibleAction.cpp",
+ "ia2AccessibleApplication.cpp",
+ "ia2AccessibleComponent.cpp",
+ "ia2AccessibleEditableText.cpp",
+ "ia2AccessibleHyperlink.cpp",
+ "ia2AccessibleHypertext.cpp",
+ "ia2AccessibleImage.cpp",
+ "ia2AccessibleRelation.cpp",
+ "ia2AccessibleText.cpp",
+ "ia2AccessibleTextSelectionContainer.cpp",
+ "ia2AccessibleValue.cpp",
+]
+
+# These files cannot be built in unified mode because they both include
+# AccessibleTable2_i.c.
+SOURCES += [
+ "ia2AccessibleTable.cpp",
+ "ia2AccessibleTableCell.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/generic",
+ "/accessible/html",
+ "/accessible/windows",
+ "/accessible/windows/msaa",
+ "/accessible/xpcom",
+ "/accessible/xul",
+]
+
+FINAL_LIBRARY = "xul"
+
+# The Windows MIDL code generator creates things like:
+#
+# #endif !_MIDL_USE_GUIDDEF_
+#
+# which clang-cl complains about. MSVC doesn't, so turn this warning off.
+if CONFIG["CC_TYPE"] == "clang-cl":
+ CXXFLAGS += ["-Wno-extra-tokens"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/accessible/windows/moz.build b/accessible/windows/moz.build
new file mode 100644
index 0000000000..53afb61f11
--- /dev/null
+++ b/accessible/windows/moz.build
@@ -0,0 +1,7 @@
+# -*- 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/.
+
+DIRS += ["msaa", "ia2", "sdn", "uia"]
diff --git a/accessible/windows/msaa/AccessibleWrap.cpp b/accessible/windows/msaa/AccessibleWrap.cpp
new file mode 100644
index 0000000000..abab007775
--- /dev/null
+++ b/accessible/windows/msaa/AccessibleWrap.cpp
@@ -0,0 +1,140 @@
+/* -*- 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 "mozilla/a11y/DocAccessibleParent.h"
+#include "AccEvent.h"
+#include "nsAccUtils.h"
+#include "nsIAccessibleEvent.h"
+#include "nsIWidget.h"
+#include "nsWindowsHelpers.h"
+#include "mozilla/a11y/HyperTextAccessible.h"
+#include "mozilla/a11y/RemoteAccessible.h"
+#include "ServiceProvider.h"
+#include "sdnAccessible.h"
+#include "LocalAccessible-inl.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+/* For documentation of the accessibility architecture,
+ * see http://lxr.mozilla.org/seamonkey/source/accessible/accessible-docs.html
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+// AccessibleWrap
+////////////////////////////////////////////////////////////////////////////////
+AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
+ : LocalAccessible(aContent, aDoc) {}
+
+NS_IMPL_ISUPPORTS_INHERITED0(AccessibleWrap, LocalAccessible)
+
+void AccessibleWrap::Shutdown() {
+ if (mMsaa) {
+ mMsaa->MsaaShutdown();
+ // Don't release mMsaa here because this will cause its id to be released
+ // immediately, which will result in immediate reuse, causing problems
+ // for clients. Instead, we release it in the destructor.
+ }
+ LocalAccessible::Shutdown();
+}
+
+//-----------------------------------------------------
+// IUnknown interface methods - see iunknown.h for documentation
+//-----------------------------------------------------
+
+MsaaAccessible* AccessibleWrap::GetMsaa() {
+ if (!mMsaa) {
+ mMsaa = MsaaAccessible::Create(this);
+ }
+ return mMsaa;
+}
+
+void AccessibleWrap::GetNativeInterface(void** aOutAccessible) {
+ RefPtr<IAccessible> result = GetMsaa();
+ return result.forget(aOutAccessible);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// AccessibleWrap
+
+//------- Helper methods ---------
+
+bool AccessibleWrap::IsRootForHWND() {
+ if (IsRoot()) {
+ return true;
+ }
+ HWND thisHwnd = MsaaAccessible::GetHWNDFor(this);
+ AccessibleWrap* parent = static_cast<AccessibleWrap*>(LocalParent());
+ MOZ_ASSERT(parent);
+ HWND parentHwnd = MsaaAccessible::GetHWNDFor(parent);
+ return thisHwnd != parentHwnd;
+}
+
+/* static */
+void AccessibleWrap::UpdateSystemCaretFor(
+ Accessible* aAccessible, const LayoutDeviceIntRect& aCaretRect) {
+ if (LocalAccessible* localAcc = aAccessible->AsLocal()) {
+ // XXX We need the widget for LocalAccessible, so we have to call
+ // HyperTextAccessible::GetCaretRect. We should find some way of avoiding
+ // the need for the widget.
+ UpdateSystemCaretFor(localAcc);
+ } else {
+ UpdateSystemCaretFor(aAccessible->AsRemote(), aCaretRect);
+ }
+}
+
+/* static */
+void AccessibleWrap::UpdateSystemCaretFor(LocalAccessible* aAccessible) {
+ // Move the system caret so that Windows Tablet Edition and tradional ATs with
+ // off-screen model can follow the caret
+ ::DestroyCaret();
+
+ HyperTextAccessible* text = aAccessible->AsHyperText();
+ if (!text) return;
+
+ nsIWidget* widget = nullptr;
+ LayoutDeviceIntRect caretRect = text->GetCaretRect(&widget);
+
+ if (!widget) {
+ return;
+ }
+
+ HWND caretWnd =
+ reinterpret_cast<HWND>(widget->GetNativeData(NS_NATIVE_WINDOW));
+ UpdateSystemCaretFor(caretWnd, caretRect);
+}
+
+/* static */
+void AccessibleWrap::UpdateSystemCaretFor(
+ RemoteAccessible* aProxy, const LayoutDeviceIntRect& aCaretRect) {
+ ::DestroyCaret();
+
+ // The HWND should be the real widget HWND, not an emulated HWND.
+ // We get the HWND from the proxy's outer doc to bypass window emulation.
+ LocalAccessible* outerDoc = aProxy->OuterDocOfRemoteBrowser();
+ UpdateSystemCaretFor(MsaaAccessible::GetHWNDFor(outerDoc), aCaretRect);
+}
+
+/* static */
+void AccessibleWrap::UpdateSystemCaretFor(
+ HWND aCaretWnd, const LayoutDeviceIntRect& aCaretRect) {
+ if (!aCaretWnd || aCaretRect.IsEmpty()) {
+ return;
+ }
+
+ // Create invisible bitmap for caret, otherwise its appearance interferes
+ // with Gecko caret
+ nsAutoBitmap caretBitMap(CreateBitmap(1, aCaretRect.Height(), 1, 1, nullptr));
+ if (::CreateCaret(aCaretWnd, caretBitMap, 1,
+ aCaretRect.Height())) { // Also destroys the last caret
+ ::ShowCaret(aCaretWnd);
+ POINT clientPoint{aCaretRect.X(), aCaretRect.Y()};
+ ::ScreenToClient(aCaretWnd, &clientPoint);
+ ::SetCaretPos(clientPoint.x, clientPoint.y);
+ }
+}
diff --git a/accessible/windows/msaa/AccessibleWrap.h b/accessible/windows/msaa/AccessibleWrap.h
new file mode 100644
index 0000000000..aeeea87e77
--- /dev/null
+++ b/accessible/windows/msaa/AccessibleWrap.h
@@ -0,0 +1,75 @@
+/* -*- 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_AccessibleWrap_h_
+#define mozilla_a11y_AccessibleWrap_h_
+
+#include "nsCOMPtr.h"
+#include "LocalAccessible.h"
+#include "MsaaAccessible.h"
+#include "mozilla/a11y/RemoteAccessible.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/mscom/Utils.h"
+#include "mozilla/StaticPtr.h"
+#include "nsXULAppAPI.h"
+#include "Units.h"
+
+namespace mozilla {
+namespace a11y {
+class DocRemoteAccessibleWrap;
+
+/**
+ * Windows specific functionality for an accessibility tree node that originated
+ * in mDoc's content process.
+ */
+class AccessibleWrap : public LocalAccessible {
+ public: // construction, destruction
+ AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+
+ public:
+ // LocalAccessible
+ virtual void Shutdown() override;
+
+ // Helper methods
+ /**
+ * System caret support: update the Windows caret position.
+ * The system caret works more universally than the MSAA caret
+ * For example, Window-Eyes, JAWS, ZoomText and Windows Tablet Edition use it
+ * We will use an invisible system caret.
+ * Gecko is still responsible for drawing its own caret
+ */
+ static void UpdateSystemCaretFor(Accessible* aAccessible,
+ const LayoutDeviceIntRect& aCaretRect);
+ static void UpdateSystemCaretFor(LocalAccessible* aAccessible);
+ static void UpdateSystemCaretFor(RemoteAccessible* aProxy,
+ const LayoutDeviceIntRect& aCaretRect);
+
+ private:
+ static void UpdateSystemCaretFor(HWND aCaretWnd,
+ const LayoutDeviceIntRect& aCaretRect);
+
+ public:
+ /**
+ * Determine whether this is the root accessible for its HWND.
+ */
+ bool IsRootForHWND();
+
+ MsaaAccessible* GetMsaa();
+ virtual void GetNativeInterface(void** aOutAccessible) override;
+
+ protected:
+ virtual ~AccessibleWrap() = default;
+
+ RefPtr<MsaaAccessible> mMsaa;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/msaa/ApplicationAccessibleWrap.cpp b/accessible/windows/msaa/ApplicationAccessibleWrap.cpp
new file mode 100644
index 0000000000..6a10a8bddc
--- /dev/null
+++ b/accessible/windows/msaa/ApplicationAccessibleWrap.cpp
@@ -0,0 +1,43 @@
+/* -*- 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/. */
+
+#include "ApplicationAccessibleWrap.h"
+
+#include "nsIGfxInfo.h"
+#include "AccAttributes.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/Components.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+NS_IMPL_ISUPPORTS_INHERITED0(ApplicationAccessibleWrap, ApplicationAccessible)
+
+already_AddRefed<AccAttributes> ApplicationAccessibleWrap::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+
+ nsCOMPtr<nsIGfxInfo> gfxInfo = components::GfxInfo::Service();
+ if (gfxInfo) {
+ bool isD2DEnabled = false;
+ gfxInfo->GetD2DEnabled(&isD2DEnabled);
+ RefPtr<nsAtom> attrName = NS_Atomize(u"D2D"_ns);
+ attributes->SetAttribute(attrName, isD2DEnabled);
+ }
+
+ return attributes.forget();
+}
+
+void ApplicationAccessibleWrap::Shutdown() {
+ // ApplicationAccessible::Shutdown doesn't call AccessibleWrap::Shutdown, so
+ // we have to call MsaaShutdown here.
+ if (mMsaa) {
+ mMsaa->MsaaShutdown();
+ }
+ ApplicationAccessible::Shutdown();
+}
diff --git a/accessible/windows/msaa/ApplicationAccessibleWrap.h b/accessible/windows/msaa/ApplicationAccessibleWrap.h
new file mode 100644
index 0000000000..b40c30fd15
--- /dev/null
+++ b/accessible/windows/msaa/ApplicationAccessibleWrap.h
@@ -0,0 +1,31 @@
+/* -*- 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 {
+
+class ApplicationAccessibleWrap : public ApplicationAccessible {
+ ~ApplicationAccessibleWrap() {}
+
+ public:
+ // nsISupporst
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // nsAccessible
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+ virtual void Shutdown() override;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/msaa/Compatibility.cpp b/accessible/windows/msaa/Compatibility.cpp
new file mode 100644
index 0000000000..440f327520
--- /dev/null
+++ b/accessible/windows/msaa/Compatibility.cpp
@@ -0,0 +1,181 @@
+/* -*- 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 "Compatibility.h"
+
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+#include "mozilla/StaticPrefs_accessibility.h"
+#include "nsExceptionHandler.h"
+#include "nsIXULRuntime.h"
+#include "nsPrintfCString.h"
+#include "nsUnicharUtils.h"
+#include "nsWinUtils.h"
+#include "Statistics.h"
+#include "AccessibleWrap.h"
+
+#include "mozilla/Preferences.h"
+
+#include <shlobj.h>
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+/**
+ * String versions of consumer flags. See GetHumanReadableConsumersStr.
+ */
+static const wchar_t* ConsumerStringMap[CONSUMERS_ENUM_LEN + 1] = {
+ L"NVDA", L"JAWS", L"OLDJAWS", L"WE", L"DOLPHIN",
+ L"SEROTEK", L"COBRA", L"ZOOMTEXT", L"KAZAGURU", L"YOUDAO",
+ L"UNKNOWN", L"UIAUTOMATION", L"VISPEROSHARED", L"\0"};
+
+bool Compatibility::IsModuleVersionLessThan(HMODULE aModuleHandle,
+ unsigned long long aVersion) {
+ LauncherResult<ModuleVersion> version = GetModuleVersion(aModuleHandle);
+ if (version.isErr()) {
+ return true;
+ }
+
+ return version.unwrap() < aVersion;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Compatibility
+////////////////////////////////////////////////////////////////////////////////
+
+uint32_t Compatibility::sConsumers = Compatibility::UNKNOWN;
+
+/**
+ * This function is safe to call multiple times.
+ */
+/* static */
+void Compatibility::InitConsumers() {
+ HMODULE jawsHandle = ::GetModuleHandleW(L"jhook");
+ if (jawsHandle) {
+ sConsumers |=
+ IsModuleVersionLessThan(jawsHandle, MAKE_FILE_VERSION(19, 0, 0, 0))
+ ? OLDJAWS
+ : JAWS;
+ }
+
+ if (::GetModuleHandleW(L"gwm32inc")) sConsumers |= WE;
+
+ if (::GetModuleHandleW(L"dolwinhk")) sConsumers |= DOLPHIN;
+
+ if (::GetModuleHandleW(L"STSA32")) sConsumers |= SEROTEK;
+
+ if (::GetModuleHandleW(L"nvdaHelperRemote")) sConsumers |= NVDA;
+
+ if (::GetModuleHandleW(L"OsmHooks") || ::GetModuleHandleW(L"OsmHks64"))
+ sConsumers |= COBRA;
+
+ if (::GetModuleHandleW(L"WebFinderRemote")) sConsumers |= ZOOMTEXT;
+
+ if (::GetModuleHandleW(L"Kazahook")) sConsumers |= KAZAGURU;
+
+ if (::GetModuleHandleW(L"TextExtractorImpl32") ||
+ ::GetModuleHandleW(L"TextExtractorImpl64"))
+ sConsumers |= YOUDAO;
+
+ if (::GetModuleHandleW(L"uiautomation") ||
+ ::GetModuleHandleW(L"uiautomationcore"))
+ sConsumers |= UIAUTOMATION;
+
+ if (::GetModuleHandleW(L"AccEventCache")) {
+ sConsumers |= VISPEROSHARED;
+ }
+
+ // If we have a known consumer remove the unknown bit.
+ if (sConsumers != Compatibility::UNKNOWN)
+ sConsumers &= ~Compatibility::UNKNOWN;
+}
+
+/* static */
+bool Compatibility::HasKnownNonUiaConsumer() {
+ InitConsumers();
+ return sConsumers & ~(Compatibility::UNKNOWN | UIAUTOMATION);
+}
+
+void Compatibility::Init() {
+ // Note we collect some AT statistics/telemetry here for convenience.
+ InitConsumers();
+
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::AccessibilityInProcClient,
+ nsPrintfCString("0x%X", sConsumers));
+
+ // Gather telemetry
+ uint32_t temp = sConsumers;
+ for (int i = 0; temp; i++) {
+ if (temp & 0x1) statistics::A11yConsumers(i);
+
+ temp >>= 1;
+ }
+
+ // Turn off new tab switching for Jaws and WE.
+ if (sConsumers & (JAWS | OLDJAWS | WE)) {
+ // Check to see if the pref for disallowing CtrlTab is already set. If so,
+ // bail out (respect the user settings). If not, set it.
+ if (!Preferences::HasUserValue("browser.ctrlTab.disallowForScreenReaders"))
+ Preferences::SetBool("browser.ctrlTab.disallowForScreenReaders", true);
+ }
+}
+
+// static
+void Compatibility::GetHumanReadableConsumersStr(nsAString& aResult) {
+ bool appened = false;
+ uint32_t index = 0;
+ for (uint32_t consumers = sConsumers; consumers; consumers = consumers >> 1) {
+ if (consumers & 0x1) {
+ if (appened) {
+ aResult.AppendLiteral(",");
+ }
+ aResult.Append(ConsumerStringMap[index]);
+ appened = true;
+ }
+ if (++index > CONSUMERS_ENUM_LEN) {
+ break;
+ }
+ }
+}
+
+// Time when SuppressA11yForClipboardCopy() was called, as returned by
+// ::GetTickCount().
+static DWORD sA11yClipboardCopySuppressionStartTime = 0;
+
+/* static */
+void Compatibility::SuppressA11yForClipboardCopy() {
+ // Bug 1774285: Windows Suggested Actions (introduced in Windows 11 22H2)
+ // might walk the a11y tree using UIA whenever anything is copied to the
+ // clipboard. This causes an unacceptable hang, particularly when the cache
+ // is disabled.
+ bool doSuppress = [&] {
+ switch (
+ StaticPrefs::accessibility_windows_suppress_after_clipboard_copy()) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ // Our workaround for Suggested Actions is needed from Windows 11 22H2
+ return IsWin1122H2OrLater();
+ }
+ }();
+
+ if (doSuppress) {
+ sA11yClipboardCopySuppressionStartTime = ::GetTickCount();
+ }
+}
+
+/* static */
+bool Compatibility::IsA11ySuppressedForClipboardCopy() {
+ constexpr DWORD kSuppressTimeout = 1500; // ms
+ if (!sA11yClipboardCopySuppressionStartTime) {
+ return false;
+ }
+ return ::GetTickCount() - sA11yClipboardCopySuppressionStartTime <
+ kSuppressTimeout;
+}
diff --git a/accessible/windows/msaa/Compatibility.h b/accessible/windows/msaa/Compatibility.h
new file mode 100644
index 0000000000..d1e1b0e9ac
--- /dev/null
+++ b/accessible/windows/msaa/Compatibility.h
@@ -0,0 +1,124 @@
+/* -*- 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 COMPATIBILITY_MANAGER_H
+#define COMPATIBILITY_MANAGER_H
+
+#include <windows.h>
+#include "nsTArray.h"
+#include "nsString.h"
+
+#include <stdint.h>
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Used to get compatibility modes. Note, modes are computed at accessibility
+ * start up time and aren't changed during lifetime.
+ */
+class Compatibility {
+ public:
+ /**
+ * Return true if JAWS mode is enabled.
+ */
+ static bool IsJAWS() { return !!(sConsumers & (JAWS | OLDJAWS)); }
+
+ /**
+ * Return true if using an e10s incompatible Jaws.
+ */
+ static bool IsOldJAWS() { return !!(sConsumers & OLDJAWS); }
+
+ /**
+ * Return true if WE mode is enabled.
+ */
+ static bool IsWE() { return !!(sConsumers & WE); }
+
+ /**
+ * Return true if Dolphin mode is enabled.
+ */
+ static bool IsDolphin() { return !!(sConsumers & DOLPHIN); }
+
+ /**
+ * Return true if JAWS, ZoomText or ZoomText Fusion 2021 or later is being
+ * used. These products share common code for interacting with Firefox and
+ * all require window emulation to be enabled.
+ */
+ static bool IsVisperoShared() { return !!(sConsumers & VISPEROSHARED); }
+
+ /**
+ * Return a string describing sConsumers suitable for about:support.
+ * Exposed through nsIXULRuntime.accessibilityInstantiator.
+ */
+ static void GetHumanReadableConsumersStr(nsAString& aResult);
+
+ /**
+ * Initialize compatibility mode information.
+ */
+ static void Init();
+
+ static void GetUiaClientPids(nsTArray<DWORD>& aPids);
+
+ /**
+ * return true if a known, non-UIA a11y consumer is present
+ */
+ static bool HasKnownNonUiaConsumer();
+
+ /**
+ * Return true if a module's version is lesser than the given version.
+ * Generally, the version should be provided using the MAKE_FILE_VERSION
+ * macro.
+ * If the version information cannot be retrieved, true is returned; i.e.
+ * no version information implies an earlier version.
+ */
+ static bool IsModuleVersionLessThan(HMODULE aModuleHandle,
+ unsigned long long aVersion);
+
+ static void SuppressA11yForClipboardCopy();
+ static bool IsA11ySuppressedForClipboardCopy();
+
+ private:
+ Compatibility();
+ Compatibility(const Compatibility&);
+ Compatibility& operator=(const Compatibility&);
+
+ static void InitConsumers();
+
+ /**
+ * List of detected consumers of a11y (used for statistics/telemetry and
+ * compat)
+ */
+ enum {
+ NVDA = 1 << 0,
+ JAWS = 1 << 1,
+ OLDJAWS = 1 << 2,
+ WE = 1 << 3,
+ DOLPHIN = 1 << 4,
+ SEROTEK = 1 << 5,
+ COBRA = 1 << 6,
+ ZOOMTEXT = 1 << 7,
+ KAZAGURU = 1 << 8,
+ YOUDAO = 1 << 9,
+ UNKNOWN = 1 << 10,
+ UIAUTOMATION = 1 << 11,
+ VISPEROSHARED = 1 << 12
+ };
+#define CONSUMERS_ENUM_LEN 13
+
+ private:
+ static uint32_t sConsumers;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+// Convert the 4 (decimal) components of a DLL version number into a
+// single unsigned long long, as needed by
+// mozilla::a11y::Compatibility::IsModuleVersionLessThan.
+#define MAKE_FILE_VERSION(a, b, c, d) \
+ ((a##ULL << 48) + (b##ULL << 32) + (c##ULL << 16) + d##ULL)
+
+#endif
diff --git a/accessible/windows/msaa/CompatibilityUIA.cpp b/accessible/windows/msaa/CompatibilityUIA.cpp
new file mode 100644
index 0000000000..1f46a35b20
--- /dev/null
+++ b/accessible/windows/msaa/CompatibilityUIA.cpp
@@ -0,0 +1,347 @@
+/* -*- 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/. */
+
+#include "Compatibility.h"
+
+#include "mozilla/a11y/Platform.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WindowsVersion.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsTHashSet.h"
+#include "nsWindowsHelpers.h"
+
+#include "NtUndoc.h"
+
+using namespace mozilla;
+
+struct ByteArrayDeleter {
+ void operator()(void* aBuf) { delete[] reinterpret_cast<std::byte*>(aBuf); }
+};
+
+typedef UniquePtr<OBJECT_DIRECTORY_INFORMATION, ByteArrayDeleter> ObjDirInfoPtr;
+
+// ComparatorFnT returns true to continue searching, or else false to indicate
+// search completion.
+template <typename ComparatorFnT>
+static bool FindNamedObject(const ComparatorFnT& aComparator) {
+ // We want to enumerate every named kernel object in our session. We do this
+ // by opening a directory object using a path constructed using the session
+ // id under which our process resides.
+ DWORD sessionId;
+ if (!::ProcessIdToSessionId(::GetCurrentProcessId(), &sessionId)) {
+ return false;
+ }
+
+ nsAutoString path;
+ path.AppendPrintf("\\Sessions\\%lu\\BaseNamedObjects", sessionId);
+
+ UNICODE_STRING baseNamedObjectsName;
+ ::RtlInitUnicodeString(&baseNamedObjectsName, path.get());
+
+ OBJECT_ATTRIBUTES attributes;
+ InitializeObjectAttributes(&attributes, &baseNamedObjectsName, 0, nullptr,
+ nullptr);
+
+ HANDLE rawBaseNamedObjects;
+ NTSTATUS ntStatus = ::NtOpenDirectoryObject(
+ &rawBaseNamedObjects, DIRECTORY_QUERY | DIRECTORY_TRAVERSE, &attributes);
+ if (!NT_SUCCESS(ntStatus)) {
+ return false;
+ }
+
+ nsAutoHandle baseNamedObjects(rawBaseNamedObjects);
+
+ ULONG context = 0, returnedLen;
+
+ ULONG objDirInfoBufLen = 1024 * sizeof(OBJECT_DIRECTORY_INFORMATION);
+ ObjDirInfoPtr objDirInfo(reinterpret_cast<OBJECT_DIRECTORY_INFORMATION*>(
+ new std::byte[objDirInfoBufLen]));
+
+ // Now query that directory object for every named object that it contains.
+
+ BOOL firstCall = TRUE;
+
+ do {
+ ntStatus = ::NtQueryDirectoryObject(baseNamedObjects, objDirInfo.get(),
+ objDirInfoBufLen, FALSE, firstCall,
+ &context, &returnedLen);
+#if defined(HAVE_64BIT_BUILD)
+ if (!NT_SUCCESS(ntStatus)) {
+ return false;
+ }
+#else
+ if (ntStatus == STATUS_BUFFER_TOO_SMALL) {
+ // This case only occurs on 32-bit builds running atop WOW64.
+ // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1423999#c3)
+ objDirInfo.reset(reinterpret_cast<OBJECT_DIRECTORY_INFORMATION*>(
+ new std::byte[returnedLen]));
+ objDirInfoBufLen = returnedLen;
+ continue;
+ } else if (!NT_SUCCESS(ntStatus)) {
+ return false;
+ }
+#endif
+
+ // NtQueryDirectoryObject gave us an array of OBJECT_DIRECTORY_INFORMATION
+ // structures whose final entry is zeroed out.
+ OBJECT_DIRECTORY_INFORMATION* curDir = objDirInfo.get();
+ while (curDir->mName.Length && curDir->mTypeName.Length) {
+ // We use nsDependentSubstring here because UNICODE_STRINGs are not
+ // guaranteed to be null-terminated.
+ nsDependentSubstring objName(curDir->mName.Buffer,
+ curDir->mName.Length / sizeof(wchar_t));
+ nsDependentSubstring typeName(curDir->mTypeName.Buffer,
+ curDir->mTypeName.Length / sizeof(wchar_t));
+
+ if (!aComparator(objName, typeName)) {
+ return true;
+ }
+
+ ++curDir;
+ }
+
+ firstCall = FALSE;
+ } while (ntStatus == STATUS_MORE_ENTRIES);
+
+ return false;
+}
+
+// ComparatorFnT returns true to continue searching, or else false to indicate
+// search completion.
+template <typename ComparatorFnT>
+static bool FindHandle(const ComparatorFnT& aComparator) {
+ NTSTATUS ntStatus;
+ // First we must query for a list of all the open handles in the system.
+ UniquePtr<std::byte[]> handleInfoBuf;
+ ULONG handleInfoBufLen = sizeof(SYSTEM_HANDLE_INFORMATION_EX) +
+ 1024 * sizeof(SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX);
+ // We must query for handle information in a loop, since we are effectively
+ // asking the kernel to take a snapshot of all the handles on the system;
+ // the size of the required buffer may fluctuate between successive calls.
+ while (true) {
+ // These allocations can be hundreds of megabytes on some computers, so
+ // we should use fallible new here.
+ handleInfoBuf = MakeUniqueFallible<std::byte[]>(handleInfoBufLen);
+ if (!handleInfoBuf) {
+ return false;
+ }
+ ntStatus = ::NtQuerySystemInformation(
+ (SYSTEM_INFORMATION_CLASS)SystemExtendedHandleInformation,
+ handleInfoBuf.get(), handleInfoBufLen, &handleInfoBufLen);
+ if (ntStatus == STATUS_INFO_LENGTH_MISMATCH) {
+ continue;
+ }
+ if (!NT_SUCCESS(ntStatus)) {
+ return false;
+ }
+ break;
+ }
+
+ auto handleInfo =
+ reinterpret_cast<SYSTEM_HANDLE_INFORMATION_EX*>(handleInfoBuf.get());
+ for (ULONG index = 0; index < handleInfo->mHandleCount; ++index) {
+ SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX& info = handleInfo->mHandles[index];
+ HANDLE handle = reinterpret_cast<HANDLE>(info.mHandle);
+ if (!aComparator(info, handle)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static void GetUiaClientPidsWin11(nsTArray<DWORD>& aPids) {
+ const DWORD ourPid = ::GetCurrentProcessId();
+ FindHandle([&](auto aInfo, auto aHandle) {
+ if (aInfo.mPid != ourPid) {
+ // We're only interested in handles in our own process.
+ return true;
+ }
+ // UIA creates a named pipe between the client and server processes. We want
+ // to find our handle to that pipe (if any). If this is a named pipe, get
+ // the process id of the remote end. We do this first because querying the
+ // name of the handle might hang in some cases. Counter-intuitively, for UIA
+ // pipes, we're the client and the remote process is the server.
+ ULONG pid = 0;
+ ::GetNamedPipeServerProcessId(aHandle, &pid);
+ if (!pid) {
+ return true;
+ }
+ // We know this is a named pipe and we have the pid. Now, get the name of
+ // the handle and check whether it's a UIA pipe.
+ ULONG objNameBufLen;
+ NTSTATUS ntStatus = ::NtQueryObject(
+ aHandle, (OBJECT_INFORMATION_CLASS)ObjectNameInformation, nullptr, 0,
+ &objNameBufLen);
+ if (ntStatus != STATUS_INFO_LENGTH_MISMATCH) {
+ return true;
+ }
+ auto objNameBuf = MakeUnique<std::byte[]>(objNameBufLen);
+ ntStatus = ::NtQueryObject(aHandle,
+ (OBJECT_INFORMATION_CLASS)ObjectNameInformation,
+ objNameBuf.get(), objNameBufLen, &objNameBufLen);
+ if (!NT_SUCCESS(ntStatus)) {
+ return true;
+ }
+ auto objNameInfo =
+ reinterpret_cast<OBJECT_NAME_INFORMATION*>(objNameBuf.get());
+ if (!objNameInfo->Name.Length) {
+ return true;
+ }
+ nsDependentString objName(objNameInfo->Name.Buffer,
+ objNameInfo->Name.Length / sizeof(wchar_t));
+ if (StringBeginsWith(objName, u"\\Device\\NamedPipe\\UIA_PIPE_"_ns)) {
+ aPids.AppendElement(pid);
+ }
+ return true;
+ });
+}
+
+static DWORD GetUiaClientPidWin10() {
+ // UIA creates a section of the form "HOOK_SHMEM_%08lx_%08lx_%08lx_%08lx"
+ constexpr auto kStrHookShmem = u"HOOK_SHMEM_"_ns;
+ // The second %08lx is the thread id.
+ nsAutoString sectionThread;
+ sectionThread.AppendPrintf("_%08lx_", ::GetCurrentThreadId());
+ // This is the number of characters from the end of the section name where
+ // the sectionThread substring begins.
+ constexpr size_t sectionThreadRPos = 27;
+ // This is the length of sectionThread.
+ constexpr size_t sectionThreadLen = 10;
+ // Find any named Section that matches the naming convention of the UIA shared
+ // memory. There can only be one of these at a time, since this only exists
+ // while UIA is processing a request and it can only process a single request
+ // on a single thread.
+ nsAutoHandle section;
+ auto objectComparator = [&](const nsDependentSubstring& aName,
+ const nsDependentSubstring& aType) -> bool {
+ if (aType.Equals(u"Section"_ns) && FindInReadable(kStrHookShmem, aName) &&
+ Substring(aName, aName.Length() - sectionThreadRPos,
+ sectionThreadLen) == sectionThread) {
+ // Get a handle to this section so we can get its kernel object and
+ // use that to find the handle for this section in the remote process.
+ section.own(::OpenFileMapping(GENERIC_READ, FALSE,
+ PromiseFlatString(aName).get()));
+ return false;
+ }
+ return true;
+ };
+ if (!FindNamedObject(objectComparator) || !section) {
+ return 0;
+ }
+
+ // Now, find the kernel object associated with our section, the handle in the
+ // remote process associated with that kernel object and thus the remote
+ // process id.
+ NTSTATUS ntStatus;
+ const DWORD ourPid = ::GetCurrentProcessId();
+ Maybe<PVOID> kernelObject;
+ static Maybe<USHORT> sectionObjTypeIndex;
+ nsTHashSet<uint32_t> nonSectionObjTypes;
+ nsTHashMap<nsVoidPtrHashKey, DWORD> objMap;
+ DWORD remotePid = 0;
+ FindHandle([&](auto aInfo, auto aHandle) {
+ // The mapping of the aInfo.mObjectTypeIndex field depends on the
+ // underlying OS kernel. As we scan through the handle list, we record the
+ // type indices such that we may use those values to skip over handles that
+ // refer to non-section objects.
+ if (sectionObjTypeIndex) {
+ // If we know the type index for Sections, that's the fastest check...
+ if (sectionObjTypeIndex.value() != aInfo.mObjectTypeIndex) {
+ // Not a section
+ return true;
+ }
+ } else if (nonSectionObjTypes.Contains(
+ static_cast<uint32_t>(aInfo.mObjectTypeIndex))) {
+ // Otherwise we check whether or not the object type is definitely _not_
+ // a Section...
+ return true;
+ } else if (ourPid == aInfo.mPid) {
+ // Otherwise we need to issue some system calls to find out the object
+ // type corresponding to the current handle's type index.
+ ULONG objTypeBufLen;
+ ntStatus = ::NtQueryObject(aHandle, ObjectTypeInformation, nullptr, 0,
+ &objTypeBufLen);
+ if (ntStatus != STATUS_INFO_LENGTH_MISMATCH) {
+ return true;
+ }
+ auto objTypeBuf = MakeUnique<std::byte[]>(objTypeBufLen);
+ ntStatus =
+ ::NtQueryObject(aHandle, ObjectTypeInformation, objTypeBuf.get(),
+ objTypeBufLen, &objTypeBufLen);
+ if (!NT_SUCCESS(ntStatus)) {
+ return true;
+ }
+ auto objType =
+ reinterpret_cast<PUBLIC_OBJECT_TYPE_INFORMATION*>(objTypeBuf.get());
+ // Now we check whether the object's type name matches "Section"
+ nsDependentSubstring objTypeName(
+ objType->TypeName.Buffer, objType->TypeName.Length / sizeof(wchar_t));
+ if (!objTypeName.Equals(u"Section"_ns)) {
+ nonSectionObjTypes.Insert(
+ static_cast<uint32_t>(aInfo.mObjectTypeIndex));
+ return true;
+ }
+ sectionObjTypeIndex = Some(aInfo.mObjectTypeIndex);
+ }
+
+ // At this point we know that aInfo references a Section object.
+ // Now we can do some actual tests on it.
+ if (ourPid != aInfo.mPid) {
+ if (kernelObject && kernelObject.value() == aInfo.mObject) {
+ // The kernel objects match -- we have found the remote pid!
+ remotePid = aInfo.mPid;
+ return false;
+ }
+ // An object that is not ours. Since we do not yet know which kernel
+ // object we're interested in, we'll save the current object for later.
+ objMap.InsertOrUpdate(aInfo.mObject, aInfo.mPid);
+ } else if (aHandle == section.get()) {
+ // This is the file mapping that we opened above. We save this mObject
+ // in order to compare to Section objects opened by other processes.
+ kernelObject = Some(aInfo.mObject);
+ }
+ return true;
+ });
+
+ if (remotePid) {
+ return remotePid;
+ }
+ if (!kernelObject) {
+ return 0;
+ }
+
+ // If we reach here, we found kernelObject *after* we saw the remote process's
+ // copy. Now we must look it up in objMap.
+ if (objMap.Get(kernelObject.value(), &remotePid)) {
+ return remotePid;
+ }
+
+ return 0;
+}
+
+namespace mozilla {
+namespace a11y {
+
+void Compatibility::GetUiaClientPids(nsTArray<DWORD>& aPids) {
+ if (!::GetModuleHandleW(L"uiautomationcore.dll")) {
+ // UIAutomationCore isn't loaded, so there is no UIA client.
+ return;
+ }
+ Telemetry::AutoTimer<Telemetry::A11Y_UIA_DETECTION_TIMING_MS> timer;
+ if (IsWin11OrLater()) {
+ GetUiaClientPidsWin11(aPids);
+ } else {
+ if (DWORD pid = GetUiaClientPidWin10()) {
+ aPids.AppendElement(pid);
+ }
+ }
+}
+
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/windows/msaa/DocAccessibleWrap.cpp b/accessible/windows/msaa/DocAccessibleWrap.cpp
new file mode 100644
index 0000000000..e7ca74ce1a
--- /dev/null
+++ b/accessible/windows/msaa/DocAccessibleWrap.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 "DocAccessibleWrap.h"
+
+#include "Compatibility.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "DocAccessibleChild.h"
+#include "nsWinUtils.h"
+#include "RootAccessible.h"
+#include "sdnDocAccessible.h"
+#include "Statistics.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// DocAccessibleWrap
+////////////////////////////////////////////////////////////////////////////////
+
+DocAccessibleWrap::DocAccessibleWrap(dom::Document* aDocument,
+ PresShell* aPresShell)
+ : DocAccessible(aDocument, aPresShell), mHWND(nullptr) {}
+
+DocAccessibleWrap::~DocAccessibleWrap() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible
+
+void DocAccessibleWrap::Shutdown() {
+ // Do window emulation specific shutdown if emulation was started.
+ if (nsWinUtils::IsWindowEmulationStarted()) {
+ // Destroy window created for root document.
+ if (mDocFlags & eTopLevelContentDocInProcess) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ HWND hWnd = static_cast<HWND>(mHWND);
+ ::RemovePropW(hWnd, kPropNameDocAcc);
+ ::DestroyWindow(hWnd);
+ }
+
+ mHWND = nullptr;
+ }
+
+ DocAccessible::Shutdown();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DocAccessible public
+
+void* DocAccessibleWrap::GetNativeWindow() const {
+ if (mHWND) {
+ return mHWND;
+ }
+ return DocAccessible::GetNativeWindow();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// DocAccessible protected
+
+void DocAccessibleWrap::DoInitialUpdate() {
+ DocAccessible::DoInitialUpdate();
+
+ if (nsWinUtils::IsWindowEmulationStarted()) {
+ // Create window for tab document.
+ if (mDocFlags & eTopLevelContentDocInProcess) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ a11y::RootAccessible* rootDocument = RootAccessible();
+ bool isActive = true;
+ LayoutDeviceIntRect rect(CW_USEDEFAULT, CW_USEDEFAULT, 0, 0);
+ if (Compatibility::IsDolphin()) {
+ rect = Bounds();
+ LayoutDeviceIntRect rootRect = rootDocument->Bounds();
+ rect.MoveToX(rootRect.X() - rect.X());
+ rect.MoveByY(-rootRect.Y());
+
+ auto* bc = mDocumentNode->GetBrowsingContext();
+ isActive = bc && bc->IsActive();
+ }
+
+ RefPtr<DocAccessibleWrap> self(this);
+ nsWinUtils::NativeWindowCreateProc onCreate([self](HWND aHwnd) -> void {
+ ::SetPropW(aHwnd, kPropNameDocAcc,
+ reinterpret_cast<HANDLE>(self.get()));
+ });
+
+ HWND parentWnd = reinterpret_cast<HWND>(rootDocument->GetNativeWindow());
+ mHWND = nsWinUtils::CreateNativeWindow(
+ kClassNameTabContent, parentWnd, rect.X(), rect.Y(), rect.Width(),
+ rect.Height(), isActive, &onCreate);
+ } else {
+ DocAccessible* parentDocument = ParentDocument();
+ if (parentDocument) mHWND = parentDocument->GetNativeWindow();
+ }
+ }
+}
diff --git a/accessible/windows/msaa/DocAccessibleWrap.h b/accessible/windows/msaa/DocAccessibleWrap.h
new file mode 100644
index 0000000000..5b1ae9d9b6
--- /dev/null
+++ b/accessible/windows/msaa/DocAccessibleWrap.h
@@ -0,0 +1,39 @@
+/* -*- 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_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();
+
+ // LocalAccessible
+ virtual void Shutdown();
+
+ // DocAccessible
+ virtual void* GetNativeWindow() const;
+
+ protected:
+ void* mHWND;
+
+ // DocAccessible
+ virtual void DoInitialUpdate();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/msaa/EnumVariant.cpp b/accessible/windows/msaa/EnumVariant.cpp
new file mode 100644
index 0000000000..866e8f69a9
--- /dev/null
+++ b/accessible/windows/msaa/EnumVariant.cpp
@@ -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/. */
+
+#include "EnumVariant.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// ChildrenEnumVariant
+////////////////////////////////////////////////////////////////////////////////
+
+IMPL_IUNKNOWN_QUERY_HEAD(ChildrenEnumVariant)
+IMPL_IUNKNOWN_QUERY_IFACE(IEnumVARIANT)
+IMPL_IUNKNOWN_QUERY_TAIL_AGGREGATED(mAnchorMsaa)
+
+STDMETHODIMP
+ChildrenEnumVariant::Next(ULONG aCount, VARIANT FAR* aItems,
+ ULONG FAR* aCountFetched) {
+ if (!aItems || !aCountFetched) return E_INVALIDARG;
+
+ *aCountFetched = 0;
+
+ Accessible* anchor = mAnchorMsaa->Acc();
+ if (!anchor || anchor->ChildAt(mCurIndex) != mCurAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ ULONG countFetched = 0;
+ while (mCurAcc && countFetched < aCount) {
+ VariantInit(aItems + countFetched);
+
+ IDispatch* accNative = MsaaAccessible::NativeAccessible(mCurAcc);
+
+ ++mCurIndex;
+ mCurAcc = anchor->ChildAt(mCurIndex);
+
+ // Don't output the accessible and count it as having been fetched unless
+ // it is non-null
+ MOZ_ASSERT(accNative);
+ if (!accNative) {
+ continue;
+ }
+
+ aItems[countFetched].pdispVal = accNative;
+ aItems[countFetched].vt = VT_DISPATCH;
+ ++countFetched;
+ }
+
+ (*aCountFetched) = countFetched;
+
+ return countFetched < aCount ? S_FALSE : S_OK;
+}
+
+STDMETHODIMP
+ChildrenEnumVariant::Skip(ULONG aCount) {
+ Accessible* anchor = mAnchorMsaa->Acc();
+ if (!anchor || anchor->ChildAt(mCurIndex) != mCurAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ mCurIndex += aCount;
+ mCurAcc = anchor->ChildAt(mCurIndex);
+
+ return mCurAcc ? S_OK : S_FALSE;
+}
+
+STDMETHODIMP
+ChildrenEnumVariant::Reset() {
+ Accessible* anchor = mAnchorMsaa->Acc();
+ if (!anchor) return CO_E_OBJNOTCONNECTED;
+
+ mCurIndex = 0;
+ mCurAcc = anchor->ChildAt(0);
+
+ return S_OK;
+}
+
+STDMETHODIMP
+ChildrenEnumVariant::Clone(IEnumVARIANT** aEnumVariant) {
+ if (!aEnumVariant) return E_INVALIDARG;
+
+ *aEnumVariant = new ChildrenEnumVariant(*this);
+ (*aEnumVariant)->AddRef();
+
+ return S_OK;
+}
diff --git a/accessible/windows/msaa/EnumVariant.h b/accessible/windows/msaa/EnumVariant.h
new file mode 100644
index 0000000000..b584380e50
--- /dev/null
+++ b/accessible/windows/msaa/EnumVariant.h
@@ -0,0 +1,62 @@
+/* -*- 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_EnumVariant_h__
+#define mozilla_a11y_EnumVariant_h__
+
+#include "IUnknownImpl.h"
+#include "MsaaAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Used to fetch accessible children.
+ */
+class ChildrenEnumVariant final : public IEnumVARIANT {
+ public:
+ explicit ChildrenEnumVariant(MsaaAccessible* aAnchor)
+ : mAnchorMsaa(aAnchor),
+ mCurAcc(mAnchorMsaa->Acc()->ChildAt(0)),
+ mCurIndex(0) {}
+
+ // IUnknown
+ DECL_IUNKNOWN
+
+ // IEnumVariant
+ virtual /* [local] */ HRESULT STDMETHODCALLTYPE Next(
+ /* [in] */ ULONG aCount,
+ /* [length_is][size_is][out] */ VARIANT* aItems,
+ /* [out] */ ULONG* aCountFetched);
+
+ virtual HRESULT STDMETHODCALLTYPE Skip(
+ /* [in] */ ULONG aCount);
+
+ virtual HRESULT STDMETHODCALLTYPE Reset();
+
+ virtual HRESULT STDMETHODCALLTYPE Clone(
+ /* [out] */ IEnumVARIANT** aEnumVaraint);
+
+ private:
+ ChildrenEnumVariant() = delete;
+ ChildrenEnumVariant& operator=(const ChildrenEnumVariant&) = delete;
+
+ ChildrenEnumVariant(const ChildrenEnumVariant& aEnumVariant)
+ : mAnchorMsaa(aEnumVariant.mAnchorMsaa),
+ mCurAcc(aEnumVariant.mCurAcc),
+ mCurIndex(aEnumVariant.mCurIndex) {}
+ virtual ~ChildrenEnumVariant() {}
+
+ protected:
+ RefPtr<MsaaAccessible> mAnchorMsaa;
+ Accessible* mCurAcc;
+ uint32_t mCurIndex;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/msaa/IUnknownImpl.cpp b/accessible/windows/msaa/IUnknownImpl.cpp
new file mode 100644
index 0000000000..b7c0ee0f39
--- /dev/null
+++ b/accessible/windows/msaa/IUnknownImpl.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 "IUnknownImpl.h"
+
+#include "nsDebug.h"
+
+namespace mozilla {
+namespace a11y {
+
+HRESULT
+GetHRESULT(nsresult aResult) {
+ switch (aResult) {
+ case NS_OK:
+ return S_OK;
+
+ case NS_ERROR_INVALID_ARG:
+ return E_INVALIDARG;
+
+ case NS_ERROR_OUT_OF_MEMORY:
+ return E_OUTOFMEMORY;
+
+ case NS_ERROR_NOT_IMPLEMENTED:
+ return E_NOTIMPL;
+
+ default:
+ return E_FAIL;
+ }
+}
+
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/windows/msaa/IUnknownImpl.h b/accessible/windows/msaa/IUnknownImpl.h
new file mode 100644
index 0000000000..7935ebedda
--- /dev/null
+++ b/accessible/windows/msaa/IUnknownImpl.h
@@ -0,0 +1,173 @@
+/* -*- 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_IUnknownImpl_h_
+#define mozilla_a11y_IUnknownImpl_h_
+
+#include <windows.h>
+#undef CreateEvent // thank you windows you're such a helper
+#include "nsError.h"
+
+// Avoid warning C4509 like "nonstandard extension used:
+// 'AccessibleWrap::[acc_getName]' uses SEH and 'name' has destructor.
+// At this point we're catching a crash which is of much greater
+// importance than the missing dereference for the nsCOMPtr<>
+#ifdef _MSC_VER
+# pragma warning(disable : 4509)
+#endif
+
+#if defined(__GNUC__) || defined(__clang__)
+# define ATTRIBUTE_UNUSED __attribute__((unused))
+#else
+# define ATTRIBUTE_UNUSED
+#endif
+
+namespace mozilla {
+namespace a11y {
+
+class AutoRefCnt {
+ public:
+ AutoRefCnt() : mValue(0) {}
+
+ ULONG operator++() { return ++mValue; }
+ ULONG operator--() { return --mValue; }
+ ULONG operator++(int) { return ++mValue; }
+ ULONG operator--(int) { return --mValue; }
+
+ operator ULONG() const { return mValue; }
+
+ private:
+ ULONG mValue;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#define DECL_IUNKNOWN \
+ public: \
+ virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**); \
+ ULONG STDMETHODCALLTYPE AddRef() override { \
+ MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt"); \
+ ++mRefCnt; \
+ return mRefCnt; \
+ } \
+ ULONG STDMETHODCALLTYPE Release() override { \
+ MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release"); \
+ --mRefCnt; \
+ if (mRefCnt) return mRefCnt; \
+ \
+ delete this; \
+ return 0; \
+ } \
+ \
+ private: \
+ mozilla::a11y::AutoRefCnt mRefCnt; \
+ \
+ public:
+
+#define DECL_IUNKNOWN_INHERITED \
+ public: \
+ virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**);
+
+#define IMPL_IUNKNOWN_QUERY_HEAD(Class) \
+ STDMETHODIMP \
+ Class::QueryInterface(REFIID aIID, void** aInstancePtr) { \
+ if (!aInstancePtr) return E_INVALIDARG; \
+ *aInstancePtr = nullptr; \
+ \
+ HRESULT hr ATTRIBUTE_UNUSED = E_NOINTERFACE;
+
+#define IMPL_IUNKNOWN_QUERY_TAIL \
+ return hr; \
+ }
+
+#define IMPL_IUNKNOWN_QUERY_TAIL_AGGREGATED(Member) \
+ return Member->QueryInterface(aIID, aInstancePtr); \
+ }
+
+#define IMPL_IUNKNOWN_QUERY_TAIL_INHERITED(BaseClass) \
+ return BaseClass::QueryInterface(aIID, aInstancePtr); \
+ }
+
+#define IMPL_IUNKNOWN_QUERY_IFACE(Iface) \
+ if (aIID == IID_##Iface) { \
+ *aInstancePtr = static_cast<Iface*>(this); \
+ AddRef(); \
+ return S_OK; \
+ }
+
+#define IMPL_IUNKNOWN_QUERY_IFACE_AMBIGIOUS(Iface, aResolveIface) \
+ if (aIID == IID_##Iface) { \
+ *aInstancePtr = static_cast<Iface*>(static_cast<aResolveIface*>(this)); \
+ AddRef(); \
+ return S_OK; \
+ }
+
+#define IMPL_IUNKNOWN_QUERY_CLASS(Class) \
+ hr = Class::QueryInterface(aIID, aInstancePtr); \
+ if (SUCCEEDED(hr)) return hr;
+
+#define IMPL_IUNKNOWN_QUERY_CLASS_COND(Class, Cond) \
+ if (Cond) { \
+ hr = Class::QueryInterface(aIID, aInstancePtr); \
+ if (SUCCEEDED(hr)) return hr; \
+ }
+
+#define IMPL_IUNKNOWN_QUERY_AGGR_COND(Member, Cond) \
+ if (Cond) { \
+ hr = Member->QueryInterface(aIID, aInstancePtr); \
+ if (SUCCEEDED(hr)) return hr; \
+ }
+
+#define IMPL_IUNKNOWN1(Class, I1) \
+ IMPL_IUNKNOWN_QUERY_HEAD(Class) \
+ IMPL_IUNKNOWN_QUERY_IFACE(I1); \
+ IMPL_IUNKNOWN_QUERY_IFACE(IUnknown); \
+ IMPL_IUNKNOWN_QUERY_TAIL
+
+#define IMPL_IUNKNOWN2(Class, I1, I2) \
+ IMPL_IUNKNOWN_QUERY_HEAD(Class) \
+ IMPL_IUNKNOWN_QUERY_IFACE(I1); \
+ IMPL_IUNKNOWN_QUERY_IFACE(I2); \
+ IMPL_IUNKNOWN_QUERY_IFACE_AMBIGIOUS(IUnknown, I1); \
+ IMPL_IUNKNOWN_QUERY_TAIL
+
+#define IMPL_IUNKNOWN_INHERITED1(Class, Super0, Super1) \
+ IMPL_IUNKNOWN_QUERY_HEAD(Class) \
+ IMPL_IUNKNOWN_QUERY_CLASS(Super1); \
+ IMPL_IUNKNOWN_QUERY_TAIL_INHERITED(Super0)
+
+#define IMPL_IUNKNOWN_INHERITED2(Class, Super0, Super1, Super2) \
+ IMPL_IUNKNOWN_QUERY_HEAD(Class) \
+ IMPL_IUNKNOWN_QUERY_CLASS(Super1); \
+ IMPL_IUNKNOWN_QUERY_CLASS(Super2); \
+ IMPL_IUNKNOWN_QUERY_TAIL_INHERITED(Super0)
+
+/**
+ * Overrides AddRef and Release to call a specific base class.
+ * If you are inheriting a single class (e.g. to override some methods), you
+ * shouldn't need to use this. However, if you are inheriting from a COM
+ * implementation and also inheriting additional COM interfaces, you will need
+ * to use this to specify which base implements reference counting.
+ */
+#define IMPL_IUNKNOWN_REFCOUNTING_INHERITED(BaseClass) \
+ public: \
+ ULONG STDMETHODCALLTYPE AddRef() override { return BaseClass::AddRef(); } \
+ ULONG STDMETHODCALLTYPE Release() override { return BaseClass::Release(); }
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Converts nsresult to HRESULT.
+ */
+HRESULT GetHRESULT(nsresult aResult);
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/msaa/LazyInstantiator.cpp b/accessible/windows/msaa/LazyInstantiator.cpp
new file mode 100644
index 0000000000..5f8ae9a8a4
--- /dev/null
+++ b/accessible/windows/msaa/LazyInstantiator.cpp
@@ -0,0 +1,779 @@
+/* -*- 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/. */
+
+#include "LazyInstantiator.h"
+
+#include "MainThreadUtils.h"
+#include "mozilla/a11y/LocalAccessible.h"
+#include "mozilla/a11y/Compatibility.h"
+#include "mozilla/a11y/Platform.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/mscom/ProcessRuntime.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+#include "MsaaRootAccessible.h"
+#include "nsAccessibilityService.h"
+#include "nsWindowsHelpers.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsXPCOM.h"
+#include "WinUtils.h"
+#include "prenv.h"
+
+#include <oaidl.h>
+
+#if !defined(STATE_SYSTEM_NORMAL)
+# define STATE_SYSTEM_NORMAL (0)
+#endif // !defined(STATE_SYSTEM_NORMAL)
+
+#define DLL_BLOCKLIST_ENTRY(name, ...) {L##name, __VA_ARGS__},
+#define DLL_BLOCKLIST_STRING_TYPE const wchar_t*
+#include "mozilla/WindowsDllBlocklistA11yDefs.h"
+
+namespace mozilla {
+namespace a11y {
+
+static const wchar_t kLazyInstantiatorProp[] =
+ L"mozilla::a11y::LazyInstantiator";
+
+Maybe<bool> LazyInstantiator::sShouldBlockUia;
+
+/* static */
+already_AddRefed<IAccessible> LazyInstantiator::GetRootAccessible(HWND aHwnd) {
+ RefPtr<IAccessible> result;
+ // At this time we only want to check whether the acc service is running. We
+ // don't actually want to create the acc service yet.
+ if (!GetAccService()) {
+ // There must only be one LazyInstantiator per HWND.
+ // To track this, we set the kLazyInstantiatorProp on the HWND with a
+ // pointer to an existing instance. We only create a new LazyInstatiator if
+ // that prop has not already been set.
+ LazyInstantiator* existingInstantiator =
+ reinterpret_cast<LazyInstantiator*>(
+ ::GetProp(aHwnd, kLazyInstantiatorProp));
+
+ if (existingInstantiator) {
+ // Temporarily disable blind aggregation until we know that we have been
+ // marshaled. See EnableBlindAggregation for more information.
+ existingInstantiator->mAllowBlindAggregation = false;
+ result = existingInstantiator;
+ return result.forget();
+ }
+
+ // a11y is not running yet, there are no existing LazyInstantiators for this
+ // HWND, so create a new one and return it as a surrogate for the root
+ // accessible.
+ result = new LazyInstantiator(aHwnd);
+ return result.forget();
+ }
+
+ // a11y is running, so we just resolve the real root accessible.
+ a11y::LocalAccessible* rootAcc =
+ widget::WinUtils::GetRootAccessibleForHWND(aHwnd);
+ if (!rootAcc) {
+ return nullptr;
+ }
+
+ if (!rootAcc->IsRoot()) {
+ // rootAcc might represent a popup as opposed to a true root accessible.
+ // In that case we just use the regular LocalAccessible::GetNativeInterface.
+ rootAcc->GetNativeInterface(getter_AddRefs(result));
+ return result.forget();
+ }
+
+ auto msaaRoot =
+ static_cast<MsaaRootAccessible*>(MsaaAccessible::GetFrom(rootAcc));
+ // Subtle: msaaRoot might still be wrapped by a LazyInstantiator, but we
+ // don't need LazyInstantiator's capabilities anymore (since a11y is already
+ // running). We can bypass LazyInstantiator by retrieving the internal
+ // unknown (which is not wrapped by the LazyInstantiator) and then querying
+ // that for IID_IAccessible.
+ RefPtr<IUnknown> punk(msaaRoot->GetInternalUnknown());
+
+ MOZ_ASSERT(punk);
+ if (!punk) {
+ return nullptr;
+ }
+
+ punk->QueryInterface(IID_IAccessible, getter_AddRefs(result));
+ return result.forget();
+}
+
+/**
+ * When marshaling an interface, COM makes a whole bunch of QueryInterface
+ * calls to determine what kind of marshaling the interface supports. We need
+ * to handle those queries without instantiating a11y, so we temporarily
+ * disable passing through of QueryInterface calls to a11y. Once we know that
+ * COM is finished marshaling, we call EnableBlindAggregation to re-enable
+ * QueryInterface passthrough.
+ */
+/* static */
+void LazyInstantiator::EnableBlindAggregation(HWND aHwnd) {
+ if (GetAccService()) {
+ // The accessibility service is already running. That means that
+ // LazyInstantiator::GetRootAccessible returned the real MsaaRootAccessible,
+ // rather than returning a LazyInstantiator with blind aggregation disabled.
+ // Thus, we have nothing to do here.
+ return;
+ }
+
+ LazyInstantiator* existingInstantiator = reinterpret_cast<LazyInstantiator*>(
+ ::GetProp(aHwnd, kLazyInstantiatorProp));
+
+ if (!existingInstantiator) {
+ return;
+ }
+
+ existingInstantiator->mAllowBlindAggregation = true;
+}
+
+LazyInstantiator::LazyInstantiator(HWND aHwnd)
+ : mHwnd(aHwnd),
+ mAllowBlindAggregation(false),
+ mWeakMsaaRoot(nullptr),
+ mWeakAccessible(nullptr),
+ mWeakDispatch(nullptr) {
+ MOZ_ASSERT(aHwnd);
+ // Assign ourselves as the designated LazyInstantiator for aHwnd
+ DebugOnly<BOOL> setPropOk =
+ ::SetProp(aHwnd, kLazyInstantiatorProp, reinterpret_cast<HANDLE>(this));
+ MOZ_ASSERT(setPropOk);
+}
+
+LazyInstantiator::~LazyInstantiator() {
+ if (mRealRootUnk) {
+ // Disconnect ourselves from the root accessible.
+ RefPtr<IUnknown> dummy(mWeakMsaaRoot->Aggregate(nullptr));
+ }
+
+ ClearProp();
+}
+
+void LazyInstantiator::ClearProp() {
+ // Remove ourselves as the designated LazyInstantiator for mHwnd
+ DebugOnly<HANDLE> removedProp = ::RemoveProp(mHwnd, kLazyInstantiatorProp);
+ MOZ_ASSERT(!removedProp ||
+ reinterpret_cast<LazyInstantiator*>(removedProp.value) == this);
+}
+
+/**
+ * Get the process id of a remote (out-of-process) MSAA/IA2 client.
+ */
+DWORD LazyInstantiator::GetRemoteMsaaClientPid() {
+ nsAutoHandle callingThread(
+ ::OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE,
+ mscom::ProcessRuntime::GetClientThreadId()));
+ if (!callingThread) {
+ return 0;
+ }
+ return ::GetProcessIdOfThread(callingThread);
+}
+
+/**
+ * This is the blocklist for known "bad" remote clients that instantiate a11y.
+ */
+static const char* gBlockedRemoteClients[] = {
+ "tbnotifier.exe", // Ask.com Toolbar, bug 1453876
+ "flow.exe", // Conexant Flow causes performance issues, bug 1569712
+ "rtop_bg.exe", // ByteFence Anti-Malware, bug 1713383
+ "osk.exe", // Windows On-Screen Keyboard, bug 1424505
+};
+
+/**
+ * Check for the presence of any known "bad" injected DLLs that may be trying
+ * to instantiate a11y.
+ *
+ * @return true to block a11y instantiation, otherwise false to continue
+ */
+bool LazyInstantiator::IsBlockedInjection() {
+ // Check debugging options see if we should disable the blocklist.
+ if (PR_GetEnv("MOZ_DISABLE_ACCESSIBLE_BLOCKLIST")) {
+ return false;
+ }
+
+ for (size_t index = 0, len = ArrayLength(gBlockedInprocDlls); index < len;
+ ++index) {
+ const DllBlockInfo& blockedDll = gBlockedInprocDlls[index];
+ HMODULE module = ::GetModuleHandleW(blockedDll.mName);
+ if (!module) {
+ // This dll isn't loaded.
+ continue;
+ }
+
+ LauncherResult<ModuleVersion> version = GetModuleVersion(module);
+ return version.isOk() && blockedDll.IsVersionBlocked(version.unwrap());
+ }
+
+ return false;
+}
+
+/**
+ * Given a remote client's process ID, determine whether we should proceed with
+ * a11y instantiation. This is where telemetry should be gathered and any
+ * potential blocking of unwanted a11y clients should occur.
+ *
+ * @return true if we should instantiate a11y
+ */
+bool LazyInstantiator::ShouldInstantiate(const DWORD aClientPid) {
+ a11y::SetInstantiator(aClientPid);
+
+ nsCOMPtr<nsIFile> clientExe;
+ if (!a11y::GetInstantiator(getter_AddRefs(clientExe))) {
+ return true;
+ }
+
+ nsresult rv;
+ if (!PR_GetEnv("MOZ_DISABLE_ACCESSIBLE_BLOCKLIST")) {
+ // Debugging option is not present, so check blocklist.
+ nsAutoString leafName;
+ rv = clientExe->GetLeafName(leafName);
+ if (NS_SUCCEEDED(rv)) {
+ for (size_t i = 0, len = ArrayLength(gBlockedRemoteClients); i < len;
+ ++i) {
+ if (leafName.EqualsIgnoreCase(gBlockedRemoteClients[i])) {
+ // If client exe is in our blocklist, do not instantiate.
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Determine whether we should proceed with a11y instantiation, considering the
+ * various different types of clients.
+ */
+bool LazyInstantiator::ShouldInstantiate() {
+ if (Compatibility::IsA11ySuppressedForClipboardCopy()) {
+ // Bug 1774285: Windows Suggested Actions (introduced in Windows 11 22H2)
+ // walks the entire a11y tree using UIA whenever anything is copied to the
+ // clipboard. This causes an unacceptable hang, particularly when the cache
+ // is disabled. Don't allow a11y to be instantiated by this.
+ return false;
+ }
+ if (DWORD pid = GetRemoteMsaaClientPid()) {
+ return ShouldInstantiate(pid);
+ }
+ if (Compatibility::HasKnownNonUiaConsumer()) {
+ // We detected a known in-process client.
+ return true;
+ }
+ // UIA client detection can be expensive, so we cache the result. See the
+ // header comment for ResetUiaDetectionCache() for details.
+ if (sShouldBlockUia.isNothing()) {
+ // Unlike MSAA, we can't tell which specific UIA client is querying us right
+ // now. We can only determine which clients have tried querying us.
+ // Therefore, we must check all of them.
+ AutoTArray<DWORD, 1> uiaPids;
+ Compatibility::GetUiaClientPids(uiaPids);
+ if (uiaPids.IsEmpty()) {
+ // No UIA clients, so don't block UIA. However, we might block for
+ // non-UIA clients below.
+ sShouldBlockUia = Some(false);
+ } else {
+ for (const DWORD pid : uiaPids) {
+ if (ShouldInstantiate(pid)) {
+ sShouldBlockUia = Some(false);
+ return true;
+ }
+ }
+ // We didn't return in the loop above, so there are only blocked UIA
+ // clients.
+ sShouldBlockUia = Some(true);
+ }
+ }
+ if (*sShouldBlockUia) {
+ return false;
+ }
+ if (IsBlockedInjection()) {
+ return false;
+ }
+ return true;
+}
+
+MsaaRootAccessible* LazyInstantiator::ResolveMsaaRoot() {
+ LocalAccessible* acc = widget::WinUtils::GetRootAccessibleForHWND(mHwnd);
+ if (!acc || !acc->IsRoot()) {
+ return nullptr;
+ }
+
+ RefPtr<IAccessible> ia;
+ acc->GetNativeInterface(getter_AddRefs(ia));
+ return static_cast<MsaaRootAccessible*>(ia.get());
+}
+
+/**
+ * With COM aggregation, the aggregated inner object usually delegates its
+ * reference counting to the outer object. In other words, we would expect
+ * mRealRootUnk to delegate its AddRef() and Release() to this LazyInstantiator.
+ *
+ * This scheme will not work in our case because the RootAccessibleWrap is
+ * cycle-collected!
+ *
+ * Instead, once a LazyInstantiator aggregates a RootAccessibleWrap, we transfer
+ * our strong references into mRealRootUnk. Any future calls to AddRef or
+ * Release now operate on mRealRootUnk instead of our intrinsic reference
+ * count. This is a bit strange, but it is the only way for these objects to
+ * share their reference count in a way that is safe for cycle collection.
+ *
+ * How do we know when it is safe to destroy ourselves? In
+ * LazyInstantiator::Release, we examine the result of mRealRootUnk->Release().
+ * If mRealRootUnk's resulting refcount is 1, then we know that the only
+ * remaining reference to mRealRootUnk is the mRealRootUnk reference itself (and
+ * thus nobody else holds references to either this or mRealRootUnk). Therefore
+ * we may now delete ourselves.
+ */
+void LazyInstantiator::TransplantRefCnt() {
+ MOZ_ASSERT(mRefCnt > 0);
+ MOZ_ASSERT(mRealRootUnk);
+
+ while (mRefCnt > 0) {
+ mRealRootUnk.get()->AddRef();
+ --mRefCnt;
+ }
+}
+
+HRESULT
+LazyInstantiator::MaybeResolveRoot() {
+ if (!NS_IsMainThread()) {
+ MOZ_ASSERT_UNREACHABLE("Called on a background thread!");
+ // Bug 1814780: This should never happen, since a caller should only be able
+ // to get this via AccessibleObjectFromWindow/AccessibleObjectFromEvent or
+ // WM_GETOBJECT/ObjectFromLresult, which should marshal any calls on
+ // a background thread to the main thread. Nevertheless, Windows sometimes
+ // calls QueryInterface from a background thread! To avoid crashes, fail
+ // gracefully here.
+ return RPC_E_WRONG_THREAD;
+ }
+
+ if (mWeakAccessible) {
+ return S_OK;
+ }
+
+ if (GetAccService() || ShouldInstantiate()) {
+ mWeakMsaaRoot = ResolveMsaaRoot();
+ if (!mWeakMsaaRoot) {
+ return E_POINTER;
+ }
+
+ // Wrap ourselves around the root accessible wrap
+ mRealRootUnk = mWeakMsaaRoot->Aggregate(static_cast<IAccessible*>(this));
+ if (!mRealRootUnk) {
+ return E_FAIL;
+ }
+
+ // Move our strong references into the root accessible (see the comments
+ // above TransplantRefCnt for explanation).
+ TransplantRefCnt();
+
+ // Now obtain mWeakAccessible which we use to forward our incoming calls
+ // to the real accesssible.
+ HRESULT hr =
+ mRealRootUnk->QueryInterface(IID_IAccessible, (void**)&mWeakAccessible);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ // mWeakAccessible is weak, so don't hold a strong ref
+ mWeakAccessible->Release();
+
+ // Now that a11y is running, we don't need to remain registered with our
+ // HWND anymore.
+ ClearProp();
+
+ return S_OK;
+ }
+
+ return E_FAIL;
+}
+
+#define RESOLVE_ROOT \
+ { \
+ HRESULT hr = MaybeResolveRoot(); \
+ if (FAILED(hr)) { \
+ return hr; \
+ } \
+ }
+
+IMPL_IUNKNOWN_QUERY_HEAD(LazyInstantiator)
+IMPL_IUNKNOWN_QUERY_IFACE_AMBIGIOUS(IUnknown, IAccessible)
+IMPL_IUNKNOWN_QUERY_IFACE(IAccessible)
+IMPL_IUNKNOWN_QUERY_IFACE(IDispatch)
+IMPL_IUNKNOWN_QUERY_IFACE(IServiceProvider)
+// See EnableBlindAggregation for comments.
+if (!mAllowBlindAggregation) {
+ return E_NOINTERFACE;
+}
+
+if (aIID == IID_IAccIdentity) {
+ return E_NOINTERFACE;
+}
+// If the client queries for an interface that LazyInstantiator does not
+// intrinsically support, then we must resolve the root accessible and pass
+// on the QueryInterface call to mRealRootUnk.
+RESOLVE_ROOT
+IMPL_IUNKNOWN_QUERY_TAIL_AGGREGATED(mRealRootUnk)
+
+ULONG
+LazyInstantiator::AddRef() {
+ // Always delegate refcounting to mRealRootUnk when it exists
+ if (mRealRootUnk) {
+ return mRealRootUnk.get()->AddRef();
+ }
+
+ return ++mRefCnt;
+}
+
+ULONG
+LazyInstantiator::Release() {
+ ULONG result;
+
+ // Always delegate refcounting to mRealRootUnk when it exists
+ if (mRealRootUnk) {
+ result = mRealRootUnk.get()->Release();
+ if (result == 1) {
+ // mRealRootUnk is the only strong reference left, so nothing else holds
+ // a strong reference to us. Drop result to zero so that we destroy
+ // ourselves (See the comments above LazyInstantiator::TransplantRefCnt
+ // for more info).
+ --result;
+ }
+ } else {
+ result = --mRefCnt;
+ }
+
+ if (!result) {
+ delete this;
+ }
+ return result;
+}
+
+/**
+ * Create a standard IDispatch implementation. mStdDispatch will translate any
+ * IDispatch::Invoke calls into real IAccessible calls.
+ */
+HRESULT
+LazyInstantiator::ResolveDispatch() {
+ if (mWeakDispatch) {
+ return S_OK;
+ }
+
+ // Extract IAccessible's type info
+ RefPtr<ITypeInfo> accTypeInfo = MsaaAccessible::GetTI(LOCALE_USER_DEFAULT);
+ if (!accTypeInfo) {
+ return E_UNEXPECTED;
+ }
+
+ // Now create the standard IDispatch for IAccessible
+ HRESULT hr = ::CreateStdDispatch(static_cast<IAccessible*>(this),
+ static_cast<IAccessible*>(this), accTypeInfo,
+ getter_AddRefs(mStdDispatch));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ hr = mStdDispatch->QueryInterface(IID_IDispatch, (void**)&mWeakDispatch);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ // WEAK reference
+ mWeakDispatch->Release();
+ return S_OK;
+}
+
+#define RESOLVE_IDISPATCH \
+ { \
+ HRESULT hr = ResolveDispatch(); \
+ if (FAILED(hr)) { \
+ return hr; \
+ } \
+ }
+
+/**
+ * The remaining methods implement IDispatch, IAccessible, and IServiceProvider,
+ * lazily resolving the real a11y objects and passing the call through.
+ */
+
+HRESULT
+LazyInstantiator::GetTypeInfoCount(UINT* pctinfo) {
+ RESOLVE_IDISPATCH;
+ return mWeakDispatch->GetTypeInfoCount(pctinfo);
+}
+
+HRESULT
+LazyInstantiator::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo) {
+ RESOLVE_IDISPATCH;
+ return mWeakDispatch->GetTypeInfo(iTInfo, lcid, ppTInfo);
+}
+
+HRESULT
+LazyInstantiator::GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames,
+ LCID lcid, DISPID* rgDispId) {
+ RESOLVE_IDISPATCH;
+ return mWeakDispatch->GetIDsOfNames(riid, rgszNames, cNames, lcid, rgDispId);
+}
+
+HRESULT
+LazyInstantiator::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid,
+ WORD wFlags, DISPPARAMS* pDispParams,
+ VARIANT* pVarResult, EXCEPINFO* pExcepInfo,
+ UINT* puArgErr) {
+ RESOLVE_IDISPATCH;
+ return mWeakDispatch->Invoke(dispIdMember, riid, lcid, wFlags, pDispParams,
+ pVarResult, pExcepInfo, puArgErr);
+}
+
+HRESULT
+LazyInstantiator::get_accParent(IDispatch** ppdispParent) {
+ if (!mWeakAccessible) {
+ // If we'd resolve the root right now this would be the codepath we'd end
+ // up in anyway. So we might as well return it here.
+ return ::CreateStdAccessibleObject(mHwnd, OBJID_WINDOW, IID_IAccessible,
+ (void**)ppdispParent);
+ }
+ RESOLVE_ROOT;
+ return mWeakAccessible->get_accParent(ppdispParent);
+}
+
+HRESULT
+LazyInstantiator::get_accChildCount(long* pcountChildren) {
+ if (!pcountChildren) {
+ return E_INVALIDARG;
+ }
+
+ RESOLVE_ROOT;
+ return mWeakAccessible->get_accChildCount(pcountChildren);
+}
+
+HRESULT
+LazyInstantiator::get_accChild(VARIANT varChild, IDispatch** ppdispChild) {
+ if (!ppdispChild) {
+ return E_INVALIDARG;
+ }
+
+ if (V_VT(&varChild) == VT_I4 && V_I4(&varChild) == CHILDID_SELF) {
+ RefPtr<IDispatch> disp(this);
+ disp.forget(ppdispChild);
+ return S_OK;
+ }
+
+ RESOLVE_ROOT;
+ return mWeakAccessible->get_accChild(varChild, ppdispChild);
+}
+
+HRESULT
+LazyInstantiator::get_accName(VARIANT varChild, BSTR* pszName) {
+ if (!pszName) {
+ return E_INVALIDARG;
+ }
+
+ RESOLVE_ROOT;
+ return mWeakAccessible->get_accName(varChild, pszName);
+}
+
+HRESULT
+LazyInstantiator::get_accValue(VARIANT varChild, BSTR* pszValue) {
+ if (!pszValue) {
+ return E_INVALIDARG;
+ }
+
+ RESOLVE_ROOT;
+ return mWeakAccessible->get_accValue(varChild, pszValue);
+}
+
+HRESULT
+LazyInstantiator::get_accDescription(VARIANT varChild, BSTR* pszDescription) {
+ if (!pszDescription) {
+ return E_INVALIDARG;
+ }
+
+ RESOLVE_ROOT;
+ return mWeakAccessible->get_accDescription(varChild, pszDescription);
+}
+
+HRESULT
+LazyInstantiator::get_accRole(VARIANT varChild, VARIANT* pvarRole) {
+ if (!pvarRole) {
+ return E_INVALIDARG;
+ }
+
+ if (V_VT(&varChild) == VT_I4 && V_I4(&varChild) == CHILDID_SELF) {
+ V_VT(pvarRole) = VT_I4;
+ V_I4(pvarRole) = ROLE_SYSTEM_APPLICATION;
+ return S_OK;
+ }
+
+ RESOLVE_ROOT;
+ return mWeakAccessible->get_accRole(varChild, pvarRole);
+}
+
+HRESULT
+LazyInstantiator::get_accState(VARIANT varChild, VARIANT* pvarState) {
+ if (!pvarState) {
+ return E_INVALIDARG;
+ }
+
+ RESOLVE_ROOT;
+ return mWeakAccessible->get_accState(varChild, pvarState);
+}
+
+HRESULT
+LazyInstantiator::get_accHelp(VARIANT varChild, BSTR* pszHelp) {
+ return E_NOTIMPL;
+}
+
+HRESULT
+LazyInstantiator::get_accHelpTopic(BSTR* pszHelpFile, VARIANT varChild,
+ long* pidTopic) {
+ return E_NOTIMPL;
+}
+
+HRESULT
+LazyInstantiator::get_accKeyboardShortcut(VARIANT varChild,
+ BSTR* pszKeyboardShortcut) {
+ if (!pszKeyboardShortcut) {
+ return E_INVALIDARG;
+ }
+
+ RESOLVE_ROOT;
+ return mWeakAccessible->get_accKeyboardShortcut(varChild,
+ pszKeyboardShortcut);
+}
+
+HRESULT
+LazyInstantiator::get_accFocus(VARIANT* pvarChild) {
+ if (!pvarChild) {
+ return E_INVALIDARG;
+ }
+
+ RESOLVE_ROOT;
+ return mWeakAccessible->get_accFocus(pvarChild);
+}
+
+HRESULT
+LazyInstantiator::get_accSelection(VARIANT* pvarChildren) {
+ if (!pvarChildren) {
+ return E_INVALIDARG;
+ }
+
+ RESOLVE_ROOT;
+ return mWeakAccessible->get_accSelection(pvarChildren);
+}
+
+HRESULT
+LazyInstantiator::get_accDefaultAction(VARIANT varChild,
+ BSTR* pszDefaultAction) {
+ if (!pszDefaultAction) {
+ return E_INVALIDARG;
+ }
+
+ RESOLVE_ROOT;
+ return mWeakAccessible->get_accDefaultAction(varChild, pszDefaultAction);
+}
+
+HRESULT
+LazyInstantiator::accSelect(long flagsSelect, VARIANT varChild) {
+ RESOLVE_ROOT;
+ return mWeakAccessible->accSelect(flagsSelect, varChild);
+}
+
+HRESULT
+LazyInstantiator::accLocation(long* pxLeft, long* pyTop, long* pcxWidth,
+ long* pcyHeight, VARIANT varChild) {
+ RESOLVE_ROOT;
+ return mWeakAccessible->accLocation(pxLeft, pyTop, pcxWidth, pcyHeight,
+ varChild);
+}
+
+HRESULT
+LazyInstantiator::accNavigate(long navDir, VARIANT varStart,
+ VARIANT* pvarEndUpAt) {
+ if (!pvarEndUpAt) {
+ return E_INVALIDARG;
+ }
+
+ RESOLVE_ROOT;
+ return mWeakAccessible->accNavigate(navDir, varStart, pvarEndUpAt);
+}
+
+HRESULT
+LazyInstantiator::accHitTest(long xLeft, long yTop, VARIANT* pvarChild) {
+ if (!pvarChild) {
+ return E_INVALIDARG;
+ }
+
+ RESOLVE_ROOT;
+ return mWeakAccessible->accHitTest(xLeft, yTop, pvarChild);
+}
+
+HRESULT
+LazyInstantiator::accDoDefaultAction(VARIANT varChild) {
+ RESOLVE_ROOT;
+ return mWeakAccessible->accDoDefaultAction(varChild);
+}
+
+HRESULT
+LazyInstantiator::put_accName(VARIANT varChild, BSTR szName) {
+ return E_NOTIMPL;
+}
+
+HRESULT
+LazyInstantiator::put_accValue(VARIANT varChild, BSTR szValue) {
+ return E_NOTIMPL;
+}
+
+static const GUID kUnsupportedServices[] = {
+ // clang-format off
+ // Unknown, queried by Windows on devices with touch screens or similar devices
+ // connected.
+ {0x33f139ee, 0xe509, 0x47f7, {0xbf, 0x39, 0x83, 0x76, 0x44, 0xf7, 0x45, 0x76}},
+ // Unknown, queried by Windows
+ {0xFDA075CF, 0x7C8B, 0x498C, { 0xB5, 0x14, 0xA9, 0xCB, 0x52, 0x1B, 0xBF, 0xB4 }},
+ // Unknown, queried by Windows
+ {0x8EDAA462, 0x21F4, 0x4C87, { 0xA0, 0x12, 0xB3, 0xCD, 0xA3, 0xAB, 0x01, 0xFC }},
+ // Unknown, queried by Windows
+ {0xacd46652, 0x829d, 0x41cb, { 0xa5, 0xfc, 0x17, 0xac, 0xf4, 0x36, 0x61, 0xac }},
+ // SID_IsUIAutomationObject (undocumented), queried by Windows
+ {0xb96fdb85, 0x7204, 0x4724, { 0x84, 0x2b, 0xc7, 0x05, 0x9d, 0xed, 0xb9, 0xd0 }},
+ // IIS_IsOleaccProxy (undocumented), queried by Windows
+ {0x902697FA, 0x80E4, 0x4560, {0x80, 0x2A, 0xA1, 0x3F, 0x22, 0xA6, 0x47, 0x09}},
+ // IID_IHTMLElement, queried by JAWS
+ {0x3050F1FF, 0x98B5, 0x11CF, {0xBB, 0x82, 0x00, 0xAA, 0x00, 0xBD, 0xCE, 0x0B}}
+ // clang-format on
+};
+
+HRESULT
+LazyInstantiator::QueryService(REFGUID aServiceId, REFIID aServiceIid,
+ void** aOutInterface) {
+ if (!aOutInterface) {
+ return E_INVALIDARG;
+ }
+
+ for (const GUID& unsupportedService : kUnsupportedServices) {
+ if (aServiceId == unsupportedService) {
+ return E_NOINTERFACE;
+ }
+ }
+
+ *aOutInterface = nullptr;
+
+ RESOLVE_ROOT;
+
+ RefPtr<IServiceProvider> servProv;
+ HRESULT hr = mRealRootUnk->QueryInterface(IID_IServiceProvider,
+ getter_AddRefs(servProv));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ return servProv->QueryService(aServiceId, aServiceIid, aOutInterface);
+}
+
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/windows/msaa/LazyInstantiator.h b/accessible/windows/msaa/LazyInstantiator.h
new file mode 100644
index 0000000000..00fa4ba6ed
--- /dev/null
+++ b/accessible/windows/msaa/LazyInstantiator.h
@@ -0,0 +1,142 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_LazyInstantiator_h
+#define mozilla_a11y_LazyInstantiator_h
+
+#include "IUnknownImpl.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "nsString.h"
+
+#include <oleacc.h>
+
+class nsIFile;
+
+namespace mozilla {
+namespace a11y {
+
+class MsaaRootAccessible;
+
+/**
+ * LazyInstantiator is an IAccessible that initially acts as a placeholder.
+ * The a11y service is not actually started until two conditions are met:
+ *
+ * (1) A method is called on the LazyInstantiator that would require a11y
+ * services in order to fulfill; and
+ * (2) LazyInstantiator::ShouldInstantiate returns true.
+ */
+class LazyInstantiator final : public IAccessible, public IServiceProvider {
+ public:
+ [[nodiscard]] static already_AddRefed<IAccessible> GetRootAccessible(
+ HWND aHwnd);
+ static void EnableBlindAggregation(HWND aHwnd);
+
+ // IUnknown
+ STDMETHODIMP QueryInterface(REFIID aIid, void** aOutInterface) override;
+ STDMETHODIMP_(ULONG) AddRef() override;
+ STDMETHODIMP_(ULONG) Release() override;
+
+ // IDispatch
+ STDMETHODIMP GetTypeInfoCount(UINT* pctinfo) override;
+ STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid,
+ ITypeInfo** ppTInfo) override;
+ STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames,
+ LCID lcid, DISPID* rgDispId) override;
+ STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags,
+ DISPPARAMS* pDispParams, VARIANT* pVarResult,
+ EXCEPINFO* pExcepInfo, UINT* puArgErr) override;
+
+ // IAccessible
+ STDMETHODIMP get_accParent(IDispatch** ppdispParent) override;
+ STDMETHODIMP get_accChildCount(long* pcountChildren) override;
+ STDMETHODIMP get_accChild(VARIANT varChild, IDispatch** ppdispChild) override;
+ STDMETHODIMP get_accName(VARIANT varChild, BSTR* pszName) override;
+ STDMETHODIMP get_accValue(VARIANT varChild, BSTR* pszValue) override;
+ STDMETHODIMP get_accDescription(VARIANT varChild,
+ BSTR* pszDescription) override;
+ STDMETHODIMP get_accRole(VARIANT varChild, VARIANT* pvarRole) override;
+ STDMETHODIMP get_accState(VARIANT varChild, VARIANT* pvarState) override;
+ STDMETHODIMP get_accHelp(VARIANT varChild, BSTR* pszHelp) override;
+ STDMETHODIMP get_accHelpTopic(BSTR* pszHelpFile, VARIANT varChild,
+ long* pidTopic) override;
+ STDMETHODIMP get_accKeyboardShortcut(VARIANT varChild,
+ BSTR* pszKeyboardShortcut) override;
+ STDMETHODIMP get_accFocus(VARIANT* pvarChild) override;
+ STDMETHODIMP get_accSelection(VARIANT* pvarChildren) override;
+ STDMETHODIMP get_accDefaultAction(VARIANT varChild,
+ BSTR* pszDefaultAction) override;
+ STDMETHODIMP accSelect(long flagsSelect, VARIANT varChild) override;
+ STDMETHODIMP accLocation(long* pxLeft, long* pyTop, long* pcxWidth,
+ long* pcyHeight, VARIANT varChild) override;
+ STDMETHODIMP accNavigate(long navDir, VARIANT varStart,
+ VARIANT* pvarEndUpAt) override;
+ STDMETHODIMP accHitTest(long xLeft, long yTop, VARIANT* pvarChild) override;
+ STDMETHODIMP accDoDefaultAction(VARIANT varChild) override;
+ STDMETHODIMP put_accName(VARIANT varChild, BSTR szName) override;
+ STDMETHODIMP put_accValue(VARIANT varChild, BSTR szValue) override;
+
+ // IServiceProvider
+ STDMETHODIMP QueryService(REFGUID aServiceId, REFIID aServiceIid,
+ void** aOutInterface) override;
+
+ /**
+ * We cache the result of UIA detection because it could be expensive if a
+ * client repeatedly queries us. This function is called to reset that cache
+ * when one of our windows comes to the foreground. If there is a new UIA
+ * client that isn't blocked, instantiation will subsequently be allowed. The
+ * hope is that a user will probably need to switch apps in order to start a
+ * new client.
+ */
+ static void ResetUiaDetectionCache() { sShouldBlockUia = Nothing(); }
+
+ private:
+ explicit LazyInstantiator(HWND aHwnd);
+ ~LazyInstantiator();
+
+ bool IsBlockedInjection();
+ bool ShouldInstantiate(const DWORD aClientPid);
+ bool ShouldInstantiate();
+
+ DWORD GetRemoteMsaaClientPid();
+
+ /**
+ * @return S_OK if we have a valid mRealRoot to invoke methods on
+ */
+ HRESULT MaybeResolveRoot();
+
+ /**
+ * @return S_OK if we have a valid mWeakDispatch to invoke methods on
+ */
+ HRESULT ResolveDispatch();
+
+ MsaaRootAccessible* ResolveMsaaRoot();
+ void TransplantRefCnt();
+ void ClearProp();
+
+ private:
+ mozilla::a11y::AutoRefCnt mRefCnt;
+ HWND mHwnd;
+ bool mAllowBlindAggregation;
+ RefPtr<IUnknown> mRealRootUnk;
+ RefPtr<IUnknown> mStdDispatch;
+ /**
+ * mWeakMsaaRoot, mWeakAccessible and mWeakDispatch are weak because they
+ * are interfaces that come from objects that we aggregate. Aggregated object
+ * interfaces share refcount methods with ours, so if we were to hold strong
+ * references to them, we would be holding strong references to ourselves,
+ * creating a cycle.
+ */
+ MsaaRootAccessible* mWeakMsaaRoot;
+ IAccessible* mWeakAccessible;
+ IDispatch* mWeakDispatch;
+ static Maybe<bool> sShouldBlockUia;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_LazyInstantiator_h
diff --git a/accessible/windows/msaa/MsaaAccessible.cpp b/accessible/windows/msaa/MsaaAccessible.cpp
new file mode 100644
index 0000000000..702f8341dc
--- /dev/null
+++ b/accessible/windows/msaa/MsaaAccessible.cpp
@@ -0,0 +1,1368 @@
+/* -*- 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 "EnumVariant.h"
+#include "ia2AccessibleApplication.h"
+#include "ia2AccessibleHypertext.h"
+#include "ia2AccessibleImage.h"
+#include "ia2AccessibleTable.h"
+#include "ia2AccessibleTableCell.h"
+#include "LocalAccessible-inl.h"
+#include "mozilla/a11y/AccessibleWrap.h"
+#include "mozilla/a11y/Compatibility.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "MsaaAccessible.h"
+#include "MsaaDocAccessible.h"
+#include "MsaaRootAccessible.h"
+#include "MsaaXULMenuAccessible.h"
+#include "nsEventMap.h"
+#include "nsViewManager.h"
+#include "nsWinUtils.h"
+#include "Relation.h"
+#include "sdnAccessible.h"
+#include "sdnTextAccessible.h"
+#include "HyperTextAccessible-inl.h"
+#include "ServiceProvider.h"
+#include "Statistics.h"
+#include "ARIAMap.h"
+#include "mozilla/PresShell.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+static const VARIANT kVarChildIdSelf = {{{VT_I4}}};
+
+// Used internally to safely get an MsaaAccessible from a COM pointer provided
+// to us by a client.
+static const GUID IID_MsaaAccessible = {
+ /* a94aded3-1a9c-4afc-a32c-d6b5c010046b */
+ 0xa94aded3,
+ 0x1a9c,
+ 0x4afc,
+ {0xa3, 0x2c, 0xd6, 0xb5, 0xc0, 0x10, 0x04, 0x6b}};
+
+MsaaIdGenerator MsaaAccessible::sIDGen;
+ITypeInfo* MsaaAccessible::gTypeInfo = nullptr;
+
+/* static */
+MsaaAccessible* MsaaAccessible::Create(Accessible* aAcc) {
+ // This should only ever be called in the parent process.
+ MOZ_ASSERT(XRE_IsParentProcess());
+ // The order of some of these is important! For example, when isRoot is true,
+ // IsDoc will also be true, so we must check IsRoot first. IsTable/Cell and
+ // IsHyperText are a similar case.
+ if (aAcc->IsRoot()) {
+ MOZ_ASSERT(aAcc->IsLocal());
+ return new MsaaRootAccessible(aAcc);
+ }
+ if (aAcc->IsDoc()) {
+ return new MsaaDocAccessible(aAcc);
+ }
+ if (aAcc->IsTable()) {
+ return new ia2AccessibleTable(aAcc);
+ }
+ if (aAcc->IsTableCell()) {
+ return new ia2AccessibleTableCell(aAcc);
+ }
+ if (aAcc->IsApplication()) {
+ MOZ_ASSERT(aAcc->IsLocal());
+ return new ia2AccessibleApplication(aAcc);
+ }
+ if (aAcc->IsImage()) {
+ return new ia2AccessibleImage(aAcc);
+ }
+ if (LocalAccessible* localAcc = aAcc->AsLocal()) {
+ if (localAcc->GetContent() &&
+ localAcc->GetContent()->IsXULElement(nsGkAtoms::menuitem)) {
+ return new MsaaXULMenuitemAccessible(aAcc);
+ }
+ }
+ if (aAcc->IsHyperText()) {
+ return new ia2AccessibleHypertext(aAcc);
+ }
+ return new MsaaAccessible(aAcc);
+}
+
+MsaaAccessible::MsaaAccessible(Accessible* aAcc) : mAcc(aAcc), mID(kNoID) {}
+
+MsaaAccessible::~MsaaAccessible() {
+ MOZ_ASSERT(!mAcc, "MsaaShutdown wasn't called!");
+ if (mID != kNoID) {
+ sIDGen.ReleaseID(WrapNotNull(this));
+ }
+}
+
+void MsaaAccessible::MsaaShutdown() {
+ // Accessibles can be shut down twice in some cases. If that happens,
+ // MsaaShutdown will also be called twice because AccessibleWrap holds
+ // the reference until its destructor is called; see the comments in
+ // AccessibleWrap::Shutdown.
+ if (!mAcc) {
+ return;
+ }
+
+ if (mID != kNoID) {
+ auto doc = MsaaDocAccessible::GetFromOwned(mAcc);
+ MOZ_ASSERT(doc);
+ doc->RemoveID(mID);
+ }
+
+ mAcc = nullptr;
+}
+
+int32_t MsaaAccessible::GetChildIDFor(Accessible* aAccessible) {
+ // A child ID of the window is required, when we use NotifyWinEvent,
+ // so that the 3rd party application can call back and get the IAccessible
+ // the event occurred on.
+
+ if (!aAccessible) {
+ return 0;
+ }
+
+ auto doc = MsaaDocAccessible::GetFromOwned(aAccessible);
+ if (!doc) {
+ return 0;
+ }
+
+ uint32_t* id = &MsaaAccessible::GetFrom(aAccessible)->mID;
+ if (*id != kNoID) return *id;
+
+ *id = sIDGen.GetID();
+ doc->AddID(*id, aAccessible);
+
+ return *id;
+}
+
+/* static */
+void MsaaAccessible::AssignChildIDTo(NotNull<sdnAccessible*> aSdnAcc) {
+ aSdnAcc->SetUniqueID(sIDGen.GetID());
+}
+
+/* static */
+void MsaaAccessible::ReleaseChildID(NotNull<sdnAccessible*> aSdnAcc) {
+ sIDGen.ReleaseID(aSdnAcc);
+}
+
+HWND MsaaAccessible::GetHWNDFor(Accessible* aAccessible) {
+ if (!aAccessible) {
+ return nullptr;
+ }
+
+ LocalAccessible* localAcc = aAccessible->AsLocal();
+ if (!localAcc) {
+ RemoteAccessible* proxy = aAccessible->AsRemote();
+ if (!proxy) {
+ return nullptr;
+ }
+
+ // If window emulation is enabled, retrieve the emulated window from the
+ // containing document document proxy.
+ if (nsWinUtils::IsWindowEmulationStarted()) {
+ DocAccessibleParent* doc = proxy->Document();
+ HWND hWnd = doc->GetEmulatedWindowHandle();
+ if (hWnd) {
+ return hWnd;
+ }
+ }
+
+ // Accessibles in child processes are said to have the HWND of the window
+ // their tab is within. Popups are always in the parent process, and so
+ // never proxied, which means this is basically correct.
+ LocalAccessible* outerDoc = proxy->OuterDocOfRemoteBrowser();
+ if (!outerDoc) {
+ // In some cases, the outer document accessible may be unattached from its
+ // document at this point, if it is scheduled for removal. Do not assert
+ // in such case. An example: putting aria-hidden="true" on HTML:iframe
+ // element will destroy iframe's document asynchroniously, but
+ // the document may be a target of selection events until then, and thus
+ // it may attempt to deliever these events to MSAA clients.
+ return nullptr;
+ }
+
+ return GetHWNDFor(outerDoc);
+ }
+
+ DocAccessible* document = localAcc->Document();
+ if (!document) return nullptr;
+
+ // Popup lives in own windows, use its HWND until the popup window is
+ // hidden to make old JAWS versions work with collapsed comboboxes (see
+ // discussion in bug 379678).
+ nsIFrame* frame = localAcc->GetFrame();
+ if (frame) {
+ nsIWidget* widget = frame->GetNearestWidget();
+ if (widget && widget->IsVisible()) {
+ if (nsViewManager* vm = document->PresShellPtr()->GetViewManager()) {
+ nsCOMPtr<nsIWidget> rootWidget = vm->GetRootWidget();
+ // Make sure the accessible belongs to popup. If not then use
+ // document HWND (which might be different from root widget in the
+ // case of window emulation).
+ if (rootWidget != widget)
+ return static_cast<HWND>(widget->GetNativeData(NS_NATIVE_WINDOW));
+ }
+ }
+ }
+
+ return static_cast<HWND>(document->GetNativeWindow());
+}
+
+void MsaaAccessible::FireWinEvent(Accessible* aTarget, uint32_t aEventType) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ static_assert(sizeof(gWinEventMap) / sizeof(gWinEventMap[0]) ==
+ nsIAccessibleEvent::EVENT_LAST_ENTRY,
+ "MSAA event map skewed");
+
+ if (aEventType == 0 || aEventType >= ArrayLength(gWinEventMap)) {
+ MOZ_ASSERT_UNREACHABLE("invalid event type");
+ return;
+ }
+
+ uint32_t winEvent = gWinEventMap[aEventType];
+ if (!winEvent) return;
+
+ int32_t childID = MsaaAccessible::GetChildIDFor(aTarget);
+ if (!childID) return; // Can't fire an event without a child ID
+
+ HWND hwnd = GetHWNDFor(aTarget);
+ if (!hwnd) {
+ return;
+ }
+
+ // Fire MSAA event for client area window.
+ ::NotifyWinEvent(winEvent, hwnd, OBJID_CLIENT, childID);
+}
+
+AccessibleWrap* MsaaAccessible::LocalAcc() {
+ if (!mAcc || mAcc->IsRemote()) {
+ return nullptr;
+ }
+ auto acc = static_cast<AccessibleWrap*>(mAcc);
+ MOZ_ASSERT(!acc || !acc->IsDefunct(),
+ "mAcc defunct but MsaaShutdown wasn't called");
+ return acc;
+}
+
+/**
+ * This function is a helper for implementing IAccessible methods that accept
+ * a Child ID as a parameter. If the child ID is CHILDID_SELF, the function
+ * returns S_OK but a null *aOutInterface. Otherwise, *aOutInterface points
+ * to the resolved IAccessible.
+ *
+ * The CHILDID_SELF case is special because in that case we actually execute
+ * the implementation of the IAccessible method, whereas in the non-self case,
+ * we delegate the method call to that object for execution.
+ *
+ * A sample invocation of this would look like:
+ *
+ * RefPtr<IAccessible> accessible;
+ * HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible));
+ * if (FAILED(hr)) {
+ * return hr;
+ * }
+ *
+ * if (accessible) {
+ * return accessible->get_accFoo(kVarChildIdSelf, pszName);
+ * }
+ *
+ * // Implementation for CHILDID_SELF case goes here
+ */
+HRESULT
+MsaaAccessible::ResolveChild(const VARIANT& aVarChild,
+ IAccessible** aOutInterface) {
+ MOZ_ASSERT(aOutInterface);
+ *aOutInterface = nullptr;
+
+ if (aVarChild.vt != VT_I4) {
+ return E_INVALIDARG;
+ }
+
+ if (!mAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ if (aVarChild.lVal == CHILDID_SELF) {
+ return S_OK;
+ }
+
+ bool isDefunct = false;
+ RefPtr<IAccessible> accessible = GetIAccessibleFor(aVarChild, &isDefunct);
+ if (!accessible) {
+ return E_INVALIDARG;
+ }
+
+ if (isDefunct) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ accessible.forget(aOutInterface);
+ return S_OK;
+}
+
+static Accessible* GetAccessibleInSubtree(DocAccessible* aDoc, uint32_t aID) {
+ Accessible* child = MsaaDocAccessible::GetFrom(aDoc)->GetAccessibleByID(aID);
+ if (child) return child;
+
+ uint32_t childDocCount = aDoc->ChildDocumentCount();
+ for (uint32_t i = 0; i < childDocCount; i++) {
+ child = GetAccessibleInSubtree(aDoc->GetChildDocumentAt(i), aID);
+ if (child) return child;
+ }
+
+ return nullptr;
+}
+
+static Accessible* GetAccessibleInSubtree(DocAccessibleParent* aDoc,
+ uint32_t aID) {
+ Accessible* child = MsaaDocAccessible::GetFrom(aDoc)->GetAccessibleByID(aID);
+ if (child) {
+ return child;
+ }
+
+ size_t childDocCount = aDoc->ChildDocCount();
+ for (size_t i = 0; i < childDocCount; i++) {
+ child = GetAccessibleInSubtree(aDoc->ChildDocAt(i), aID);
+ if (child) {
+ return child;
+ }
+ }
+
+ return nullptr;
+}
+
+static bool IsInclusiveDescendantOf(DocAccessible* aAncestor,
+ DocAccessible* aDescendant) {
+ for (DocAccessible* doc = aDescendant; doc; doc = doc->ParentDocument()) {
+ if (doc == aAncestor) {
+ return true;
+ }
+ }
+ return false;
+}
+
+already_AddRefed<IAccessible> MsaaAccessible::GetIAccessibleFor(
+ const VARIANT& aVarChild, bool* aIsDefunct) {
+ if (aVarChild.vt != VT_I4) return nullptr;
+
+ VARIANT varChild = aVarChild;
+
+ MOZ_ASSERT(aIsDefunct);
+ *aIsDefunct = false;
+
+ RefPtr<IAccessible> result;
+
+ if (!mAcc) {
+ *aIsDefunct = true;
+ return nullptr;
+ }
+
+ if (varChild.lVal == CHILDID_SELF) {
+ result = this;
+ return result.forget();
+ }
+
+ if (varChild.ulVal != GetExistingID() && nsAccUtils::MustPrune(mAcc)) {
+ // This accessible should have no subtree in platform, return null for its
+ // children.
+ return nullptr;
+ }
+
+ if (varChild.lVal > 0) {
+ // Gecko child indices are 0-based in contrast to indices used in MSAA.
+ Accessible* xpAcc = mAcc->ChildAt(varChild.lVal - 1);
+ if (!xpAcc) {
+ return nullptr;
+ }
+ MOZ_ASSERT(xpAcc->IsRemote() || !xpAcc->AsLocal()->IsDefunct(),
+ "Shouldn't get a defunct child");
+ result = MsaaAccessible::GetFrom(xpAcc);
+ return result.forget();
+ }
+
+ // If lVal negative then it is treated as child ID and we should look for
+ // accessible through whole accessible subtree including subdocuments.
+ Accessible* doc = nullptr;
+ Accessible* child = nullptr;
+ auto id = static_cast<uint32_t>(varChild.lVal);
+ if (LocalAccessible* localAcc = mAcc->AsLocal()) {
+ DocAccessible* localDoc = localAcc->Document();
+ doc = localDoc;
+ child = GetAccessibleInSubtree(localDoc, id);
+ if (!child) {
+ // Search remote documents which are descendants of this local document.
+ const auto remoteDocs = DocManager::TopLevelRemoteDocs();
+ if (!remoteDocs) {
+ return nullptr;
+ }
+ for (DocAccessibleParent* remoteDoc : *remoteDocs) {
+ LocalAccessible* outerDoc = remoteDoc->OuterDocOfRemoteBrowser();
+ if (!outerDoc ||
+ !IsInclusiveDescendantOf(localDoc, outerDoc->Document())) {
+ continue;
+ }
+ child = GetAccessibleInSubtree(remoteDoc, id);
+ if (child) {
+ break;
+ }
+ }
+ }
+ } else {
+ DocAccessibleParent* remoteDoc = mAcc->AsRemote()->Document();
+ doc = remoteDoc;
+ child = GetAccessibleInSubtree(remoteDoc, id);
+ }
+ if (!child) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(child->IsRemote() || !child->AsLocal()->IsDefunct(),
+ "Shouldn't get a defunct child");
+ // If this method is being called on the document we searched, we can just
+ // return child.
+ if (mAcc == doc) {
+ result = MsaaAccessible::GetFrom(child);
+ return result.forget();
+ }
+
+ // Otherwise, this method was called on a descendant, so we searched an
+ // ancestor. We must check whether child is really a descendant. This is used
+ // for ARIA documents and popups.
+ Accessible* parent = child;
+ while (parent && parent != doc) {
+ if (parent == mAcc) {
+ result = MsaaAccessible::GetFrom(child);
+ return result.forget();
+ }
+
+ parent = parent->Parent();
+ }
+
+ return nullptr;
+}
+
+IDispatch* MsaaAccessible::NativeAccessible(Accessible* aAccessible) {
+ if (!aAccessible) {
+ NS_WARNING("Not passing in an aAccessible");
+ return nullptr;
+ }
+
+ RefPtr<IDispatch> disp;
+ disp = MsaaAccessible::GetFrom(aAccessible);
+ IDispatch* rawDisp;
+ disp.forget(&rawDisp);
+ return rawDisp;
+}
+
+ITypeInfo* MsaaAccessible::GetTI(LCID lcid) {
+ if (gTypeInfo) return gTypeInfo;
+
+ ITypeLib* typeLib = nullptr;
+ HRESULT hr = LoadRegTypeLib(LIBID_Accessibility, 1, 0, lcid, &typeLib);
+ if (FAILED(hr)) return nullptr;
+
+ hr = typeLib->GetTypeInfoOfGuid(IID_IAccessible, &gTypeInfo);
+ typeLib->Release();
+
+ if (FAILED(hr)) return nullptr;
+
+ return gTypeInfo;
+}
+
+/* static */
+MsaaAccessible* MsaaAccessible::GetFrom(Accessible* aAcc) {
+ if (!aAcc) {
+ return nullptr;
+ }
+
+ if (RemoteAccessible* remoteAcc = aAcc->AsRemote()) {
+ return reinterpret_cast<MsaaAccessible*>(remoteAcc->GetWrapper());
+ }
+ return static_cast<AccessibleWrap*>(aAcc)->GetMsaa();
+}
+
+/* static */
+Accessible* MsaaAccessible::GetAccessibleFrom(IUnknown* aUnknown) {
+ RefPtr<MsaaAccessible> msaa;
+ aUnknown->QueryInterface(IID_MsaaAccessible, getter_AddRefs(msaa));
+ if (!msaa) {
+ return nullptr;
+ }
+ return msaa->Acc();
+}
+
+// IUnknown methods
+STDMETHODIMP
+MsaaAccessible::QueryInterface(REFIID iid, void** ppv) {
+ if (!ppv) return E_INVALIDARG;
+
+ *ppv = nullptr;
+
+ if (IID_IClientSecurity == iid) {
+ // Some code might QI(IID_IClientSecurity) to detect whether or not we are
+ // a proxy. Right now that can potentially happen off the main thread, so we
+ // look for this condition immediately so that we don't trigger other code
+ // that might not be thread-safe.
+ return E_NOINTERFACE;
+ }
+
+ // These interfaces are always available. We can support querying to them
+ // even if the Accessible is dead.
+ if (IID_IUnknown == iid) {
+ *ppv = static_cast<IAccessible*>(this);
+ } else if (IID_MsaaAccessible == iid) {
+ *ppv = static_cast<MsaaAccessible*>(this);
+ } else if (IID_IDispatch == iid || IID_IAccessible == iid) {
+ *ppv = static_cast<IAccessible*>(this);
+ } else if (IID_IServiceProvider == iid) {
+ *ppv = new ServiceProvider(this);
+ } else {
+ HRESULT hr = ia2Accessible::QueryInterface(iid, ppv);
+ if (SUCCEEDED(hr)) {
+ return hr;
+ }
+ }
+ if (*ppv) {
+ (reinterpret_cast<IUnknown*>(*ppv))->AddRef();
+ return S_OK;
+ }
+
+ // For interfaces below this point, we have to query the Accessible to
+ // determine if they are available.
+ if (!mAcc) {
+ // mscom::Interceptor (and maybe other callers) expects either S_OK or
+ // E_NOINTERFACE, so don't return CO_E_OBJNOTCONNECTED like we normally
+ // would for a dead object.
+ return E_NOINTERFACE;
+ }
+ AccessibleWrap* localAcc = LocalAcc();
+ if (IID_IEnumVARIANT == iid) {
+ // We don't support this interface for leaf elements.
+ if (!mAcc->HasChildren() || nsAccUtils::MustPrune(mAcc)) {
+ return E_NOINTERFACE;
+ }
+ *ppv = static_cast<IEnumVARIANT*>(new ChildrenEnumVariant(this));
+ } else if (IID_ISimpleDOMNode == iid) {
+ if (mAcc->IsDoc() || (localAcc && !localAcc->HasOwnContent())) {
+ return E_NOINTERFACE;
+ }
+
+ *ppv = static_cast<ISimpleDOMNode*>(new sdnAccessible(WrapNotNull(this)));
+ } else if (iid == IID_ISimpleDOMText && localAcc && localAcc->IsTextLeaf()) {
+ statistics::ISimpleDOMUsed();
+ *ppv = static_cast<ISimpleDOMText*>(new sdnTextAccessible(this));
+ static_cast<IUnknown*>(*ppv)->AddRef();
+ return S_OK;
+ }
+
+ if (!*ppv && localAcc) {
+ HRESULT hr = ia2AccessibleComponent::QueryInterface(iid, ppv);
+ if (SUCCEEDED(hr)) return hr;
+ }
+
+ if (!*ppv) {
+ HRESULT hr = ia2AccessibleHyperlink::QueryInterface(iid, ppv);
+ if (SUCCEEDED(hr)) return hr;
+ }
+
+ if (!*ppv) {
+ HRESULT hr = ia2AccessibleValue::QueryInterface(iid, ppv);
+ if (SUCCEEDED(hr)) return hr;
+ }
+
+ if (nullptr == *ppv) return E_NOINTERFACE;
+
+ (reinterpret_cast<IUnknown*>(*ppv))->AddRef();
+ return S_OK;
+}
+
+// IAccessible methods
+
+STDMETHODIMP
+MsaaAccessible::get_accParent(IDispatch __RPC_FAR* __RPC_FAR* ppdispParent) {
+ if (!ppdispParent) return E_INVALIDARG;
+
+ *ppdispParent = nullptr;
+
+ if (!mAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ Accessible* xpParentAcc = mAcc->Parent();
+ if (!xpParentAcc) return S_FALSE;
+
+ *ppdispParent = NativeAccessible(xpParentAcc);
+ return S_OK;
+}
+
+STDMETHODIMP
+MsaaAccessible::get_accChildCount(long __RPC_FAR* pcountChildren) {
+ if (!pcountChildren) return E_INVALIDARG;
+
+ *pcountChildren = 0;
+
+ if (!mAcc) return CO_E_OBJNOTCONNECTED;
+
+ if (Compatibility::IsA11ySuppressedForClipboardCopy() && mAcc->IsRoot()) {
+ // Bug 1798098: Windows Suggested Actions (introduced in Windows 11 22H2)
+ // might walk the entire a11y tree using UIA whenever anything is copied to
+ // the clipboard. This causes an unacceptable hang, particularly when the
+ // cache is disabled. We prevent this tree walk by returning a 0 child count
+ // for the root window, from which Windows might walk.
+ return S_OK;
+ }
+
+ if (nsAccUtils::MustPrune(mAcc)) return S_OK;
+
+ *pcountChildren = mAcc->ChildCount();
+ return S_OK;
+}
+
+STDMETHODIMP
+MsaaAccessible::get_accChild(
+ /* [in] */ VARIANT varChild,
+ /* [retval][out] */ IDispatch __RPC_FAR* __RPC_FAR* ppdispChild) {
+ if (!ppdispChild) return E_INVALIDARG;
+
+ *ppdispChild = nullptr;
+ if (!mAcc) return CO_E_OBJNOTCONNECTED;
+
+ // IAccessible::accChild is used to return this accessible or child accessible
+ // at the given index or to get an accessible by child ID in the case of
+ // document accessible.
+ // The getting an accessible by child ID is used by
+ // AccessibleObjectFromEvent() called by AT when AT handles our MSAA event.
+ bool isDefunct = false;
+ RefPtr<IAccessible> child = GetIAccessibleFor(varChild, &isDefunct);
+ if (!child) {
+ return E_INVALIDARG;
+ }
+
+ if (isDefunct) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ child.forget(ppdispChild);
+ return S_OK;
+}
+
+STDMETHODIMP
+MsaaAccessible::get_accName(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszName) {
+ if (!pszName || varChild.vt != VT_I4) return E_INVALIDARG;
+
+ *pszName = nullptr;
+
+ RefPtr<IAccessible> accessible;
+ HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ if (accessible) {
+ return accessible->get_accName(kVarChildIdSelf, pszName);
+ }
+
+ nsAutoString name;
+ Acc()->Name(name);
+
+ if (name.IsVoid()) return S_FALSE;
+
+ *pszName = ::SysAllocStringLen(name.get(), name.Length());
+ if (!*pszName) return E_OUTOFMEMORY;
+ return S_OK;
+}
+
+STDMETHODIMP
+MsaaAccessible::get_accValue(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszValue) {
+ if (!pszValue) return E_INVALIDARG;
+
+ *pszValue = nullptr;
+
+ RefPtr<IAccessible> accessible;
+ HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ if (accessible) {
+ return accessible->get_accValue(kVarChildIdSelf, pszValue);
+ }
+
+ nsAutoString value;
+ Acc()->Value(value);
+
+ // See bug 438784: need to expose URL on doc's value attribute. For this,
+ // reverting part of fix for bug 425693 to make this MSAA method behave
+ // IAccessible2-style.
+ if (value.IsEmpty()) return S_FALSE;
+
+ *pszValue = ::SysAllocStringLen(value.get(), value.Length());
+ if (!*pszValue) return E_OUTOFMEMORY;
+ return S_OK;
+}
+
+STDMETHODIMP
+MsaaAccessible::get_accDescription(VARIANT varChild,
+ BSTR __RPC_FAR* pszDescription) {
+ if (!pszDescription) return E_INVALIDARG;
+
+ *pszDescription = nullptr;
+
+ RefPtr<IAccessible> accessible;
+ HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ if (accessible) {
+ return accessible->get_accDescription(kVarChildIdSelf, pszDescription);
+ }
+
+ nsAutoString description;
+ Acc()->Description(description);
+
+ *pszDescription =
+ ::SysAllocStringLen(description.get(), description.Length());
+ return *pszDescription ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+MsaaAccessible::get_accRole(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ VARIANT __RPC_FAR* pvarRole) {
+ if (!pvarRole) return E_INVALIDARG;
+
+ VariantInit(pvarRole);
+
+ RefPtr<IAccessible> accessible;
+ HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ if (accessible) {
+ return accessible->get_accRole(kVarChildIdSelf, pvarRole);
+ }
+
+ a11y::role geckoRole;
+#ifdef DEBUG
+ if (mAcc->IsLocal()) {
+ NS_ASSERTION(nsAccUtils::IsTextInterfaceSupportCorrect(mAcc->AsLocal()),
+ "Does not support Text when it should");
+ }
+#endif
+ geckoRole = mAcc->Role();
+
+ uint32_t msaaRole = 0;
+
+#define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \
+ _msaaRole, ia2Role, androidClass, nameRule) \
+ case roles::_geckoRole: \
+ msaaRole = _msaaRole; \
+ break;
+
+ switch (geckoRole) {
+#include "RoleMap.h"
+ default:
+ MOZ_CRASH("Unknown role.");
+ }
+
+#undef ROLE
+
+ // Special case, if there is a ROLE_ROW inside of a ROLE_TREE_TABLE, then call
+ // the MSAA role a ROLE_OUTLINEITEM for consistency and compatibility. We need
+ // this because ARIA has a role of "row" for both grid and treegrid
+ if (geckoRole == roles::ROW) {
+ Accessible* xpParent = mAcc->Parent();
+ if (xpParent && xpParent->Role() == roles::TREE_TABLE)
+ msaaRole = ROLE_SYSTEM_OUTLINEITEM;
+ }
+
+ pvarRole->vt = VT_I4;
+ pvarRole->lVal = msaaRole;
+ return S_OK;
+}
+
+STDMETHODIMP
+MsaaAccessible::get_accState(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ VARIANT __RPC_FAR* pvarState) {
+ if (!pvarState) return E_INVALIDARG;
+
+ VariantInit(pvarState);
+ pvarState->vt = VT_I4;
+ pvarState->lVal = 0;
+
+ RefPtr<IAccessible> accessible;
+ HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ if (accessible) {
+ return accessible->get_accState(kVarChildIdSelf, pvarState);
+ }
+
+ // MSAA only has 31 states and the lowest 31 bits of our state bit mask
+ // are the same states as MSAA.
+ // Note: we map the following Gecko states to different MSAA states:
+ // REQUIRED -> ALERT_LOW
+ // ALERT -> ALERT_MEDIUM
+ // INVALID -> ALERT_HIGH
+ // CHECKABLE -> MARQUEED
+
+ uint64_t state = Acc()->State();
+
+ uint32_t msaaState = 0;
+ nsAccUtils::To32States(state, &msaaState, nullptr);
+ pvarState->lVal = msaaState;
+ return S_OK;
+}
+
+STDMETHODIMP
+MsaaAccessible::get_accHelp(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszHelp) {
+ if (!pszHelp) return E_INVALIDARG;
+
+ *pszHelp = nullptr;
+ return S_FALSE;
+}
+
+STDMETHODIMP
+MsaaAccessible::get_accHelpTopic(
+ /* [out] */ BSTR __RPC_FAR* pszHelpFile,
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ long __RPC_FAR* pidTopic) {
+ if (!pszHelpFile || !pidTopic) return E_INVALIDARG;
+
+ *pszHelpFile = nullptr;
+ *pidTopic = 0;
+ return S_FALSE;
+}
+
+STDMETHODIMP
+MsaaAccessible::get_accKeyboardShortcut(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszKeyboardShortcut) {
+ if (!pszKeyboardShortcut) return E_INVALIDARG;
+ *pszKeyboardShortcut = nullptr;
+
+ RefPtr<IAccessible> accessible;
+ HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ if (accessible) {
+ return accessible->get_accKeyboardShortcut(kVarChildIdSelf,
+ pszKeyboardShortcut);
+ }
+
+ KeyBinding keyBinding = mAcc->AccessKey();
+ if (keyBinding.IsEmpty()) {
+ if (LocalAccessible* localAcc = mAcc->AsLocal()) {
+ keyBinding = localAcc->KeyboardShortcut();
+ }
+ }
+
+ nsAutoString shortcut;
+ keyBinding.ToString(shortcut);
+
+ *pszKeyboardShortcut = ::SysAllocStringLen(shortcut.get(), shortcut.Length());
+ return *pszKeyboardShortcut ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+MsaaAccessible::get_accFocus(
+ /* [retval][out] */ VARIANT __RPC_FAR* pvarChild) {
+ if (!pvarChild) return E_INVALIDARG;
+
+ VariantInit(pvarChild);
+
+ // clang-format off
+ // VT_EMPTY: None. This object does not have the keyboard focus itself
+ // and does not contain a child that has the keyboard focus.
+ // VT_I4: lVal is CHILDID_SELF. The object itself has the keyboard focus.
+ // VT_I4: lVal contains the child ID of the child element with the keyboard focus.
+ // VT_DISPATCH: pdispVal member is the address of the IDispatch interface
+ // for the child object with the keyboard focus.
+ // clang-format on
+ if (!mAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+ // Return the current IAccessible child that has focus
+ Accessible* focusedAccessible = mAcc->FocusedChild();
+ if (focusedAccessible == mAcc) {
+ pvarChild->vt = VT_I4;
+ pvarChild->lVal = CHILDID_SELF;
+ } else if (focusedAccessible) {
+ pvarChild->vt = VT_DISPATCH;
+ pvarChild->pdispVal = NativeAccessible(focusedAccessible);
+ } else {
+ pvarChild->vt = VT_EMPTY; // No focus or focus is not a child
+ }
+
+ return S_OK;
+}
+
+/**
+ * This helper class implements IEnumVARIANT for a nsTArray containing
+ * accessible objects.
+ */
+class AccessibleEnumerator final : public IEnumVARIANT {
+ public:
+ explicit AccessibleEnumerator(const nsTArray<Accessible*>& aArray)
+ : mArray(aArray.Clone()), mCurIndex(0) {}
+ AccessibleEnumerator(const AccessibleEnumerator& toCopy)
+ : mArray(toCopy.mArray.Clone()), mCurIndex(toCopy.mCurIndex) {}
+ ~AccessibleEnumerator() {}
+
+ // IUnknown
+ DECL_IUNKNOWN
+
+ // IEnumVARIANT
+ STDMETHODIMP Next(unsigned long celt, VARIANT FAR* rgvar,
+ unsigned long FAR* pceltFetched);
+ STDMETHODIMP Skip(unsigned long celt);
+ STDMETHODIMP Reset() {
+ mCurIndex = 0;
+ return S_OK;
+ }
+ STDMETHODIMP Clone(IEnumVARIANT FAR* FAR* ppenum);
+
+ private:
+ nsTArray<Accessible*> mArray;
+ uint32_t mCurIndex;
+};
+
+STDMETHODIMP
+AccessibleEnumerator::QueryInterface(REFIID iid, void** ppvObject) {
+ if (iid == IID_IEnumVARIANT) {
+ *ppvObject = static_cast<IEnumVARIANT*>(this);
+ AddRef();
+ return S_OK;
+ }
+ if (iid == IID_IUnknown) {
+ *ppvObject = static_cast<IUnknown*>(this);
+ AddRef();
+ return S_OK;
+ }
+
+ *ppvObject = nullptr;
+ return E_NOINTERFACE;
+}
+
+STDMETHODIMP
+AccessibleEnumerator::Next(unsigned long celt, VARIANT FAR* rgvar,
+ unsigned long FAR* pceltFetched) {
+ uint32_t length = mArray.Length();
+ HRESULT hr = S_OK;
+
+ // Can't get more elements than there are...
+ if (celt > length - mCurIndex) {
+ hr = S_FALSE;
+ celt = length - mCurIndex;
+ }
+
+ // Copy the elements of the array into rgvar.
+ for (uint32_t i = 0; i < celt; ++i, ++mCurIndex) {
+ rgvar[i].vt = VT_DISPATCH;
+ rgvar[i].pdispVal = MsaaAccessible::NativeAccessible(mArray[mCurIndex]);
+ }
+
+ if (pceltFetched) *pceltFetched = celt;
+
+ return hr;
+}
+
+STDMETHODIMP
+AccessibleEnumerator::Clone(IEnumVARIANT FAR* FAR* ppenum) {
+ *ppenum = new AccessibleEnumerator(*this);
+ NS_ADDREF(*ppenum);
+ return S_OK;
+}
+
+STDMETHODIMP
+AccessibleEnumerator::Skip(unsigned long celt) {
+ uint32_t length = mArray.Length();
+ // Check if we can skip the requested number of elements
+ if (celt > length - mCurIndex) {
+ mCurIndex = length;
+ return S_FALSE;
+ }
+ mCurIndex += celt;
+ return S_OK;
+}
+
+/**
+ * This method is called when a client wants to know which children of a node
+ * are selected. Note that this method can only find selected children for
+ * accessible object which implement SelectAccessible.
+ *
+ * The VARIANT return value arguement is expected to either contain a single
+ * IAccessible or an IEnumVARIANT of IAccessibles. We return the IEnumVARIANT
+ * regardless of the number of children selected, unless there are none selected
+ * in which case we return an empty VARIANT.
+ *
+ * We get the selected options from the select's accessible object and wrap
+ * those in an AccessibleEnumerator which we then put in the return VARIANT.
+ *
+ * returns a VT_EMPTY VARIANT if:
+ * - there are no selected children for this object
+ * - the object is not the type that can have children selected
+ */
+STDMETHODIMP
+MsaaAccessible::get_accSelection(VARIANT __RPC_FAR* pvarChildren) {
+ if (!pvarChildren) return E_INVALIDARG;
+
+ VariantInit(pvarChildren);
+ pvarChildren->vt = VT_EMPTY;
+
+ if (!mAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+ Accessible* acc = Acc();
+
+ if (!acc->IsSelect()) {
+ return S_OK;
+ }
+
+ AutoTArray<Accessible*, 10> selectedItems;
+ acc->SelectedItems(&selectedItems);
+ uint32_t count = selectedItems.Length();
+ if (count == 1) {
+ pvarChildren->vt = VT_DISPATCH;
+ pvarChildren->pdispVal = NativeAccessible(selectedItems[0]);
+ } else if (count > 1) {
+ RefPtr<AccessibleEnumerator> pEnum =
+ new AccessibleEnumerator(selectedItems);
+ pvarChildren->vt =
+ VT_UNKNOWN; // this must be VT_UNKNOWN for an IEnumVARIANT
+ NS_ADDREF(pvarChildren->punkVal = pEnum);
+ }
+ // If count == 0, vt is already VT_EMPTY, so there's nothing else to do.
+
+ return S_OK;
+}
+
+STDMETHODIMP
+MsaaAccessible::get_accDefaultAction(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszDefaultAction) {
+ if (!pszDefaultAction) return E_INVALIDARG;
+
+ *pszDefaultAction = nullptr;
+
+ RefPtr<IAccessible> accessible;
+ HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ if (accessible) {
+ return accessible->get_accDefaultAction(kVarChildIdSelf, pszDefaultAction);
+ }
+
+ nsAutoString defaultAction;
+ mAcc->ActionNameAt(0, defaultAction);
+
+ *pszDefaultAction =
+ ::SysAllocStringLen(defaultAction.get(), defaultAction.Length());
+ return *pszDefaultAction ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+MsaaAccessible::accSelect(
+ /* [in] */ long flagsSelect,
+ /* [optional][in] */ VARIANT varChild) {
+ RefPtr<IAccessible> accessible;
+ HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ if (accessible) {
+ return accessible->accSelect(flagsSelect, kVarChildIdSelf);
+ }
+
+ if (flagsSelect & SELFLAG_TAKEFOCUS) {
+ mAcc->TakeFocus();
+ return S_OK;
+ }
+
+ if (flagsSelect & SELFLAG_TAKESELECTION) {
+ mAcc->TakeSelection();
+ return S_OK;
+ }
+
+ if (flagsSelect & SELFLAG_ADDSELECTION) {
+ mAcc->SetSelected(true);
+ return S_OK;
+ }
+
+ if (flagsSelect & SELFLAG_REMOVESELECTION) {
+ mAcc->SetSelected(false);
+ return S_OK;
+ }
+
+ return E_FAIL;
+}
+
+STDMETHODIMP
+MsaaAccessible::accLocation(
+ /* [out] */ long __RPC_FAR* pxLeft,
+ /* [out] */ long __RPC_FAR* pyTop,
+ /* [out] */ long __RPC_FAR* pcxWidth,
+ /* [out] */ long __RPC_FAR* pcyHeight,
+ /* [optional][in] */ VARIANT varChild) {
+ if (!pxLeft || !pyTop || !pcxWidth || !pcyHeight) return E_INVALIDARG;
+
+ *pxLeft = 0;
+ *pyTop = 0;
+ *pcxWidth = 0;
+ *pcyHeight = 0;
+
+ RefPtr<IAccessible> accessible;
+ HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ if (accessible) {
+ return accessible->accLocation(pxLeft, pyTop, pcxWidth, pcyHeight,
+ kVarChildIdSelf);
+ }
+
+ LayoutDeviceIntRect rect = Acc()->Bounds();
+ *pxLeft = rect.X();
+ *pyTop = rect.Y();
+ *pcxWidth = rect.Width();
+ *pcyHeight = rect.Height();
+ return S_OK;
+}
+
+STDMETHODIMP
+MsaaAccessible::accNavigate(
+ /* [in] */ long navDir,
+ /* [optional][in] */ VARIANT varStart,
+ /* [retval][out] */ VARIANT __RPC_FAR* pvarEndUpAt) {
+ if (!pvarEndUpAt) return E_INVALIDARG;
+
+ VariantInit(pvarEndUpAt);
+
+ RefPtr<IAccessible> accessible;
+ HRESULT hr = ResolveChild(varStart, getter_AddRefs(accessible));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ if (accessible) {
+ return accessible->accNavigate(navDir, kVarChildIdSelf, pvarEndUpAt);
+ }
+
+ Accessible* navAccessible = nullptr;
+ Maybe<RelationType> xpRelation;
+
+#define RELATIONTYPE(geckoType, stringType, atkType, msaaType, ia2Type) \
+ case msaaType: \
+ xpRelation.emplace(RelationType::geckoType); \
+ break;
+
+ switch (navDir) {
+ case NAVDIR_FIRSTCHILD:
+ if (!nsAccUtils::MustPrune(mAcc)) {
+ navAccessible = mAcc->FirstChild();
+ }
+ break;
+ case NAVDIR_LASTCHILD:
+ if (!nsAccUtils::MustPrune(mAcc)) {
+ navAccessible = mAcc->LastChild();
+ }
+ break;
+ case NAVDIR_NEXT:
+ navAccessible = mAcc->NextSibling();
+ break;
+ case NAVDIR_PREVIOUS:
+ navAccessible = mAcc->PrevSibling();
+ break;
+ case NAVDIR_DOWN:
+ case NAVDIR_LEFT:
+ case NAVDIR_RIGHT:
+ case NAVDIR_UP:
+ return E_NOTIMPL;
+
+ // MSAA relationship extensions to accNavigate
+#include "RelationTypeMap.h"
+
+ default:
+ return E_INVALIDARG;
+ }
+
+#undef RELATIONTYPE
+
+ pvarEndUpAt->vt = VT_EMPTY;
+
+ if (xpRelation) {
+ Relation rel = mAcc->RelationByType(*xpRelation);
+ navAccessible = rel.Next();
+ }
+
+ if (!navAccessible) return E_FAIL;
+
+ pvarEndUpAt->pdispVal = NativeAccessible(navAccessible);
+ pvarEndUpAt->vt = VT_DISPATCH;
+ return S_OK;
+}
+
+STDMETHODIMP
+MsaaAccessible::accHitTest(
+ /* [in] */ long xLeft,
+ /* [in] */ long yTop,
+ /* [retval][out] */ VARIANT __RPC_FAR* pvarChild) {
+ if (!pvarChild) return E_INVALIDARG;
+
+ VariantInit(pvarChild);
+
+ if (!mAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ // The MSAA documentation says accHitTest should return a child. However,
+ // clients call AccessibleObjectFromPoint, which ends up walking the
+ // descendants calling accHitTest on each one. Since clients want the
+ // deepest descendant anyway, it's faster and probably more accurate to
+ // just do this ourselves.
+ Accessible* accessible = mAcc->ChildAtPoint(
+ xLeft, yTop, Accessible::EWhichChildAtPoint::DeepestChild);
+
+ // if we got a child
+ if (accessible) {
+ if (accessible != mAcc && accessible->IsTextLeaf()) {
+ Accessible* parent = accessible->Parent();
+ if (parent != mAcc && parent->Role() == roles::LINK) {
+ // Bug 1843832: The UI Automation -> IAccessible2 proxy barfs if we
+ // return the text leaf child of a link when hit testing an ancestor of
+ // the link. Therefore, we return the link instead. MSAA clients which
+ // call AccessibleObjectFromPoint will still get to the text leaf, since
+ // AccessibleObjectFromPoint keeps calling accHitTest until it can't
+ // descend any further. We should remove this tragic hack once we have
+ // a native UIA implementation.
+ accessible = parent;
+ }
+ }
+ if (accessible == mAcc) {
+ pvarChild->vt = VT_I4;
+ pvarChild->lVal = CHILDID_SELF;
+ } else {
+ pvarChild->vt = VT_DISPATCH;
+ pvarChild->pdispVal = NativeAccessible(accessible);
+ }
+ } else {
+ // no child at that point
+ pvarChild->vt = VT_EMPTY;
+ return S_FALSE;
+ }
+ return S_OK;
+}
+
+STDMETHODIMP
+MsaaAccessible::accDoDefaultAction(
+ /* [optional][in] */ VARIANT varChild) {
+ RefPtr<IAccessible> accessible;
+ HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ if (accessible) {
+ return accessible->accDoDefaultAction(kVarChildIdSelf);
+ }
+
+ return mAcc->DoAction(0) ? S_OK : E_INVALIDARG;
+}
+
+STDMETHODIMP
+MsaaAccessible::put_accName(
+ /* [optional][in] */ VARIANT varChild,
+ /* [in] */ BSTR szName) {
+ return E_NOTIMPL;
+}
+
+STDMETHODIMP
+MsaaAccessible::put_accValue(
+ /* [optional][in] */ VARIANT varChild,
+ /* [in] */ BSTR szValue) {
+ RefPtr<IAccessible> accessible;
+ HRESULT hr = ResolveChild(varChild, getter_AddRefs(accessible));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ if (accessible) {
+ return accessible->put_accValue(kVarChildIdSelf, szValue);
+ }
+
+ HyperTextAccessibleBase* ht = mAcc->AsHyperTextBase();
+ if (!ht) {
+ return E_NOTIMPL;
+ }
+
+ uint32_t length = ::SysStringLen(szValue);
+ nsAutoString text(szValue, length);
+ ht->ReplaceText(text);
+ return S_OK;
+}
+
+// IDispatch methods
+
+STDMETHODIMP
+MsaaAccessible::GetTypeInfoCount(UINT* pctinfo) {
+ if (!pctinfo) return E_INVALIDARG;
+
+ *pctinfo = 1;
+ return S_OK;
+}
+
+STDMETHODIMP
+MsaaAccessible::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo) {
+ if (!ppTInfo) return E_INVALIDARG;
+
+ *ppTInfo = nullptr;
+
+ if (iTInfo != 0) return DISP_E_BADINDEX;
+
+ ITypeInfo* typeInfo = GetTI(lcid);
+ if (!typeInfo) return E_FAIL;
+
+ typeInfo->AddRef();
+ *ppTInfo = typeInfo;
+
+ return S_OK;
+}
+
+STDMETHODIMP
+MsaaAccessible::GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cNames,
+ LCID lcid, DISPID* rgDispId) {
+ ITypeInfo* typeInfo = GetTI(lcid);
+ if (!typeInfo) return E_FAIL;
+
+ HRESULT hr = DispGetIDsOfNames(typeInfo, rgszNames, cNames, rgDispId);
+ return hr;
+}
+
+STDMETHODIMP
+MsaaAccessible::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags,
+ DISPPARAMS* pDispParams, VARIANT* pVarResult,
+ EXCEPINFO* pExcepInfo, UINT* puArgErr) {
+ ITypeInfo* typeInfo = GetTI(lcid);
+ if (!typeInfo) return E_FAIL;
+
+ return typeInfo->Invoke(static_cast<IAccessible*>(this), dispIdMember, wFlags,
+ pDispParams, pVarResult, pExcepInfo, puArgErr);
+}
diff --git a/accessible/windows/msaa/MsaaAccessible.h b/accessible/windows/msaa/MsaaAccessible.h
new file mode 100644
index 0000000000..034db860ed
--- /dev/null
+++ b/accessible/windows/msaa/MsaaAccessible.h
@@ -0,0 +1,204 @@
+/* -*- 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_MsaaAccessible_h_
+#define mozilla_a11y_MsaaAccessible_h_
+
+#include "ia2Accessible.h"
+#include "ia2AccessibleComponent.h"
+#include "ia2AccessibleHyperlink.h"
+#include "ia2AccessibleValue.h"
+#include "IUnknownImpl.h"
+#include "mozilla/a11y/MsaaIdGenerator.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+namespace a11y {
+class Accessible;
+class AccessibleWrap;
+class LocalAccessible;
+class sdnAccessible;
+
+class MsaaAccessible : public ia2Accessible,
+ public ia2AccessibleComponent,
+ public ia2AccessibleHyperlink,
+ public ia2AccessibleValue {
+ public:
+ static MsaaAccessible* Create(Accessible* aAcc);
+
+ Accessible* Acc() { return mAcc; }
+ AccessibleWrap* LocalAcc();
+
+ uint32_t GetExistingID() const { return mID; }
+ static const uint32_t kNoID = 0;
+
+ static int32_t GetChildIDFor(Accessible* aAccessible);
+ static void AssignChildIDTo(NotNull<sdnAccessible*> aSdnAcc);
+ static void ReleaseChildID(NotNull<sdnAccessible*> aSdnAcc);
+ static HWND GetHWNDFor(Accessible* aAccessible);
+ static void FireWinEvent(Accessible* aTarget, uint32_t aEventType);
+
+ /**
+ * Find an accessible by the given child ID in cached documents.
+ */
+ [[nodiscard]] already_AddRefed<IAccessible> GetIAccessibleFor(
+ const VARIANT& aVarChild, bool* aIsDefunct);
+
+ void MsaaShutdown();
+
+ static IDispatch* NativeAccessible(Accessible* aAccessible);
+
+ static MsaaAccessible* GetFrom(Accessible* aAcc);
+
+ /**
+ * Creates ITypeInfo for LIBID_Accessibility if it's needed and returns it.
+ */
+ static ITypeInfo* GetTI(LCID lcid);
+
+ static Accessible* GetAccessibleFrom(IUnknown* aUnknown);
+
+ DECL_IUNKNOWN
+
+ // IAccessible
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accParent(
+ /* [retval][out] */ IDispatch __RPC_FAR* __RPC_FAR* ppdispParent)
+ override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accChildCount(
+ /* [retval][out] */ long __RPC_FAR* pcountChildren) override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accChild(
+ /* [in] */ VARIANT varChild,
+ /* [retval][out] */ IDispatch __RPC_FAR* __RPC_FAR* ppdispChild) override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accName(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszName) override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accValue(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszValue) override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accDescription(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszDescription) override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accRole(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ VARIANT __RPC_FAR* pvarRole) override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accState(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ VARIANT __RPC_FAR* pvarState) override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accHelp(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszHelp) override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accHelpTopic(
+ /* [out] */ BSTR __RPC_FAR* pszHelpFile,
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ long __RPC_FAR* pidTopic) override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accKeyboardShortcut(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszKeyboardShortcut) override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accFocus(
+ /* [retval][out] */ VARIANT __RPC_FAR* pvarChild) override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accSelection(
+ /* [retval][out] */ VARIANT __RPC_FAR* pvarChildren) override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accDefaultAction(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszDefaultAction) override;
+ virtual /* [id] */ HRESULT STDMETHODCALLTYPE accSelect(
+ /* [in] */ long flagsSelect,
+ /* [optional][in] */ VARIANT varChild) override;
+ virtual /* [id] */ HRESULT STDMETHODCALLTYPE accLocation(
+ /* [out] */ long __RPC_FAR* pxLeft,
+ /* [out] */ long __RPC_FAR* pyTop,
+ /* [out] */ long __RPC_FAR* pcxWidth,
+ /* [out] */ long __RPC_FAR* pcyHeight,
+ /* [optional][in] */ VARIANT varChild) override;
+ virtual /* [id] */ HRESULT STDMETHODCALLTYPE accNavigate(
+ /* [in] */ long navDir,
+ /* [optional][in] */ VARIANT varStart,
+ /* [retval][out] */ VARIANT __RPC_FAR* pvarEndUpAt) override;
+ virtual /* [id] */ HRESULT STDMETHODCALLTYPE accHitTest(
+ /* [in] */ long xLeft,
+ /* [in] */ long yTop,
+ /* [retval][out] */ VARIANT __RPC_FAR* pvarChild) override;
+ virtual /* [id] */ HRESULT STDMETHODCALLTYPE accDoDefaultAction(
+ /* [optional][in] */ VARIANT varChild) override;
+ virtual /* [id][propput] */ HRESULT STDMETHODCALLTYPE put_accName(
+ /* [optional][in] */ VARIANT varChild,
+ /* [in] */ BSTR szName) override;
+ virtual /* [id][propput] */ HRESULT STDMETHODCALLTYPE put_accValue(
+ /* [optional][in] */ VARIANT varChild,
+ /* [in] */ BSTR szValue) override;
+
+ // IDispatch (support of scripting languages like VB)
+ virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT* pctinfo) override;
+ virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT iTInfo, LCID lcid,
+ ITypeInfo** ppTInfo) override;
+ virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(REFIID riid,
+ LPOLESTR* rgszNames,
+ UINT cNames, LCID lcid,
+ DISPID* rgDispId) override;
+ virtual HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember, REFIID riid,
+ LCID lcid, WORD wFlags,
+ DISPPARAMS* pDispParams,
+ VARIANT* pVarResult,
+ EXCEPINFO* pExcepInfo,
+ UINT* puArgErr) override;
+
+ protected:
+ explicit MsaaAccessible(Accessible* aAcc);
+ virtual ~MsaaAccessible();
+
+ Accessible* mAcc;
+
+ uint32_t mID;
+ static MsaaIdGenerator sIDGen;
+
+ HRESULT
+ ResolveChild(const VARIANT& aVarChild, IAccessible** aOutInterface);
+
+ enum navRelations {
+ NAVRELATION_CONTROLLED_BY = 0x1000,
+ NAVRELATION_CONTROLLER_FOR = 0x1001,
+ NAVRELATION_LABEL_FOR = 0x1002,
+ NAVRELATION_LABELLED_BY = 0x1003,
+ NAVRELATION_MEMBER_OF = 0x1004,
+ NAVRELATION_NODE_CHILD_OF = 0x1005,
+ NAVRELATION_FLOWS_TO = 0x1006,
+ NAVRELATION_FLOWS_FROM = 0x1007,
+ NAVRELATION_SUBWINDOW_OF = 0x1008,
+ NAVRELATION_EMBEDS = 0x1009,
+ NAVRELATION_EMBEDDED_BY = 0x100a,
+ NAVRELATION_POPUP_FOR = 0x100b,
+ NAVRELATION_PARENT_WINDOW_OF = 0x100c,
+ NAVRELATION_DEFAULT_BUTTON = 0x100d,
+ NAVRELATION_DESCRIBED_BY = 0x100e,
+ NAVRELATION_DESCRIPTION_FOR = 0x100f,
+ NAVRELATION_NODE_PARENT_OF = 0x1010,
+ NAVRELATION_CONTAINING_DOCUMENT = 0x1011,
+ NAVRELATION_CONTAINING_TAB_PANE = 0x1012,
+ NAVRELATION_CONTAINING_WINDOW = 0x1013,
+ NAVRELATION_CONTAINING_APPLICATION = 0x1014,
+ NAVRELATION_DETAILS = 0x1015,
+ NAVRELATION_DETAILS_FOR = 0x1016,
+ NAVRELATION_ERROR = 0x1017,
+ NAVRELATION_ERROR_FOR = 0x1018,
+ NAVRELATION_LINKS_TO = 0x1019
+ };
+
+ private:
+ static ITypeInfo* gTypeInfo;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#ifdef XP_WIN
+// Undo the windows.h damage
+# undef GetMessage
+# undef CreateEvent
+# undef GetClassName
+# undef GetBinaryType
+# undef RemoveDirectory
+#endif
+
+#endif
diff --git a/accessible/windows/msaa/MsaaDocAccessible.cpp b/accessible/windows/msaa/MsaaDocAccessible.cpp
new file mode 100644
index 0000000000..615f2e5d39
--- /dev/null
+++ b/accessible/windows/msaa/MsaaDocAccessible.cpp
@@ -0,0 +1,133 @@
+/* -*- 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 "MsaaDocAccessible.h"
+
+#include "MsaaDocAccessible.h"
+#include "DocAccessibleChild.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "nsAccessibilityService.h"
+#include "nsAccUtils.h"
+#include "nsWinUtils.h"
+#include "Statistics.h"
+#include "sdnDocAccessible.h"
+#include "mozilla/a11y/Role.h"
+#include "ISimpleDOM.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+DocAccessible* MsaaDocAccessible::DocAcc() {
+ return static_cast<DocAccessible*>(LocalAcc());
+}
+
+/* static */
+MsaaDocAccessible* MsaaDocAccessible::GetFrom(DocAccessible* aDoc) {
+ return static_cast<MsaaDocAccessible*>(MsaaAccessible::GetFrom(aDoc));
+}
+
+/* static */
+MsaaDocAccessible* MsaaDocAccessible::GetFrom(DocAccessibleParent* aDoc) {
+ return static_cast<MsaaDocAccessible*>(
+ reinterpret_cast<MsaaAccessible*>(aDoc->GetWrapper()));
+}
+
+/* static */
+MsaaDocAccessible* MsaaDocAccessible::GetFromOwned(Accessible* aAcc) {
+ if (RemoteAccessible* remoteAcc = aAcc->AsRemote()) {
+ DocAccessibleParent* doc = remoteAcc->Document();
+ if (!doc) {
+ return nullptr;
+ }
+ return MsaaDocAccessible::GetFrom(doc);
+ }
+ DocAccessible* doc = aAcc->AsLocal()->Document();
+ if (!doc) {
+ return nullptr;
+ }
+ return MsaaDocAccessible::GetFrom(doc);
+}
+
+// IUnknown
+IMPL_IUNKNOWN_QUERY_HEAD(MsaaDocAccessible)
+if (aIID == IID_ISimpleDOMDocument && LocalAcc()) {
+ statistics::ISimpleDOMUsed();
+ *aInstancePtr = static_cast<ISimpleDOMDocument*>(new sdnDocAccessible(this));
+ static_cast<IUnknown*>(*aInstancePtr)->AddRef();
+ return S_OK;
+}
+IMPL_IUNKNOWN_QUERY_TAIL_INHERITED(ia2AccessibleHypertext)
+
+STDMETHODIMP
+MsaaDocAccessible::get_accParent(
+ /* [retval][out] */ IDispatch __RPC_FAR* __RPC_FAR* ppdispParent) {
+ if (!mAcc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ if (mAcc->IsRemote()) {
+ DocAccessibleParent* remoteDoc = mAcc->AsRemote()->AsDoc();
+ if (nsWinUtils::IsWindowEmulationStarted() && remoteDoc->IsTopLevel()) {
+ // Window emulation is enabled and this is a top level document. Return
+ // window system accessible object.
+ HWND hwnd = remoteDoc->GetEmulatedWindowHandle();
+ MOZ_ASSERT(hwnd);
+ if (hwnd &&
+ SUCCEEDED(::CreateStdAccessibleObject(
+ hwnd, OBJID_WINDOW, IID_IAccessible, (void**)ppdispParent))) {
+ return S_OK;
+ }
+ }
+ return MsaaAccessible::get_accParent(ppdispParent);
+ }
+
+ DocAccessible* docAcc = DocAcc();
+ MOZ_ASSERT(docAcc);
+
+ // Return window system accessible object for root document accessibles, as
+ // well as tab document accessibles if window emulation is enabled.
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if ((!docAcc->ParentDocument() ||
+ (nsWinUtils::IsWindowEmulationStarted() &&
+ nsCoreUtils::IsTopLevelContentDocInProcess(docAcc->DocumentNode())))) {
+ HWND hwnd = static_cast<HWND>(docAcc->GetNativeWindow());
+ if (hwnd &&
+ SUCCEEDED(::CreateStdAccessibleObject(
+ hwnd, OBJID_WINDOW, IID_IAccessible, (void**)ppdispParent))) {
+ return S_OK;
+ }
+ }
+
+ return MsaaAccessible::get_accParent(ppdispParent);
+}
+
+STDMETHODIMP
+MsaaDocAccessible::get_accValue(VARIANT aVarChild, BSTR __RPC_FAR* aValue) {
+ if (!aValue) return E_INVALIDARG;
+ *aValue = nullptr;
+
+ // For backwards-compat, we still support old MSAA hack to provide URL in
+ // accValue Check for real value first
+ HRESULT hr = MsaaAccessible::get_accValue(aVarChild, aValue);
+ if (FAILED(hr) || *aValue || aVarChild.lVal != CHILDID_SELF) return hr;
+
+ // MsaaAccessible::get_accValue should have failed (and thus we should have
+ // returned early) if the Accessible is dead.
+ MOZ_ASSERT(mAcc);
+ // If document is being used to create a widget, don't use the URL hack
+ roles::Role role = mAcc->Role();
+ if (role != roles::DOCUMENT && role != roles::APPLICATION &&
+ role != roles::DIALOG && role != roles::ALERT &&
+ role != roles::NON_NATIVE_DOCUMENT)
+ return hr;
+
+ nsAutoString url;
+ nsAccUtils::DocumentURL(mAcc, url);
+ if (url.IsEmpty()) return S_FALSE;
+
+ *aValue = ::SysAllocStringLen(url.get(), url.Length());
+ return *aValue ? S_OK : E_OUTOFMEMORY;
+}
diff --git a/accessible/windows/msaa/MsaaDocAccessible.h b/accessible/windows/msaa/MsaaDocAccessible.h
new file mode 100644
index 0000000000..4bb36670a4
--- /dev/null
+++ b/accessible/windows/msaa/MsaaDocAccessible.h
@@ -0,0 +1,71 @@
+/* -*- 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_MsaaDocAccessible_h__
+#define mozilla_a11y_MsaaDocAccessible_h__
+
+#include "ia2AccessibleHypertext.h"
+#include "nsTHashMap.h"
+
+namespace mozilla {
+
+namespace a11y {
+class Accessible;
+class DocAccessible;
+class DocAccessibleParent;
+
+class MsaaDocAccessible : public ia2AccessibleHypertext {
+ public:
+ DocAccessible* DocAcc();
+
+ // IUnknown
+ DECL_IUNKNOWN_INHERITED
+ IMPL_IUNKNOWN_REFCOUNTING_INHERITED(ia2AccessibleHypertext)
+
+ // IAccessible
+
+ // Override get_accParent for e10s
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accParent(
+ /* [retval][out] */ IDispatch __RPC_FAR* __RPC_FAR* ppdispParent)
+ override;
+
+ // Override get_accValue to provide URL when no other value is available
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accValue(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszValue) override;
+
+ /**
+ * Manage the mapping from id to Accessible.
+ */
+ void AddID(uint32_t aID, Accessible* aAcc) {
+ mIDToAccessibleMap.InsertOrUpdate(aID, aAcc);
+ }
+ void RemoveID(uint32_t aID) { mIDToAccessibleMap.Remove(aID); }
+ Accessible* GetAccessibleByID(uint32_t aID) const {
+ return mIDToAccessibleMap.Get(aID);
+ }
+
+ static MsaaDocAccessible* GetFrom(DocAccessible* aDoc);
+ static MsaaDocAccessible* GetFrom(DocAccessibleParent* aDoc);
+
+ /**
+ * Get the MsaaDocAccessible for the document which owns the given Accessible.
+ */
+ static MsaaDocAccessible* GetFromOwned(Accessible* aAcc);
+
+ protected:
+ using ia2AccessibleHypertext::ia2AccessibleHypertext;
+
+ /*
+ * This provides a mapping from 32 bit id to accessible objects.
+ */
+ nsTHashMap<nsUint32HashKey, Accessible*> mIDToAccessibleMap;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/msaa/MsaaIdGenerator.cpp b/accessible/windows/msaa/MsaaIdGenerator.cpp
new file mode 100644
index 0000000000..9f220365e3
--- /dev/null
+++ b/accessible/windows/msaa/MsaaIdGenerator.cpp
@@ -0,0 +1,87 @@
+/* -*- 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 "MsaaIdGenerator.h"
+
+#include "mozilla/a11y/MsaaAccessible.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Unused.h"
+#include "nsAccessibilityService.h"
+#include "sdnAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+uint32_t MsaaIdGenerator::GetID() {
+ if (!mGetIDCalled) {
+ mGetIDCalled = true;
+ // this is a static instance, so capturing this here is safe.
+ RunOnShutdown([this] {
+ if (mReleaseIDTimer) {
+ mReleaseIDTimer->Cancel();
+ ReleasePendingIDs();
+ }
+ });
+ }
+ uint32_t id = mIDSet.GetID();
+ MOZ_ASSERT(id <= ((1UL << kNumFullIDBits) - 1UL));
+ return ~id;
+}
+
+void MsaaIdGenerator::ReleasePendingIDs() {
+ for (auto id : mIDsToRelease) {
+ mIDSet.ReleaseID(~id);
+ }
+ mIDsToRelease.Clear();
+ mReleaseIDTimer = nullptr;
+}
+
+bool MsaaIdGenerator::ReleaseID(uint32_t aID) {
+ MOZ_ASSERT(aID != MsaaAccessible::kNoID);
+ // Releasing an id means it can be reused. Reusing ids too quickly can
+ // cause problems for clients which process events asynchronously.
+ // Therefore, we release ids after a short delay. This doesn't seem to be
+ // necessary when the cache is disabled, perhaps because the COM runtime
+ // holds references to our objects for longer.
+ if (nsAccessibilityService::IsShutdown()) {
+ // If accessibility is shut down, no more Accessibles will be created.
+ // Also, if the service is shut down, it's possible XPCOM is also shutting
+ // down, in which case timers won't work. Thus, we release the id
+ // immediately.
+ mIDSet.ReleaseID(~aID);
+ return true;
+ }
+ const uint32_t kReleaseDelay = 1000;
+ mIDsToRelease.AppendElement(aID);
+ if (mReleaseIDTimer) {
+ mReleaseIDTimer->SetDelay(kReleaseDelay);
+ } else {
+ NS_NewTimerWithCallback(
+ getter_AddRefs(mReleaseIDTimer),
+ // mReleaseIDTimer is cancelled on shutdown and this is a static
+ // instance, so capturing this here is safe.
+ [this](nsITimer* aTimer) { ReleasePendingIDs(); }, kReleaseDelay,
+ nsITimer::TYPE_ONE_SHOT, "a11y::MsaaIdGenerator::ReleaseIDDelayed");
+ }
+ return true;
+}
+
+void MsaaIdGenerator::ReleaseID(NotNull<MsaaAccessible*> aMsaaAcc) {
+ ReleaseID(aMsaaAcc->GetExistingID());
+}
+
+void MsaaIdGenerator::ReleaseID(NotNull<sdnAccessible*> aSdnAcc) {
+ Maybe<uint32_t> id = aSdnAcc->ReleaseUniqueID();
+ if (id.isSome()) {
+ DebugOnly<bool> released = ReleaseID(id.value());
+ MOZ_ASSERT(released);
+ }
+}
+
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/windows/msaa/MsaaIdGenerator.h b/accessible/windows/msaa/MsaaIdGenerator.h
new file mode 100644
index 0000000000..2063a32f71
--- /dev/null
+++ b/accessible/windows/msaa/MsaaIdGenerator.h
@@ -0,0 +1,47 @@
+/* -*- 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_MsaaIdGenerator_h
+#define mozilla_a11y_MsaaIdGenerator_h
+
+#include "mozilla/a11y/IDSet.h"
+
+#include "mozilla/NotNull.h"
+#include "nsITimer.h"
+
+namespace mozilla {
+namespace a11y {
+
+class MsaaAccessible;
+class sdnAccessible;
+
+/**
+ * This class is responsible for generating child IDs used by our MSAA
+ * implementation.
+ */
+class MsaaIdGenerator {
+ public:
+ uint32_t GetID();
+ void ReleaseID(NotNull<MsaaAccessible*> aMsaaAcc);
+ void ReleaseID(NotNull<sdnAccessible*> aSdnAcc);
+
+ private:
+ bool ReleaseID(uint32_t aID);
+ void ReleasePendingIDs();
+
+ private:
+ static constexpr uint32_t kNumFullIDBits = 31UL;
+ IDSet mIDSet{kNumFullIDBits};
+ nsTArray<uint32_t> mIDsToRelease;
+ nsCOMPtr<nsITimer> mReleaseIDTimer;
+ // Whether GetID has been called yet this session.
+ bool mGetIDCalled = false;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_MsaaIdGenerator_h
diff --git a/accessible/windows/msaa/MsaaRootAccessible.cpp b/accessible/windows/msaa/MsaaRootAccessible.cpp
new file mode 100644
index 0000000000..ac747ff3d1
--- /dev/null
+++ b/accessible/windows/msaa/MsaaRootAccessible.cpp
@@ -0,0 +1,67 @@
+/* -*- 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 "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/WindowsVersion.h"
+#include "MsaaRootAccessible.h"
+#include "Relation.h"
+#include "RootAccessible.h"
+#include "EnumVariant.h"
+
+#include <oleauto.h>
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+RootAccessible* MsaaRootAccessible::RootAcc() {
+ return static_cast<RootAccessible*>(LocalAcc());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Aggregated IUnknown
+HRESULT
+MsaaRootAccessible::InternalQueryInterface(REFIID aIid, void** aOutInterface) {
+ if (!aOutInterface) {
+ return E_INVALIDARG;
+ }
+
+ // InternalQueryInterface should always return its internal unknown
+ // when queried for IID_IUnknown...
+ if (aIid == IID_IUnknown) {
+ RefPtr<IUnknown> punk(&mInternalUnknown);
+ punk.forget(aOutInterface);
+ return S_OK;
+ }
+
+ // ...Otherwise we pass through to the base COM implementation of
+ // QueryInterface which is provided by MsaaDocAccessible.
+ return MsaaDocAccessible::QueryInterface(aIid, aOutInterface);
+}
+
+ULONG
+MsaaRootAccessible::InternalAddRef() { return MsaaDocAccessible::AddRef(); }
+
+ULONG
+MsaaRootAccessible::InternalRelease() { return MsaaDocAccessible::Release(); }
+
+already_AddRefed<IUnknown> MsaaRootAccessible::Aggregate(IUnknown* aOuter) {
+ MOZ_ASSERT(mOuter &&
+ (mOuter == &mInternalUnknown || mOuter == aOuter || !aOuter));
+ if (!aOuter) {
+ // If there is no aOuter then we should always set mOuter to
+ // mInternalUnknown. This is standard COM aggregation stuff.
+ mOuter = &mInternalUnknown;
+ return nullptr;
+ }
+
+ mOuter = aOuter;
+ return GetInternalUnknown();
+}
+
+already_AddRefed<IUnknown> MsaaRootAccessible::GetInternalUnknown() {
+ RefPtr<IUnknown> result(&mInternalUnknown);
+ return result.forget();
+}
diff --git a/accessible/windows/msaa/MsaaRootAccessible.h b/accessible/windows/msaa/MsaaRootAccessible.h
new file mode 100644
index 0000000000..a51533d7ba
--- /dev/null
+++ b/accessible/windows/msaa/MsaaRootAccessible.h
@@ -0,0 +1,50 @@
+/* -*- 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_MsaaRootAccessible_h__
+#define mozilla_a11y_MsaaRootAccessible_h__
+
+#include "mozilla/mscom/Aggregation.h"
+#include "MsaaDocAccessible.h"
+
+namespace mozilla {
+
+namespace a11y {
+
+class MsaaRootAccessible : public MsaaDocAccessible {
+ public:
+ explicit MsaaRootAccessible(Accessible* aAcc)
+ : MsaaDocAccessible(aAcc), mOuter(&mInternalUnknown) {}
+
+ /**
+ * This method enables a RootAccessibleWrap to be wrapped by a
+ * LazyInstantiator.
+ *
+ * @param aOuter The IUnknown of the object that is wrapping this
+ * RootAccessibleWrap, or nullptr to unwrap the aOuter from
+ * a previous call.
+ * @return This objects own IUnknown (as opposed to aOuter's IUnknown).
+ */
+ already_AddRefed<IUnknown> Aggregate(IUnknown* aOuter);
+
+ /**
+ * @return This object's own IUnknown, as opposed to its wrapper's IUnknown
+ * which is what would be returned by QueryInterface(IID_IUnknown).
+ */
+ already_AddRefed<IUnknown> GetInternalUnknown();
+
+ private:
+ // DECLARE_AGGREGATABLE declares the internal IUnknown methods as well as
+ // mInternalUnknown.
+ DECLARE_AGGREGATABLE(MsaaRootAccessible);
+ IUnknown* mOuter;
+
+ RootAccessible* RootAcc();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/msaa/MsaaXULMenuAccessible.cpp b/accessible/windows/msaa/MsaaXULMenuAccessible.cpp
new file mode 100644
index 0000000000..9d65c6d21e
--- /dev/null
+++ b/accessible/windows/msaa/MsaaXULMenuAccessible.cpp
@@ -0,0 +1,83 @@
+/* -*- 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 "MsaaXULMenuAccessible.h"
+#include "mozilla/dom/NameSpaceConstants.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/a11y/AccessibleWrap.h"
+#include "LocalAccessible-inl.h"
+#include "nsGkAtoms.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// MsaaXULMenuitemAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+STDMETHODIMP
+MsaaXULMenuitemAccessible::get_accKeyboardShortcut(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszKeyboardShortcut) {
+ if (!pszKeyboardShortcut) return E_INVALIDARG;
+ *pszKeyboardShortcut = nullptr;
+
+ if (varChild.vt != VT_I4 || varChild.lVal != CHILDID_SELF) {
+ return MsaaAccessible::get_accKeyboardShortcut(varChild,
+ pszKeyboardShortcut);
+ }
+
+ LocalAccessible* acc = LocalAcc();
+ if (!acc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ KeyBinding keyBinding = acc->AccessKey();
+ if (keyBinding.IsEmpty()) {
+ return S_FALSE;
+ }
+
+ nsAutoString shortcut;
+ keyBinding.ToString(shortcut);
+
+ *pszKeyboardShortcut = ::SysAllocStringLen(shortcut.get(), shortcut.Length());
+ return *pszKeyboardShortcut ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+MsaaXULMenuitemAccessible::get_accName(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszName) {
+ if (varChild.vt != VT_I4 || varChild.lVal != CHILDID_SELF) {
+ return MsaaAccessible::get_accName(varChild, pszName);
+ }
+ if (!pszName) {
+ return E_INVALIDARG;
+ }
+ LocalAccessible* acc = LocalAcc();
+ if (!acc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+
+ *pszName = nullptr;
+ nsAutoString name;
+ acc->Name(name);
+ if (name.IsVoid()) {
+ return S_FALSE;
+ }
+
+ nsAutoString accel;
+ if (dom::Element* el = acc->Elm()) {
+ el->GetAttr(nsGkAtoms::acceltext, accel);
+ }
+ if (!accel.IsEmpty()) {
+ name += u"\t"_ns + accel;
+ }
+
+ *pszName = ::SysAllocStringLen(name.get(), name.Length());
+ if (!*pszName) {
+ return E_OUTOFMEMORY;
+ }
+ return S_OK;
+}
diff --git a/accessible/windows/msaa/MsaaXULMenuAccessible.h b/accessible/windows/msaa/MsaaXULMenuAccessible.h
new file mode 100644
index 0000000000..ed5d5a4aa2
--- /dev/null
+++ b/accessible/windows/msaa/MsaaXULMenuAccessible.h
@@ -0,0 +1,30 @@
+/* -*- 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_MsaaXULMenuAccessible_h__
+#define mozilla_a11y_MsaaXULMenuAccessible_h__
+
+#include "MsaaAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+class MsaaXULMenuitemAccessible : public MsaaAccessible {
+ public:
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accKeyboardShortcut(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszKeyboardShortcut) override;
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accName(
+ /* [optional][in] */ VARIANT varChild,
+ /* [retval][out] */ BSTR __RPC_FAR* pszName) override;
+
+ protected:
+ using MsaaAccessible::MsaaAccessible;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/msaa/NtUndoc.h b/accessible/windows/msaa/NtUndoc.h
new file mode 100644
index 0000000000..b11b791dfe
--- /dev/null
+++ b/accessible/windows/msaa/NtUndoc.h
@@ -0,0 +1,85 @@
+/* -*- 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/. */
+
+#ifndef mozilla_NtUndoc_h
+#define mozilla_NtUndoc_h
+
+#include <winternl.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+#ifndef STATUS_INFO_LENGTH_MISMATCH
+# define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)
+#endif
+
+#ifndef STATUS_BUFFER_TOO_SMALL
+# define STATUS_BUFFER_TOO_SMALL ((NTSTATUS)0xC0000023L)
+#endif
+
+#ifndef STATUS_MORE_ENTRIES
+# define STATUS_MORE_ENTRIES ((NTSTATUS)0x00000105L)
+#endif
+
+enum UndocSystemInformationClass { SystemExtendedHandleInformation = 64 };
+
+struct SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX {
+ PVOID mObject;
+ ULONG_PTR mPid;
+ ULONG_PTR mHandle;
+ ACCESS_MASK mGrantedAccess;
+ USHORT mCreatorBackTraceIndex;
+ USHORT mObjectTypeIndex;
+ ULONG mAttributes;
+ ULONG mReserved;
+};
+
+struct SYSTEM_HANDLE_INFORMATION_EX {
+ ULONG_PTR mHandleCount;
+ ULONG_PTR mReserved;
+ SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX mHandles[1];
+};
+
+#ifndef __MINGW32__
+enum UndocObjectInformationClass { ObjectNameInformation = 1 };
+
+struct OBJECT_NAME_INFORMATION {
+ UNICODE_STRING Name;
+};
+#endif
+
+// The following declarations are documented on MSDN but are not included in
+// public user-mode headers.
+
+enum DirectoryObjectAccessFlags {
+ DIRECTORY_QUERY = 0x0001,
+ DIRECTORY_TRAVERSE = 0x0002,
+ DIRECTORY_CREATE_OBJECT = 0x0004,
+ DIRECTORY_CREATE_SUBDIRECTORY = 0x0008,
+ DIRECTORY_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | 0x000F
+};
+
+NTSTATUS WINAPI NtOpenDirectoryObject(PHANDLE aDirectoryHandle,
+ ACCESS_MASK aDesiredAccess,
+ POBJECT_ATTRIBUTES aObjectAttributes);
+
+struct OBJECT_DIRECTORY_INFORMATION {
+ UNICODE_STRING mName;
+ UNICODE_STRING mTypeName;
+};
+
+NTSTATUS WINAPI NtQueryDirectoryObject(HANDLE aDirectoryHandle,
+ PVOID aOutBuffer, ULONG aBufferLength,
+ BOOLEAN aReturnSingleEntry,
+ BOOLEAN aRestartScan, PULONG aContext,
+ PULONG aOutReturnLength);
+
+#if defined(__cplusplus)
+} // extern "C"
+#endif
+
+#endif // mozilla_NtUndoc_h
diff --git a/accessible/windows/msaa/Platform.cpp b/accessible/windows/msaa/Platform.cpp
new file mode 100644
index 0000000000..40dddac215
--- /dev/null
+++ b/accessible/windows/msaa/Platform.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 "Platform.h"
+
+#include "AccEvent.h"
+#include "Compatibility.h"
+#include "HyperTextAccessible.h"
+#include "nsWinUtils.h"
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/a11y/RemoteAccessible.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+#include "WinUtils.h"
+#include "ia2AccessibleText.h"
+
+#include <tuple>
+
+#if defined(MOZ_TELEMETRY_REPORTING)
+# include "mozilla/Telemetry.h"
+#endif // defined(MOZ_TELEMETRY_REPORTING)
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+using namespace mozilla::mscom;
+
+static StaticRefPtr<nsIFile> gInstantiator;
+
+void a11y::PlatformInit() {
+ nsWinUtils::MaybeStartWindowEmulation();
+ ia2AccessibleText::InitTextChangeData();
+}
+
+void a11y::PlatformShutdown() {
+ ::DestroyCaret();
+
+ nsWinUtils::ShutdownWindowEmulation();
+
+ if (gInstantiator) {
+ gInstantiator = nullptr;
+ }
+}
+
+void a11y::ProxyCreated(RemoteAccessible* aProxy) {
+ MsaaAccessible* msaa = MsaaAccessible::Create(aProxy);
+ msaa->AddRef();
+ aProxy->SetWrapper(reinterpret_cast<uintptr_t>(msaa));
+}
+
+void a11y::ProxyDestroyed(RemoteAccessible* aProxy) {
+ MsaaAccessible* msaa =
+ reinterpret_cast<MsaaAccessible*>(aProxy->GetWrapper());
+ if (!msaa) {
+ return;
+ }
+ msaa->MsaaShutdown();
+ aProxy->SetWrapper(0);
+ msaa->Release();
+
+ if (aProxy->IsDoc() && nsWinUtils::IsWindowEmulationStarted()) {
+ aProxy->AsDoc()->SetEmulatedWindowHandle(nullptr);
+ }
+}
+
+void a11y::PlatformEvent(Accessible* aTarget, uint32_t aEventType) {
+ MsaaAccessible::FireWinEvent(aTarget, aEventType);
+}
+
+void a11y::PlatformStateChangeEvent(Accessible* aTarget, uint64_t, bool) {
+ MsaaAccessible::FireWinEvent(aTarget, nsIAccessibleEvent::EVENT_STATE_CHANGE);
+}
+
+void a11y::PlatformFocusEvent(Accessible* aTarget,
+ const LayoutDeviceIntRect& aCaretRect) {
+ if (aTarget->IsRemote() && FocusMgr() &&
+ FocusMgr()->FocusedLocalAccessible()) {
+ // This is a focus event from a remote document, but focus has moved out
+ // of that document into the chrome since that event was sent. For example,
+ // this can happen when choosing File menu -> New Tab. See bug 1471466.
+ // Note that this does not handle the case where a focus event is sent from
+ // one remote document, but focus moved into a second remote document
+ // since that event was sent. However, this isn't something anyone has been
+ // able to trigger.
+ return;
+ }
+
+ AccessibleWrap::UpdateSystemCaretFor(aTarget, aCaretRect);
+ MsaaAccessible::FireWinEvent(aTarget, nsIAccessibleEvent::EVENT_FOCUS);
+}
+
+void a11y::PlatformCaretMoveEvent(Accessible* aTarget, int32_t aOffset,
+ bool aIsSelectionCollapsed,
+ int32_t aGranularity,
+ const LayoutDeviceIntRect& aCaretRect,
+ bool aFromUser) {
+ AccessibleWrap::UpdateSystemCaretFor(aTarget, aCaretRect);
+ MsaaAccessible::FireWinEvent(aTarget,
+ nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED);
+}
+
+void a11y::PlatformTextChangeEvent(Accessible* aText, const nsAString& aStr,
+ int32_t aStart, uint32_t aLen, bool aInsert,
+ bool) {
+ uint32_t eventType = aInsert ? nsIAccessibleEvent::EVENT_TEXT_INSERTED
+ : nsIAccessibleEvent::EVENT_TEXT_REMOVED;
+ MOZ_ASSERT(aText->IsHyperText());
+ ia2AccessibleText::UpdateTextChangeData(aText->AsHyperTextBase(), aInsert,
+ aStr, aStart, aLen);
+ MsaaAccessible::FireWinEvent(aText, eventType);
+}
+
+void a11y::PlatformShowHideEvent(Accessible* aTarget, Accessible*, bool aInsert,
+ bool) {
+ uint32_t event =
+ aInsert ? nsIAccessibleEvent::EVENT_SHOW : nsIAccessibleEvent::EVENT_HIDE;
+ MsaaAccessible::FireWinEvent(aTarget, event);
+}
+
+void a11y::PlatformSelectionEvent(Accessible* aTarget, Accessible*,
+ uint32_t aType) {
+ MsaaAccessible::FireWinEvent(aTarget, aType);
+}
+
+static bool GetInstantiatorExecutable(const DWORD aPid,
+ nsIFile** aOutClientExe) {
+ nsAutoHandle callingProcess(
+ ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, aPid));
+ if (!callingProcess) {
+ return false;
+ }
+
+ DWORD bufLen = MAX_PATH;
+ UniquePtr<wchar_t[]> buf;
+
+ while (true) {
+ buf = MakeUnique<wchar_t[]>(bufLen);
+ if (::QueryFullProcessImageName(callingProcess, 0, buf.get(), &bufLen)) {
+ break;
+ }
+
+ DWORD lastError = ::GetLastError();
+ MOZ_ASSERT(lastError == ERROR_INSUFFICIENT_BUFFER);
+ if (lastError != ERROR_INSUFFICIENT_BUFFER) {
+ return false;
+ }
+
+ bufLen *= 2;
+ }
+
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_NewLocalFile(nsDependentString(buf.get(), bufLen), false,
+ getter_AddRefs(file));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ file.forget(aOutClientExe);
+ return NS_SUCCEEDED(rv);
+}
+
+/**
+ * Appends version information in the format "|a.b.c.d".
+ * If there is no version information, we append nothing.
+ */
+static void AppendVersionInfo(nsIFile* aClientExe, nsAString& aStrToAppend) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ LauncherResult<ModuleVersion> version = GetModuleVersion(aClientExe);
+ if (version.isErr()) {
+ return;
+ }
+
+ auto [major, minor, patch, build] = version.unwrap().AsTuple();
+
+ aStrToAppend.AppendLiteral(u"|");
+
+ constexpr auto dot = u"."_ns;
+
+ aStrToAppend.AppendInt(major);
+ aStrToAppend.Append(dot);
+ aStrToAppend.AppendInt(minor);
+ aStrToAppend.Append(dot);
+ aStrToAppend.AppendInt(patch);
+ aStrToAppend.Append(dot);
+ aStrToAppend.AppendInt(build);
+}
+
+static void AccumulateInstantiatorTelemetry(const nsAString& aValue) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aValue.IsEmpty()) {
+#if defined(MOZ_TELEMETRY_REPORTING)
+ Telemetry::ScalarSet(Telemetry::ScalarID::A11Y_INSTANTIATORS, aValue);
+#endif // defined(MOZ_TELEMETRY_REPORTING)
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::AccessibilityClient,
+ NS_ConvertUTF16toUTF8(aValue));
+ }
+}
+
+static void GatherInstantiatorTelemetry(nsIFile* aClientExe) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ nsString value;
+ nsresult rv = aClientExe->GetLeafName(value);
+ if (NS_SUCCEEDED(rv)) {
+ AppendVersionInfo(aClientExe, value);
+ }
+
+ nsCOMPtr<nsIRunnable> runnable(
+ NS_NewRunnableFunction("a11y::AccumulateInstantiatorTelemetry",
+ [value = std::move(value)]() -> void {
+ AccumulateInstantiatorTelemetry(value);
+ }));
+
+ // Now that we've (possibly) obtained version info, send the resulting
+ // string back to the main thread to accumulate in telemetry.
+ NS_DispatchToMainThread(runnable.forget());
+}
+
+void a11y::SetInstantiator(const uint32_t aPid) {
+ nsCOMPtr<nsIFile> clientExe;
+ if (!GetInstantiatorExecutable(aPid, getter_AddRefs(clientExe))) {
+ AccumulateInstantiatorTelemetry(
+ u"(Failed to retrieve client image name)"_ns);
+ return;
+ }
+
+ // Only record the instantiator if it is the first instantiator, or if it does
+ // not match the previous one. Some blocked clients are repeatedly requesting
+ // a11y over and over so we don't want to be spawning countless telemetry
+ // threads.
+ if (gInstantiator) {
+ bool equal;
+ nsresult rv = gInstantiator->Equals(clientExe, &equal);
+ if (NS_SUCCEEDED(rv) && equal) {
+ return;
+ }
+ }
+
+ gInstantiator = clientExe;
+
+ nsCOMPtr<nsIRunnable> runnable(
+ NS_NewRunnableFunction("a11y::GatherInstantiatorTelemetry",
+ [clientExe = std::move(clientExe)]() -> void {
+ GatherInstantiatorTelemetry(clientExe);
+ }));
+
+ DebugOnly<nsresult> rv =
+ NS_DispatchBackgroundTask(runnable.forget(), NS_DISPATCH_EVENT_MAY_BLOCK);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+bool a11y::GetInstantiator(nsIFile** aOutInstantiator) {
+ if (!gInstantiator) {
+ return false;
+ }
+
+ return NS_SUCCEEDED(gInstantiator->Clone(aOutInstantiator));
+}
diff --git a/accessible/windows/msaa/RootAccessibleWrap.cpp b/accessible/windows/msaa/RootAccessibleWrap.cpp
new file mode 100644
index 0000000000..3a2477e877
--- /dev/null
+++ b/accessible/windows/msaa/RootAccessibleWrap.cpp
@@ -0,0 +1,44 @@
+/* -*- 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 "RootAccessibleWrap.h"
+
+#include "Compatibility.h"
+#include "mozilla/PresShell.h"
+#include "nsCoreUtils.h"
+#include "nsWinUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// Constructor/destructor
+
+RootAccessibleWrap::RootAccessibleWrap(dom::Document* aDocument,
+ PresShell* aPresShell)
+ : RootAccessible(aDocument, aPresShell) {}
+
+RootAccessibleWrap::~RootAccessibleWrap() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// RootAccessible
+
+void RootAccessibleWrap::DocumentActivated(DocAccessible* aDocument) {
+ // This check will never work with e10s enabled, in other words, as of
+ // Firefox 57.
+ if (Compatibility::IsDolphin() &&
+ nsCoreUtils::IsTopLevelContentDocInProcess(aDocument->DocumentNode())) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ uint32_t count = mChildDocuments.Length();
+ for (uint32_t idx = 0; idx < count; idx++) {
+ DocAccessible* childDoc = mChildDocuments[idx];
+ HWND childDocHWND = static_cast<HWND>(childDoc->GetNativeWindow());
+ if (childDoc != aDocument)
+ nsWinUtils::HideNativeWindow(childDocHWND);
+ else
+ nsWinUtils::ShowNativeWindow(childDocHWND);
+ }
+ }
+}
diff --git a/accessible/windows/msaa/RootAccessibleWrap.h b/accessible/windows/msaa/RootAccessibleWrap.h
new file mode 100644
index 0000000000..20b3e8edfc
--- /dev/null
+++ b/accessible/windows/msaa/RootAccessibleWrap.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_RootAccessibleWrap_h__
+#define mozilla_a11y_RootAccessibleWrap_h__
+
+#include "RootAccessible.h"
+
+namespace mozilla {
+
+class PresShell;
+
+namespace a11y {
+
+/**
+ * Windows 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();
+
+ // RootAccessible
+ virtual void DocumentActivated(DocAccessible* aDocument);
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/msaa/ServiceProvider.cpp b/accessible/windows/msaa/ServiceProvider.cpp
new file mode 100644
index 0000000000..b2b005f3b4
--- /dev/null
+++ b/accessible/windows/msaa/ServiceProvider.cpp
@@ -0,0 +1,106 @@
+/* -*- 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/. */
+
+#include "ServiceProvider.h"
+
+#include "AccessibleApplication_i.c"
+#include "ApplicationAccessibleWrap.h"
+#include "DocAccessible.h"
+#include "nsAccUtils.h"
+#include "nsCoreUtils.h"
+#include "Relation.h"
+#include "RootAccessible.h"
+#include "uiaRawElmProvider.h"
+
+#include "mozilla/a11y/DocAccessibleChild.h"
+#include "mozilla/Preferences.h"
+
+#include "ISimpleDOM.h"
+
+namespace mozilla {
+namespace a11y {
+
+IMPL_IUNKNOWN_QUERY_HEAD(ServiceProvider)
+IMPL_IUNKNOWN_QUERY_IFACE(IServiceProvider)
+IMPL_IUNKNOWN_QUERY_TAIL_AGGREGATED(mMsaa)
+
+////////////////////////////////////////////////////////////////////////////////
+// IServiceProvider
+
+STDMETHODIMP
+ServiceProvider::QueryService(REFGUID aGuidService, REFIID aIID,
+ void** aInstancePtr) {
+ if (!aInstancePtr) return E_INVALIDARG;
+
+ *aInstancePtr = nullptr;
+ Accessible* acc = mMsaa->Acc();
+ if (!acc) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+ AccessibleWrap* localAcc = mMsaa->LocalAcc();
+
+ // UIA IAccessibleEx
+ if (aGuidService == IID_IAccessibleEx &&
+ Preferences::GetBool("accessibility.uia.enable") && localAcc) {
+ uiaRawElmProvider* accEx = new uiaRawElmProvider(localAcc);
+ HRESULT hr = accEx->QueryInterface(aIID, aInstancePtr);
+ if (FAILED(hr)) delete accEx;
+
+ return hr;
+ }
+
+ // Provide a special service ID for getting the accessible for the browser tab
+ // document that contains this accessible object. If this accessible object
+ // is not inside a browser tab then the service fails with E_NOINTERFACE.
+ // A use case for this is for screen readers that need to switch context or
+ // 'virtual buffer' when focus moves from one browser tab area to another.
+ static const GUID SID_IAccessibleContentDocument = {
+ 0xa5d8e1f3,
+ 0x3571,
+ 0x4d8f,
+ {0x95, 0x21, 0x07, 0xed, 0x28, 0xfb, 0x07, 0x2e}};
+ if (aGuidService == SID_IAccessibleContentDocument) {
+ if (aIID != IID_IAccessible) return E_NOINTERFACE;
+
+ Relation rel = acc->RelationByType(RelationType::CONTAINING_TAB_PANE);
+ RefPtr<IAccessible> next = MsaaAccessible::GetFrom(rel.Next());
+ if (!next) {
+ return E_NOINTERFACE;
+ }
+
+ next.forget(aInstancePtr);
+ return S_OK;
+ }
+
+ // Can get to IAccessibleApplication from any node via QS
+ // Note: in case of JAWS we want to check if aIID is
+ // IID_IAccessibleApplication.
+ if (aGuidService == IID_IAccessibleApplication ||
+ aIID == IID_IAccessibleApplication) {
+ ApplicationAccessibleWrap* applicationAcc =
+ static_cast<ApplicationAccessibleWrap*>(ApplicationAcc());
+ if (!applicationAcc) return E_NOINTERFACE;
+
+ RefPtr<IAccessible> appIa;
+ applicationAcc->GetNativeInterface(getter_AddRefs(appIa));
+ return appIa->QueryInterface(aIID, aInstancePtr);
+ }
+
+ static const GUID IID_SimpleDOMDeprecated = {
+ 0x0c539790,
+ 0x12e4,
+ 0x11cf,
+ {0xb6, 0x61, 0x00, 0xaa, 0x00, 0x4c, 0xd6, 0xd8}};
+ if (aGuidService == IID_ISimpleDOMNode ||
+ aGuidService == IID_SimpleDOMDeprecated ||
+ aGuidService == IID_IAccessible || aGuidService == IID_IAccessible2)
+ return mMsaa->QueryInterface(aIID, aInstancePtr);
+
+ return E_INVALIDARG;
+}
+
+} // namespace a11y
+} // namespace mozilla
diff --git a/accessible/windows/msaa/ServiceProvider.h b/accessible/windows/msaa/ServiceProvider.h
new file mode 100644
index 0000000000..0545bccf62
--- /dev/null
+++ b/accessible/windows/msaa/ServiceProvider.h
@@ -0,0 +1,37 @@
+/* -*- 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/. */
+
+#ifndef mozilla_a11y_ServiceProvider_h_
+#define mozilla_a11y_ServiceProvider_h_
+
+#include <servprov.h>
+
+#include "IUnknownImpl.h"
+#include "MsaaAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+class ServiceProvider final : public IServiceProvider {
+ public:
+ explicit ServiceProvider(MsaaAccessible* aMsaa) : mMsaa(aMsaa) {}
+ ~ServiceProvider() {}
+
+ DECL_IUNKNOWN
+
+ // IServiceProvider
+ virtual HRESULT STDMETHODCALLTYPE QueryService(REFGUID aGuidService,
+ REFIID aIID,
+ void** aInstancePtr);
+
+ private:
+ RefPtr<MsaaAccessible> mMsaa;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/msaa/moz.build b/accessible/windows/msaa/moz.build
new file mode 100644
index 0000000000..1b56c2eacf
--- /dev/null
+++ b/accessible/windows/msaa/moz.build
@@ -0,0 +1,73 @@
+# -*- 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 += [
+ "IUnknownImpl.h",
+]
+
+EXPORTS.mozilla.a11y += [
+ "AccessibleWrap.h",
+ "Compatibility.h",
+ "LazyInstantiator.h",
+ "MsaaAccessible.h",
+ "MsaaIdGenerator.h",
+ "nsWinUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "AccessibleWrap.cpp",
+ "ApplicationAccessibleWrap.cpp",
+ "Compatibility.cpp",
+ "CompatibilityUIA.cpp",
+ "DocAccessibleWrap.cpp",
+ "EnumVariant.cpp",
+ "IUnknownImpl.cpp",
+ "LazyInstantiator.cpp",
+ "MsaaAccessible.cpp",
+ "MsaaDocAccessible.cpp",
+ "MsaaIdGenerator.cpp",
+ "MsaaRootAccessible.cpp",
+ "MsaaXULMenuAccessible.cpp",
+ "nsWinUtils.cpp",
+ "Platform.cpp",
+ "RootAccessibleWrap.cpp",
+]
+
+SOURCES += [
+ # This file cannot be built in unified mode because it includes ISimpleDOMNode_i.c.
+ "ServiceProvider.cpp",
+]
+
+OS_LIBS += [
+ "ntdll",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/generic",
+ "/accessible/html",
+ "/accessible/ipc",
+ "/accessible/windows",
+ "/accessible/windows/ia2",
+ "/accessible/windows/sdn",
+ "/accessible/windows/uia",
+ "/accessible/xpcom",
+ "/accessible/xul",
+ "/dom/base",
+ "/layout/style",
+]
+
+# The Windows MIDL code generator creates things like:
+#
+# #endif !_MIDL_USE_GUIDDEF_
+#
+# which clang-cl complains about. MSVC doesn't, so turn this warning off.
+if CONFIG["CC_TYPE"] == "clang-cl":
+ CXXFLAGS += ["-Wno-extra-tokens"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/accessible/windows/msaa/nsEventMap.h b/accessible/windows/msaa/nsEventMap.h
new file mode 100644
index 0000000000..9030f0e551
--- /dev/null
+++ b/accessible/windows/msaa/nsEventMap.h
@@ -0,0 +1,57 @@
+/* -*- 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/. */
+
+#include <winuser.h>
+#include "AccessibleEventId.h"
+
+const uint32_t kEVENT_WIN_UNKNOWN = 0x00000000;
+
+static const uint32_t gWinEventMap[] = {
+ // clang-format off
+ kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent doesn't have 0 constant
+ EVENT_OBJECT_SHOW, // nsIAccessibleEvent::EVENT_SHOW
+ EVENT_OBJECT_HIDE, // nsIAccessibleEvent::EVENT_HIDE
+ EVENT_OBJECT_REORDER, // nsIAccessibleEvent::EVENT_REORDER
+ EVENT_OBJECT_FOCUS, // nsIAccessibleEvent::EVENT_FOCUS
+ EVENT_OBJECT_STATECHANGE, // nsIAccessibleEvent::EVENT_STATE_CHANGE
+ EVENT_OBJECT_NAMECHANGE, // nsIAccessibleEvent::EVENT_NAME_CHANGE
+ EVENT_OBJECT_DESCRIPTIONCHANGE, // nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE
+ EVENT_OBJECT_VALUECHANGE, // nsIAccessibleEvent::EVENT_VALUE_CHANGE
+ EVENT_OBJECT_SELECTION, // nsIAccessibleEvent::EVENT_SELECTION
+ EVENT_OBJECT_SELECTIONADD, // nsIAccessibleEvent::EVENT_SELECTION_ADD
+ EVENT_OBJECT_SELECTIONREMOVE, // nsIAccessibleEvent::EVENT_SELECTION_REMOVE
+ EVENT_OBJECT_SELECTIONWITHIN, // nsIAccessibleEvent::EVENT_SELECTION_WITHIN
+ EVENT_SYSTEM_ALERT, // nsIAccessibleEvent::EVENT_ALERT
+ EVENT_SYSTEM_MENUSTART, // nsIAccessibleEvent::EVENT_MENU_START
+ EVENT_SYSTEM_MENUEND, // nsIAccessibleEvent::EVENT_MENU_END
+ EVENT_SYSTEM_MENUPOPUPSTART, // nsIAccessibleEvent::EVENT_MENUPOPUP_START
+ EVENT_SYSTEM_MENUPOPUPEND, // nsIAccessibleEvent::EVENT_MENUPOPUP_END
+ EVENT_SYSTEM_DRAGDROPSTART, // nsIAccessibleEvent::EVENT_DRAGDROP_START
+ EVENT_SYSTEM_SCROLLINGSTART, // nsIAccessibleEvent::EVENT_SCROLLING_START
+ EVENT_SYSTEM_SCROLLINGEND, // nsIAccessibleEvent::EVENT_SCROLLING_END
+ IA2_EVENT_DOCUMENT_LOAD_COMPLETE, // nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE
+ IA2_EVENT_DOCUMENT_RELOAD, // nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD
+ IA2_EVENT_DOCUMENT_LOAD_STOPPED, // nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_STOPPED
+ IA2_EVENT_TEXT_ATTRIBUTE_CHANGED, // nsIAccessibleEvent::EVENT_TEXT_ATTRIBUTE_CHANGED
+ IA2_EVENT_TEXT_CARET_MOVED, // nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED
+ IA2_EVENT_TEXT_INSERTED, // nsIAccessibleEvent::EVENT_TEXT_INSERTED
+ IA2_EVENT_TEXT_REMOVED, // nsIAccessibleEvent::EVENT_TEXT_REMOVED
+ IA2_EVENT_TEXT_SELECTION_CHANGED, // nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED
+ kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_WINDOW_ACTIVATE
+ kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_WINDOW_DEACTIVATE
+ kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_WINDOW_MAXIMIZE
+ kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_WINDOW_MINIMIZE
+ kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_WINDOW_RESTORE
+ IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED, // nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED
+ EVENT_OBJECT_VALUECHANGE, // nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE
+ kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_SCROLLING
+ kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_ANNOUNCEMENT
+ kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED
+ kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED
+ kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_INNER_REORDER
+ // clang-format on
+};
diff --git a/accessible/windows/msaa/nsWinUtils.cpp b/accessible/windows/msaa/nsWinUtils.cpp
new file mode 100644
index 0000000000..6eb7d290dd
--- /dev/null
+++ b/accessible/windows/msaa/nsWinUtils.cpp
@@ -0,0 +1,167 @@
+/* -*- 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 "nsWinUtils.h"
+
+#include "Compatibility.h"
+#include "DocAccessible.h"
+#include "nsAccessibilityService.h"
+#include "nsCoreUtils.h"
+
+#include "mozilla/a11y/DocAccessibleParent.h"
+#include "mozilla/Preferences.h"
+#include "nsArrayUtils.h"
+#include "nsICSSDeclaration.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "nsXULAppAPI.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+using mozilla::dom::Element;
+
+// Window property used by ipc related code in identifying accessible
+// tab windows.
+const wchar_t* kPropNameTabContent = L"AccessibleTabWindow";
+
+/**
+ * WindowProc to process WM_GETOBJECT messages, used in windows emulation mode.
+ */
+static LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg, WPARAM wParam,
+ LPARAM lParam);
+
+bool nsWinUtils::sWindowEmulationStarted = false;
+
+already_AddRefed<nsICSSDeclaration> nsWinUtils::GetComputedStyleDeclaration(
+ nsIContent* aContent) {
+ nsIContent* elm = nsCoreUtils::GetDOMElementFor(aContent);
+ if (!elm) return nullptr;
+
+ // Returns number of items in style declaration
+ nsCOMPtr<nsPIDOMWindowInner> window = elm->OwnerDoc()->GetInnerWindow();
+ if (!window) return nullptr;
+
+ ErrorResult dummy;
+ nsCOMPtr<Element> domElement(do_QueryInterface(elm));
+ nsCOMPtr<nsICSSDeclaration> cssDecl =
+ window->GetComputedStyle(*domElement, u""_ns, dummy);
+ dummy.SuppressException();
+ return cssDecl.forget();
+}
+
+bool nsWinUtils::MaybeStartWindowEmulation() {
+ // Register window class that'll be used for document accessibles associated
+ // with tabs.
+ if (IPCAccessibilityActive()) return false;
+
+ if (Compatibility::IsJAWS() || Compatibility::IsWE() ||
+ Compatibility::IsDolphin() || Compatibility::IsVisperoShared()) {
+ RegisterNativeWindow(kClassNameTabContent);
+ sWindowEmulationStarted = true;
+ return true;
+ }
+
+ return false;
+}
+
+void nsWinUtils::ShutdownWindowEmulation() {
+ // Unregister window call that's used for document accessibles associated
+ // with tabs.
+ if (IsWindowEmulationStarted()) {
+ ::UnregisterClassW(kClassNameTabContent, GetModuleHandle(nullptr));
+ sWindowEmulationStarted = false;
+ }
+}
+
+void nsWinUtils::RegisterNativeWindow(LPCWSTR aWindowClass) {
+ WNDCLASSW wc;
+ wc.style = CS_GLOBALCLASS;
+ wc.lpfnWndProc = WindowProc;
+ wc.cbClsExtra = 0;
+ wc.cbWndExtra = 0;
+ wc.hInstance = GetModuleHandle(nullptr);
+ wc.hIcon = nullptr;
+ wc.hCursor = nullptr;
+ wc.hbrBackground = nullptr;
+ wc.lpszMenuName = nullptr;
+ wc.lpszClassName = aWindowClass;
+ ::RegisterClassW(&wc);
+}
+
+HWND nsWinUtils::CreateNativeWindow(LPCWSTR aWindowClass, HWND aParentWnd,
+ int aX, int aY, int aWidth, int aHeight,
+ bool aIsActive,
+ NativeWindowCreateProc* aOnCreateProc) {
+ return ::CreateWindowExW(
+ WS_EX_TRANSPARENT, aWindowClass, L"NetscapeDispatchWnd",
+ WS_CHILD | (aIsActive ? WS_VISIBLE : 0), aX, aY, aWidth, aHeight,
+ aParentWnd, nullptr, GetModuleHandle(nullptr), aOnCreateProc);
+}
+
+void nsWinUtils::ShowNativeWindow(HWND aWnd) { ::ShowWindow(aWnd, SW_SHOW); }
+
+void nsWinUtils::HideNativeWindow(HWND aWnd) {
+ ::SetWindowPos(
+ aWnd, nullptr, 0, 0, 0, 0,
+ SWP_HIDEWINDOW | SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
+}
+
+LRESULT CALLBACK WindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
+ // Note, this window's message handling should not invoke any call that
+ // may result in a cross-process ipc call. Doing so may violate RPC
+ // message semantics.
+
+ switch (msg) {
+ case WM_CREATE: {
+ // Mark this window so that ipc related code can identify it.
+ ::SetPropW(hWnd, kPropNameTabContent, reinterpret_cast<HANDLE>(1));
+
+ auto createStruct = reinterpret_cast<CREATESTRUCT*>(lParam);
+ auto createProc = reinterpret_cast<nsWinUtils::NativeWindowCreateProc*>(
+ createStruct->lpCreateParams);
+
+ if (createProc && *createProc) {
+ (*createProc)(hWnd);
+ }
+
+ return 0;
+ }
+ case WM_GETOBJECT: {
+ // Do explicit casting to make it working on 64bit systems (see bug 649236
+ // for details).
+ int32_t objId = static_cast<DWORD>(lParam);
+ if (objId == OBJID_CLIENT) {
+ RefPtr<IAccessible> msaaAccessible;
+ DocAccessible* document =
+ reinterpret_cast<DocAccessible*>(::GetPropW(hWnd, kPropNameDocAcc));
+ if (document) {
+ document->GetNativeInterface(getter_AddRefs(msaaAccessible));
+ } else {
+ DocAccessibleParent* docParent = static_cast<DocAccessibleParent*>(
+ ::GetPropW(hWnd, kPropNameDocAccParent));
+ if (docParent) {
+ msaaAccessible = MsaaAccessible::GetFrom(docParent);
+ }
+ }
+ if (msaaAccessible) {
+ LRESULT result =
+ ::LresultFromObject(IID_IAccessible, wParam,
+ msaaAccessible); // does an addref
+ return result;
+ }
+ }
+ return 0;
+ }
+ case WM_NCHITTEST: {
+ LRESULT lRet = ::DefWindowProc(hWnd, msg, wParam, lParam);
+ if (HTCLIENT == lRet) lRet = HTTRANSPARENT;
+ return lRet;
+ }
+ }
+
+ return ::DefWindowProcW(hWnd, msg, wParam, lParam);
+}
diff --git a/accessible/windows/msaa/nsWinUtils.h b/accessible/windows/msaa/nsWinUtils.h
new file mode 100644
index 0000000000..7ae0cadc5d
--- /dev/null
+++ b/accessible/windows/msaa/nsWinUtils.h
@@ -0,0 +1,105 @@
+/* -*- 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 nsWinUtils_h_
+#define nsWinUtils_h_
+
+#include <functional>
+#include <windows.h>
+
+#include "nsICSSDeclaration.h"
+#include "nsCOMPtr.h"
+
+class nsIContent;
+
+namespace mozilla {
+namespace a11y {
+
+class DocAccessible;
+
+const LPCWSTR kClassNameRoot = L"MozillaUIWindowClass";
+const LPCWSTR kClassNameTabContent = L"MozillaContentWindowClass";
+const LPCWSTR kPropNameDocAcc = L"MozDocAccessible";
+const LPCWSTR kPropNameDocAccParent = L"MozDocAccessibleParent";
+
+class nsWinUtils {
+ public:
+ /**
+ * Return computed styles declaration for the given node.
+ *
+ * @note Please use it carefully since it can shutdown the accessible tree
+ * you operate on.
+ */
+ static already_AddRefed<nsICSSDeclaration> GetComputedStyleDeclaration(
+ nsIContent* aContent);
+
+ /**
+ * Start window emulation if presence of specific AT is detected.
+ */
+ static bool MaybeStartWindowEmulation();
+
+ /**
+ * Free resources used for window emulation.
+ */
+ static void ShutdownWindowEmulation();
+
+ /**
+ * Return true if window emulation is started.
+ */
+ static bool IsWindowEmulationStarted() { return sWindowEmulationStarted; }
+
+ /**
+ * Helper to register window class.
+ */
+ static void RegisterNativeWindow(LPCWSTR aWindowClass);
+
+ typedef std::function<void(HWND)> NativeWindowCreateProc;
+
+ /**
+ * Helper to create a window.
+ *
+ * NB: If additional setup needs to be done once the window has been created,
+ * you should do so via aOnCreateProc. Hooks will fire during the
+ * CreateNativeWindow call, thus triggering events in the AT.
+ * Using aOnCreateProc guarantees that your additional initialization will
+ * have completed prior to the AT receiving window creation events.
+ *
+ * For example:
+ *
+ * nsWinUtils::NativeWindowCreateProc onCreate([](HWND aHwnd) -> void {
+ * DoSomeAwesomeInitializationStuff(aHwnd);
+ * DoMoreAwesomeInitializationStuff(aHwnd);
+ * });
+ * HWND hwnd = nsWinUtils::CreateNativeWindow(..., &onCreate);
+ * // Doing further initialization work to hwnd on this line is too late!
+ */
+ static HWND CreateNativeWindow(
+ LPCWSTR aWindowClass, HWND aParentWnd, int aX, int aY, int aWidth,
+ int aHeight, bool aIsActive,
+ NativeWindowCreateProc* aOnCreateProc = nullptr);
+
+ /**
+ * Helper to show window.
+ */
+ static void ShowNativeWindow(HWND aWnd);
+
+ /**
+ * Helper to hide window.
+ */
+ static void HideNativeWindow(HWND aWnd);
+
+ private:
+ /**
+ * Flag that indicates if window emulation is started.
+ */
+ static bool sWindowEmulationStarted;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/sdn/moz.build b/accessible/windows/sdn/moz.build
new file mode 100644
index 0000000000..8d76b7482e
--- /dev/null
+++ b/accessible/windows/sdn/moz.build
@@ -0,0 +1,24 @@
+# -*- 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/.
+
+UNIFIED_SOURCES += [
+ "sdnAccessible.cpp",
+ "sdnDocAccessible.cpp",
+ "sdnTextAccessible.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/generic",
+ "/accessible/html",
+ "/accessible/windows/msaa",
+ "/accessible/xpcom",
+ "/accessible/xul",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/accessible/windows/sdn/sdnAccessible-inl.h b/accessible/windows/sdn/sdnAccessible-inl.h
new file mode 100644
index 0000000000..76f8796f42
--- /dev/null
+++ b/accessible/windows/sdn/sdnAccessible-inl.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 mozilla_a11y_sdnAccessible_inl_h_
+#define mozilla_a11y_sdnAccessible_inl_h_
+
+#include "sdnAccessible.h"
+
+#include "DocAccessible.h"
+#include "nsAccessibilityService.h"
+
+namespace mozilla {
+namespace a11y {
+
+inline DocAccessible* sdnAccessible::GetDocument() const {
+ MOZ_ASSERT(mNode);
+ return GetExistingDocAccessible(mNode->OwnerDoc());
+}
+
+inline MsaaAccessible* sdnAccessible::GetMsaa() {
+ if (mMsaa) {
+ return mMsaa;
+ }
+
+ DocAccessible* document = GetDocument();
+ if (!document) {
+ return nullptr;
+ }
+
+ // Once we have an accessible, we should hold a reference to it so that we
+ // may preserve object identity.
+ AccessibleWrap* wrap = static_cast<AccessibleWrap*>(
+ document->GetAccessibleEvenIfNotInMap(mNode));
+
+ if (!wrap) {
+ return nullptr;
+ }
+
+ wrap->GetNativeInterface(getter_AddRefs(mMsaa));
+ return mMsaa;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_sdnAccessible_inl_h_
diff --git a/accessible/windows/sdn/sdnAccessible.cpp b/accessible/windows/sdn/sdnAccessible.cpp
new file mode 100644
index 0000000000..780bea3758
--- /dev/null
+++ b/accessible/windows/sdn/sdnAccessible.cpp
@@ -0,0 +1,522 @@
+/* -*- 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 "sdnAccessible-inl.h"
+#include "ISimpleDOM_i.c"
+
+#include "DocAccessibleWrap.h"
+
+#include "nsAttrName.h"
+#include "nsCoreUtils.h"
+#include "nsIAccessibleTypes.h"
+#include "nsICSSDeclaration.h"
+#include "nsNameSpaceManager.h"
+#include "nsServiceManagerUtils.h"
+#include "nsWinUtils.h"
+#include "nsRange.h"
+
+#include "mozilla/dom/BorrowedAttrInfo.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/PresShell.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+sdnAccessible::~sdnAccessible() {
+ if (mUniqueId.isSome()) {
+ MsaaAccessible::ReleaseChildID(WrapNotNull(this));
+ }
+}
+
+STDMETHODIMP
+sdnAccessible::QueryInterface(REFIID aREFIID, void** aInstancePtr) {
+ if (!aInstancePtr) return E_FAIL;
+ *aInstancePtr = nullptr;
+
+ if (aREFIID == IID_IClientSecurity) {
+ // Some code might QI(IID_IClientSecurity) to detect whether or not we are
+ // a proxy. Right now that can potentially happen off the main thread, so we
+ // look for this condition immediately so that we don't trigger other code
+ // that might not be thread-safe.
+ return E_NOINTERFACE;
+ }
+
+ if (aREFIID == IID_ISimpleDOMNode) {
+ *aInstancePtr = static_cast<ISimpleDOMNode*>(this);
+ AddRef();
+ return S_OK;
+ }
+
+ MsaaAccessible* accessible = GetMsaa();
+ if (accessible) return accessible->QueryInterface(aREFIID, aInstancePtr);
+
+ // IUnknown* is the canonical one if and only if this accessible doesn't have
+ // an accessible.
+ if (aREFIID == IID_IUnknown) {
+ *aInstancePtr = static_cast<ISimpleDOMNode*>(this);
+ AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+STDMETHODIMP
+sdnAccessible::get_nodeInfo(BSTR __RPC_FAR* aNodeName,
+ short __RPC_FAR* aNameSpaceID,
+ BSTR __RPC_FAR* aNodeValue,
+ unsigned int __RPC_FAR* aNumChildren,
+ unsigned int __RPC_FAR* aUniqueID,
+ unsigned short __RPC_FAR* aNodeType) {
+ if (!aNodeName || !aNameSpaceID || !aNodeValue || !aNumChildren ||
+ !aUniqueID || !aNodeType)
+ return E_INVALIDARG;
+
+ *aNodeName = nullptr;
+ *aNameSpaceID = 0;
+ *aNodeValue = nullptr;
+ *aNumChildren = 0;
+ *aUniqueID = 0;
+ *aNodeType = 0;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+
+ // This is a unique ID for every content node. The 3rd party accessibility
+ // application can compare this to the childID we return for events such as
+ // focus events, to correlate back to data nodes in their internal object
+ // model.
+ MsaaAccessible* accessible = GetMsaa();
+ if (accessible) {
+ *aUniqueID = MsaaAccessible::GetChildIDFor(accessible->Acc());
+ } else {
+ if (mUniqueId.isNothing()) {
+ MsaaAccessible::AssignChildIDTo(WrapNotNull(this));
+ }
+ MOZ_ASSERT(mUniqueId.isSome());
+ *aUniqueID = mUniqueId.value();
+ }
+
+ if (!mNode) {
+ RemoteAccessible* remoteAcc = accessible->Acc()->AsRemote();
+ MOZ_ASSERT(remoteAcc);
+ if (remoteAcc->IsText()) {
+ *aNodeType = nsINode::TEXT_NODE;
+ } else if (remoteAcc->IsDoc()) {
+ *aNodeType = nsINode::DOCUMENT_NODE;
+ } else {
+ *aNodeType = nsINode::ELEMENT_NODE;
+ }
+ if (nsAtom* tag = remoteAcc->TagName()) {
+ nsAutoString nodeName;
+ tag->ToString(nodeName);
+ *aNodeName = ::SysAllocString(nodeName.get());
+ }
+ return S_OK;
+ }
+
+ uint16_t nodeType = mNode->NodeType();
+ *aNodeType = static_cast<unsigned short>(nodeType);
+
+ if (*aNodeType != NODETYPE_TEXT) {
+ *aNodeName = ::SysAllocString(mNode->NodeName().get());
+ }
+
+ nsAutoString nodeValue;
+ mNode->GetNodeValue(nodeValue);
+ *aNodeValue = ::SysAllocString(nodeValue.get());
+
+ *aNameSpaceID = mNode->IsContent()
+ ? static_cast<short>(mNode->AsContent()->GetNameSpaceID())
+ : 0;
+
+ *aNumChildren = mNode->GetChildCount();
+
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnAccessible::get_attributes(unsigned short aMaxAttribs,
+ BSTR __RPC_FAR* aAttribNames,
+ short __RPC_FAR* aNameSpaceIDs,
+ BSTR __RPC_FAR* aAttribValues,
+ unsigned short __RPC_FAR* aNumAttribs) {
+ if (!aAttribNames || !aNameSpaceIDs || !aAttribValues || !aNumAttribs)
+ return E_INVALIDARG;
+
+ *aNumAttribs = 0;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+ if (!mNode) {
+ MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
+ return E_NOTIMPL;
+ }
+
+ if (!mNode->IsElement()) return S_FALSE;
+
+ dom::Element* elm = mNode->AsElement();
+ uint32_t numAttribs = elm->GetAttrCount();
+ if (numAttribs > aMaxAttribs) numAttribs = aMaxAttribs;
+
+ *aNumAttribs = static_cast<unsigned short>(numAttribs);
+
+ for (uint32_t index = 0; index < numAttribs; index++) {
+ aNameSpaceIDs[index] = 0;
+ aAttribValues[index] = aAttribNames[index] = nullptr;
+ nsAutoString attributeValue;
+
+ dom::BorrowedAttrInfo attr = elm->GetAttrInfoAt(index);
+ attr.mValue->ToString(attributeValue);
+
+ aNameSpaceIDs[index] = static_cast<short>(attr.mName->NamespaceID());
+ aAttribNames[index] =
+ ::SysAllocString(attr.mName->LocalName()->GetUTF16String());
+ aAttribValues[index] = ::SysAllocString(attributeValue.get());
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnAccessible::get_attributesForNames(unsigned short aMaxAttribs,
+ BSTR __RPC_FAR* aAttribNames,
+ short __RPC_FAR* aNameSpaceID,
+ BSTR __RPC_FAR* aAttribValues) {
+ if (!aAttribNames || !aNameSpaceID || !aAttribValues) return E_INVALIDARG;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+ if (!mNode) {
+ MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
+ // NVDA expects this to succeed for MathML and won't call innerHTML if this
+ // fails. Therefore, return S_FALSE here instead of E_NOTIMPL, indicating
+ // that the attributes aren't present.
+ return S_FALSE;
+ }
+
+ if (!mNode->IsElement()) return S_FALSE;
+
+ dom::Element* domElement = mNode->AsElement();
+ nsNameSpaceManager* nameSpaceManager = nsNameSpaceManager::GetInstance();
+
+ int32_t index = 0;
+ for (index = 0; index < aMaxAttribs; index++) {
+ aAttribValues[index] = nullptr;
+ if (aAttribNames[index]) {
+ nsAutoString attributeValue, nameSpaceURI;
+ nsAutoString attributeName(
+ nsDependentString(static_cast<const wchar_t*>(aAttribNames[index])));
+
+ if (aNameSpaceID[index] > 0 &&
+ NS_SUCCEEDED(nameSpaceManager->GetNameSpaceURI(aNameSpaceID[index],
+ nameSpaceURI))) {
+ domElement->GetAttributeNS(nameSpaceURI, attributeName, attributeValue);
+ } else {
+ domElement->GetAttribute(attributeName, attributeValue);
+ }
+
+ aAttribValues[index] = ::SysAllocString(attributeValue.get());
+ }
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnAccessible::get_computedStyle(
+ unsigned short aMaxStyleProperties, boolean aUseAlternateView,
+ BSTR __RPC_FAR* aStyleProperties, BSTR __RPC_FAR* aStyleValues,
+ unsigned short __RPC_FAR* aNumStyleProperties) {
+ if (!aStyleProperties || aStyleValues || !aNumStyleProperties)
+ return E_INVALIDARG;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+ if (!mNode) {
+ MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
+ return E_NOTIMPL;
+ }
+
+ *aNumStyleProperties = 0;
+
+ if (mNode->IsDocument()) return S_FALSE;
+
+ nsCOMPtr<nsICSSDeclaration> cssDecl =
+ nsWinUtils::GetComputedStyleDeclaration(mNode->AsContent());
+ NS_ENSURE_TRUE(cssDecl, E_FAIL);
+
+ uint32_t length = cssDecl->Length();
+
+ uint32_t index = 0, realIndex = 0;
+ for (index = realIndex = 0; index < length && realIndex < aMaxStyleProperties;
+ index++) {
+ nsAutoCString property;
+ nsAutoCString value;
+
+ // Ignore -moz-* properties.
+ cssDecl->Item(index, property);
+ if (property.CharAt(0) != '-')
+ cssDecl->GetPropertyValue(property, value); // Get property value
+
+ if (!value.IsEmpty()) {
+ aStyleProperties[realIndex] =
+ ::SysAllocString(NS_ConvertUTF8toUTF16(property).get());
+ aStyleValues[realIndex] =
+ ::SysAllocString(NS_ConvertUTF8toUTF16(value).get());
+ ++realIndex;
+ }
+ }
+
+ *aNumStyleProperties = static_cast<unsigned short>(realIndex);
+
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnAccessible::get_computedStyleForProperties(
+ unsigned short aNumStyleProperties, boolean aUseAlternateView,
+ BSTR __RPC_FAR* aStyleProperties, BSTR __RPC_FAR* aStyleValues) {
+ if (!aStyleProperties || !aStyleValues) return E_INVALIDARG;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+ if (!mNode) {
+ MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
+ return E_NOTIMPL;
+ }
+
+ if (mNode->IsDocument()) return S_FALSE;
+
+ nsCOMPtr<nsICSSDeclaration> cssDecl =
+ nsWinUtils::GetComputedStyleDeclaration(mNode->AsContent());
+ NS_ENSURE_TRUE(cssDecl, E_FAIL);
+
+ uint32_t index = 0;
+ for (index = 0; index < aNumStyleProperties; index++) {
+ nsAutoCString value;
+ if (aStyleProperties[index])
+ cssDecl->GetPropertyValue(
+ NS_ConvertUTF16toUTF8(nsDependentString(aStyleProperties[index])),
+ value); // Get property value
+ aStyleValues[index] = ::SysAllocString(NS_ConvertUTF8toUTF16(value).get());
+ }
+
+ return S_OK;
+}
+
+// XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294.
+MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP
+sdnAccessible::scrollTo(boolean aScrollTopLeft) {
+ if (IsDefunct()) {
+ return CO_E_OBJNOTCONNECTED;
+ }
+ if (!mNode) {
+ MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
+ return E_NOTIMPL;
+ }
+
+ DocAccessible* document = GetDocument();
+ MOZ_ASSERT(document);
+ if (!mNode->IsContent()) return S_FALSE;
+
+ uint32_t scrollType = aScrollTopLeft
+ ? nsIAccessibleScrollType::SCROLL_TYPE_TOP_LEFT
+ : nsIAccessibleScrollType::SCROLL_TYPE_BOTTOM_RIGHT;
+
+ RefPtr<PresShell> presShell = document->PresShellPtr();
+ nsCOMPtr<nsIContent> content = mNode->AsContent();
+ nsCoreUtils::ScrollTo(presShell, content, scrollType);
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnAccessible::get_parentNode(ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode) {
+ if (!aNode) return E_INVALIDARG;
+ *aNode = nullptr;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+ if (!mNode) {
+ MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
+ return E_NOTIMPL;
+ }
+
+ nsINode* resultNode = mNode->GetParentNode();
+ if (resultNode) {
+ *aNode = static_cast<ISimpleDOMNode*>(new sdnAccessible(resultNode));
+ (*aNode)->AddRef();
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnAccessible::get_firstChild(ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode) {
+ if (!aNode) return E_INVALIDARG;
+ *aNode = nullptr;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+ if (!mNode) {
+ MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
+ return E_NOTIMPL;
+ }
+
+ nsINode* resultNode = mNode->GetFirstChild();
+ if (resultNode) {
+ *aNode = static_cast<ISimpleDOMNode*>(new sdnAccessible(resultNode));
+ (*aNode)->AddRef();
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnAccessible::get_lastChild(ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode) {
+ if (!aNode) return E_INVALIDARG;
+ *aNode = nullptr;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+ if (!mNode) {
+ MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
+ return E_NOTIMPL;
+ }
+
+ nsINode* resultNode = mNode->GetLastChild();
+ if (resultNode) {
+ *aNode = static_cast<ISimpleDOMNode*>(new sdnAccessible(resultNode));
+ (*aNode)->AddRef();
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnAccessible::get_previousSibling(ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode) {
+ if (!aNode) return E_INVALIDARG;
+ *aNode = nullptr;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+ if (!mNode) {
+ MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
+ return E_NOTIMPL;
+ }
+
+ nsINode* resultNode = mNode->GetPreviousSibling();
+ if (resultNode) {
+ *aNode = static_cast<ISimpleDOMNode*>(new sdnAccessible(resultNode));
+ (*aNode)->AddRef();
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnAccessible::get_nextSibling(ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode) {
+ if (!aNode) return E_INVALIDARG;
+ *aNode = nullptr;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+ if (!mNode) {
+ MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
+ return E_NOTIMPL;
+ }
+
+ nsINode* resultNode = mNode->GetNextSibling();
+ if (resultNode) {
+ *aNode = static_cast<ISimpleDOMNode*>(new sdnAccessible(resultNode));
+ (*aNode)->AddRef();
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnAccessible::get_childAt(unsigned aChildIndex,
+ ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode) {
+ if (!aNode) return E_INVALIDARG;
+ *aNode = nullptr;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+ if (!mNode) {
+ MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
+ return E_NOTIMPL;
+ }
+
+ nsINode* resultNode = mNode->GetChildAt_Deprecated(aChildIndex);
+ if (resultNode) {
+ *aNode = static_cast<ISimpleDOMNode*>(new sdnAccessible(resultNode));
+ (*aNode)->AddRef();
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnAccessible::get_innerHTML(BSTR __RPC_FAR* aInnerHTML) {
+ if (!aInnerHTML) return E_INVALIDARG;
+ *aInnerHTML = nullptr;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+
+ nsAutoString innerHTML;
+ if (!mNode) {
+ RemoteAccessible* remoteAcc = mMsaa->Acc()->AsRemote();
+ MOZ_ASSERT(remoteAcc);
+ if (!remoteAcc->mCachedFields) {
+ return S_FALSE;
+ }
+ remoteAcc->mCachedFields->GetAttribute(CacheKey::InnerHTML, innerHTML);
+ } else {
+ if (!mNode->IsElement()) {
+ return S_FALSE;
+ }
+ mNode->AsElement()->GetInnerHTML(innerHTML, IgnoreErrors());
+ }
+
+ if (innerHTML.IsEmpty()) return S_FALSE;
+
+ *aInnerHTML = ::SysAllocStringLen(innerHTML.get(), innerHTML.Length());
+ if (!*aInnerHTML) return E_OUTOFMEMORY;
+
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnAccessible::get_localInterface(void __RPC_FAR* __RPC_FAR* aLocalInterface) {
+ if (!aLocalInterface) return E_INVALIDARG;
+ *aLocalInterface = nullptr;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+
+ *aLocalInterface = this;
+ AddRef();
+
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnAccessible::get_language(BSTR __RPC_FAR* aLanguage) {
+ if (!aLanguage) return E_INVALIDARG;
+ *aLanguage = nullptr;
+
+ if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
+ if (!mNode) {
+ MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
+ return E_NOTIMPL;
+ }
+
+ nsAutoString language;
+ if (mNode->IsContent())
+ nsCoreUtils::GetLanguageFor(mNode->AsContent(), nullptr, language);
+ if (language.IsEmpty()) { // Nothing found, so use document's language
+ mNode->OwnerDoc()->GetHeaderData(nsGkAtoms::headerContentLanguage,
+ language);
+ }
+
+ if (language.IsEmpty()) return S_FALSE;
+
+ *aLanguage = ::SysAllocStringLen(language.get(), language.Length());
+ if (!*aLanguage) return E_OUTOFMEMORY;
+
+ return S_OK;
+}
diff --git a/accessible/windows/sdn/sdnAccessible.h b/accessible/windows/sdn/sdnAccessible.h
new file mode 100644
index 0000000000..610a4ca5de
--- /dev/null
+++ b/accessible/windows/sdn/sdnAccessible.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_sdnAccessible_h_
+#define mozilla_a11y_sdnAccessible_h_
+
+#include "ISimpleDOM.h"
+#include "AccessibleWrap.h"
+#include "IUnknownImpl.h"
+#include "MsaaAccessible.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NotNull.h"
+
+namespace mozilla {
+namespace a11y {
+
+class sdnAccessible final : public ISimpleDOMNode {
+ public:
+ explicit sdnAccessible(nsINode* aNode) : mNode(aNode) {
+ if (!mNode) MOZ_CRASH();
+ }
+
+ explicit sdnAccessible(NotNull<MsaaAccessible*> aMsaa) : mMsaa(aMsaa) {
+ Accessible* acc = aMsaa->Acc();
+ MOZ_ASSERT(acc);
+ if (LocalAccessible* localAcc = acc->AsLocal()) {
+ mNode = localAcc->GetNode();
+ }
+ }
+
+ ~sdnAccessible();
+
+ /**
+ * Return if the object is defunct.
+ */
+ bool IsDefunct() const {
+ if (mMsaa && !mMsaa->Acc()) {
+ return true;
+ }
+ if (!mNode) {
+ MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
+ return false;
+ }
+ return !GetDocument();
+ }
+
+ /**
+ * Return a local document accessible it belongs to if any.
+ */
+ DocAccessible* GetDocument() const;
+
+ /*
+ * Return associated MsaaAccessible if any.
+ */
+ MsaaAccessible* GetMsaa();
+
+ void SetUniqueID(uint32_t aNewUniqueId) { mUniqueId = Some(aNewUniqueId); }
+
+ Maybe<uint32_t> ReleaseUniqueID() {
+ Maybe<uint32_t> result = mUniqueId;
+ mUniqueId = Nothing();
+ return result;
+ }
+
+ // IUnknown
+ DECL_IUNKNOWN
+
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_nodeInfo(
+ /* [out] */ BSTR __RPC_FAR* aNodeName,
+ /* [out] */ short __RPC_FAR* aNameSpaceID,
+ /* [out] */ BSTR __RPC_FAR* aNodeValue,
+ /* [out] */ unsigned int __RPC_FAR* aNumChildren,
+ /* [out] */ unsigned int __RPC_FAR* aUniqueID,
+ /* [out][retval] */ unsigned short __RPC_FAR* aNodeType);
+
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_attributes(
+ /* [in] */ unsigned short aMaxAttribs,
+ /* [length_is][size_is][out] */ BSTR __RPC_FAR* aAttribNames,
+ /* [length_is][size_is][out] */ short __RPC_FAR* aNameSpaceIDs,
+ /* [length_is][size_is][out] */ BSTR __RPC_FAR* aAttribValues,
+ /* [out][retval] */ unsigned short __RPC_FAR* aNumAttribs);
+
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_attributesForNames(
+ /* [in] */ unsigned short aMaxAttribs,
+ /* [length_is][size_is][in] */ BSTR __RPC_FAR* aAttribNames,
+ /* [length_is][size_is][in] */ short __RPC_FAR* aNameSpaceID,
+ /* [length_is][size_is][retval] */ BSTR __RPC_FAR* aAttribValues);
+
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_computedStyle(
+ /* [in] */ unsigned short aMaxStyleProperties,
+ /* [in] */ boolean aUseAlternateView,
+ /* [length_is][size_is][out] */ BSTR __RPC_FAR* aStyleProperties,
+ /* [length_is][size_is][out] */ BSTR __RPC_FAR* aStyleValues,
+ /* [out][retval] */ unsigned short __RPC_FAR* aNumStyleProperties);
+
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE
+ get_computedStyleForProperties(
+ /* [in] */ unsigned short aNumStyleProperties,
+ /* [in] */ boolean aUseAlternateView,
+ /* [length_is][size_is][in] */ BSTR __RPC_FAR* aStyleProperties,
+ /* [length_is][size_is][out][retval] */ BSTR __RPC_FAR* aStyleValues);
+
+ virtual HRESULT STDMETHODCALLTYPE scrollTo(/* [in] */ boolean aScrollTopLeft);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_parentNode(
+ /* [out][retval] */ ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_firstChild(
+ /* [out][retval] */ ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_lastChild(
+ /* [out][retval] */ ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_previousSibling(
+ /* [out][retval] */ ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_nextSibling(
+ /* [out][retval] */ ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_childAt(
+ /* [in] */ unsigned aChildIndex,
+ /* [out][retval] */ ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_innerHTML(
+ /* [out][retval] */ BSTR __RPC_FAR* aInnerHTML);
+
+ virtual /* [local][propget] */ HRESULT STDMETHODCALLTYPE get_localInterface(
+ /* [retval][out] */ void __RPC_FAR* __RPC_FAR* aLocalInterface);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_language(
+ /* [out][retval] */ BSTR __RPC_FAR* aLanguage);
+
+ private:
+ // mNode will be null for a RemoteAccessible. In that case, we only partially
+ // implement this interface using data from the RemoteAccessible cache.
+ nsCOMPtr<nsINode> mNode;
+ RefPtr<MsaaAccessible> mMsaa;
+ Maybe<uint32_t> mUniqueId;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_sdnAccessible_h_
diff --git a/accessible/windows/sdn/sdnDocAccessible.cpp b/accessible/windows/sdn/sdnDocAccessible.cpp
new file mode 100644
index 0000000000..e773f1a5a0
--- /dev/null
+++ b/accessible/windows/sdn/sdnDocAccessible.cpp
@@ -0,0 +1,117 @@
+/* -*- 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 "sdnDocAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "ISimpleDOM.h"
+
+#include "nsNameSpaceManager.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// sdnDocAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+IMPL_IUNKNOWN_QUERY_HEAD(sdnDocAccessible)
+IMPL_IUNKNOWN_QUERY_IFACE(ISimpleDOMDocument)
+IMPL_IUNKNOWN_QUERY_TAIL_AGGREGATED(mMsaa)
+
+STDMETHODIMP
+sdnDocAccessible::get_URL(BSTR __RPC_FAR* aURL) {
+ if (!aURL) return E_INVALIDARG;
+ *aURL = nullptr;
+
+ DocAccessible* acc = mMsaa->DocAcc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ nsAutoString URL;
+ acc->URL(URL);
+ if (URL.IsEmpty()) return S_FALSE;
+
+ *aURL = ::SysAllocStringLen(URL.get(), URL.Length());
+ return *aURL ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+sdnDocAccessible::get_title(BSTR __RPC_FAR* aTitle) {
+ if (!aTitle) return E_INVALIDARG;
+ *aTitle = nullptr;
+
+ DocAccessible* acc = mMsaa->DocAcc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ nsAutoString title;
+ acc->Title(title);
+ *aTitle = ::SysAllocStringLen(title.get(), title.Length());
+ return *aTitle ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+sdnDocAccessible::get_mimeType(BSTR __RPC_FAR* aMimeType) {
+ if (!aMimeType) return E_INVALIDARG;
+ *aMimeType = nullptr;
+
+ DocAccessible* acc = mMsaa->DocAcc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ nsAutoString mimeType;
+ acc->MimeType(mimeType);
+ if (mimeType.IsEmpty()) return S_FALSE;
+
+ *aMimeType = ::SysAllocStringLen(mimeType.get(), mimeType.Length());
+ return *aMimeType ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+sdnDocAccessible::get_docType(BSTR __RPC_FAR* aDocType) {
+ if (!aDocType) return E_INVALIDARG;
+ *aDocType = nullptr;
+
+ DocAccessible* acc = mMsaa->DocAcc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ nsAutoString docType;
+ acc->DocType(docType);
+ if (docType.IsEmpty()) return S_FALSE;
+
+ *aDocType = ::SysAllocStringLen(docType.get(), docType.Length());
+ return *aDocType ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+sdnDocAccessible::get_nameSpaceURIForID(short aNameSpaceID,
+ BSTR __RPC_FAR* aNameSpaceURI) {
+ if (!aNameSpaceURI) return E_INVALIDARG;
+ *aNameSpaceURI = nullptr;
+
+ if (!mMsaa->DocAcc()) return CO_E_OBJNOTCONNECTED;
+
+ if (aNameSpaceID < 0) return E_INVALIDARG; // -1 is kNameSpaceID_Unknown
+
+ nsAutoString nameSpaceURI;
+ nsNameSpaceManager* nameSpaceManager = nsNameSpaceManager::GetInstance();
+ if (nameSpaceManager)
+ nameSpaceManager->GetNameSpaceURI(aNameSpaceID, nameSpaceURI);
+
+ if (nameSpaceURI.IsEmpty()) return S_FALSE;
+
+ *aNameSpaceURI =
+ ::SysAllocStringLen(nameSpaceURI.get(), nameSpaceURI.Length());
+
+ return *aNameSpaceURI ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+sdnDocAccessible::put_alternateViewMediaTypes(
+ BSTR __RPC_FAR* aCommaSeparatedMediaTypes) {
+ if (!aCommaSeparatedMediaTypes) return E_INVALIDARG;
+ *aCommaSeparatedMediaTypes = nullptr;
+
+ return !mMsaa->DocAcc() ? CO_E_OBJNOTCONNECTED : E_NOTIMPL;
+}
diff --git a/accessible/windows/sdn/sdnDocAccessible.h b/accessible/windows/sdn/sdnDocAccessible.h
new file mode 100644
index 0000000000..452b43ec85
--- /dev/null
+++ b/accessible/windows/sdn/sdnDocAccessible.h
@@ -0,0 +1,52 @@
+/* -*- 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_sdnDocAccessible_h_
+#define mozilla_a11y_sdnDocAccessible_h_
+
+#include "ISimpleDOM.h"
+#include "IUnknownImpl.h"
+
+#include "MsaaDocAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+class sdnDocAccessible final : public ISimpleDOMDocument {
+ public:
+ explicit sdnDocAccessible(MsaaDocAccessible* aMsaa) : mMsaa(aMsaa){};
+ ~sdnDocAccessible(){};
+
+ DECL_IUNKNOWN
+
+ // ISimpleDOMDocument
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_URL(
+ /* [out] */ BSTR __RPC_FAR* url);
+
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_title(
+ /* [out] */ BSTR __RPC_FAR* title);
+
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_mimeType(
+ /* [out] */ BSTR __RPC_FAR* mimeType);
+
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_docType(
+ /* [out] */ BSTR __RPC_FAR* docType);
+
+ virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_nameSpaceURIForID(
+ /* [in] */ short nameSpaceID,
+ /* [out] */ BSTR __RPC_FAR* nameSpaceURI);
+
+ virtual /* [id] */ HRESULT STDMETHODCALLTYPE put_alternateViewMediaTypes(
+ /* [in] */ BSTR __RPC_FAR* commaSeparatedMediaTypes);
+
+ protected:
+ RefPtr<MsaaDocAccessible> mMsaa;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/sdn/sdnTextAccessible.cpp b/accessible/windows/sdn/sdnTextAccessible.cpp
new file mode 100644
index 0000000000..73ef4eb6e8
--- /dev/null
+++ b/accessible/windows/sdn/sdnTextAccessible.cpp
@@ -0,0 +1,166 @@
+/* -*- 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 "sdnTextAccessible.h"
+
+#include "ISimpleDOM.h"
+
+#include "nsCoreUtils.h"
+#include "DocAccessible.h"
+
+#include "nsIFrame.h"
+#include "nsFontMetrics.h"
+#include "nsPresContext.h"
+#include "nsLayoutUtils.h"
+#include "nsRange.h"
+#include "gfxTextRun.h"
+#include "nsIAccessibleTypes.h"
+#include "mozilla/gfx/2D.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// sdnTextAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+IMPL_IUNKNOWN_QUERY_HEAD(sdnTextAccessible)
+IMPL_IUNKNOWN_QUERY_IFACE(ISimpleDOMText)
+IMPL_IUNKNOWN_QUERY_TAIL_AGGREGATED(mMsaa)
+
+STDMETHODIMP
+sdnTextAccessible::get_domText(BSTR __RPC_FAR* aText) {
+ if (!aText) return E_INVALIDARG;
+ *aText = nullptr;
+
+ AccessibleWrap* acc = mMsaa->LocalAcc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ nsAutoString nodeValue;
+
+ acc->GetContent()->GetNodeValue(nodeValue);
+ if (nodeValue.IsEmpty()) return S_FALSE;
+
+ *aText = ::SysAllocStringLen(nodeValue.get(), nodeValue.Length());
+ return *aText ? S_OK : E_OUTOFMEMORY;
+}
+
+STDMETHODIMP
+sdnTextAccessible::get_clippedSubstringBounds(
+ unsigned int aStartIndex, unsigned int aEndIndex, int __RPC_FAR* aX,
+ int __RPC_FAR* aY, int __RPC_FAR* aWidth, int __RPC_FAR* aHeight) {
+ nscoord x = 0, y = 0, width = 0, height = 0;
+ HRESULT rv = get_unclippedSubstringBounds(aStartIndex, aEndIndex, &x, &y,
+ &width, &height);
+ if (FAILED(rv)) return rv;
+
+ DocAccessible* document = mMsaa->LocalAcc()->Document();
+ NS_ASSERTION(
+ document,
+ "There must always be a doc accessible, but there isn't. Crash!");
+
+ LayoutDeviceIntRect docRect = document->Bounds();
+ LayoutDeviceIntRect unclippedRect(x, y, width, height);
+
+ LayoutDeviceIntRect clippedRect;
+ clippedRect.IntersectRect(unclippedRect, docRect);
+
+ *aX = clippedRect.X();
+ *aY = clippedRect.Y();
+ *aWidth = clippedRect.Width();
+ *aHeight = clippedRect.Height();
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnTextAccessible::get_unclippedSubstringBounds(
+ unsigned int aStartIndex, unsigned int aEndIndex, int __RPC_FAR* aX,
+ int __RPC_FAR* aY, int __RPC_FAR* aWidth, int __RPC_FAR* aHeight) {
+ if (!aX || !aY || !aWidth || !aHeight) return E_INVALIDARG;
+ *aX = *aY = *aWidth = *aHeight = 0;
+
+ AccessibleWrap* acc = mMsaa->LocalAcc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ nsIFrame* frame = acc->GetFrame();
+ NS_ENSURE_TRUE(frame, E_FAIL);
+
+ nsPoint startPoint, endPoint;
+ nsIFrame* startFrame =
+ GetPointFromOffset(frame, aStartIndex, true, startPoint);
+ nsIFrame* endFrame = GetPointFromOffset(frame, aEndIndex, false, endPoint);
+ if (!startFrame || !endFrame) return E_FAIL;
+
+ nsRect sum;
+ nsIFrame* iter = startFrame;
+ nsIFrame* stopLoopFrame = endFrame->GetNextContinuation();
+ for (; iter != stopLoopFrame; iter = iter->GetNextContinuation()) {
+ nsRect rect = iter->GetScreenRectInAppUnits();
+ nscoord start = (iter == startFrame) ? startPoint.x : 0;
+ nscoord end = (iter == endFrame) ? endPoint.x : rect.Width();
+ rect.MoveByX(start);
+ rect.SetWidth(end - start);
+ sum.UnionRect(sum, rect);
+ }
+
+ nsPresContext* presContext = acc->Document()->PresContext();
+ *aX = presContext->AppUnitsToDevPixels(sum.X());
+ *aY = presContext->AppUnitsToDevPixels(sum.Y());
+ *aWidth = presContext->AppUnitsToDevPixels(sum.Width());
+ *aHeight = presContext->AppUnitsToDevPixels(sum.Height());
+
+ return S_OK;
+}
+
+STDMETHODIMP
+sdnTextAccessible::scrollToSubstring(unsigned int aStartIndex,
+ unsigned int aEndIndex) {
+ AccessibleWrap* acc = mMsaa->LocalAcc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ RefPtr<nsRange> range = nsRange::Create(acc->GetContent());
+ if (NS_FAILED(range->SetStart(acc->GetContent(), aStartIndex))) return E_FAIL;
+
+ if (NS_FAILED(range->SetEnd(acc->GetContent(), aEndIndex))) return E_FAIL;
+
+ nsresult rv = nsCoreUtils::ScrollSubstringTo(
+ acc->GetFrame(), range, nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE);
+ return GetHRESULT(rv);
+}
+
+STDMETHODIMP
+sdnTextAccessible::get_fontFamily(BSTR __RPC_FAR* aFontFamily) {
+ if (!aFontFamily) return E_INVALIDARG;
+ *aFontFamily = nullptr;
+
+ AccessibleWrap* acc = mMsaa->LocalAcc();
+ if (!acc) return CO_E_OBJNOTCONNECTED;
+
+ nsIFrame* frame = acc->GetFrame();
+ if (!frame) return E_FAIL;
+
+ RefPtr<nsFontMetrics> fm = nsLayoutUtils::GetFontMetricsForFrame(frame, 1.0f);
+ RefPtr<gfxFont> font = fm->GetThebesFontGroup()->GetFirstValidFont();
+
+ const nsCString& name = font->GetName();
+ if (name.IsEmpty()) return S_FALSE;
+
+ NS_ConvertUTF8toUTF16 str(name);
+ *aFontFamily = ::SysAllocStringLen(str.get(), str.Length());
+ return *aFontFamily ? S_OK : E_OUTOFMEMORY;
+}
+
+nsIFrame* sdnTextAccessible::GetPointFromOffset(nsIFrame* aContainingFrame,
+ int32_t aOffset,
+ bool aPreferNext,
+ nsPoint& aOutPoint) {
+ nsIFrame* textFrame = nullptr;
+ int32_t outOffset;
+ aContainingFrame->GetChildFrameContainingOffset(aOffset, aPreferNext,
+ &outOffset, &textFrame);
+ if (textFrame) textFrame->GetPointFromOffset(aOffset, &aOutPoint);
+
+ return textFrame;
+}
diff --git a/accessible/windows/sdn/sdnTextAccessible.h b/accessible/windows/sdn/sdnTextAccessible.h
new file mode 100644
index 0000000000..3f93c20fcc
--- /dev/null
+++ b/accessible/windows/sdn/sdnTextAccessible.h
@@ -0,0 +1,69 @@
+/* -*- 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_sdnTextAccessible_h_
+#define mozilla_a11y_sdnTextAccessible_h_
+
+#include "ISimpleDOM.h"
+#include "IUnknownImpl.h"
+
+#include "MsaaAccessible.h"
+
+class nsIFrame;
+struct nsPoint;
+
+namespace mozilla {
+namespace a11y {
+
+class sdnTextAccessible final : public ISimpleDOMText {
+ public:
+ explicit sdnTextAccessible(MsaaAccessible* aMsaa) : mMsaa(aMsaa){};
+ ~sdnTextAccessible() {}
+
+ DECL_IUNKNOWN
+
+ // ISimpleDOMText
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_domText(
+ /* [retval][out] */ BSTR __RPC_FAR* aText);
+
+ virtual HRESULT STDMETHODCALLTYPE get_clippedSubstringBounds(
+ /* [in] */ unsigned int startIndex,
+ /* [in] */ unsigned int endIndex,
+ /* [out] */ int __RPC_FAR* aX,
+ /* [out] */ int __RPC_FAR* aY,
+ /* [out] */ int __RPC_FAR* aWidth,
+ /* [out] */ int __RPC_FAR* aHeight);
+
+ virtual HRESULT STDMETHODCALLTYPE get_unclippedSubstringBounds(
+ /* [in] */ unsigned int aStartIndex,
+ /* [in] */ unsigned int aEndIndex,
+ /* [out] */ int __RPC_FAR* aX,
+ /* [out] */ int __RPC_FAR* aY,
+ /* [out] */ int __RPC_FAR* aWidth,
+ /* [out] */ int __RPC_FAR* aHeight);
+
+ virtual HRESULT STDMETHODCALLTYPE scrollToSubstring(
+ /* [in] */ unsigned int aStartIndex,
+ /* [in] */ unsigned int aEndIndex);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_fontFamily(
+ /* [retval][out] */ BSTR __RPC_FAR* aFontFamily);
+
+ private:
+ /**
+ * Return child frame containing offset on success.
+ */
+ nsIFrame* GetPointFromOffset(nsIFrame* aContainingFrame, int32_t aOffset,
+ bool aPreferNext, nsPoint& aOutPoint);
+
+ RefPtr<MsaaAccessible> mMsaa;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/windows/uia/moz.build b/accessible/windows/uia/moz.build
new file mode 100644
index 0000000000..058aacc579
--- /dev/null
+++ b/accessible/windows/uia/moz.build
@@ -0,0 +1,22 @@
+# -*- 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/.
+
+SOURCES += [
+ "uiaRawElmProvider.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/generic",
+ "/accessible/html",
+ "/accessible/windows/msaa",
+ "/accessible/xpcom",
+ "/accessible/xul",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/accessible/windows/uia/uiaRawElmProvider.cpp b/accessible/windows/uia/uiaRawElmProvider.cpp
new file mode 100644
index 0000000000..c78dbca7fe
--- /dev/null
+++ b/accessible/windows/uia/uiaRawElmProvider.cpp
@@ -0,0 +1,206 @@
+/* -*- 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 "uiaRawElmProvider.h"
+
+#include "AccAttributes.h"
+#include "AccessibleWrap.h"
+#include "ARIAMap.h"
+#include "LocalAccessible-inl.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// uiaRawElmProvider
+////////////////////////////////////////////////////////////////////////////////
+
+IMPL_IUNKNOWN2(uiaRawElmProvider, IAccessibleEx, IRawElementProviderSimple)
+
+////////////////////////////////////////////////////////////////////////////////
+// IAccessibleEx
+
+STDMETHODIMP
+uiaRawElmProvider::GetObjectForChild(
+ long aIdChild, __RPC__deref_out_opt IAccessibleEx** aAccEx) {
+ if (!aAccEx) return E_INVALIDARG;
+
+ *aAccEx = nullptr;
+
+ return mAcc->IsDefunct() ? CO_E_OBJNOTCONNECTED : S_OK;
+}
+
+STDMETHODIMP
+uiaRawElmProvider::GetIAccessiblePair(__RPC__deref_out_opt IAccessible** aAcc,
+ __RPC__out long* aIdChild) {
+ if (!aAcc || !aIdChild) return E_INVALIDARG;
+
+ *aAcc = nullptr;
+ *aIdChild = 0;
+
+ if (mAcc->IsDefunct()) return CO_E_OBJNOTCONNECTED;
+
+ *aIdChild = CHILDID_SELF;
+ RefPtr<IAccessible> copy;
+ mAcc->GetNativeInterface(getter_AddRefs(copy));
+ copy.forget(aAcc);
+
+ return S_OK;
+}
+
+STDMETHODIMP
+uiaRawElmProvider::GetRuntimeId(__RPC__deref_out_opt SAFEARRAY** aRuntimeIds) {
+ if (!aRuntimeIds) return E_INVALIDARG;
+
+ int ids[] = {UiaAppendRuntimeId,
+ static_cast<int>(reinterpret_cast<intptr_t>(mAcc->UniqueID()))};
+ *aRuntimeIds = SafeArrayCreateVector(VT_I4, 0, 2);
+ if (!*aRuntimeIds) return E_OUTOFMEMORY;
+
+ for (LONG i = 0; i < (LONG)ArrayLength(ids); i++)
+ SafeArrayPutElement(*aRuntimeIds, &i, (void*)&(ids[i]));
+
+ return S_OK;
+}
+
+STDMETHODIMP
+uiaRawElmProvider::ConvertReturnedElement(
+ __RPC__in_opt IRawElementProviderSimple* aRawElmProvider,
+ __RPC__deref_out_opt IAccessibleEx** aAccEx) {
+ if (!aRawElmProvider || !aAccEx) return E_INVALIDARG;
+
+ *aAccEx = nullptr;
+
+ void* instancePtr = nullptr;
+ HRESULT hr = aRawElmProvider->QueryInterface(IID_IAccessibleEx, &instancePtr);
+ if (SUCCEEDED(hr)) *aAccEx = static_cast<IAccessibleEx*>(instancePtr);
+
+ return hr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// IRawElementProviderSimple
+
+STDMETHODIMP
+uiaRawElmProvider::get_ProviderOptions(
+ __RPC__out enum ProviderOptions* aOptions) {
+ if (!aOptions) return E_INVALIDARG;
+
+ // This method is not used with IAccessibleEx implementations.
+ *aOptions = ProviderOptions_ServerSideProvider;
+ return S_OK;
+}
+
+STDMETHODIMP
+uiaRawElmProvider::GetPatternProvider(
+ PATTERNID aPatternId, __RPC__deref_out_opt IUnknown** aPatternProvider) {
+ if (!aPatternProvider) return E_INVALIDARG;
+
+ *aPatternProvider = nullptr;
+ return S_OK;
+}
+
+STDMETHODIMP
+uiaRawElmProvider::GetPropertyValue(PROPERTYID aPropertyId,
+ __RPC__out VARIANT* aPropertyValue) {
+ if (!aPropertyValue) return E_INVALIDARG;
+
+ if (mAcc->IsDefunct()) return CO_E_OBJNOTCONNECTED;
+
+ aPropertyValue->vt = VT_EMPTY;
+
+ switch (aPropertyId) {
+ // Accelerator Key / shortcut.
+ case UIA_AcceleratorKeyPropertyId: {
+ nsAutoString keyString;
+
+ mAcc->KeyboardShortcut().ToString(keyString);
+
+ if (!keyString.IsEmpty()) {
+ aPropertyValue->vt = VT_BSTR;
+ aPropertyValue->bstrVal = ::SysAllocString(keyString.get());
+ return S_OK;
+ }
+
+ break;
+ }
+
+ // Access Key / mneumonic.
+ case UIA_AccessKeyPropertyId: {
+ nsAutoString keyString;
+
+ mAcc->AccessKey().ToString(keyString);
+
+ if (!keyString.IsEmpty()) {
+ aPropertyValue->vt = VT_BSTR;
+ aPropertyValue->bstrVal = ::SysAllocString(keyString.get());
+ return S_OK;
+ }
+
+ break;
+ }
+
+ // ARIA Role / shortcut
+ case UIA_AriaRolePropertyId: {
+ nsAutoString xmlRoles;
+
+ RefPtr<AccAttributes> attributes = mAcc->Attributes();
+ attributes->GetAttribute(nsGkAtoms::xmlroles, xmlRoles);
+
+ if (!xmlRoles.IsEmpty()) {
+ aPropertyValue->vt = VT_BSTR;
+ aPropertyValue->bstrVal = ::SysAllocString(xmlRoles.get());
+ return S_OK;
+ }
+
+ break;
+ }
+
+ // ARIA Properties
+ case UIA_AriaPropertiesPropertyId: {
+ nsAutoString ariaProperties;
+
+ aria::AttrIterator attribIter(mAcc->GetContent());
+ while (attribIter.Next()) {
+ nsAutoString attribName, attribValue;
+ nsAutoString value;
+ attribIter.AttrName()->ToString(attribName);
+ attribIter.AttrValue(attribValue);
+ if (StringBeginsWith(attribName, u"aria-"_ns)) {
+ // Found 'aria-'
+ attribName.ReplaceLiteral(0, 5, u"");
+ }
+
+ ariaProperties.Append(attribName);
+ ariaProperties.Append('=');
+ ariaProperties.Append(attribValue);
+ ariaProperties.Append(';');
+ }
+
+ if (!ariaProperties.IsEmpty()) {
+ // remove last delimiter:
+ ariaProperties.Truncate(ariaProperties.Length() - 1);
+ aPropertyValue->vt = VT_BSTR;
+ aPropertyValue->bstrVal = ::SysAllocString(ariaProperties.get());
+ return S_OK;
+ }
+
+ break;
+ }
+ }
+
+ return S_OK;
+}
+
+STDMETHODIMP
+uiaRawElmProvider::get_HostRawElementProvider(
+ __RPC__deref_out_opt IRawElementProviderSimple** aRawElmProvider) {
+ if (!aRawElmProvider) return E_INVALIDARG;
+
+ // This method is not used with IAccessibleEx implementations.
+ *aRawElmProvider = nullptr;
+ return S_OK;
+}
diff --git a/accessible/windows/uia/uiaRawElmProvider.h b/accessible/windows/uia/uiaRawElmProvider.h
new file mode 100644
index 0000000000..4a4aecdbe2
--- /dev/null
+++ b/accessible/windows/uia/uiaRawElmProvider.h
@@ -0,0 +1,75 @@
+/* -*- 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_uiaRawElmProvider_h__
+#define mozilla_a11y_uiaRawElmProvider_h__
+
+#include "objbase.h"
+#include "AccessibleWrap.h"
+#include "IUnknownImpl.h"
+#include "uiautomation.h"
+
+namespace mozilla {
+namespace a11y {
+
+class AccessibleWrap;
+
+/**
+ * IRawElementProviderSimple implementation (maintains IAccessibleEx approach).
+ */
+class uiaRawElmProvider final : public IAccessibleEx,
+ public IRawElementProviderSimple {
+ public:
+ explicit uiaRawElmProvider(AccessibleWrap* aAcc) : mAcc(aAcc) {}
+
+ // IUnknown
+ DECL_IUNKNOWN
+
+ // IAccessibleEx
+ virtual HRESULT STDMETHODCALLTYPE GetObjectForChild(
+ /* [in] */ long aIdChild,
+ /* [retval][out] */ __RPC__deref_out_opt IAccessibleEx** aAccEx);
+
+ virtual HRESULT STDMETHODCALLTYPE GetIAccessiblePair(
+ /* [out] */ __RPC__deref_out_opt IAccessible** aAcc,
+ /* [out] */ __RPC__out long* aIdChild);
+
+ virtual HRESULT STDMETHODCALLTYPE GetRuntimeId(
+ /* [retval][out] */ __RPC__deref_out_opt SAFEARRAY** aRuntimeIds);
+
+ virtual HRESULT STDMETHODCALLTYPE ConvertReturnedElement(
+ /* [in] */ __RPC__in_opt IRawElementProviderSimple* aRawElmProvider,
+ /* [out] */ __RPC__deref_out_opt IAccessibleEx** aAccEx);
+
+ // IRawElementProviderSimple
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_ProviderOptions(
+ /* [retval][out] */ __RPC__out enum ProviderOptions* aProviderOptions);
+
+ virtual HRESULT STDMETHODCALLTYPE GetPatternProvider(
+ /* [in] */ PATTERNID aPatternId,
+ /* [retval][out] */ __RPC__deref_out_opt IUnknown** aPatternProvider);
+
+ virtual HRESULT STDMETHODCALLTYPE GetPropertyValue(
+ /* [in] */ PROPERTYID aPropertyId,
+ /* [retval][out] */ __RPC__out VARIANT* aPropertyValue);
+
+ virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_HostRawElementProvider(
+ /* [retval][out] */ __RPC__deref_out_opt IRawElementProviderSimple**
+ aRawElmProvider);
+
+ private:
+ uiaRawElmProvider() = delete;
+ uiaRawElmProvider& operator=(const uiaRawElmProvider&) = delete;
+ uiaRawElmProvider(const uiaRawElmProvider&) = delete;
+
+ protected:
+ RefPtr<AccessibleWrap> mAcc;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xpcom/AccEventGen.py b/accessible/xpcom/AccEventGen.py
new file mode 100755
index 0000000000..791b57827d
--- /dev/null
+++ b/accessible/xpcom/AccEventGen.py
@@ -0,0 +1,256 @@
+#!/usr/bin/env 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/.
+
+import os
+
+import buildconfig
+import mozpack.path as mozpath
+from xpidl import xpidl
+
+# Load the webidl configuration file.
+glbl = {}
+exec(
+ open(
+ mozpath.join(buildconfig.topsrcdir, "dom", "bindings", "Bindings.conf")
+ ).read(),
+ glbl,
+)
+webidlconfig = glbl["DOMInterfaces"]
+
+# Instantiate the parser.
+p = xpidl.IDLParser()
+
+
+def findIDL(includePath, interfaceFileName):
+ for d in includePath:
+ path = mozpath.join(d, interfaceFileName)
+ if os.path.exists(path):
+ return path
+ raise BaseException(
+ "No IDL file found for interface %s "
+ "in include path %r" % (interfaceFileName, includePath)
+ )
+
+
+def loadEventIDL(parser, includePath, eventname):
+ eventidl = "nsIAccessible%s.idl" % eventname
+ idlFile = findIDL(includePath, eventidl)
+ idl = p.parse(open(idlFile).read(), idlFile)
+ idl.resolve(includePath, p, webidlconfig)
+ return idl, idlFile
+
+
+class Configuration:
+ def __init__(self, filename):
+ config = {}
+ exec(open(filename).read(), config)
+ self.simple_events = config.get("simple_events", [])
+
+
+def firstCap(str):
+ return str[0].upper() + str[1:]
+
+
+def writeAttributeParams(a):
+ return "%s a%s" % (a.realtype.nativeType("in"), firstCap(a.name))
+
+
+def print_header_file(fd, conf, incdirs):
+ idl_paths = set()
+
+ fd.write("/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n")
+ fd.write(
+ "#ifndef _mozilla_a11y_generated_AccEvents_h_\n"
+ "#define _mozilla_a11y_generated_AccEvents_h_\n\n"
+ )
+ fd.write('#include "nscore.h"\n')
+ fd.write('#include "nsCOMPtr.h"\n')
+ fd.write('#include "nsCycleCollectionParticipant.h"\n')
+ fd.write('#include "nsString.h"\n')
+ for e in conf.simple_events:
+ fd.write('#include "nsIAccessible%s.h"\n' % e)
+ for e in conf.simple_events:
+ idl, idl_path = loadEventIDL(p, incdirs, e)
+ idl_paths.add(idl_path)
+ for iface in filter(lambda p: p.kind == "interface", idl.productions):
+ classname = "xpcAcc%s" % e
+ baseinterfaces = interfaces(iface)
+
+ fd.write("\nclass %s final : public %s\n" % (classname, iface.name))
+ fd.write("{\n")
+ fd.write("public:\n")
+
+ attributes = allAttributes(iface)
+ args = map(writeAttributeParams, attributes)
+ fd.write(" %s(%s) :\n" % (classname, ", ".join(args)))
+
+ initializers = []
+ for a in attributes:
+ initializers.append("m%s(a%s)" % (firstCap(a.name), firstCap(a.name)))
+ fd.write(" %s\n {}\n\n" % ", ".join(initializers))
+ fd.write(" NS_DECL_CYCLE_COLLECTING_ISUPPORTS\n")
+ fd.write(" NS_DECL_CYCLE_COLLECTION_CLASS(%s)\n" % (classname))
+
+ for iface in filter(lambda i: i.name != "nsISupports", baseinterfaces):
+ fd.write(" NS_DECL_%s\n" % iface.name.upper())
+
+ fd.write("\nprivate:\n")
+ fd.write(" ~%s() {}\n\n" % classname)
+ for a in attributes:
+ fd.write(" %s\n" % attributeVariableTypeAndName(a))
+ fd.write("};\n\n")
+
+ fd.write("#endif\n")
+
+ return idl_paths
+
+
+def interfaceAttributeTypes(idl):
+ ifaces = filter(lambda p: p.kind == "interface", idl.productions)
+ attributes = []
+ for i in ifaces:
+ ifaceAttributes = allAttributes(i)
+ attributes.extend(ifaceAttributes)
+ ifaceAttrs = filter(lambda a: a.realtype.nativeType("in").endswith("*"), attributes)
+ return map(lambda a: a.realtype.nativeType("in").strip(" *"), ifaceAttrs)
+
+
+def print_cpp(idl, fd, conf, eventname):
+ for p in idl.productions:
+ if p.kind == "interface":
+ write_cpp(eventname, p, fd)
+
+
+def print_cpp_file(fd, conf, incdirs):
+ idl_paths = set()
+ fd.write("/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n\n")
+ fd.write('#include "xpcAccEvents.h"\n')
+
+ includes = []
+ for e in conf.simple_events:
+ if e not in includes:
+ includes.append(("nsIAccessible%s" % e))
+
+ types = []
+ for e in conf.simple_events:
+ idl, idl_path = loadEventIDL(p, incdirs, e)
+ idl_paths.add(idl_path)
+ types.extend(interfaceAttributeTypes(idl))
+
+ for c in types:
+ fd.write('#include "%s.h"\n' % c)
+
+ fd.write("\n")
+ for e in conf.simple_events:
+ idl, idl_path = loadEventIDL(p, incdirs, e)
+ idl_paths.add(idl_path)
+ print_cpp(idl, fd, conf, e)
+
+ return idl_paths
+
+
+def attributeVariableTypeAndName(a):
+ if a.realtype.nativeType("in").endswith("*"):
+ l = [
+ "nsCOMPtr<%s> m%s;"
+ % (a.realtype.nativeType("in").strip("* "), firstCap(a.name))
+ ]
+ elif a.realtype.nativeType("in").count("nsAString"):
+ l = ["nsString m%s;" % firstCap(a.name)]
+ elif a.realtype.nativeType("in").count("nsACString"):
+ l = ["nsCString m%s;" % firstCap(a.name)]
+ else:
+ l = ["%sm%s;" % (a.realtype.nativeType("in"), firstCap(a.name))]
+ return ", ".join(l)
+
+
+def writeAttributeGetter(fd, classname, a):
+ fd.write("NS_IMETHODIMP\n")
+ fd.write("%s::Get%s(" % (classname, firstCap(a.name)))
+ if a.realtype.nativeType("in").endswith("*"):
+ fd.write(
+ "%s** a%s" % (a.realtype.nativeType("in").strip("* "), firstCap(a.name))
+ )
+ elif a.realtype.nativeType("in").count("nsAString"):
+ fd.write("nsAString& a%s" % firstCap(a.name))
+ elif a.realtype.nativeType("in").count("nsACString"):
+ fd.write("nsACString& a%s" % firstCap(a.name))
+ else:
+ fd.write("%s*a%s" % (a.realtype.nativeType("in"), firstCap(a.name)))
+ fd.write(")\n")
+ fd.write("{\n")
+ if a.realtype.nativeType("in").endswith("*"):
+ fd.write(" NS_IF_ADDREF(*a%s = m%s);\n" % (firstCap(a.name), firstCap(a.name)))
+ elif a.realtype.nativeType("in").count("nsAString"):
+ fd.write(" a%s = m%s;\n" % (firstCap(a.name), firstCap(a.name)))
+ elif a.realtype.nativeType("in").count("nsACString"):
+ fd.write(" a%s = m%s;\n" % (firstCap(a.name), firstCap(a.name)))
+ else:
+ fd.write(" *a%s = m%s;\n" % (firstCap(a.name), firstCap(a.name)))
+ fd.write(" return NS_OK;\n")
+ fd.write("}\n\n")
+
+
+def interfaces(iface):
+ interfaces = []
+ while iface.base:
+ interfaces.append(iface)
+ iface = iface.idl.getName(xpidl.TypeId(iface.base), iface.location)
+ interfaces.append(iface)
+ interfaces.reverse()
+ return interfaces
+
+
+def allAttributes(iface):
+ attributes = []
+ for i in interfaces(iface):
+ attrs = filter(lambda m: isinstance(m, xpidl.Attribute), i.members)
+ attributes.extend(attrs)
+
+ return attributes
+
+
+def write_cpp(eventname, iface, fd):
+ classname = "xpcAcc%s" % eventname
+ attributes = allAttributes(iface)
+ ccattributes = filter(
+ lambda m: m.realtype.nativeType("in").endswith("*"), attributes
+ )
+ fd.write("NS_IMPL_CYCLE_COLLECTION(%s" % classname)
+ for c in ccattributes:
+ fd.write(", m%s" % firstCap(c.name))
+ fd.write(")\n\n")
+
+ fd.write("NS_IMPL_CYCLE_COLLECTING_ADDREF(%s)\n" % classname)
+ fd.write("NS_IMPL_CYCLE_COLLECTING_RELEASE(%s)\n\n" % classname)
+
+ fd.write("NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(%s)\n" % classname)
+ for baseiface in interfaces(iface):
+ fd.write(" NS_INTERFACE_MAP_ENTRY(%s)\n" % baseiface.name)
+ fd.write("NS_INTERFACE_MAP_END\n\n")
+
+ for a in attributes:
+ writeAttributeGetter(fd, classname, a)
+
+
+def get_conf(conf_file):
+ conf = Configuration(conf_file)
+ inc_dir = [
+ mozpath.join(buildconfig.topsrcdir, "accessible", "interfaces"),
+ mozpath.join(buildconfig.topsrcdir, "xpcom", "base"),
+ ]
+ return conf, inc_dir
+
+
+def gen_files(fd, conf_file):
+ deps = set()
+ conf, inc_dir = get_conf(conf_file)
+ deps.update(print_header_file(fd, conf, inc_dir))
+ with open(
+ os.path.join(os.path.dirname(fd.name), "xpcAccEvents.cpp"), "w"
+ ) as cpp_fd:
+ deps.update(print_cpp_file(cpp_fd, conf, inc_dir))
+ return deps
diff --git a/accessible/xpcom/AccEvents.conf b/accessible/xpcom/AccEvents.conf
new file mode 100644
index 0000000000..682559a3c6
--- /dev/null
+++ b/accessible/xpcom/AccEvents.conf
@@ -0,0 +1,20 @@
+""" -*- Mode: 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/.
+
+ The name of the event which real interface should have nsIAccessible-prefix,
+ and should be in nsIAccessible<name>.idl file"""
+
+simple_events = [
+ 'Event',
+ 'StateChangeEvent',
+ 'TextChangeEvent',
+ 'TextSelectionChangeEvent',
+ 'HideEvent',
+ 'CaretMoveEvent',
+ 'ObjectAttributeChangedEvent',
+ 'TableChangeEvent',
+ 'ScrollingEvent',
+ 'AnnouncementEvent'
+ ]
diff --git a/accessible/xpcom/moz.build b/accessible/xpcom/moz.build
new file mode 100644
index 0000000000..2ce13081a2
--- /dev/null
+++ b/accessible/xpcom/moz.build
@@ -0,0 +1,80 @@
+# -*- 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/.
+
+UNIFIED_SOURCES += [
+ "nsAccessibleRelation.cpp",
+ "xpcAccessibilityService.cpp",
+ "xpcAccessible.cpp",
+ "xpcAccessibleApplication.cpp",
+ "xpcAccessibleDocument.cpp",
+ "xpcAccessibleGeneric.cpp",
+ "xpcAccessibleHyperLink.cpp",
+ "xpcAccessibleHyperText.cpp",
+ "xpcAccessibleImage.cpp",
+ "xpcAccessiblePivot.cpp",
+ "xpcAccessibleSelectable.cpp",
+ "xpcAccessibleTable.cpp",
+ "xpcAccessibleTableCell.cpp",
+ "xpcAccessibleTextLeafRange.cpp",
+ "xpcAccessibleTextRange.cpp",
+ "xpcAccessibleValue.cpp",
+]
+
+SOURCES += [
+ "!xpcAccEvents.cpp",
+]
+
+EXPORTS += [
+ "!xpcAccEvents.h",
+ "xpcAccessibilityService.h",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/basetypes",
+ "/accessible/generic",
+]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ LOCAL_INCLUDES += [
+ "/accessible/atk",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+ LOCAL_INCLUDES += [
+ "/accessible/windows/msaa",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ LOCAL_INCLUDES += [
+ "/accessible/ipc",
+ "/accessible/mac",
+ ]
+ UNIFIED_SOURCES += ["xpcAccessibleMacInterface.mm"]
+ EXPORTS += [
+ "xpcAccessibleMacInterface.h",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
+ LOCAL_INCLUDES += [
+ "/accessible/android",
+ ]
+else:
+ LOCAL_INCLUDES += [
+ "/accessible/other",
+ ]
+
+
+GeneratedFile(
+ "xpcAccEvents.h",
+ "xpcAccEvents.cpp",
+ script="AccEventGen.py",
+ entry_point="gen_files",
+ inputs=[
+ "AccEvents.conf",
+ ],
+)
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/accessible/xpcom/nsAccessibleRelation.cpp b/accessible/xpcom/nsAccessibleRelation.cpp
new file mode 100644
index 0000000000..d376c15e91
--- /dev/null
+++ b/accessible/xpcom/nsAccessibleRelation.cpp
@@ -0,0 +1,59 @@
+/* -*- 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 "nsAccessibleRelation.h"
+
+#include "Relation.h"
+#include "xpcAccessibleDocument.h"
+
+#include "nsArrayUtils.h"
+#include "nsComponentManagerUtils.h"
+
+using namespace mozilla::a11y;
+
+nsAccessibleRelation::nsAccessibleRelation(uint32_t aType, Relation* aRel)
+ : mType(aType) {
+ mTargets = do_CreateInstance(NS_ARRAY_CONTRACTID);
+ Accessible* targetAcc = nullptr;
+ while ((targetAcc = aRel->Next())) {
+ mTargets->AppendElement(static_cast<nsIAccessible*>(ToXPC(targetAcc)));
+ }
+}
+
+nsAccessibleRelation::~nsAccessibleRelation() {}
+
+// nsISupports
+NS_IMPL_ISUPPORTS(nsAccessibleRelation, nsIAccessibleRelation)
+
+// nsIAccessibleRelation
+NS_IMETHODIMP
+nsAccessibleRelation::GetRelationType(uint32_t* aType) {
+ NS_ENSURE_ARG_POINTER(aType);
+ *aType = mType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAccessibleRelation::GetTargetsCount(uint32_t* aCount) {
+ NS_ENSURE_ARG_POINTER(aCount);
+ *aCount = 0;
+ return mTargets->GetLength(aCount);
+}
+
+NS_IMETHODIMP
+nsAccessibleRelation::GetTarget(uint32_t aIndex, nsIAccessible** aTarget) {
+ NS_ENSURE_ARG_POINTER(aTarget);
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIAccessible> target = do_QueryElementAt(mTargets, aIndex, &rv);
+ target.forget(aTarget);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsAccessibleRelation::GetTargets(nsIArray** aTargets) {
+ NS_ENSURE_ARG_POINTER(aTargets);
+ NS_ADDREF(*aTargets = mTargets);
+ return NS_OK;
+}
diff --git a/accessible/xpcom/nsAccessibleRelation.h b/accessible/xpcom/nsAccessibleRelation.h
new file mode 100644
index 0000000000..f8776951f4
--- /dev/null
+++ b/accessible/xpcom/nsAccessibleRelation.h
@@ -0,0 +1,46 @@
+/* -*- 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 _nsAccessibleRelation_H_
+#define _nsAccessibleRelation_H_
+
+#include "nsIAccessibleRelation.h"
+
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "nsIMutableArray.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/a11y/RemoteAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+class Relation;
+
+/**
+ * Class represents an accessible relation.
+ */
+class nsAccessibleRelation final : public nsIAccessibleRelation {
+ public:
+ nsAccessibleRelation(uint32_t aType, Relation* aRel);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIACCESSIBLERELATION
+
+ private:
+ nsAccessibleRelation();
+ ~nsAccessibleRelation();
+
+ nsAccessibleRelation(const nsAccessibleRelation&);
+ nsAccessibleRelation& operator=(const nsAccessibleRelation&);
+
+ uint32_t mType;
+ nsCOMPtr<nsIMutableArray> mTargets;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xpcom/xpcAccessibilityService.cpp b/accessible/xpcom/xpcAccessibilityService.cpp
new file mode 100644
index 0000000000..a7b70b6f33
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibilityService.cpp
@@ -0,0 +1,307 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 "xpcAccessibilityService.h"
+
+#include "mozilla/dom/Document.h"
+
+#include "xpcAccessiblePivot.h"
+#include "nsAccessibilityService.h"
+#include "xpcAccessibleApplication.h"
+#include "xpcAccessibleDocument.h"
+#include "xpcAccessibleTextLeafRange.h"
+
+#ifdef A11Y_LOG
+# include "Logging.h"
+#endif
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+using namespace mozilla::dom;
+
+xpcAccessibilityService* xpcAccessibilityService::gXPCAccessibilityService =
+ nullptr;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+void xpcAccessibilityService::ShutdownCallback(nsITimer* aTimer,
+ void* aClosure) {
+ MaybeShutdownAccService(nsAccessibilityService::eXPCOM);
+ xpcAccessibilityService* xpcAccService =
+ reinterpret_cast<xpcAccessibilityService*>(aClosure);
+
+ if (xpcAccService->mShutdownTimer) {
+ xpcAccService->mShutdownTimer->Cancel();
+ xpcAccService->mShutdownTimer = nullptr;
+ }
+}
+
+NS_IMETHODIMP_(MozExternalRefCountType)
+xpcAccessibilityService::AddRef(void) {
+ MOZ_ASSERT_TYPE_OK_FOR_REFCOUNTING(xpcAccessibilityService)
+ MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt");
+ if (!nsAutoRefCnt::isThreadSafe) {
+ NS_ASSERT_OWNINGTHREAD(xpcAccessibilityService);
+ }
+ nsrefcnt count = ++mRefCnt;
+ NS_LOG_ADDREF(this, count, "xpcAccessibilityService", sizeof(*this));
+
+ // We want refcount to be > 1 because one reference is added in the XPCOM
+ // accessibility service getter.
+ if (mRefCnt > 1) {
+ if (mShutdownTimer) {
+ mShutdownTimer->Cancel();
+ mShutdownTimer = nullptr;
+ }
+
+ GetOrCreateAccService(nsAccessibilityService::eXPCOM);
+ }
+
+ return count;
+}
+
+NS_IMETHODIMP_(MozExternalRefCountType)
+xpcAccessibilityService::Release(void) {
+ MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release");
+
+ if (!nsAutoRefCnt::isThreadSafe) {
+ NS_ASSERT_OWNINGTHREAD(xpcAccessibilityService);
+ }
+
+ nsrefcnt count = --mRefCnt;
+ NS_LOG_RELEASE(this, count, "xpcAccessibilityService");
+
+ if (count == 0) {
+ if (!nsAutoRefCnt::isThreadSafe) {
+ NS_ASSERT_OWNINGTHREAD(xpcAccessibilityService);
+ }
+
+ mRefCnt = 1; /* stabilize */
+ delete (this);
+ return 0;
+ }
+
+ // When ref count goes down to 1 (held internally as a static reference),
+ // it means that there are no more external references to the
+ // xpcAccessibilityService and we can attempt to shut down acceessiblity
+ // service.
+ if (count == 1 && !mShutdownTimer) {
+ NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mShutdownTimer), ShutdownCallback, this, 100,
+ nsITimer::TYPE_ONE_SHOT, "xpcAccessibilityService::Release");
+ }
+
+ return count;
+}
+
+NS_IMPL_QUERY_INTERFACE(xpcAccessibilityService, nsIAccessibilityService)
+
+NS_IMETHODIMP
+xpcAccessibilityService::GetApplicationAccessible(
+ nsIAccessible** aAccessibleApplication) {
+ NS_ENSURE_ARG_POINTER(aAccessibleApplication);
+
+ NS_IF_ADDREF(*aAccessibleApplication = XPCApplicationAcc());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibilityService::GetAccessibleFor(nsINode* aNode,
+ nsIAccessible** aAccessible) {
+ NS_ENSURE_ARG_POINTER(aAccessible);
+ *aAccessible = nullptr;
+ if (!aNode) {
+ return NS_OK;
+ }
+
+ nsAccessibilityService* accService = GetAccService();
+ if (!accService) {
+ return NS_ERROR_SERVICE_NOT_AVAILABLE;
+ }
+
+ DocAccessible* document = accService->GetDocAccessible(aNode->OwnerDoc());
+ if (document) {
+ NS_IF_ADDREF(*aAccessible = ToXPC(document->GetAccessible(aNode)));
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibilityService::GetAccessibleDescendantFor(
+ nsINode* aNode, nsIAccessible** aAccessible) {
+ NS_ENSURE_ARG_POINTER(aAccessible);
+ *aAccessible = nullptr;
+ if (!aNode) {
+ return NS_OK;
+ }
+
+ nsAccessibilityService* accService = GetAccService();
+ if (!accService) {
+ return NS_ERROR_SERVICE_NOT_AVAILABLE;
+ }
+
+ DocAccessible* document = accService->GetDocAccessible(aNode->OwnerDoc());
+ if (document) {
+ NS_IF_ADDREF(*aAccessible =
+ ToXPC(document->GetAccessibleOrDescendant(aNode)));
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibilityService::GetStringRole(uint32_t aRole, nsAString& aString) {
+ nsAccessibilityService* accService = GetAccService();
+ if (!accService) {
+ return NS_ERROR_SERVICE_NOT_AVAILABLE;
+ }
+
+ accService->GetStringRole(aRole, aString);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibilityService::GetStringStates(uint32_t aState, uint32_t aExtraState,
+ nsISupports** aStringStates) {
+ nsAccessibilityService* accService = GetAccService();
+ if (!accService) {
+ return NS_ERROR_SERVICE_NOT_AVAILABLE;
+ }
+
+ accService->GetStringStates(aState, aExtraState, aStringStates);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibilityService::GetStringEventType(uint32_t aEventType,
+ nsAString& aString) {
+ nsAccessibilityService* accService = GetAccService();
+ if (!accService) {
+ return NS_ERROR_SERVICE_NOT_AVAILABLE;
+ }
+
+ accService->GetStringEventType(aEventType, aString);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibilityService::GetStringRelationType(uint32_t aRelationType,
+ nsAString& aString) {
+ nsAccessibilityService* accService = GetAccService();
+ if (!accService) {
+ return NS_ERROR_SERVICE_NOT_AVAILABLE;
+ }
+
+ accService->GetStringRelationType(aRelationType, aString);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibilityService::GetAccessibleFromCache(nsINode* aNode,
+ nsIAccessible** aAccessible) {
+ NS_ENSURE_ARG_POINTER(aAccessible);
+ *aAccessible = nullptr;
+ if (!aNode) {
+ return NS_OK;
+ }
+
+ nsAccessibilityService* accService = GetAccService();
+ if (!accService) {
+ return NS_ERROR_SERVICE_NOT_AVAILABLE;
+ }
+
+ // Search for an accessible in each of our per document accessible object
+ // caches. If we don't find it, and the given node is itself a document, check
+ // our cache of document accessibles (document cache). Note usually shutdown
+ // document accessibles are not stored in the document cache, however an
+ // "unofficially" shutdown document (i.e. not from DocManager) can still
+ // exist in the document cache.
+ LocalAccessible* accessible = accService->FindAccessibleInCache(aNode);
+ if (!accessible && aNode->IsDocument()) {
+ accessible = mozilla::a11y::GetExistingDocAccessible(aNode->AsDocument());
+ }
+
+ NS_IF_ADDREF(*aAccessible = ToXPC(accessible));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibilityService::CreateAccessiblePivot(nsIAccessible* aRoot,
+ nsIAccessiblePivot** aPivot) {
+ NS_ENSURE_ARG_POINTER(aPivot);
+ NS_ENSURE_ARG(aRoot);
+ *aPivot = nullptr;
+
+ xpcAccessiblePivot* pivot = new xpcAccessiblePivot(aRoot);
+ NS_ADDREF(*aPivot = pivot);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibilityService::CreateTextLeafPoint(
+ nsIAccessible* aAccessible, int32_t aOffset,
+ nsIAccessibleTextLeafPoint** aPoint) {
+ NS_ENSURE_ARG_POINTER(aPoint);
+ NS_ENSURE_ARG(aAccessible);
+ *aPoint = nullptr;
+
+ RefPtr<xpcAccessibleTextLeafPoint> point =
+ new xpcAccessibleTextLeafPoint(aAccessible, aOffset);
+ point.forget(aPoint);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibilityService::SetLogging(const nsACString& aModules) {
+#ifdef A11Y_LOG
+ logging::Enable(PromiseFlatCString(aModules));
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibilityService::IsLogged(const nsAString& aModule, bool* aIsLogged) {
+ NS_ENSURE_ARG_POINTER(aIsLogged);
+ *aIsLogged = false;
+
+#ifdef A11Y_LOG
+ *aIsLogged = logging::IsEnabled(aModule);
+#endif
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibilityService::GetConsumers(nsAString& aString) {
+ nsAccessibilityService* accService = GetAccService();
+ if (!accService) {
+ return NS_ERROR_SERVICE_NOT_AVAILABLE;
+ }
+
+ accService->GetConsumers(aString);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// NS_GetAccessibilityService
+////////////////////////////////////////////////////////////////////////////////
+
+nsresult NS_GetAccessibilityService(nsIAccessibilityService** aResult) {
+ NS_ENSURE_TRUE(aResult, NS_ERROR_NULL_POINTER);
+ *aResult = nullptr;
+
+ if (!GetOrCreateAccService(nsAccessibilityService::eXPCOM)) {
+ return NS_ERROR_SERVICE_NOT_AVAILABLE;
+ }
+
+ xpcAccessibilityService* service = new xpcAccessibilityService();
+ xpcAccessibilityService::gXPCAccessibilityService = service;
+ NS_ADDREF(*aResult = service);
+
+ return NS_OK;
+}
diff --git a/accessible/xpcom/xpcAccessibilityService.h b/accessible/xpcom/xpcAccessibilityService.h
new file mode 100644
index 0000000000..04c23e7075
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibilityService.h
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_a11y_xpcAccessibilityService_h_
+#define mozilla_a11y_xpcAccessibilityService_h_
+
+#include "nsIAccessibilityService.h"
+#include "nsITimer.h"
+
+class xpcAccessibilityService : public nsIAccessibilityService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIACCESSIBILITYSERVICE
+
+ /**
+ * Return true if xpc accessibility service is in use.
+ */
+ static bool IsInUse() {
+ // When ref count goes down to 1 (held internally as a static reference),
+ // it means that there are no more external references and thus it is not in
+ // use.
+ return gXPCAccessibilityService ? gXPCAccessibilityService->mRefCnt > 1
+ : false;
+ }
+
+ protected:
+ virtual ~xpcAccessibilityService() {
+ if (mShutdownTimer) {
+ mShutdownTimer->Cancel();
+ mShutdownTimer = nullptr;
+ }
+ gXPCAccessibilityService = nullptr;
+ }
+
+ private:
+ // xpcAccessibilityService creation is controlled by friend
+ // NS_GetAccessibilityService, keep constructor private.
+ xpcAccessibilityService() = default;
+
+ nsCOMPtr<nsITimer> mShutdownTimer;
+
+ /**
+ * Reference for xpc accessibility service instance.
+ */
+ static xpcAccessibilityService* gXPCAccessibilityService;
+
+ /**
+ * Used to shutdown nsAccessibilityService if xpcom accessible service is not
+ * in use any more.
+ */
+ static void ShutdownCallback(nsITimer* aTimer, void* aClosure);
+
+ friend nsresult NS_GetAccessibilityService(nsIAccessibilityService** aResult);
+};
+
+// for component registration
+// {3b265b69-f813-48ff-880d-d88d101af404}
+#define NS_ACCESSIBILITY_SERVICE_CID \
+ { \
+ 0x3b265b69, 0xf813, 0x48ff, { \
+ 0x88, 0x0d, 0xd8, 0x8d, 0x10, 0x1a, 0xf4, 0x04 \
+ } \
+ }
+
+extern nsresult NS_GetAccessibilityService(nsIAccessibilityService** aResult);
+
+#endif
diff --git a/accessible/xpcom/xpcAccessible.cpp b/accessible/xpcom/xpcAccessible.cpp
new file mode 100644
index 0000000000..37901e3ec2
--- /dev/null
+++ b/accessible/xpcom/xpcAccessible.cpp
@@ -0,0 +1,665 @@
+/* -*- 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 "AccAttributes.h"
+#include "nsAccUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIAccessibleRelation.h"
+#include "nsIAccessibleRole.h"
+#include "nsAccessibleRelation.h"
+#include "Relation.h"
+#include "RootAccessible.h"
+#include "xpcAccessibleDocument.h"
+
+#include "nsIMutableArray.h"
+#include "nsPersistentProperties.h"
+
+#ifdef MOZ_WIDGET_COCOA
+# include "xpcAccessibleMacInterface.h"
+#endif
+
+using namespace mozilla::a11y;
+
+NS_IMETHODIMP
+xpcAccessible::GetParent(nsIAccessible** aParent) {
+ NS_ENSURE_ARG_POINTER(aParent);
+ *aParent = nullptr;
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ Accessible* parent = IntlGeneric()->Parent();
+ NS_IF_ADDREF(*aParent = ToXPC(parent));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetNextSibling(nsIAccessible** aNextSibling) {
+ NS_ENSURE_ARG_POINTER(aNextSibling);
+ *aNextSibling = nullptr;
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aNextSibling = ToXPC(IntlGeneric()->NextSibling()));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetPreviousSibling(nsIAccessible** aPreviousSibling) {
+ NS_ENSURE_ARG_POINTER(aPreviousSibling);
+ *aPreviousSibling = nullptr;
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aPreviousSibling = ToXPC(IntlGeneric()->PrevSibling()));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetFirstChild(nsIAccessible** aFirstChild) {
+ NS_ENSURE_ARG_POINTER(aFirstChild);
+ *aFirstChild = nullptr;
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aFirstChild = ToXPC(IntlGeneric()->FirstChild()));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetLastChild(nsIAccessible** aLastChild) {
+ NS_ENSURE_ARG_POINTER(aLastChild);
+ *aLastChild = nullptr;
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aLastChild = ToXPC(IntlGeneric()->LastChild()));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetChildCount(int32_t* aChildCount) {
+ NS_ENSURE_ARG_POINTER(aChildCount);
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ *aChildCount = IntlGeneric()->ChildCount();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetChildAt(int32_t aChildIndex, nsIAccessible** aChild) {
+ NS_ENSURE_ARG_POINTER(aChild);
+ *aChild = nullptr;
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ // If child index is negative, then return last child.
+ // XXX: do we really need this?
+ if (aChildIndex < 0) aChildIndex = IntlGeneric()->ChildCount() - 1;
+
+ Accessible* child = IntlGeneric()->ChildAt(aChildIndex);
+ if (!child) return NS_ERROR_INVALID_ARG;
+
+ NS_ADDREF(*aChild = ToXPC(child));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetChildren(nsIArray** aChildren) {
+ NS_ENSURE_ARG_POINTER(aChildren);
+ *aChildren = nullptr;
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMutableArray> children =
+ do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t childCount = IntlGeneric()->ChildCount();
+ for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
+ Accessible* child = IntlGeneric()->ChildAt(childIdx);
+ children->AppendElement(static_cast<nsIAccessible*>(ToXPC(child)));
+ }
+
+ children.forget(aChildren);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetIndexInParent(int32_t* aIndexInParent) {
+ NS_ENSURE_ARG_POINTER(aIndexInParent);
+ *aIndexInParent = -1;
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ *aIndexInParent = IntlGeneric()->IndexInParent();
+
+ return *aIndexInParent != -1 ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetUniqueID(int64_t* aUniqueID) {
+ NS_ENSURE_ARG_POINTER(aUniqueID);
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ if (IntlGeneric()->IsLocal()) {
+ *aUniqueID = reinterpret_cast<uintptr_t>(Intl()->UniqueID());
+ } else if (IntlGeneric()->IsRemote()) {
+ *aUniqueID = IntlGeneric()->AsRemote()->ID();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetDOMNode(nsINode** aDOMNode) {
+ NS_ENSURE_ARG_POINTER(aDOMNode);
+ *aDOMNode = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsINode> node = Intl()->GetNode();
+ node.forget(aDOMNode);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetId(nsAString& aID) {
+ if (!IntlGeneric()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RemoteAccessible* proxy = IntlGeneric()->AsRemote();
+ if (!proxy) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsString id;
+ proxy->DOMNodeID(id);
+ aID.Assign(id);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetDocument(nsIAccessibleDocument** aDocument) {
+ NS_ENSURE_ARG_POINTER(aDocument);
+ *aDocument = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aDocument = ToXPCDocument(Intl()->Document()));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetRootDocument(nsIAccessibleDocument** aRootDocument) {
+ NS_ENSURE_ARG_POINTER(aRootDocument);
+ *aRootDocument = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aRootDocument = ToXPCDocument(Intl()->RootAccessible()));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetRole(uint32_t* aRole) {
+ NS_ENSURE_ARG_POINTER(aRole);
+ *aRole = nsIAccessibleRole::ROLE_NOTHING;
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ *aRole = IntlGeneric()->Role();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetState(uint32_t* aState, uint32_t* aExtraState) {
+ NS_ENSURE_ARG_POINTER(aState);
+
+ Accessible* acc = IntlGeneric();
+ if (acc) {
+ nsAccUtils::To32States(acc->State(), aState, aExtraState);
+ } else {
+ nsAccUtils::To32States(states::DEFUNCT, aState, aExtraState);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetName(nsAString& aName) {
+ aName.Truncate();
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ nsAutoString name;
+ IntlGeneric()->Name(name);
+
+ aName.Assign(name);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetDescription(nsAString& aDescription) {
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ nsAutoString desc;
+ IntlGeneric()->Description(desc);
+
+ aDescription.Assign(desc);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetLanguage(nsAString& aLanguage) {
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ nsAutoString lang;
+ IntlGeneric()->Language(lang);
+
+ aLanguage.Assign(lang);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetValue(nsAString& aValue) {
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ nsAutoString value;
+ IntlGeneric()->Value(value);
+
+ aValue.Assign(value);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetAccessKey(nsAString& aAccessKey) {
+ aAccessKey.Truncate();
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ IntlGeneric()->AccessKey().ToString(aAccessKey);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetKeyboardShortcut(nsAString& aKeyBinding) {
+ aKeyBinding.Truncate();
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ if (IntlGeneric()->IsRemote()) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ Intl()->KeyboardShortcut().ToString(aKeyBinding);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetAttributes(nsIPersistentProperties** aAttributes) {
+ NS_ENSURE_ARG_POINTER(aAttributes);
+ *aAttributes = nullptr;
+
+ if (!IntlGeneric()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<nsPersistentProperties> props = new nsPersistentProperties();
+
+ RefPtr<AccAttributes> attributes = IntlGeneric()->Attributes();
+
+ nsAutoString unused;
+ for (auto iter : *attributes) {
+ nsAutoString name;
+ iter.NameAsString(name);
+
+ nsAutoString value;
+ iter.ValueAsString(value);
+
+ props->SetStringProperty(NS_ConvertUTF16toUTF8(name), value, unused);
+ }
+
+ props.forget(aAttributes);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetCache(nsIPersistentProperties** aCachedFields) {
+ NS_ENSURE_ARG_POINTER(aCachedFields);
+ *aCachedFields = nullptr;
+
+ if (!IntlGeneric()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<nsPersistentProperties> props = new nsPersistentProperties();
+ if (RemoteAccessible* remoteAcc = IntlGeneric()->AsRemote()) {
+ if (RefPtr<AccAttributes> cachedFields = remoteAcc->mCachedFields) {
+ nsAutoString unused;
+ for (auto iter : *cachedFields) {
+ nsAutoString name;
+ iter.NameAsString(name);
+
+ nsAutoString value;
+ iter.ValueAsString(value);
+
+ props->SetStringProperty(NS_ConvertUTF16toUTF8(name), value, unused);
+ }
+ }
+ }
+
+ props.forget(aCachedFields);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetNativeInterface(nsISupports** aNativeInterface) {
+#ifdef MOZ_WIDGET_COCOA
+ NS_ENSURE_ARG_POINTER(aNativeInterface);
+
+ // We don't cache or store this instance anywhere so each get returns a
+ // different instance. So `acc.nativeInterface != acc.nativeInterface`. This
+ // just seems simpler and more robust for now.
+ nsCOMPtr<nsISupports> macIface = static_cast<nsIAccessibleMacInterface*>(
+ new xpcAccessibleMacInterface(IntlGeneric()));
+ macIface.swap(*aNativeInterface);
+
+ return NS_OK;
+#else
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetBounds(int32_t* aX, int32_t* aY, int32_t* aWidth,
+ int32_t* aHeight) {
+ NS_ENSURE_ARG_POINTER(aX);
+ *aX = 0;
+ NS_ENSURE_ARG_POINTER(aY);
+ *aY = 0;
+ NS_ENSURE_ARG_POINTER(aWidth);
+ *aWidth = 0;
+ NS_ENSURE_ARG_POINTER(aHeight);
+ *aHeight = 0;
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ LayoutDeviceIntRect rect = IntlGeneric()->Bounds();
+ rect.GetRect(aX, aY, aWidth, aHeight);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetBoundsInCSSPixels(int32_t* aX, int32_t* aY, int32_t* aWidth,
+ int32_t* aHeight) {
+ NS_ENSURE_ARG_POINTER(aX);
+ *aX = 0;
+ NS_ENSURE_ARG_POINTER(aY);
+ *aY = 0;
+ NS_ENSURE_ARG_POINTER(aWidth);
+ *aWidth = 0;
+ NS_ENSURE_ARG_POINTER(aHeight);
+ *aHeight = 0;
+
+ if (!IntlGeneric()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsIntRect rect = IntlGeneric()->BoundsInCSSPixels();
+ rect.GetRect(aX, aY, aWidth, aHeight);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GroupPosition(int32_t* aGroupLevel,
+ int32_t* aSimilarItemsInGroup,
+ int32_t* aPositionInGroup) {
+ NS_ENSURE_ARG_POINTER(aGroupLevel);
+ NS_ENSURE_ARG_POINTER(aSimilarItemsInGroup);
+ NS_ENSURE_ARG_POINTER(aPositionInGroup);
+
+ GroupPos groupPos = IntlGeneric()->GroupPosition();
+
+ *aGroupLevel = groupPos.level;
+ *aSimilarItemsInGroup = groupPos.setSize;
+ *aPositionInGroup = groupPos.posInSet;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetRelationByType(uint32_t aType,
+ nsIAccessibleRelation** aRelation) {
+ NS_ENSURE_ARG_POINTER(aRelation);
+ *aRelation = nullptr;
+
+ NS_ENSURE_ARG(aType <= static_cast<uint32_t>(RelationType::LAST));
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ Relation rel =
+ IntlGeneric()->RelationByType(static_cast<RelationType>(aType));
+ NS_ADDREF(*aRelation = new nsAccessibleRelation(aType, &rel));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetRelations(nsIArray** aRelations) {
+ NS_ENSURE_ARG_POINTER(aRelations);
+ *aRelations = nullptr;
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIMutableArray> relations = do_CreateInstance(NS_ARRAY_CONTRACTID);
+ NS_ENSURE_TRUE(relations, NS_ERROR_OUT_OF_MEMORY);
+
+ for (uint32_t type = 0; type <= static_cast<uint32_t>(RelationType::LAST);
+ ++type) {
+ nsCOMPtr<nsIAccessibleRelation> relation;
+ nsresult rv = GetRelationByType(type, getter_AddRefs(relation));
+
+ if (NS_SUCCEEDED(rv) && relation) {
+ uint32_t targets = 0;
+ relation->GetTargetsCount(&targets);
+ if (targets) relations->AppendElement(relation);
+ }
+ }
+
+ NS_ADDREF(*aRelations = relations);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetFocusedChild(nsIAccessible** aChild) {
+ NS_ENSURE_ARG_POINTER(aChild);
+ *aChild = nullptr;
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aChild = ToXPC(IntlGeneric()->FocusedChild()));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetChildAtPoint(int32_t aX, int32_t aY,
+ nsIAccessible** aAccessible) {
+ NS_ENSURE_ARG_POINTER(aAccessible);
+ *aAccessible = nullptr;
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aAccessible = ToXPC(IntlGeneric()->ChildAtPoint(
+ aX, aY, Accessible::EWhichChildAtPoint::DirectChild)));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetDeepestChildAtPoint(int32_t aX, int32_t aY,
+ nsIAccessible** aAccessible) {
+ NS_ENSURE_ARG_POINTER(aAccessible);
+ *aAccessible = nullptr;
+
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aAccessible = ToXPC(IntlGeneric()->ChildAtPoint(
+ aX, aY, Accessible::EWhichChildAtPoint::DeepestChild)));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetDeepestChildAtPointInProcess(int32_t aX, int32_t aY,
+ nsIAccessible** aAccessible) {
+ NS_ENSURE_ARG_POINTER(aAccessible);
+ *aAccessible = nullptr;
+
+ Accessible* generic = IntlGeneric();
+ if (!generic || generic->IsRemote()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_IF_ADDREF(*aAccessible = ToXPC(Intl()->LocalChildAtPoint(
+ aX, aY, Accessible::EWhichChildAtPoint::DeepestChild)));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::SetSelected(bool aSelect) {
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ IntlGeneric()->SetSelected(aSelect);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::TakeSelection() {
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ IntlGeneric()->TakeSelection();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::TakeFocus() {
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ IntlGeneric()->TakeFocus();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetActionCount(uint8_t* aActionCount) {
+ NS_ENSURE_ARG_POINTER(aActionCount);
+ *aActionCount = 0;
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ *aActionCount = IntlGeneric()->ActionCount();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetActionName(uint8_t aIndex, nsAString& aName) {
+ aName.Truncate();
+
+ if (!IntlGeneric()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aIndex >= IntlGeneric()->ActionCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoString name;
+ IntlGeneric()->ActionNameAt(aIndex, name);
+
+ aName.Assign(name);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetActionDescription(uint8_t aIndex, nsAString& aDescription) {
+ aDescription.Truncate();
+
+ if (!IntlGeneric()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aIndex >= IntlGeneric()->ActionCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoString description;
+ IntlGeneric()->ActionDescriptionAt(aIndex, description);
+
+ aDescription.Assign(description);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::DoAction(uint8_t aIndex) {
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ return IntlGeneric()->DoAction(aIndex) ? NS_OK : NS_ERROR_INVALID_ARG;
+}
+
+NS_IMETHODIMP
+xpcAccessible::ScrollTo(uint32_t aHow) {
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ IntlGeneric()->ScrollTo(aHow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::ScrollToPoint(uint32_t aCoordinateType, int32_t aX, int32_t aY) {
+ if (!IntlGeneric()) return NS_ERROR_FAILURE;
+
+ IntlGeneric()->ScrollToPoint(aCoordinateType, aX, aY);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::Announce(const nsAString& aAnnouncement, uint16_t aPriority) {
+ if (RemoteAccessible* proxy = IntlGeneric()->AsRemote()) {
+#if defined(XP_WIN)
+ return NS_ERROR_NOT_IMPLEMENTED;
+#else
+ nsString announcement(aAnnouncement);
+ proxy->Announce(announcement, aPriority);
+#endif
+ } else {
+ Intl()->Announce(aAnnouncement, aPriority);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessible::GetComputedARIARole(nsAString& aRole) {
+ if (!IntlGeneric()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsStaticAtom* ariaRole = IntlGeneric()->ComputedARIARole();
+ if (ariaRole) {
+ ariaRole->ToString(aRole);
+ }
+
+ return NS_OK;
+}
diff --git a/accessible/xpcom/xpcAccessible.h b/accessible/xpcom/xpcAccessible.h
new file mode 100644
index 0000000000..02168ecf5d
--- /dev/null
+++ b/accessible/xpcom/xpcAccessible.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 mozilla_a11y_xpcAccessible_h_
+#define mozilla_a11y_xpcAccessible_h_
+
+#include "nsIAccessible.h"
+
+class nsIAccessible;
+
+namespace mozilla {
+namespace a11y {
+
+class Accessible;
+class LocalAccessible;
+
+/**
+ * XPCOM nsIAccessible interface implementation, used by xpcAccessibleGeneric
+ * class.
+ */
+class xpcAccessible : public nsIAccessible {
+ public:
+ // nsIAccessible
+ NS_IMETHOD GetParent(nsIAccessible** aParent) final;
+ NS_IMETHOD GetNextSibling(nsIAccessible** aNextSibling) final;
+ NS_IMETHOD GetPreviousSibling(nsIAccessible** aPreviousSibling) final;
+ NS_IMETHOD GetFirstChild(nsIAccessible** aFirstChild) final;
+ NS_IMETHOD GetLastChild(nsIAccessible** aLastChild) final;
+ NS_IMETHOD GetChildCount(int32_t* aChildCount) final;
+ NS_IMETHOD GetChildAt(int32_t aChildIndex, nsIAccessible** aChild) final;
+ NS_IMETHOD GetChildren(nsIArray** aChildren) final;
+ NS_IMETHOD GetIndexInParent(int32_t* aIndexInParent) final;
+
+ NS_IMETHOD GetUniqueID(int64_t* aUniqueID) final;
+ NS_IMETHOD GetDOMNode(nsINode** aDOMNode) final;
+ NS_IMETHOD GetId(nsAString& aID) final;
+ NS_IMETHOD GetDocument(nsIAccessibleDocument** aDocument) final;
+ NS_IMETHOD GetRootDocument(nsIAccessibleDocument** aRootDocument) final;
+
+ NS_IMETHOD GetRole(uint32_t* aRole) final;
+ NS_IMETHOD GetState(uint32_t* aState, uint32_t* aExtraState) final;
+
+ NS_IMETHOD GetDescription(nsAString& aDescription) final;
+ NS_IMETHOD GetName(nsAString& aName) final;
+ NS_IMETHOD GetLanguage(nsAString& aLanguage) final;
+ NS_IMETHOD GetValue(nsAString& aValue) final;
+
+ NS_IMETHOD GetAccessKey(nsAString& aAccessKey) final;
+ NS_IMETHOD GetKeyboardShortcut(nsAString& aKeyBinding) final;
+
+ NS_IMETHOD GetAttributes(nsIPersistentProperties** aAttributes) final;
+
+ NS_IMETHOD GetCache(nsIPersistentProperties** aCachedFields) final;
+
+ NS_IMETHOD GetNativeInterface(nsISupports** aNativeInterface) final;
+
+ NS_IMETHOD GetBounds(int32_t* aX, int32_t* aY, int32_t* aWidth,
+ int32_t* aHeight) final;
+ NS_IMETHOD GetBoundsInCSSPixels(int32_t* aX, int32_t* aY, int32_t* aWidth,
+ int32_t* aHeight) final;
+ NS_IMETHOD GroupPosition(int32_t* aGroupLevel, int32_t* aSimilarItemsInGroup,
+ int32_t* aPositionInGroup) final;
+ NS_IMETHOD GetRelationByType(uint32_t aType,
+ nsIAccessibleRelation** aRelation) final;
+ NS_IMETHOD GetRelations(nsIArray** aRelations) final;
+
+ NS_IMETHOD GetFocusedChild(nsIAccessible** aChild) final;
+ NS_IMETHOD GetChildAtPoint(int32_t aX, int32_t aY,
+ nsIAccessible** aAccessible) final;
+ NS_IMETHOD GetDeepestChildAtPoint(int32_t aX, int32_t aY,
+ nsIAccessible** aAccessible) final;
+ NS_IMETHOD GetDeepestChildAtPointInProcess(int32_t aX, int32_t aY,
+ nsIAccessible** aAccessible) final;
+
+ NS_IMETHOD SetSelected(bool aSelect) final;
+ NS_IMETHOD TakeSelection() final;
+ NS_IMETHOD TakeFocus() final;
+
+ NS_IMETHOD GetActionCount(uint8_t* aActionCount) final;
+ NS_IMETHOD GetActionName(uint8_t aIndex, nsAString& aName) final;
+ NS_IMETHOD GetActionDescription(uint8_t aIndex,
+ nsAString& aDescription) final;
+ NS_IMETHOD DoAction(uint8_t aIndex) final;
+
+ MOZ_CAN_RUN_SCRIPT
+ NS_IMETHOD ScrollTo(uint32_t aHow) final;
+ NS_IMETHOD ScrollToPoint(uint32_t aCoordinateType, int32_t aX,
+ int32_t aY) final;
+
+ NS_IMETHOD Announce(const nsAString& aAnnouncement, uint16_t aPriority) final;
+
+ NS_IMETHOD GetComputedARIARole(nsAString& aRole) final;
+
+ protected:
+ xpcAccessible() {}
+ virtual ~xpcAccessible() {}
+
+ private:
+ LocalAccessible* Intl();
+ Accessible* IntlGeneric();
+
+ xpcAccessible(const xpcAccessible&) = delete;
+ xpcAccessible& operator=(const xpcAccessible&) = delete;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xpcom/xpcAccessibleApplication.cpp b/accessible/xpcom/xpcAccessibleApplication.cpp
new file mode 100644
index 0000000000..133c255778
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleApplication.cpp
@@ -0,0 +1,60 @@
+/* -*- 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 "xpcAccessibleApplication.h"
+
+#include "ApplicationAccessible.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_IMPL_ISUPPORTS_INHERITED(xpcAccessibleApplication, xpcAccessibleGeneric,
+ nsIAccessibleApplication)
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessibleApplication
+
+NS_IMETHODIMP
+xpcAccessibleApplication::GetAppName(nsAString& aName) {
+ aName.Truncate();
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ Intl()->AppName(aName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleApplication::GetAppVersion(nsAString& aVersion) {
+ aVersion.Truncate();
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ Intl()->AppVersion(aVersion);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleApplication::GetPlatformName(nsAString& aName) {
+ aName.Truncate();
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ Intl()->PlatformName(aName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleApplication::GetPlatformVersion(nsAString& aVersion) {
+ aVersion.Truncate();
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ Intl()->PlatformVersion(aVersion);
+ return NS_OK;
+}
diff --git a/accessible/xpcom/xpcAccessibleApplication.h b/accessible/xpcom/xpcAccessibleApplication.h
new file mode 100644
index 0000000000..432d0a07bb
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleApplication.h
@@ -0,0 +1,47 @@
+/* -*- 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_xpcAccessibleApplication_h_
+#define mozilla_a11y_xpcAccessibleApplication_h_
+
+#include "nsIAccessibleApplication.h"
+#include "ApplicationAccessible.h"
+#include "xpcAccessibleGeneric.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * XPCOM wrapper around ApplicationAccessible class.
+ */
+class xpcAccessibleApplication : public xpcAccessibleGeneric,
+ public nsIAccessibleApplication {
+ public:
+ explicit xpcAccessibleApplication(Accessible* aIntl)
+ : xpcAccessibleGeneric(aIntl) {}
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // nsIAccessibleApplication
+ NS_IMETHOD GetAppName(nsAString& aName) final;
+ NS_IMETHOD GetAppVersion(nsAString& aVersion) final;
+ NS_IMETHOD GetPlatformName(nsAString& aName) final;
+ NS_IMETHOD GetPlatformVersion(nsAString& aVersion) final;
+
+ protected:
+ virtual ~xpcAccessibleApplication() { Shutdown(); }
+
+ private:
+ ApplicationAccessible* Intl() { return mIntl->AsLocal()->AsApplication(); }
+
+ xpcAccessibleApplication(const xpcAccessibleApplication&) = delete;
+ xpcAccessibleApplication& operator=(const xpcAccessibleApplication&) = delete;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xpcom/xpcAccessibleDocument.cpp b/accessible/xpcom/xpcAccessibleDocument.cpp
new file mode 100644
index 0000000000..d616e476b2
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleDocument.cpp
@@ -0,0 +1,179 @@
+/* -*- 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 "xpcAccessibleDocument.h"
+#include "xpcAccessibleImage.h"
+#include "xpcAccessibleTable.h"
+#include "xpcAccessibleTableCell.h"
+
+#include "nsAccUtils.h"
+#include "DocAccessible-inl.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_IMPL_QUERY_INTERFACE_INHERITED(xpcAccessibleDocument, xpcAccessibleHyperText,
+ nsIAccessibleDocument)
+NS_IMPL_ADDREF_INHERITED(xpcAccessibleDocument, xpcAccessibleHyperText)
+NS_IMETHODIMP_(MozExternalRefCountType) xpcAccessibleDocument::Release(void) {
+ nsrefcnt r = xpcAccessibleHyperText::Release();
+ NS_LOG_RELEASE(this, r, "xpcAccessibleDocument");
+
+ // The only reference to the xpcAccessibleDocument is in DocManager's cache.
+ if (r == 1 && !!mIntl && mCache.Count() == 0) {
+ if (mIntl->IsLocal()) {
+ GetAccService()->RemoveFromXPCDocumentCache(mIntl->AsLocal()->AsDoc());
+ } else {
+ GetAccService()->RemoveFromRemoteXPCDocumentCache(
+ mIntl->AsRemote()->AsDoc());
+ }
+ }
+ return r;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessibleDocument
+
+NS_IMETHODIMP
+xpcAccessibleDocument::GetURL(nsAString& aURL) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ nsAccUtils::DocumentURL(mIntl, aURL);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleDocument::GetTitle(nsAString& aTitle) {
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ nsAutoString title;
+ Intl()->Title(title);
+ aTitle = title;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleDocument::GetMimeType(nsAString& aType) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ nsAccUtils::DocumentMimeType(mIntl, aType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleDocument::GetDocType(nsAString& aType) {
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ Intl()->DocType(aType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleDocument::GetDOMDocument(dom::Document** aDOMDocument) {
+ NS_ENSURE_ARG_POINTER(aDOMDocument);
+ *aDOMDocument = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (Intl()->DocumentNode()) NS_ADDREF(*aDOMDocument = Intl()->DocumentNode());
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleDocument::GetWindow(mozIDOMWindowProxy** aDOMWindow) {
+ NS_ENSURE_ARG_POINTER(aDOMWindow);
+ *aDOMWindow = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aDOMWindow = Intl()->DocumentNode()->GetWindow());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleDocument::GetParentDocument(nsIAccessibleDocument** aDocument) {
+ NS_ENSURE_ARG_POINTER(aDocument);
+ *aDocument = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aDocument = ToXPCDocument(Intl()->ParentDocument()));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleDocument::GetChildDocumentCount(uint32_t* aCount) {
+ NS_ENSURE_ARG_POINTER(aCount);
+ *aCount = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aCount = Intl()->ChildDocumentCount();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleDocument::GetChildDocumentAt(uint32_t aIndex,
+ nsIAccessibleDocument** aDocument) {
+ NS_ENSURE_ARG_POINTER(aDocument);
+ *aDocument = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aDocument = ToXPCDocument(Intl()->GetChildDocumentAt(aIndex)));
+ return *aDocument ? NS_OK : NS_ERROR_INVALID_ARG;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// xpcAccessibleDocument
+
+xpcAccessibleGeneric* xpcAccessibleDocument::GetAccessible(
+ Accessible* aAccessible) {
+ if (aAccessible->IsLocal() &&
+ ToXPCDocument(aAccessible->AsLocal()->Document()) != this) {
+ NS_ERROR(
+ "This XPCOM document is not related with given internal accessible!");
+ return nullptr;
+ }
+
+ if (aAccessible->IsRemote() &&
+ ToXPCDocument(aAccessible->AsRemote()->Document()) != this) {
+ NS_ERROR(
+ "This XPCOM document is not related with given internal accessible!");
+ return nullptr;
+ }
+
+ if (aAccessible->IsDoc()) return this;
+
+ return mCache.LookupOrInsertWith(aAccessible, [&]() -> xpcAccessibleGeneric* {
+ if (aAccessible->IsImage()) {
+ return new xpcAccessibleImage(aAccessible);
+ }
+ if (aAccessible->IsTable()) {
+ return new xpcAccessibleTable(aAccessible);
+ }
+ if (aAccessible->IsTableCell()) {
+ return new xpcAccessibleTableCell(aAccessible);
+ }
+ if (aAccessible->IsHyperText()) {
+ return new xpcAccessibleHyperText(aAccessible);
+ }
+
+ return new xpcAccessibleGeneric(aAccessible);
+ });
+}
+
+void xpcAccessibleDocument::Shutdown() {
+ for (auto iter = mCache.Iter(); !iter.Done(); iter.Next()) {
+ iter.Data()->Shutdown();
+ iter.Remove();
+ }
+ xpcAccessibleGeneric::Shutdown();
+}
diff --git a/accessible/xpcom/xpcAccessibleDocument.h b/accessible/xpcom/xpcAccessibleDocument.h
new file mode 100644
index 0000000000..8e9bf2b413
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleDocument.h
@@ -0,0 +1,136 @@
+/* -*- 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_xpcAccessibleDocument_h_
+#define mozilla_a11y_xpcAccessibleDocument_h_
+
+#include "nsIAccessibleDocument.h"
+
+#include "DocAccessible.h"
+#include "nsAccessibilityService.h"
+#include "xpcAccessibleApplication.h"
+#include "xpcAccessibleHyperText.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * XPCOM wrapper around DocAccessible class.
+ */
+class xpcAccessibleDocument : public xpcAccessibleHyperText,
+ public nsIAccessibleDocument {
+ public:
+ explicit xpcAccessibleDocument(Accessible* aIntl)
+ : xpcAccessibleHyperText(aIntl),
+ mCache(kDefaultCacheLength),
+ mRemote(aIntl->IsRemote()) {
+ // XXX: Once there is a base doc class that both remote and local
+ // accessibles inherit, we can add the type to the prototype and remove this
+ // assert.
+ MOZ_ASSERT(aIntl->IsDoc());
+ }
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // nsIAccessibleDocument
+ NS_IMETHOD GetURL(nsAString& aURL) final;
+ NS_IMETHOD GetTitle(nsAString& aTitle) final;
+ NS_IMETHOD GetMimeType(nsAString& aType) final;
+ NS_IMETHOD GetDocType(nsAString& aType) final;
+ NS_IMETHOD GetDOMDocument(dom::Document** aDOMDocument) final;
+ NS_IMETHOD GetWindow(mozIDOMWindowProxy** aDOMWindow) final;
+ NS_IMETHOD GetParentDocument(nsIAccessibleDocument** aDocument) final;
+ NS_IMETHOD GetChildDocumentCount(uint32_t* aCount) final;
+ NS_IMETHOD GetChildDocumentAt(uint32_t aIndex,
+ nsIAccessibleDocument** aDocument) final;
+
+ /**
+ * Return XPCOM wrapper for the internal accessible.
+ */
+ xpcAccessibleGeneric* GetAccessible(Accessible* aAccessible);
+
+ virtual void Shutdown() override;
+
+ protected:
+ virtual ~xpcAccessibleDocument() {}
+
+ private:
+ DocAccessible* Intl() {
+ if (LocalAccessible* acc = mIntl->AsLocal()) {
+ return acc->AsDoc();
+ }
+
+ return nullptr;
+ }
+
+ void NotifyOfShutdown(Accessible* aAccessible) {
+ xpcAccessibleGeneric* xpcAcc = mCache.Get(aAccessible);
+ if (xpcAcc) {
+ xpcAcc->Shutdown();
+ }
+
+ mCache.Remove(aAccessible);
+ if (mCache.Count() == 0 && mRefCnt == 1) {
+ if (mIntl->IsLocal()) {
+ GetAccService()->RemoveFromXPCDocumentCache(mIntl->AsLocal()->AsDoc());
+ } else {
+ GetAccService()->RemoveFromRemoteXPCDocumentCache(
+ mIntl->AsRemote()->AsDoc());
+ }
+ }
+ }
+
+ friend class DocManager;
+ friend class DocAccessible;
+ friend class RemoteAccessible;
+ friend class xpcAccessibleGeneric;
+
+ xpcAccessibleDocument(const xpcAccessibleDocument&) = delete;
+ xpcAccessibleDocument& operator=(const xpcAccessibleDocument&) = delete;
+
+ nsTHashMap<nsPtrHashKey<const void>, xpcAccessibleGeneric*> mCache;
+ bool mRemote;
+};
+
+inline xpcAccessibleGeneric* ToXPC(Accessible* aAccessible) {
+ if (!aAccessible) return nullptr;
+
+ if (aAccessible->IsApplication()) return XPCApplicationAcc();
+
+ xpcAccessibleDocument* xpcDoc =
+ aAccessible->IsLocal()
+ ? GetAccService()->GetXPCDocument(aAccessible->AsLocal()->Document())
+ : GetAccService()->GetXPCDocument(
+ aAccessible->AsRemote()->Document());
+ return xpcDoc ? xpcDoc->GetAccessible(aAccessible) : nullptr;
+}
+
+inline xpcAccessibleHyperText* ToXPCText(Accessible* aAccessible) {
+ if (!aAccessible || !aAccessible->IsHyperText()) {
+ return nullptr;
+ }
+
+ xpcAccessibleDocument* xpcDoc =
+ aAccessible->IsLocal()
+ ? GetAccService()->GetXPCDocument(aAccessible->AsLocal()->Document())
+ : nsAccessibilityService::GetXPCDocument(
+ aAccessible->AsRemote()->Document());
+ return static_cast<xpcAccessibleHyperText*>(
+ xpcDoc ? xpcDoc->GetAccessible(aAccessible) : nullptr);
+}
+
+inline xpcAccessibleDocument* ToXPCDocument(DocAccessible* aAccessible) {
+ return GetAccService()->GetXPCDocument(aAccessible);
+}
+
+inline xpcAccessibleDocument* ToXPCDocument(DocAccessibleParent* aAccessible) {
+ return GetAccService()->GetXPCDocument(aAccessible);
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xpcom/xpcAccessibleGeneric.cpp b/accessible/xpcom/xpcAccessibleGeneric.cpp
new file mode 100644
index 0000000000..aaef5ac14c
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleGeneric.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 "xpcAccessibleGeneric.h"
+
+#include "xpcAccessibleDocument.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_INTERFACE_MAP_BEGIN(xpcAccessibleGeneric)
+ NS_INTERFACE_MAP_ENTRY(nsIAccessible)
+ NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIAccessibleSelectable,
+ mSupportedIfaces & eSelectable)
+ NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIAccessibleValue,
+ mSupportedIfaces & eValue)
+ NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIAccessibleHyperLink,
+ mSupportedIfaces & eHyperLink)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAccessible)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(xpcAccessibleGeneric)
+NS_IMPL_RELEASE(xpcAccessibleGeneric)
+
+xpcAccessibleGeneric::~xpcAccessibleGeneric() {
+ if (!mIntl) {
+ return;
+ }
+
+ xpcAccessibleDocument* xpcDoc = nullptr;
+ if (mIntl->IsLocal()) {
+ LocalAccessible* acc = mIntl->AsLocal();
+ if (!acc->IsDoc() && !acc->IsApplication()) {
+ xpcDoc = GetAccService()->GetXPCDocument(acc->Document());
+ xpcDoc->NotifyOfShutdown(acc);
+ }
+ } else {
+ RemoteAccessible* proxy = mIntl->AsRemote();
+ if (!proxy->IsDoc()) {
+ xpcDoc = GetAccService()->GetXPCDocument(proxy->Document());
+ xpcDoc->NotifyOfShutdown(proxy);
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessible
+
+LocalAccessible* xpcAccessibleGeneric::ToInternalAccessible() {
+ return mIntl->AsLocal();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// xpcAccessibleGeneric
+
+void xpcAccessibleGeneric::Shutdown() { mIntl = nullptr; }
diff --git a/accessible/xpcom/xpcAccessibleGeneric.h b/accessible/xpcom/xpcAccessibleGeneric.h
new file mode 100644
index 0000000000..65df7e6ec7
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleGeneric.h
@@ -0,0 +1,96 @@
+/* -*- 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_xpcAccessibleGeneric_h_
+#define mozilla_a11y_xpcAccessibleGeneric_h_
+
+#include "xpcAccessible.h"
+#include "xpcAccessibleHyperLink.h"
+#include "xpcAccessibleSelectable.h"
+#include "xpcAccessibleValue.h"
+
+#include "LocalAccessible.h"
+#include "mozilla/a11y/Accessible.h"
+#include "mozilla/a11y/RemoteAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * XPCOM wrapper around Accessible class.
+ */
+class xpcAccessibleGeneric : public xpcAccessible,
+ public xpcAccessibleHyperLink,
+ public xpcAccessibleSelectable,
+ public xpcAccessibleValue {
+ public:
+ explicit xpcAccessibleGeneric(Accessible* aInternal)
+ : mIntl(aInternal), mSupportedIfaces(0) {
+ if (aInternal->IsSelect()) mSupportedIfaces |= eSelectable;
+ if (aInternal->HasNumericValue()) mSupportedIfaces |= eValue;
+ if (aInternal->IsLink()) mSupportedIfaces |= eHyperLink;
+ }
+
+ NS_DECL_ISUPPORTS
+
+ // nsIAccessible
+ LocalAccessible* ToInternalAccessible() final;
+ Accessible* ToInternalGeneric() final { return mIntl; }
+
+ // xpcAccessibleGeneric
+ virtual void Shutdown();
+
+ protected:
+ virtual ~xpcAccessibleGeneric();
+
+ Accessible* mIntl;
+
+ enum {
+ eSelectable = 1 << 0,
+ eValue = 1 << 1,
+ eHyperLink = 1 << 2,
+ eText = 1 << 3
+ };
+ uint8_t mSupportedIfaces;
+
+ private:
+ friend class LocalAccessible;
+ friend class xpcAccessible;
+ friend class xpcAccessibleHyperLink;
+ friend class xpcAccessibleSelectable;
+ friend class xpcAccessibleValue;
+
+ xpcAccessibleGeneric(const xpcAccessibleGeneric&) = delete;
+ xpcAccessibleGeneric& operator=(const xpcAccessibleGeneric&) = delete;
+};
+
+inline LocalAccessible* xpcAccessible::Intl() {
+ if (!static_cast<xpcAccessibleGeneric*>(this)->mIntl) {
+ return nullptr;
+ }
+ return static_cast<xpcAccessibleGeneric*>(this)->mIntl->AsLocal();
+}
+
+inline Accessible* xpcAccessible::IntlGeneric() {
+ return static_cast<xpcAccessibleGeneric*>(this)->mIntl;
+}
+
+inline Accessible* xpcAccessibleHyperLink::Intl() {
+ return static_cast<xpcAccessibleGeneric*>(this)->mIntl;
+}
+
+inline Accessible* xpcAccessibleSelectable::Intl() {
+ return static_cast<xpcAccessibleGeneric*>(this)->mIntl;
+}
+
+inline Accessible* xpcAccessibleValue::Intl() {
+ return static_cast<xpcAccessibleGeneric*>(this)->mIntl;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xpcom/xpcAccessibleHyperLink.cpp b/accessible/xpcom/xpcAccessibleHyperLink.cpp
new file mode 100644
index 0000000000..113e8d3297
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleHyperLink.cpp
@@ -0,0 +1,92 @@
+/* -*- 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 "xpcAccessibleHyperLink.h"
+
+#include "xpcAccessibleDocument.h"
+
+using namespace mozilla::a11y;
+
+NS_IMETHODIMP
+xpcAccessibleHyperLink::GetStartIndex(int32_t* aStartIndex) {
+ NS_ENSURE_ARG_POINTER(aStartIndex);
+ *aStartIndex = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aStartIndex = static_cast<int32_t>(Intl()->StartOffset());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperLink::GetEndIndex(int32_t* aEndIndex) {
+ NS_ENSURE_ARG_POINTER(aEndIndex);
+ *aEndIndex = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aEndIndex = static_cast<int32_t>(Intl()->EndOffset());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperLink::GetAnchorCount(int32_t* aAnchorCount) {
+ NS_ENSURE_ARG_POINTER(aAnchorCount);
+ *aAnchorCount = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aAnchorCount = Intl()->AnchorCount();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperLink::GetURI(int32_t aIndex, nsIURI** aURI) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ if (!Intl()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aIndex < 0 || aIndex >= static_cast<int32_t>(Intl()->AnchorCount())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<nsIURI>(Intl()->AnchorURIAt(aIndex)).forget(aURI);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperLink::GetAnchor(int32_t aIndex, nsIAccessible** aAccessible) {
+ NS_ENSURE_ARG_POINTER(aAccessible);
+ *aAccessible = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (aIndex < 0) return NS_ERROR_INVALID_ARG;
+
+ if (aIndex >= static_cast<int32_t>(Intl()->AnchorCount())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ NS_IF_ADDREF(*aAccessible = ToXPC(Intl()->AnchorAt(aIndex)));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperLink::GetValid(bool* aValid) {
+ NS_ENSURE_ARG_POINTER(aValid);
+ *aValid = false;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aValid = Intl()->IsLinkValid();
+
+ return NS_OK;
+}
diff --git a/accessible/xpcom/xpcAccessibleHyperLink.h b/accessible/xpcom/xpcAccessibleHyperLink.h
new file mode 100644
index 0000000000..85e686eb54
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleHyperLink.h
@@ -0,0 +1,48 @@
+/* -*- 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_xpcAccessibleHyperLink_h_
+#define mozilla_a11y_xpcAccessibleHyperLink_h_
+
+#include "nsIAccessibleHyperLink.h"
+
+#include "mozilla/a11y/Accessible.h"
+
+class nsIAccessible;
+
+namespace mozilla {
+namespace a11y {
+
+class LocalAccessible;
+
+/**
+ * XPCOM nsIAccessibleHyperLink implementation, used by xpcAccessibleGeneric
+ * class.
+ */
+class xpcAccessibleHyperLink : public nsIAccessibleHyperLink {
+ public:
+ NS_IMETHOD GetAnchorCount(int32_t* aAnchorCount) final;
+ NS_IMETHOD GetStartIndex(int32_t* aStartIndex) final;
+ NS_IMETHOD GetEndIndex(int32_t* aEndIndex) final;
+ NS_IMETHOD GetURI(int32_t aIndex, nsIURI** aURI) final;
+ NS_IMETHOD GetAnchor(int32_t aIndex, nsIAccessible** aAccessible) final;
+ NS_IMETHOD GetValid(bool* aValid) final;
+
+ protected:
+ xpcAccessibleHyperLink() {}
+ virtual ~xpcAccessibleHyperLink() {}
+
+ private:
+ xpcAccessibleHyperLink(const xpcAccessibleHyperLink&) = delete;
+ xpcAccessibleHyperLink& operator=(const xpcAccessibleHyperLink&) = delete;
+
+ Accessible* Intl();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xpcom/xpcAccessibleHyperText.cpp b/accessible/xpcom/xpcAccessibleHyperText.cpp
new file mode 100644
index 0000000000..a1e86ac6cd
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleHyperText.cpp
@@ -0,0 +1,477 @@
+/* -*- 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 "xpcAccessibleHyperText.h"
+
+#include "TextRange.h"
+#include "AccAttributes.h"
+#include "nsComponentManagerUtils.h"
+#include "nsPersistentProperties.h"
+#include "xpcAccessibleDocument.h"
+#include "xpcAccessibleTextRange.h"
+
+#include "nsIMutableArray.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_INTERFACE_MAP_BEGIN(xpcAccessibleHyperText)
+ NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIAccessibleText,
+ mSupportedIfaces & eText)
+ NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIAccessibleEditableText,
+ mSupportedIfaces & eText)
+ NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIAccessibleHyperText,
+ mSupportedIfaces & eText)
+NS_INTERFACE_MAP_END_INHERITING(xpcAccessibleGeneric)
+
+NS_IMPL_ADDREF_INHERITED(xpcAccessibleHyperText, xpcAccessibleGeneric)
+NS_IMPL_RELEASE_INHERITED(xpcAccessibleHyperText, xpcAccessibleGeneric)
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessibleText
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetCharacterCount(int32_t* aCharacterCount) {
+ NS_ENSURE_ARG_POINTER(aCharacterCount);
+ *aCharacterCount = 0;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ *aCharacterCount = static_cast<int32_t>(Intl()->CharacterCount());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetText(int32_t aStartOffset, int32_t aEndOffset,
+ nsAString& aText) {
+ aText.Truncate();
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->TextSubstring(aStartOffset, aEndOffset, aText);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetTextBeforeOffset(
+ int32_t aOffset, AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset, int32_t* aEndOffset, nsAString& aText) {
+ NS_ENSURE_ARG_POINTER(aStartOffset);
+ NS_ENSURE_ARG_POINTER(aEndOffset);
+ *aStartOffset = *aEndOffset = 0;
+ aText.Truncate();
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->TextBeforeOffset(aOffset, aBoundaryType, aStartOffset, aEndOffset,
+ aText);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetTextAtOffset(int32_t aOffset,
+ AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset, nsAString& aText) {
+ NS_ENSURE_ARG_POINTER(aStartOffset);
+ NS_ENSURE_ARG_POINTER(aEndOffset);
+ *aStartOffset = *aEndOffset = 0;
+ aText.Truncate();
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->TextAtOffset(aOffset, aBoundaryType, aStartOffset, aEndOffset, aText);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetTextAfterOffset(int32_t aOffset,
+ AccessibleTextBoundary aBoundaryType,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset,
+ nsAString& aText) {
+ NS_ENSURE_ARG_POINTER(aStartOffset);
+ NS_ENSURE_ARG_POINTER(aEndOffset);
+ *aStartOffset = *aEndOffset = 0;
+ aText.Truncate();
+
+ Intl()->TextAfterOffset(aOffset, aBoundaryType, aStartOffset, aEndOffset,
+ aText);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetCharacterAtOffset(int32_t aOffset,
+ char16_t* aCharacter) {
+ NS_ENSURE_ARG_POINTER(aCharacter);
+ *aCharacter = L'\0';
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ *aCharacter = Intl()->CharAt(aOffset);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetTextAttributes(
+ bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset,
+ int32_t* aEndOffset, nsIPersistentProperties** aAttributes) {
+ NS_ENSURE_ARG_POINTER(aStartOffset);
+ NS_ENSURE_ARG_POINTER(aEndOffset);
+ NS_ENSURE_ARG_POINTER(aAttributes);
+ *aStartOffset = *aEndOffset = 0;
+ *aAttributes = nullptr;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ RefPtr<AccAttributes> attributes = Intl()->TextAttributes(
+ aIncludeDefAttrs, aOffset, aStartOffset, aEndOffset);
+ RefPtr<nsPersistentProperties> props = new nsPersistentProperties();
+ nsAutoString unused;
+ for (auto iter : *attributes) {
+ nsAutoString name;
+ iter.NameAsString(name);
+
+ nsAutoString value;
+ iter.ValueAsString(value);
+
+ props->SetStringProperty(NS_ConvertUTF16toUTF8(name), value, unused);
+ }
+
+ props.forget(aAttributes);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetDefaultTextAttributes(
+ nsIPersistentProperties** aAttributes) {
+ NS_ENSURE_ARG_POINTER(aAttributes);
+ *aAttributes = nullptr;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ RefPtr<AccAttributes> attributes = Intl()->DefaultTextAttributes();
+ RefPtr<nsPersistentProperties> props = new nsPersistentProperties();
+ nsAutoString unused;
+ for (auto iter : *attributes) {
+ nsAutoString name;
+ iter.NameAsString(name);
+
+ nsAutoString value;
+ iter.ValueAsString(value);
+
+ props->SetStringProperty(NS_ConvertUTF16toUTF8(name), value, unused);
+ }
+
+ props.forget(aAttributes);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetCharacterExtents(int32_t aOffset, int32_t* aX,
+ int32_t* aY, int32_t* aWidth,
+ int32_t* aHeight,
+ uint32_t aCoordType) {
+ NS_ENSURE_ARG_POINTER(aX);
+ NS_ENSURE_ARG_POINTER(aY);
+ NS_ENSURE_ARG_POINTER(aWidth);
+ NS_ENSURE_ARG_POINTER(aHeight);
+ *aX = *aY = *aWidth = *aHeight;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ LayoutDeviceIntRect rect = Intl()->CharBounds(aOffset, aCoordType);
+ rect.GetRect(aX, aY, aWidth, aHeight);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetRangeExtents(int32_t aStartOffset,
+ int32_t aEndOffset, int32_t* aX,
+ int32_t* aY, int32_t* aWidth,
+ int32_t* aHeight, uint32_t aCoordType) {
+ NS_ENSURE_ARG_POINTER(aX);
+ NS_ENSURE_ARG_POINTER(aY);
+ NS_ENSURE_ARG_POINTER(aWidth);
+ NS_ENSURE_ARG_POINTER(aHeight);
+ *aX = *aY = *aWidth = *aHeight = 0;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ LayoutDeviceIntRect rect =
+ Intl()->TextBounds(aStartOffset, aEndOffset, aCoordType);
+ rect.GetRect(aX, aY, aWidth, aHeight);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetOffsetAtPoint(int32_t aX, int32_t aY,
+ uint32_t aCoordType,
+ int32_t* aOffset) {
+ NS_ENSURE_ARG_POINTER(aOffset);
+ *aOffset = -1;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ *aOffset = Intl()->OffsetAtPoint(aX, aY, aCoordType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetCaretOffset(int32_t* aCaretOffset) {
+ NS_ENSURE_ARG_POINTER(aCaretOffset);
+ *aCaretOffset = -1;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ *aCaretOffset = Intl()->CaretOffset();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::SetCaretOffset(int32_t aCaretOffset) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->SetCaretOffset(aCaretOffset);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetCaretRect(int32_t* aX, int32_t* aY, int32_t* aWidth,
+ int32_t* aHeight) {
+ NS_ENSURE_ARG_POINTER(aX);
+ NS_ENSURE_ARG_POINTER(aY);
+ NS_ENSURE_ARG_POINTER(aWidth);
+ NS_ENSURE_ARG_POINTER(aHeight);
+ *aX = *aY = *aWidth = *aHeight;
+
+ if (!mIntl) {
+ return NS_ERROR_FAILURE;
+ }
+ if (mIntl->IsRemote()) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ nsIWidget* widget;
+ LayoutDeviceIntRect rect = IntlLocal()->GetCaretRect(&widget);
+ rect.GetRect(aX, aY, aWidth, aHeight);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetSelectionCount(int32_t* aSelectionCount) {
+ NS_ENSURE_ARG_POINTER(aSelectionCount);
+ *aSelectionCount = 0;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ *aSelectionCount = Intl()->SelectionCount();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetSelectionBounds(int32_t aSelectionNum,
+ int32_t* aStartOffset,
+ int32_t* aEndOffset) {
+ NS_ENSURE_ARG_POINTER(aStartOffset);
+ NS_ENSURE_ARG_POINTER(aEndOffset);
+ *aStartOffset = *aEndOffset = 0;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ if (aSelectionNum < 0) return NS_ERROR_INVALID_ARG;
+
+ if (aSelectionNum >= Intl()->SelectionCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ Intl()->SelectionBoundsAt(aSelectionNum, aStartOffset, aEndOffset);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::SetSelectionBounds(int32_t aSelectionNum,
+ int32_t aStartOffset,
+ int32_t aEndOffset) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ if (aSelectionNum < 0) return NS_ERROR_INVALID_ARG;
+
+ Intl()->SetSelectionBoundsAt(aSelectionNum, aStartOffset, aEndOffset);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::AddSelection(int32_t aStartOffset, int32_t aEndOffset) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->AddToSelection(aStartOffset, aEndOffset);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::RemoveSelection(int32_t aSelectionNum) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->RemoveFromSelection(aSelectionNum);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::ScrollSubstringTo(int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aScrollType) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->ScrollSubstringTo(aStartOffset, aEndOffset, aScrollType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::ScrollSubstringToPoint(int32_t aStartOffset,
+ int32_t aEndOffset,
+ uint32_t aCoordinateType,
+ int32_t aX, int32_t aY) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->ScrollSubstringToPoint(aStartOffset, aEndOffset, aCoordinateType, aX,
+ aY);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetSelectionRanges(nsIArray** aRanges) {
+ NS_ENSURE_ARG_POINTER(aRanges);
+ *aRanges = nullptr;
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMutableArray> xpcRanges =
+ do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoTArray<TextRange, 1> ranges;
+ Intl()->SelectionRanges(&ranges);
+ uint32_t len = ranges.Length();
+ for (uint32_t idx = 0; idx < len; idx++) {
+ xpcRanges->AppendElement(new xpcAccessibleTextRange(ranges[idx]));
+ }
+
+ xpcRanges.forget(aRanges);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessibleEditableText
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::SetTextContents(const nsAString& aText) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->ReplaceText(aText);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::InsertText(const nsAString& aText, int32_t aOffset) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->InsertText(aText, aOffset);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::CopyText(int32_t aStartOffset, int32_t aEndOffset) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->CopyText(aStartOffset, aEndOffset);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::CutText(int32_t aStartOffset, int32_t aEndOffset) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->CutText(aStartOffset, aEndOffset);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::DeleteText(int32_t aStartOffset, int32_t aEndOffset) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->DeleteText(aStartOffset, aEndOffset);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::PasteText(int32_t aOffset) {
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ Intl()->PasteText(aOffset);
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessibleHyperText
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetLinkCount(int32_t* aLinkCount) {
+ NS_ENSURE_ARG_POINTER(aLinkCount);
+ *aLinkCount = 0;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ *aLinkCount = static_cast<int32_t>(Intl()->LinkCount());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetLinkAt(int32_t aIndex,
+ nsIAccessibleHyperLink** aLink) {
+ NS_ENSURE_ARG_POINTER(aLink);
+ *aLink = nullptr;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aLink = ToXPC(Intl()->LinkAt(aIndex)));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetLinkIndex(nsIAccessibleHyperLink* aLink,
+ int32_t* aIndex) {
+ NS_ENSURE_ARG_POINTER(aLink);
+ NS_ENSURE_ARG_POINTER(aIndex);
+ *aIndex = -1;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIAccessible> xpcLink(do_QueryInterface(aLink));
+ Accessible* accLink = xpcLink->ToInternalGeneric();
+ *aIndex = Intl()->LinkIndexOf(accLink);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetLinkIndexAtOffset(int32_t aOffset,
+ int32_t* aLinkIndex) {
+ NS_ENSURE_ARG_POINTER(aLinkIndex);
+ *aLinkIndex = -1; // API says this magic value means 'not found'
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ *aLinkIndex = Intl()->LinkIndexAtOffset(aOffset);
+ return NS_OK;
+}
diff --git a/accessible/xpcom/xpcAccessibleHyperText.h b/accessible/xpcom/xpcAccessibleHyperText.h
new file mode 100644
index 0000000000..06cdba2d7a
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleHyperText.h
@@ -0,0 +1,57 @@
+/* -*- 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_xpcAccessibleHyperText_h_
+#define mozilla_a11y_xpcAccessibleHyperText_h_
+
+#include "nsIAccessibleText.h"
+#include "nsIAccessibleHyperText.h"
+#include "nsIAccessibleEditableText.h"
+
+#include "HyperTextAccessible.h"
+#include "xpcAccessibleGeneric.h"
+
+namespace mozilla {
+namespace a11y {
+
+class xpcAccessibleHyperText : public xpcAccessibleGeneric,
+ public nsIAccessibleText,
+ public nsIAccessibleEditableText,
+ public nsIAccessibleHyperText {
+ public:
+ explicit xpcAccessibleHyperText(Accessible* aIntl)
+ : xpcAccessibleGeneric(aIntl) {
+ if (aIntl->IsHyperText() && aIntl->IsTextRole()) mSupportedIfaces |= eText;
+ }
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_NSIACCESSIBLETEXT
+ NS_DECL_NSIACCESSIBLEHYPERTEXT
+ NS_DECL_NSIACCESSIBLEEDITABLETEXT
+
+ protected:
+ virtual ~xpcAccessibleHyperText() {}
+
+ private:
+ HyperTextAccessibleBase* Intl() { return mIntl->AsHyperTextBase(); }
+
+ HyperTextAccessible* IntlLocal() {
+ if (LocalAccessible* acc = mIntl->AsLocal()) {
+ return acc->AsHyperText();
+ }
+
+ return nullptr;
+ }
+
+ xpcAccessibleHyperText(const xpcAccessibleHyperText&) = delete;
+ xpcAccessibleHyperText& operator=(const xpcAccessibleHyperText&) = delete;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_xpcAccessibleHyperText_h_
diff --git a/accessible/xpcom/xpcAccessibleImage.cpp b/accessible/xpcom/xpcAccessibleImage.cpp
new file mode 100644
index 0000000000..137ebe6791
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleImage.cpp
@@ -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/. */
+
+#include "xpcAccessibleImage.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_IMPL_ISUPPORTS_INHERITED(xpcAccessibleImage, xpcAccessibleGeneric,
+ nsIAccessibleImage)
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessibleImage
+
+NS_IMETHODIMP
+xpcAccessibleImage::GetImagePosition(uint32_t aCoordType, int32_t* aX,
+ int32_t* aY) {
+ NS_ENSURE_ARG_POINTER(aX);
+ *aX = 0;
+ NS_ENSURE_ARG_POINTER(aY);
+ *aY = 0;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ LayoutDeviceIntPoint point = mIntl->Position(aCoordType);
+ *aX = point.x;
+ *aY = point.y;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleImage::GetImageSize(int32_t* aWidth, int32_t* aHeight) {
+ NS_ENSURE_ARG_POINTER(aWidth);
+ *aWidth = 0;
+ NS_ENSURE_ARG_POINTER(aHeight);
+ *aHeight = 0;
+
+ if (!mIntl) return NS_ERROR_FAILURE;
+
+ LayoutDeviceIntSize size = mIntl->Size();
+ *aWidth = size.width;
+ *aHeight = size.height;
+ return NS_OK;
+}
diff --git a/accessible/xpcom/xpcAccessibleImage.h b/accessible/xpcom/xpcAccessibleImage.h
new file mode 100644
index 0000000000..ca70ab35f8
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleImage.h
@@ -0,0 +1,40 @@
+/* -*- 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_xpcAccessibleImage_h_
+#define mozilla_a11y_xpcAccessibleImage_h_
+
+#include "nsIAccessibleImage.h"
+
+#include "xpcAccessibleGeneric.h"
+
+namespace mozilla {
+namespace a11y {
+
+class xpcAccessibleImage : public xpcAccessibleGeneric,
+ public nsIAccessibleImage {
+ public:
+ explicit xpcAccessibleImage(Accessible* aIntl)
+ : xpcAccessibleGeneric(aIntl) {}
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_IMETHOD GetImagePosition(uint32_t aCoordType, int32_t* aX,
+ int32_t* aY) final;
+ NS_IMETHOD GetImageSize(int32_t* aWidth, int32_t* aHeight) final;
+
+ protected:
+ virtual ~xpcAccessibleImage() {}
+
+ private:
+ xpcAccessibleImage(const xpcAccessibleImage&) = delete;
+ xpcAccessibleImage& operator=(const xpcAccessibleImage&) = delete;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xpcom/xpcAccessibleMacInterface.h b/accessible/xpcom/xpcAccessibleMacInterface.h
new file mode 100644
index 0000000000..18b5955087
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleMacInterface.h
@@ -0,0 +1,104 @@
+/* -*- 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_xpcAccessibleMacInterface_h_
+#define mozilla_a11y_xpcAccessibleMacInterface_h_
+
+#include "mozilla/a11y/Accessible.h"
+#include "nsIAccessibleMacInterface.h"
+
+class nsIAccessibleMacInterface;
+
+namespace mozilla {
+namespace a11y {
+
+class xpcAccessibleMacNSObjectWrapper : public nsIAccessibleMacNSObjectWrapper {
+ public:
+ explicit xpcAccessibleMacNSObjectWrapper(id aTextMarker);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIACCESSIBLEMACNSOBJECTWRAPPER
+
+ protected:
+ virtual ~xpcAccessibleMacNSObjectWrapper();
+
+ id mNativeObject;
+};
+
+class xpcAccessibleMacInterface : public xpcAccessibleMacNSObjectWrapper,
+ public nsIAccessibleMacInterface {
+ public:
+ // Construct an xpcAccessibleMacInterface using this
+ // native object that conforms to the NSAccessibility protocol.
+ explicit xpcAccessibleMacInterface(id aNativeObj)
+ : xpcAccessibleMacNSObjectWrapper(aNativeObj) {}
+
+ // Construct an xpcAccessibleMacInterface using the native object
+ // associated with this accessible.
+ explicit xpcAccessibleMacInterface(Accessible* aObj);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIACCESSIBLEMACINTERFACE
+
+ // Convert an NSObject (which can be anything, string, number, array, etc.)
+ // into a properly typed js value populated in the aResult handle.
+ static nsresult NSObjectToJsValue(id aObj, JSContext* aCx,
+ JS::MutableHandleValue aResult);
+
+ protected:
+ virtual ~xpcAccessibleMacInterface() {}
+
+ // Return true if our native object responds to this selector and
+ // if it implements isAccessibilitySelectorAllowed check that it returns true
+ // too.
+ bool SupportsSelector(SEL aSelector);
+
+ // Convert a js value to an NSObject. This is called recursively for arrays.
+ // If the conversion fails, aResult is set to an error and nil is returned.
+ id JsValueToNSObject(JS::HandleValue aValue, JSContext* aCx,
+ nsresult* aResult);
+
+ // Convert a js value to an NSValue NSObject. This is called
+ // by JsValueToNSObject when encountering a JS object with
+ // a "value" and "valueType" property.
+ id JsValueToNSValue(JS::HandleObject aObject, JSContext* aCx,
+ nsresult* aResult);
+
+ // Convert a js value to a specified NSObject. This is called
+ // by JsValueToNSObject when encountering a JS object with
+ // a "object" and "objcetType" property.
+ id JsValueToSpecifiedNSObject(JS::HandleObject aObject, JSContext* aCx,
+ nsresult* aResult);
+
+ private:
+ xpcAccessibleMacInterface(const xpcAccessibleMacInterface&) = delete;
+ xpcAccessibleMacInterface& operator=(const xpcAccessibleMacInterface&) =
+ delete;
+};
+
+class xpcAccessibleMacEvent : public nsIAccessibleMacEvent {
+ public:
+ explicit xpcAccessibleMacEvent(id aNativeObj, id aData);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIACCESSIBLEMACEVENT;
+
+ // This sends notifications via nsIObserverService to be consumed by our
+ // mochitests. aNativeObj is a NSAccessibility protocol object,
+ // and aNotification is a NSString.
+ static void FireEvent(id aNativeObj, id aNotification, id aUserInfo);
+
+ protected:
+ virtual ~xpcAccessibleMacEvent();
+
+ id mNativeObject;
+ id mData;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xpcom/xpcAccessibleMacInterface.mm b/accessible/xpcom/xpcAccessibleMacInterface.mm
new file mode 100644
index 0000000000..e05937360b
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleMacInterface.mm
@@ -0,0 +1,581 @@
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
+/* 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 "xpcAccessibleMacInterface.h"
+
+#include "nsCocoaUtils.h"
+#include "nsContentUtils.h"
+#include "nsIObserverService.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIXPConnect.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/Services.h"
+#include "nsString.h"
+#include "js/PropertyAndElement.h" // JS_Enumerate, JS_GetElement, JS_GetProperty, JS_GetPropertyById, JS_HasOwnProperty, JS_SetUCProperty
+
+#import "mozAccessible.h"
+
+using namespace mozilla::a11y;
+
+// xpcAccessibleMacNSObjectWrapper
+
+NS_IMPL_ISUPPORTS(xpcAccessibleMacNSObjectWrapper,
+ nsIAccessibleMacNSObjectWrapper)
+
+xpcAccessibleMacNSObjectWrapper::xpcAccessibleMacNSObjectWrapper(id aNativeObj)
+ : mNativeObject(aNativeObj) {
+ [mNativeObject retain];
+}
+
+xpcAccessibleMacNSObjectWrapper::~xpcAccessibleMacNSObjectWrapper() {
+ [mNativeObject release];
+}
+
+id xpcAccessibleMacNSObjectWrapper::GetNativeObject() { return mNativeObject; }
+
+// xpcAccessibleMacInterface
+
+NS_IMPL_ISUPPORTS_INHERITED(xpcAccessibleMacInterface,
+ xpcAccessibleMacNSObjectWrapper,
+ nsIAccessibleMacInterface)
+
+xpcAccessibleMacInterface::xpcAccessibleMacInterface(Accessible* aObj)
+ : xpcAccessibleMacNSObjectWrapper(GetNativeFromGeckoAccessible(aObj)) {}
+
+NS_IMETHODIMP
+xpcAccessibleMacInterface::GetAttributeNames(
+ nsTArray<nsString>& aAttributeNames) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+
+ if (!mNativeObject || [mNativeObject isExpired]) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ for (NSString* name in [mNativeObject accessibilityAttributeNames]) {
+ nsAutoString attribName;
+ nsCocoaUtils::GetStringForNSString(name, attribName);
+ aAttributeNames.AppendElement(attribName);
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
+}
+
+NS_IMETHODIMP
+xpcAccessibleMacInterface::GetParameterizedAttributeNames(
+ nsTArray<nsString>& aAttributeNames) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+
+ if (!mNativeObject || [mNativeObject isExpired]) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ for (NSString* name in
+ [mNativeObject accessibilityParameterizedAttributeNames]) {
+ nsAutoString attribName;
+ nsCocoaUtils::GetStringForNSString(name, attribName);
+ aAttributeNames.AppendElement(attribName);
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
+}
+
+NS_IMETHODIMP
+xpcAccessibleMacInterface::GetActionNames(nsTArray<nsString>& aActionNames) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+
+ if (!mNativeObject || [mNativeObject isExpired]) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ for (NSString* name in [mNativeObject accessibilityActionNames]) {
+ nsAutoString actionName;
+ nsCocoaUtils::GetStringForNSString(name, actionName);
+ aActionNames.AppendElement(actionName);
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
+}
+
+NS_IMETHODIMP
+xpcAccessibleMacInterface::PerformAction(const nsAString& aActionName) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+
+ if (!mNativeObject || [mNativeObject isExpired]) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ NSString* actionName = nsCocoaUtils::ToNSString(aActionName);
+ [mNativeObject accessibilityPerformAction:actionName];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
+}
+
+NS_IMETHODIMP
+xpcAccessibleMacInterface::GetAttributeValue(const nsAString& aAttributeName,
+ JSContext* aCx,
+ JS::MutableHandleValue aResult) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN
+
+ if (!mNativeObject || [mNativeObject isExpired]) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ NSString* attribName = nsCocoaUtils::ToNSString(aAttributeName);
+ return NSObjectToJsValue(
+ [mNativeObject accessibilityAttributeValue:attribName], aCx, aResult);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
+}
+
+NS_IMETHODIMP
+xpcAccessibleMacInterface::IsAttributeSettable(const nsAString& aAttributeName,
+ bool* aIsSettable) {
+ NS_ENSURE_ARG_POINTER(aIsSettable);
+
+ NSString* attribName = nsCocoaUtils::ToNSString(aAttributeName);
+ if ([mNativeObject
+ respondsToSelector:@selector(accessibilityIsAttributeSettable:)]) {
+ *aIsSettable = [mNativeObject accessibilityIsAttributeSettable:attribName];
+ return NS_OK;
+ }
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+xpcAccessibleMacInterface::SetAttributeValue(const nsAString& aAttributeName,
+ JS::HandleValue aAttributeValue,
+ JSContext* aCx) {
+ nsresult rv = NS_OK;
+ id obj = JsValueToNSObject(aAttributeValue, aCx, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NSString* attribName = nsCocoaUtils::ToNSString(aAttributeName);
+ if ([mNativeObject respondsToSelector:@selector(accessibilitySetValue:
+ forAttribute:)]) {
+ // The NSObject has an attribute setter, call that.
+ [mNativeObject accessibilitySetValue:obj forAttribute:attribName];
+ return NS_OK;
+ }
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+xpcAccessibleMacInterface::GetParameterizedAttributeValue(
+ const nsAString& aAttributeName, JS::HandleValue aParameter, JSContext* aCx,
+ JS::MutableHandleValue aResult) {
+ nsresult rv = NS_OK;
+ id paramObj = JsValueToNSObject(aParameter, aCx, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NSString* attribName = nsCocoaUtils::ToNSString(aAttributeName);
+ return NSObjectToJsValue([mNativeObject accessibilityAttributeValue:attribName
+ forParameter:paramObj],
+ aCx, aResult);
+}
+
+bool xpcAccessibleMacInterface::SupportsSelector(SEL aSelector) {
+ // return true if we have this selector, and if isAccessibilitySelectorAllowed
+ // is implemented too whether it is "allowed".
+ return [mNativeObject respondsToSelector:aSelector] &&
+ (![mNativeObject respondsToSelector:@selector
+ (isAccessibilitySelectorAllowed:selector:)] ||
+ [mNativeObject isAccessibilitySelectorAllowed:aSelector]);
+}
+
+nsresult xpcAccessibleMacInterface::NSObjectToJsValue(
+ id aObj, JSContext* aCx, JS::MutableHandleValue aResult) {
+ if (!aObj) {
+ aResult.set(JS::NullValue());
+ } else if ([aObj isKindOfClass:[NSString class]]) {
+ nsAutoString strVal;
+ nsCocoaUtils::GetStringForNSString((NSString*)aObj, strVal);
+ if (!mozilla::dom::ToJSValue(aCx, strVal, aResult)) {
+ return NS_ERROR_FAILURE;
+ }
+ } else if ([aObj isKindOfClass:[NSNumber class]]) {
+ // If the type being held by the NSNumber is a BOOL set js value
+ // to boolean. Otherwise use a double value.
+ if (strcmp([(NSNumber*)aObj objCType], @encode(BOOL)) == 0) {
+ if (!mozilla::dom::ToJSValue(aCx, [(NSNumber*)aObj boolValue], aResult)) {
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ if (!mozilla::dom::ToJSValue(aCx, [(NSNumber*)aObj doubleValue],
+ aResult)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ } else if ([aObj isKindOfClass:[NSValue class]] &&
+ strcmp([(NSValue*)aObj objCType], @encode(NSPoint)) == 0) {
+ NSPoint point = [(NSValue*)aObj pointValue];
+ return NSObjectToJsValue(
+ @[
+ [NSNumber numberWithDouble:point.x],
+ [NSNumber numberWithDouble:point.y]
+ ],
+ aCx, aResult);
+ } else if ([aObj isKindOfClass:[NSValue class]] &&
+ strcmp([(NSValue*)aObj objCType], @encode(NSSize)) == 0) {
+ NSSize size = [(NSValue*)aObj sizeValue];
+ return NSObjectToJsValue(
+ @[
+ [NSNumber numberWithDouble:size.width],
+ [NSNumber numberWithDouble:size.height]
+ ],
+ aCx, aResult);
+ } else if ([aObj isKindOfClass:[NSValue class]] &&
+ strcmp([(NSValue*)aObj objCType], @encode(NSRange)) == 0) {
+ NSRange range = [(NSValue*)aObj rangeValue];
+ return NSObjectToJsValue(@[ @(range.location), @(range.length) ], aCx,
+ aResult);
+ } else if ([aObj isKindOfClass:[NSValue class]] &&
+ strcmp([(NSValue*)aObj objCType], @encode(NSRect)) == 0) {
+ NSRect rect = [(NSValue*)aObj rectValue];
+ return NSObjectToJsValue(@{
+ @"origin" : [NSValue valueWithPoint:rect.origin],
+ @"size" : [NSValue valueWithSize:rect.size]
+ },
+ aCx, aResult);
+ } else if ([aObj isKindOfClass:[NSArray class]]) {
+ NSArray* objArr = (NSArray*)aObj;
+
+ JS::RootedVector<JS::Value> v(aCx);
+ if (!v.resize([objArr count])) {
+ return NS_ERROR_FAILURE;
+ }
+ for (size_t i = 0; i < [objArr count]; ++i) {
+ nsresult rv = NSObjectToJsValue(objArr[i], aCx, v[i]);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ JSObject* arrayObj = JS::NewArrayObject(aCx, v);
+ if (!arrayObj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*arrayObj);
+ } else if ([aObj isKindOfClass:[NSDictionary class]]) {
+ JS::RootedObject obj(aCx, JS_NewPlainObject(aCx));
+ for (NSString* key in aObj) {
+ nsAutoString strKey;
+ nsCocoaUtils::GetStringForNSString(key, strKey);
+ JS::RootedValue value(aCx);
+ nsresult rv = NSObjectToJsValue(aObj[key], aCx, &value);
+ NS_ENSURE_SUCCESS(rv, rv);
+ JS_SetUCProperty(aCx, obj, strKey.get(), strKey.Length(), value);
+ }
+ aResult.setObject(*obj);
+ } else if ([aObj isKindOfClass:[NSAttributedString class]]) {
+ NSAttributedString* attrStr = (NSAttributedString*)aObj;
+ __block NSMutableArray* attrRunArray = [[NSMutableArray alloc] init];
+
+ [attrStr
+ enumerateAttributesInRange:NSMakeRange(0, [attrStr length])
+ options:
+ NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+ usingBlock:^(NSDictionary* attributes, NSRange range,
+ BOOL* stop) {
+ NSString* str =
+ [[attrStr string] substringWithRange:range];
+ if (!str || !attributes) {
+ return;
+ }
+
+ NSMutableDictionary* attrRun =
+ [attributes mutableCopy];
+ attrRun[@"string"] = str;
+
+ [attrRunArray addObject:attrRun];
+ }];
+
+ // The attributed string is represented in js as an array of objects.
+ // Each object represents a run of text where the "string" property is the
+ // string value and all the AX* properties are the attributes.
+ return NSObjectToJsValue(attrRunArray, aCx, aResult);
+ } else if (CFGetTypeID(aObj) == CGColorGetTypeID()) {
+ const CGFloat* components = CGColorGetComponents((CGColorRef)aObj);
+ NSString* hexString = [NSString
+ stringWithFormat:@"#%02x%02x%02x", (int)(components[0] * 0xff),
+ (int)(components[1] * 0xff),
+ (int)(components[2] * 0xff)];
+ return NSObjectToJsValue(hexString, aCx, aResult);
+ } else if ([aObj respondsToSelector:@selector(isAccessibilityElement)]) {
+ // We expect all of our accessibility objects to implement
+ // isAccessibilityElement at the very least. If it is implemented we will
+ // assume its an accessibility object.
+ nsCOMPtr<nsIAccessibleMacInterface> obj =
+ new xpcAccessibleMacInterface(aObj);
+ return nsContentUtils::WrapNative(
+ aCx, obj, &NS_GET_IID(nsIAccessibleMacInterface), aResult);
+ } else {
+ // If this is any other kind of NSObject, just wrap it and return it.
+ // It will be opaque and immutable on the JS side, but it can be
+ // brought back to us in an argument.
+ nsCOMPtr<nsIAccessibleMacNSObjectWrapper> obj =
+ new xpcAccessibleMacNSObjectWrapper(aObj);
+ return nsContentUtils::WrapNative(
+ aCx, obj, &NS_GET_IID(nsIAccessibleMacNSObjectWrapper), aResult);
+ }
+
+ return NS_OK;
+}
+
+id xpcAccessibleMacInterface::JsValueToNSObject(JS::HandleValue aValue,
+ JSContext* aCx,
+ nsresult* aResult) {
+ *aResult = NS_OK;
+ if (aValue.isInt32()) {
+ return [NSNumber numberWithInteger:aValue.toInt32()];
+ } else if (aValue.isBoolean()) {
+ return [NSNumber numberWithBool:aValue.toBoolean()];
+ } else if (aValue.isString()) {
+ nsAutoJSString temp;
+ if (!temp.init(aCx, aValue)) {
+ NS_WARNING("cannot init string with given value");
+ *aResult = NS_ERROR_FAILURE;
+ return nil;
+ }
+ return nsCocoaUtils::ToNSString(temp);
+ } else if (aValue.isObject()) {
+ JS::Rooted<JSObject*> obj(aCx, aValue.toObjectOrNull());
+
+ bool isArray;
+ JS::IsArrayObject(aCx, obj, &isArray);
+ if (isArray) {
+ // If this is an array, we construct an NSArray and insert the js
+ // array's elements by recursively calling this function.
+ uint32_t len;
+ JS::GetArrayLength(aCx, obj, &len);
+ NSMutableArray* array = [NSMutableArray arrayWithCapacity:len];
+ for (uint32_t i = 0; i < len; i++) {
+ JS::RootedValue v(aCx);
+ JS_GetElement(aCx, obj, i, &v);
+ [array addObject:JsValueToNSObject(v, aCx, aResult)];
+ NS_ENSURE_SUCCESS(*aResult, nil);
+ }
+ return array;
+ }
+
+ bool hasValueType;
+ bool hasValue;
+ JS_HasOwnProperty(aCx, obj, "valueType", &hasValueType);
+ JS_HasOwnProperty(aCx, obj, "value", &hasValue);
+ if (hasValueType && hasValue) {
+ // A js object representin an NSValue looks like this:
+ // { valueType: "NSRange", value: [1, 3] }
+ return JsValueToNSValue(obj, aCx, aResult);
+ }
+
+ bool hasObjectType;
+ bool hasObject;
+ JS_HasOwnProperty(aCx, obj, "objectType", &hasObjectType);
+ JS_HasOwnProperty(aCx, obj, "object", &hasObject);
+ if (hasObjectType && hasObject) {
+ // A js object representing an NSDictionary looks like this:
+ // { objectType: "NSDictionary", value: {k: v, k: v, ...} }
+ return JsValueToSpecifiedNSObject(obj, aCx, aResult);
+ }
+
+ // This may be another nsIAccessibleMacInterface instance.
+ // If so, return the wrapped NSObject.
+ nsCOMPtr<nsIXPConnect> xpc = nsIXPConnect::XPConnect();
+
+ nsCOMPtr<nsIXPConnectWrappedNative> wrappedObj;
+ nsresult rv =
+ xpc->GetWrappedNativeOfJSObject(aCx, obj, getter_AddRefs(wrappedObj));
+ NS_ENSURE_SUCCESS(rv, nil);
+ nsCOMPtr<nsIAccessibleMacNSObjectWrapper> macObjIface =
+ do_QueryInterface(wrappedObj->Native());
+ return macObjIface->GetNativeObject();
+ }
+
+ *aResult = NS_ERROR_FAILURE;
+ return nil;
+}
+
+id xpcAccessibleMacInterface::JsValueToNSValue(JS::HandleObject aObject,
+ JSContext* aCx,
+ nsresult* aResult) {
+ *aResult = NS_ERROR_FAILURE;
+ JS::RootedValue valueTypeValue(aCx);
+ if (!JS_GetProperty(aCx, aObject, "valueType", &valueTypeValue)) {
+ NS_WARNING("Could not get valueType");
+ return nil;
+ }
+
+ JS::RootedValue valueValue(aCx);
+ if (!JS_GetProperty(aCx, aObject, "value", &valueValue)) {
+ NS_WARNING("Could not get value");
+ return nil;
+ }
+
+ nsAutoJSString valueType;
+ if (!valueTypeValue.isString() || !valueType.init(aCx, valueTypeValue)) {
+ NS_WARNING("valueType is not a string");
+ return nil;
+ }
+
+ bool isArray;
+ JS::IsArrayObject(aCx, valueValue, &isArray);
+ if (!isArray) {
+ NS_WARNING("value is not an array");
+ return nil;
+ }
+
+ JS::Rooted<JSObject*> value(aCx, valueValue.toObjectOrNull());
+
+ if (valueType.EqualsLiteral("NSRange")) {
+ uint32_t len;
+ JS::GetArrayLength(aCx, value, &len);
+ if (len != 2) {
+ NS_WARNING("Expected a 2 member array");
+ return nil;
+ }
+
+ JS::RootedValue locationValue(aCx);
+ JS_GetElement(aCx, value, 0, &locationValue);
+ JS::RootedValue lengthValue(aCx);
+ JS_GetElement(aCx, value, 1, &lengthValue);
+ if (!locationValue.isInt32() || !lengthValue.isInt32()) {
+ NS_WARNING("Expected an array of integers");
+ return nil;
+ }
+
+ *aResult = NS_OK;
+ return [NSValue valueWithRange:NSMakeRange(locationValue.toInt32(),
+ lengthValue.toInt32())];
+ }
+
+ return nil;
+}
+
+id xpcAccessibleMacInterface::JsValueToSpecifiedNSObject(
+ JS::HandleObject aObject, JSContext* aCx, nsresult* aResult) {
+ *aResult = NS_ERROR_FAILURE;
+ JS::RootedValue objectTypeValue(aCx);
+ if (!JS_GetProperty(aCx, aObject, "objectType", &objectTypeValue)) {
+ NS_WARNING("Could not get objectType");
+ return nil;
+ }
+
+ JS::RootedValue objectValue(aCx);
+ if (!JS_GetProperty(aCx, aObject, "object", &objectValue)) {
+ NS_WARNING("Could not get object");
+ return nil;
+ }
+
+ nsAutoJSString objectType;
+ if (!objectTypeValue.isString()) {
+ NS_WARNING("objectType is not a string");
+ return nil;
+ }
+
+ if (!objectType.init(aCx, objectTypeValue)) {
+ NS_WARNING("cannot init string with object type");
+ return nil;
+ }
+
+ bool isObject = objectValue.isObjectOrNull();
+ if (!isObject) {
+ NS_WARNING("object is not a JSON object");
+ return nil;
+ }
+
+ JS::Rooted<JSObject*> object(aCx, objectValue.toObjectOrNull());
+
+ if (objectType.EqualsLiteral("NSDictionary")) {
+ JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, object, &ids)) {
+ NS_WARNING("Unable to get keys from dictionary object");
+ return nil;
+ }
+
+ NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
+
+ for (size_t i = 0, n = ids.length(); i < n; i++) {
+ nsresult rv = NS_OK;
+ // get current key
+ JS::RootedValue currentKey(aCx);
+ JS_IdToValue(aCx, ids[i], &currentKey);
+ id unwrappedKey = JsValueToNSObject(currentKey, aCx, &rv);
+ NS_ENSURE_SUCCESS(rv, nil);
+ MOZ_ASSERT([unwrappedKey isKindOfClass:[NSString class]]);
+
+ // get associated value for current key
+ JS::RootedValue currentValue(aCx);
+ JS_GetPropertyById(aCx, object, ids[i], &currentValue);
+ id unwrappedValue = JsValueToNSObject(currentValue, aCx, &rv);
+ NS_ENSURE_SUCCESS(rv, nil);
+ dict[unwrappedKey] = unwrappedValue;
+ }
+
+ *aResult = NS_OK;
+ return dict;
+ }
+
+ return nil;
+}
+
+NS_IMPL_ISUPPORTS(xpcAccessibleMacEvent, nsIAccessibleMacEvent)
+
+xpcAccessibleMacEvent::xpcAccessibleMacEvent(id aNativeObj, id aData)
+ : mNativeObject(aNativeObj), mData(aData) {
+ [mNativeObject retain];
+ [mData retain];
+}
+
+xpcAccessibleMacEvent::~xpcAccessibleMacEvent() {
+ [mNativeObject release];
+ [mData release];
+}
+
+NS_IMETHODIMP
+xpcAccessibleMacEvent::GetMacIface(nsIAccessibleMacInterface** aMacIface) {
+ RefPtr<xpcAccessibleMacInterface> macIface =
+ new xpcAccessibleMacInterface(mNativeObject);
+ macIface.forget(aMacIface);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleMacEvent::GetData(JSContext* aCx, JS::MutableHandleValue aData) {
+ return xpcAccessibleMacInterface::NSObjectToJsValue(mData, aCx, aData);
+}
+
+void xpcAccessibleMacEvent::FireEvent(id aNativeObj, id aNotification,
+ id aUserInfo) {
+ if (nsCOMPtr<nsIObserverService> obsService =
+ services::GetObserverService()) {
+ nsCOMPtr<nsISimpleEnumerator> observers;
+ // Get all observers for the mac event topic.
+ obsService->EnumerateObservers(NS_ACCESSIBLE_MAC_EVENT_TOPIC,
+ getter_AddRefs(observers));
+ if (observers) {
+ bool hasObservers = false;
+ observers->HasMoreElements(&hasObservers);
+ // If we have observers, notify them.
+ if (hasObservers) {
+ nsCOMPtr<nsIAccessibleMacEvent> xpcIface =
+ new xpcAccessibleMacEvent(aNativeObj, aUserInfo);
+ nsAutoString notificationStr;
+ nsCocoaUtils::GetStringForNSString(aNotification, notificationStr);
+ obsService->NotifyObservers(xpcIface, NS_ACCESSIBLE_MAC_EVENT_TOPIC,
+ notificationStr.get());
+ }
+ }
+ }
+}
diff --git a/accessible/xpcom/xpcAccessiblePivot.cpp b/accessible/xpcom/xpcAccessiblePivot.cpp
new file mode 100644
index 0000000000..862cb88bdd
--- /dev/null
+++ b/accessible/xpcom/xpcAccessiblePivot.cpp
@@ -0,0 +1,155 @@
+/* -*- 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 "xpcAccessiblePivot.h"
+#include "xpcAccessibleDocument.h"
+
+#include "Pivot.h"
+
+using namespace mozilla::a11y;
+
+using mozilla::DebugOnly;
+
+/**
+ * An object that stores a given traversal rule during the pivot movement.
+ */
+class xpcPivotRule : public PivotRule {
+ public:
+ explicit xpcPivotRule(nsIAccessibleTraversalRule* aRule) : mRule(aRule) {}
+ ~xpcPivotRule() {}
+
+ virtual uint16_t Match(Accessible* aAcc) override;
+
+ private:
+ nsCOMPtr<nsIAccessibleTraversalRule> mRule;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// xpcAccessiblePivot
+
+xpcAccessiblePivot::xpcAccessiblePivot(nsIAccessible* aRoot) : mRoot(aRoot) {
+ NS_ASSERTION(aRoot, "A root accessible is required");
+}
+
+xpcAccessiblePivot::~xpcAccessiblePivot() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_IMPL_CYCLE_COLLECTION(xpcAccessiblePivot, mRoot)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(xpcAccessiblePivot)
+ NS_INTERFACE_MAP_ENTRY(nsIAccessiblePivot)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAccessiblePivot)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(xpcAccessiblePivot)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(xpcAccessiblePivot)
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessiblePivot
+
+NS_IMETHODIMP
+xpcAccessiblePivot::Next(nsIAccessible* aAnchor,
+ nsIAccessibleTraversalRule* aRule, bool aIncludeStart,
+ uint8_t aArgc, nsIAccessible** aResult) {
+ NS_ENSURE_ARG(aResult);
+ NS_ENSURE_ARG(aRule);
+
+ Accessible* root = Root();
+ Accessible* anchor = aAnchor->ToInternalGeneric();
+ NS_ENSURE_TRUE(root && anchor, NS_ERROR_NOT_IN_TREE);
+
+ Pivot pivot(Root());
+ xpcPivotRule rule(aRule);
+ Accessible* result =
+ pivot.Next(anchor, rule, (aArgc > 0) ? aIncludeStart : false);
+ NS_IF_ADDREF(*aResult = ToXPC(result));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessiblePivot::Prev(nsIAccessible* aAnchor,
+ nsIAccessibleTraversalRule* aRule, bool aIncludeStart,
+ uint8_t aArgc, nsIAccessible** aResult) {
+ NS_ENSURE_ARG(aResult);
+ NS_ENSURE_ARG(aRule);
+
+ Accessible* root = Root();
+ Accessible* anchor = aAnchor->ToInternalGeneric();
+ NS_ENSURE_TRUE(root && anchor, NS_ERROR_NOT_IN_TREE);
+
+ Pivot pivot(Root());
+ xpcPivotRule rule(aRule);
+ Accessible* result =
+ pivot.Prev(anchor, rule, (aArgc > 0) ? aIncludeStart : false);
+ NS_IF_ADDREF(*aResult = ToXPC(result));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessiblePivot::First(nsIAccessibleTraversalRule* aRule,
+ nsIAccessible** aResult) {
+ NS_ENSURE_ARG(aResult);
+ NS_ENSURE_ARG(aRule);
+
+ Accessible* root = Root();
+ NS_ENSURE_TRUE(root, NS_ERROR_NOT_IN_TREE);
+
+ Pivot pivot(root);
+ xpcPivotRule rule(aRule);
+ Accessible* result = pivot.First(rule);
+ NS_IF_ADDREF(*aResult = ToXPC(result));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessiblePivot::Last(nsIAccessibleTraversalRule* aRule,
+ nsIAccessible** aResult) {
+ NS_ENSURE_ARG(aResult);
+ NS_ENSURE_ARG(aRule);
+
+ Accessible* root = Root();
+ NS_ENSURE_TRUE(root, NS_ERROR_NOT_IN_TREE);
+
+ Pivot pivot(root);
+ xpcPivotRule rule(aRule);
+ Accessible* result = pivot.Last(rule);
+ NS_IF_ADDREF(*aResult = ToXPC(result));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessiblePivot::AtPoint(int32_t aX, int32_t aY,
+ nsIAccessibleTraversalRule* aRule,
+ nsIAccessible** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ENSURE_ARG_POINTER(aRule);
+
+ Accessible* root = Root();
+ NS_ENSURE_TRUE(root, NS_ERROR_NOT_IN_TREE);
+
+ xpcPivotRule rule(aRule);
+ Pivot pivot(root);
+
+ Accessible* result = pivot.AtPoint(aX, aY, rule);
+ NS_IF_ADDREF(*aResult = ToXPC(result));
+
+ return NS_OK;
+}
+
+uint16_t xpcPivotRule::Match(Accessible* aAcc) {
+ uint16_t matchResult = nsIAccessibleTraversalRule::FILTER_IGNORE;
+
+ DebugOnly<nsresult> rv = mRule->Match(ToXPC(aAcc), &matchResult);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ return matchResult;
+}
diff --git a/accessible/xpcom/xpcAccessiblePivot.h b/accessible/xpcom/xpcAccessiblePivot.h
new file mode 100644
index 0000000000..db5fdbdc3d
--- /dev/null
+++ b/accessible/xpcom/xpcAccessiblePivot.h
@@ -0,0 +1,47 @@
+/* -*- 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 _xpcAccessiblePivot_H_
+#define _xpcAccessiblePivot_H_
+
+#include "nsIAccessiblePivot.h"
+
+#include "Accessible.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/Attributes.h"
+#include "xpcAccessible.h"
+
+namespace mozilla::a11y {
+/**
+ * Class represents an accessible pivot.
+ */
+class xpcAccessiblePivot final : public nsIAccessiblePivot {
+ public:
+ explicit xpcAccessiblePivot(nsIAccessible* aRoot);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(xpcAccessiblePivot,
+ nsIAccessiblePivot)
+
+ NS_DECL_NSIACCESSIBLEPIVOT
+
+ private:
+ ~xpcAccessiblePivot();
+ xpcAccessiblePivot() = delete;
+ xpcAccessiblePivot(const xpcAccessiblePivot&) = delete;
+ void operator=(const xpcAccessiblePivot&) = delete;
+
+ Accessible* Root() { return mRoot ? mRoot->ToInternalGeneric() : nullptr; }
+
+ /*
+ * The root accessible.
+ */
+ RefPtr<nsIAccessible> mRoot;
+};
+
+} // namespace mozilla::a11y
+
+#endif
diff --git a/accessible/xpcom/xpcAccessibleSelectable.cpp b/accessible/xpcom/xpcAccessibleSelectable.cpp
new file mode 100644
index 0000000000..b3c2b424f0
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleSelectable.cpp
@@ -0,0 +1,119 @@
+/* -*- 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 "nsComponentManagerUtils.h"
+#include "nsIAccessible.h"
+#include "nsIMutableArray.h"
+#include "xpcAccessibleDocument.h"
+#include "xpcAccessibleSelectable.h"
+
+using namespace mozilla::a11y;
+
+NS_IMETHODIMP
+xpcAccessibleSelectable::GetSelectedItems(nsIArray** aSelectedItems) {
+ NS_ENSURE_ARG_POINTER(aSelectedItems);
+ *aSelectedItems = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+ MOZ_ASSERT(Intl()->IsSelect(), "Called on non selectable widget!");
+
+ AutoTArray<Accessible*, 10> items;
+ Intl()->SelectedItems(&items);
+
+ uint32_t itemCount = items.Length();
+ if (itemCount == 0) return NS_OK;
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMutableArray> xpcItems =
+ do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t idx = 0; idx < itemCount; idx++) {
+ xpcItems->AppendElement(static_cast<nsIAccessible*>(ToXPC(items[idx])));
+ }
+
+ NS_ADDREF(*aSelectedItems = xpcItems);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleSelectable::GetSelectedItemCount(uint32_t* aSelectionCount) {
+ NS_ENSURE_ARG_POINTER(aSelectionCount);
+ *aSelectionCount = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+ MOZ_ASSERT(Intl()->IsSelect(), "Called on non selectable widget!");
+
+ *aSelectionCount = Intl()->SelectedItemCount();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleSelectable::GetSelectedItemAt(uint32_t aIndex,
+ nsIAccessible** aSelected) {
+ NS_ENSURE_ARG_POINTER(aSelected);
+ *aSelected = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+ MOZ_ASSERT(Intl()->IsSelect(), "Called on non selectable widget!");
+
+ *aSelected = ToXPC(Intl()->GetSelectedItem(aIndex));
+ if (*aSelected) {
+ NS_ADDREF(*aSelected);
+ return NS_OK;
+ }
+
+ return NS_ERROR_INVALID_ARG;
+}
+
+NS_IMETHODIMP
+xpcAccessibleSelectable::IsItemSelected(uint32_t aIndex, bool* aIsSelected) {
+ NS_ENSURE_ARG_POINTER(aIsSelected);
+ *aIsSelected = false;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+ MOZ_ASSERT(Intl()->IsSelect(), "Called on non selectable widget!");
+
+ *aIsSelected = Intl()->IsItemSelected(aIndex);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleSelectable::AddItemToSelection(uint32_t aIndex) {
+ if (!Intl()) return NS_ERROR_FAILURE;
+ MOZ_ASSERT(Intl()->IsSelect(), "Called on non selectable widget!");
+
+ return Intl()->AddItemToSelection(aIndex) ? NS_OK : NS_ERROR_INVALID_ARG;
+}
+
+NS_IMETHODIMP
+xpcAccessibleSelectable::RemoveItemFromSelection(uint32_t aIndex) {
+ if (!Intl()) return NS_ERROR_FAILURE;
+ MOZ_ASSERT(Intl()->IsSelect(), "Called on non selectable widget!");
+
+ return Intl()->RemoveItemFromSelection(aIndex) ? NS_OK : NS_ERROR_INVALID_ARG;
+}
+
+NS_IMETHODIMP
+xpcAccessibleSelectable::SelectAll(bool* aIsMultiSelect) {
+ NS_ENSURE_ARG_POINTER(aIsMultiSelect);
+ *aIsMultiSelect = false;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+ MOZ_ASSERT(Intl()->IsSelect(), "Called on non selectable widget!");
+
+ *aIsMultiSelect = Intl()->SelectAll();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleSelectable::UnselectAll() {
+ if (!Intl()) return NS_ERROR_FAILURE;
+ MOZ_ASSERT(Intl()->IsSelect(), "Called on non selectable widget!");
+
+ Intl()->UnselectAll();
+ return NS_OK;
+}
diff --git a/accessible/xpcom/xpcAccessibleSelectable.h b/accessible/xpcom/xpcAccessibleSelectable.h
new file mode 100644
index 0000000000..706690df2e
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleSelectable.h
@@ -0,0 +1,50 @@
+/* -*- 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_xpcAccessibleSelectable_h_
+#define mozilla_a11y_xpcAccessibleSelectable_h_
+
+#include "nsIAccessibleSelectable.h"
+
+class nsIAccessible;
+class nsIArray;
+
+namespace mozilla {
+namespace a11y {
+
+class Accessible;
+
+/**
+ * XPCOM nsIAccessibleSelectable inteface implementation, used by
+ * xpcAccessibleGeneric class.
+ */
+class xpcAccessibleSelectable : public nsIAccessibleSelectable {
+ public:
+ // nsIAccessibleSelectable
+ NS_IMETHOD GetSelectedItems(nsIArray** aSelectedItems) final;
+ NS_IMETHOD GetSelectedItemCount(uint32_t* aSelectedItemCount) final;
+ NS_IMETHOD GetSelectedItemAt(uint32_t aIndex, nsIAccessible** aItem) final;
+ NS_IMETHOD IsItemSelected(uint32_t aIndex, bool* aIsSelected) final;
+ NS_IMETHOD AddItemToSelection(uint32_t aIndex) final;
+ NS_IMETHOD RemoveItemFromSelection(uint32_t aIndex) final;
+ NS_IMETHOD SelectAll(bool* aIsMultiSelect) final;
+ NS_IMETHOD UnselectAll() final;
+
+ protected:
+ xpcAccessibleSelectable() {}
+ virtual ~xpcAccessibleSelectable() {}
+
+ private:
+ xpcAccessibleSelectable(const xpcAccessibleSelectable&) = delete;
+ xpcAccessibleSelectable& operator=(const xpcAccessibleSelectable&) = delete;
+
+ Accessible* Intl();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xpcom/xpcAccessibleTable.cpp b/accessible/xpcom/xpcAccessibleTable.cpp
new file mode 100644
index 0000000000..b7de1cd85c
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleTable.cpp
@@ -0,0 +1,362 @@
+/* -*- 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 "xpcAccessibleTable.h"
+
+#include "mozilla/a11y/TableAccessible.h"
+
+#include "nsIMutableArray.h"
+#include "nsComponentManagerUtils.h"
+#include "xpcAccessibleDocument.h"
+
+using namespace mozilla::a11y;
+
+static const uint32_t XPC_TABLE_DEFAULT_SIZE = 40;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_IMPL_ISUPPORTS_INHERITED(xpcAccessibleTable, xpcAccessibleHyperText,
+ nsIAccessibleTable)
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessibleTable
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetCaption(nsIAccessible** aCaption) {
+ NS_ENSURE_ARG_POINTER(aCaption);
+ *aCaption = nullptr;
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ NS_IF_ADDREF(*aCaption = ToXPC(Intl()->Caption()));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetColumnCount(int32_t* aColumnCount) {
+ NS_ENSURE_ARG_POINTER(aColumnCount);
+ *aColumnCount = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aColumnCount = Intl()->ColCount();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetRowCount(int32_t* aRowCount) {
+ NS_ENSURE_ARG_POINTER(aRowCount);
+ *aRowCount = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aRowCount = Intl()->RowCount();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetCellAt(int32_t aRowIdx, int32_t aColIdx,
+ nsIAccessible** aCell) {
+ NS_ENSURE_ARG_POINTER(aCell);
+ *aCell = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (aRowIdx < 0 || static_cast<uint32_t>(aRowIdx) >= Intl()->RowCount() ||
+ aColIdx < 0 || static_cast<uint32_t>(aColIdx) >= Intl()->ColCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ NS_IF_ADDREF(*aCell = ToXPC(Intl()->CellAt(aRowIdx, aColIdx)));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetCellIndexAt(int32_t aRowIdx, int32_t aColIdx,
+ int32_t* aCellIdx) {
+ NS_ENSURE_ARG_POINTER(aCellIdx);
+ *aCellIdx = -1;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (aRowIdx < 0 || static_cast<uint32_t>(aRowIdx) >= Intl()->RowCount() ||
+ aColIdx < 0 || static_cast<uint32_t>(aColIdx) >= Intl()->ColCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aCellIdx = Intl()->CellIndexAt(aRowIdx, aColIdx);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetColumnExtentAt(int32_t aRowIdx, int32_t aColIdx,
+ int32_t* aColumnExtent) {
+ NS_ENSURE_ARG_POINTER(aColumnExtent);
+ *aColumnExtent = -1;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (aRowIdx < 0 || static_cast<uint32_t>(aRowIdx) >= Intl()->RowCount() ||
+ aColIdx < 0 || static_cast<uint32_t>(aColIdx) >= Intl()->ColCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aColumnExtent = Intl()->ColExtentAt(aRowIdx, aColIdx);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetRowExtentAt(int32_t aRowIdx, int32_t aColIdx,
+ int32_t* aRowExtent) {
+ NS_ENSURE_ARG_POINTER(aRowExtent);
+ *aRowExtent = -1;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (aRowIdx < 0 || static_cast<uint32_t>(aRowIdx) >= Intl()->RowCount() ||
+ aColIdx < 0 || static_cast<uint32_t>(aColIdx) >= Intl()->ColCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aRowExtent = Intl()->RowExtentAt(aRowIdx, aColIdx);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetColumnDescription(int32_t aColIdx,
+ nsAString& aDescription) {
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (aColIdx < 0 || static_cast<uint32_t>(aColIdx) >= Intl()->ColCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoString description;
+ Intl()->ColDescription(aColIdx, description);
+ aDescription.Assign(description);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetRowDescription(int32_t aRowIdx,
+ nsAString& aDescription) {
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (aRowIdx < 0 || static_cast<uint32_t>(aRowIdx) >= Intl()->ColCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoString description;
+ Intl()->RowDescription(aRowIdx, description);
+ aDescription.Assign(description);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::IsColumnSelected(int32_t aColIdx, bool* aIsSelected) {
+ NS_ENSURE_ARG_POINTER(aIsSelected);
+ *aIsSelected = false;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (aColIdx < 0 || static_cast<uint32_t>(aColIdx) >= Intl()->ColCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aIsSelected = Intl()->IsColSelected(aColIdx);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::IsRowSelected(int32_t aRowIdx, bool* aIsSelected) {
+ NS_ENSURE_ARG_POINTER(aIsSelected);
+ *aIsSelected = false;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (aRowIdx < 0 || static_cast<uint32_t>(aRowIdx) >= Intl()->RowCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aIsSelected = Intl()->IsRowSelected(aRowIdx);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::IsCellSelected(int32_t aRowIdx, int32_t aColIdx,
+ bool* aIsSelected) {
+ NS_ENSURE_ARG_POINTER(aIsSelected);
+ *aIsSelected = false;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (aRowIdx < 0 || static_cast<uint32_t>(aRowIdx) >= Intl()->RowCount() ||
+ aColIdx < 0 || static_cast<uint32_t>(aColIdx) >= Intl()->ColCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aIsSelected = Intl()->IsCellSelected(aRowIdx, aColIdx);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetSelectedCellCount(uint32_t* aSelectedCellCount) {
+ NS_ENSURE_ARG_POINTER(aSelectedCellCount);
+ *aSelectedCellCount = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aSelectedCellCount = Intl()->SelectedCellCount();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetSelectedColumnCount(uint32_t* aSelectedColumnCount) {
+ NS_ENSURE_ARG_POINTER(aSelectedColumnCount);
+ *aSelectedColumnCount = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aSelectedColumnCount = Intl()->SelectedColCount();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetSelectedRowCount(uint32_t* aSelectedRowCount) {
+ NS_ENSURE_ARG_POINTER(aSelectedRowCount);
+ *aSelectedRowCount = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aSelectedRowCount = Intl()->SelectedRowCount();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetSelectedCells(nsIArray** aSelectedCells) {
+ NS_ENSURE_ARG_POINTER(aSelectedCells);
+ *aSelectedCells = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMutableArray> selCells =
+ do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoTArray<Accessible*, XPC_TABLE_DEFAULT_SIZE> cellsArray;
+ Intl()->SelectedCells(&cellsArray);
+
+ uint32_t totalCount = cellsArray.Length();
+ for (uint32_t idx = 0; idx < totalCount; idx++) {
+ Accessible* cell = cellsArray.ElementAt(idx);
+ selCells->AppendElement(static_cast<nsIAccessible*>(ToXPC(cell)));
+ }
+
+ NS_ADDREF(*aSelectedCells = selCells);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetSelectedCellIndices(nsTArray<uint32_t>& aCellsArray) {
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ Intl()->SelectedCellIndices(&aCellsArray);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetSelectedColumnIndices(nsTArray<uint32_t>& aColsArray) {
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ Intl()->SelectedColIndices(&aColsArray);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetSelectedRowIndices(nsTArray<uint32_t>& aRowsArray) {
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ Intl()->SelectedRowIndices(&aRowsArray);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetColumnIndexAt(int32_t aCellIdx, int32_t* aColIdx) {
+ NS_ENSURE_ARG_POINTER(aColIdx);
+ *aColIdx = -1;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (aCellIdx < 0 || static_cast<uint32_t>(aCellIdx) >=
+ Intl()->RowCount() * Intl()->ColCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aColIdx = Intl()->ColIndexAt(aCellIdx);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetRowIndexAt(int32_t aCellIdx, int32_t* aRowIdx) {
+ NS_ENSURE_ARG_POINTER(aRowIdx);
+ *aRowIdx = -1;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (aCellIdx < 0 || static_cast<uint32_t>(aCellIdx) >=
+ Intl()->RowCount() * Intl()->ColCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ *aRowIdx = Intl()->RowIndexAt(aCellIdx);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetRowAndColumnIndicesAt(int32_t aCellIdx, int32_t* aRowIdx,
+ int32_t* aColIdx) {
+ NS_ENSURE_ARG_POINTER(aRowIdx);
+ *aRowIdx = -1;
+ NS_ENSURE_ARG_POINTER(aColIdx);
+ *aColIdx = -1;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (aCellIdx < 0 || static_cast<uint32_t>(aCellIdx) >=
+ Intl()->RowCount() * Intl()->ColCount()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ Intl()->RowAndColIndicesAt(aCellIdx, aRowIdx, aColIdx);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::GetSummary(nsAString& aSummary) {
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ nsAutoString summary;
+ Intl()->Summary(summary);
+ aSummary.Assign(summary);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTable::IsProbablyForLayout(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aResult = Intl()->IsProbablyLayoutTable();
+ return NS_OK;
+}
diff --git a/accessible/xpcom/xpcAccessibleTable.h b/accessible/xpcom/xpcAccessibleTable.h
new file mode 100644
index 0000000000..1691ce9e7d
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleTable.h
@@ -0,0 +1,74 @@
+/* -*- 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_xpcAccessibleTable_h_
+#define mozilla_a11y_xpcAccessibleTable_h_
+
+#include "nsIAccessibleTable.h"
+#include "xpcAccessibleHyperText.h"
+
+namespace mozilla {
+namespace a11y {
+class TableAccessible;
+
+/**
+ * XPCOM wrapper around TableAccessible class.
+ */
+class xpcAccessibleTable : public xpcAccessibleHyperText,
+ public nsIAccessibleTable {
+ public:
+ explicit xpcAccessibleTable(Accessible* aIntl)
+ : xpcAccessibleHyperText(aIntl) {}
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // nsIAccessibleTable
+ NS_IMETHOD GetCaption(nsIAccessible** aCaption) final;
+ NS_IMETHOD GetSummary(nsAString& aSummary) final;
+ NS_IMETHOD GetColumnCount(int32_t* aColumnCount) final;
+ NS_IMETHOD GetRowCount(int32_t* aRowCount) final;
+ NS_IMETHOD GetCellAt(int32_t aRowIndex, int32_t aColumnIndex,
+ nsIAccessible** aCell) final;
+ NS_IMETHOD GetCellIndexAt(int32_t aRowIndex, int32_t aColumnIndex,
+ int32_t* aCellIndex) final;
+ NS_IMETHOD GetColumnIndexAt(int32_t aCellIndex, int32_t* aColumnIndex) final;
+ NS_IMETHOD GetRowIndexAt(int32_t aCellIndex, int32_t* aRowIndex) final;
+ NS_IMETHOD GetRowAndColumnIndicesAt(int32_t aCellIndex, int32_t* aRowIndex,
+ int32_t* aColumnIndex) final;
+ NS_IMETHOD GetColumnExtentAt(int32_t row, int32_t column,
+ int32_t* aColumnExtent) final;
+ NS_IMETHOD GetRowExtentAt(int32_t row, int32_t column,
+ int32_t* aRowExtent) final;
+ NS_IMETHOD GetColumnDescription(int32_t aColIdx,
+ nsAString& aDescription) final;
+ NS_IMETHOD GetRowDescription(int32_t aRowIdx, nsAString& aDescription) final;
+ NS_IMETHOD IsColumnSelected(int32_t aColIdx, bool* _retval) final;
+ NS_IMETHOD IsRowSelected(int32_t aRowIdx, bool* _retval) final;
+ NS_IMETHOD IsCellSelected(int32_t aRowIdx, int32_t aColIdx,
+ bool* _retval) final;
+ NS_IMETHOD GetSelectedCellCount(uint32_t* aSelectedCellCount) final;
+ NS_IMETHOD GetSelectedColumnCount(uint32_t* aSelectedColumnCount) final;
+ NS_IMETHOD GetSelectedRowCount(uint32_t* aSelectedRowCount) final;
+ NS_IMETHOD GetSelectedCells(nsIArray** aSelectedCell) final;
+ NS_IMETHOD GetSelectedCellIndices(nsTArray<uint32_t>& aCellsArray) final;
+ NS_IMETHOD GetSelectedColumnIndices(nsTArray<uint32_t>& aColsArray) final;
+ NS_IMETHOD GetSelectedRowIndices(nsTArray<uint32_t>& aRowsArray) final;
+ NS_IMETHOD IsProbablyForLayout(bool* aIsForLayout) final;
+
+ protected:
+ virtual ~xpcAccessibleTable() {}
+
+ private:
+ TableAccessible* Intl() { return mIntl->AsTable(); }
+
+ xpcAccessibleTable(const xpcAccessibleTable&) = delete;
+ xpcAccessibleTable& operator=(const xpcAccessibleTable&) = delete;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_xpcAccessibleTable_h_
diff --git a/accessible/xpcom/xpcAccessibleTableCell.cpp b/accessible/xpcom/xpcAccessibleTableCell.cpp
new file mode 100644
index 0000000000..37da0d4852
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleTableCell.cpp
@@ -0,0 +1,140 @@
+/* -*- 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 "xpcAccessibleTableCell.h"
+
+#include "mozilla/a11y/TableAccessible.h"
+#include "mozilla/a11y/TableCellAccessible.h"
+#include "nsIAccessibleTable.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsIMutableArray.h"
+#include "xpcAccessibleDocument.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// nsISupports
+
+NS_IMPL_ISUPPORTS_INHERITED(xpcAccessibleTableCell, xpcAccessibleHyperText,
+ nsIAccessibleTableCell)
+
+////////////////////////////////////////////////////////////////////////////////
+// nsIAccessibleTableCell
+
+NS_IMETHODIMP
+xpcAccessibleTableCell::GetTable(nsIAccessibleTable** aTable) {
+ NS_ENSURE_ARG_POINTER(aTable);
+ *aTable = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ TableAccessible* table = Intl()->Table();
+ if (!table) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIAccessibleTable> xpcTable = do_QueryInterface(
+ static_cast<nsIAccessible*>(ToXPC(table->AsAccessible())));
+ xpcTable.forget(aTable);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTableCell::GetColumnIndex(int32_t* aColIdx) {
+ NS_ENSURE_ARG_POINTER(aColIdx);
+ *aColIdx = -1;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aColIdx = Intl()->ColIdx();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTableCell::GetRowIndex(int32_t* aRowIdx) {
+ NS_ENSURE_ARG_POINTER(aRowIdx);
+ *aRowIdx = -1;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aRowIdx = Intl()->RowIdx();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTableCell::GetColumnExtent(int32_t* aExtent) {
+ NS_ENSURE_ARG_POINTER(aExtent);
+ *aExtent = -1;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aExtent = Intl()->ColExtent();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTableCell::GetRowExtent(int32_t* aExtent) {
+ NS_ENSURE_ARG_POINTER(aExtent);
+ *aExtent = -1;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aExtent = Intl()->RowExtent();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTableCell::GetColumnHeaderCells(nsIArray** aHeaderCells) {
+ NS_ENSURE_ARG_POINTER(aHeaderCells);
+ *aHeaderCells = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ AutoTArray<Accessible*, 10> headerCells;
+ Intl()->ColHeaderCells(&headerCells);
+
+ nsCOMPtr<nsIMutableArray> cells = do_CreateInstance(NS_ARRAY_CONTRACTID);
+ NS_ENSURE_TRUE(cells, NS_ERROR_FAILURE);
+
+ for (uint32_t idx = 0; idx < headerCells.Length(); idx++) {
+ cells->AppendElement(static_cast<nsIAccessible*>(ToXPC(headerCells[idx])));
+ }
+
+ NS_ADDREF(*aHeaderCells = cells);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTableCell::GetRowHeaderCells(nsIArray** aHeaderCells) {
+ NS_ENSURE_ARG_POINTER(aHeaderCells);
+ *aHeaderCells = nullptr;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ AutoTArray<Accessible*, 10> headerCells;
+ Intl()->RowHeaderCells(&headerCells);
+
+ nsCOMPtr<nsIMutableArray> cells = do_CreateInstance(NS_ARRAY_CONTRACTID);
+ NS_ENSURE_TRUE(cells, NS_ERROR_FAILURE);
+
+ for (uint32_t idx = 0; idx < headerCells.Length(); idx++) {
+ cells->AppendElement(static_cast<nsIAccessible*>(ToXPC(headerCells[idx])));
+ }
+
+ NS_ADDREF(*aHeaderCells = cells);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTableCell::IsSelected(bool* aSelected) {
+ NS_ENSURE_ARG_POINTER(aSelected);
+ *aSelected = false;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ *aSelected = Intl()->Selected();
+ return NS_OK;
+}
diff --git a/accessible/xpcom/xpcAccessibleTableCell.h b/accessible/xpcom/xpcAccessibleTableCell.h
new file mode 100644
index 0000000000..8a8ea2bb53
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleTableCell.h
@@ -0,0 +1,52 @@
+/* -*- 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_xpcom_xpcAccessibletableCell_h_
+#define mozilla_a11y_xpcom_xpcAccessibletableCell_h_
+
+#include "nsIAccessibleTable.h"
+
+#include "xpcAccessibleHyperText.h"
+
+namespace mozilla {
+namespace a11y {
+class TableCellAccessible;
+
+/**
+ * XPCOM wrapper around TableAccessibleCell class.
+ */
+class xpcAccessibleTableCell : public xpcAccessibleHyperText,
+ public nsIAccessibleTableCell {
+ public:
+ explicit xpcAccessibleTableCell(Accessible* aIntl)
+ : xpcAccessibleHyperText(aIntl) {}
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // nsIAccessibleTableCell
+ NS_IMETHOD GetTable(nsIAccessibleTable** aTable) final;
+ NS_IMETHOD GetColumnIndex(int32_t* aColIdx) final;
+ NS_IMETHOD GetRowIndex(int32_t* aRowIdx) final;
+ NS_IMETHOD GetColumnExtent(int32_t* aExtent) final;
+ NS_IMETHOD GetRowExtent(int32_t* aExtent) final;
+ NS_IMETHOD GetColumnHeaderCells(nsIArray** aHeaderCells) final;
+ NS_IMETHOD GetRowHeaderCells(nsIArray** aHeaderCells) final;
+ NS_IMETHOD IsSelected(bool* aSelected) final;
+
+ protected:
+ virtual ~xpcAccessibleTableCell() {}
+
+ private:
+ TableCellAccessible* Intl() { return mIntl->AsTableCell(); }
+
+ xpcAccessibleTableCell(const xpcAccessibleTableCell&) = delete;
+ xpcAccessibleTableCell& operator=(const xpcAccessibleTableCell&) = delete;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif // mozilla_a11y_xpcom_xpcAccessibletableCell_h_
diff --git a/accessible/xpcom/xpcAccessibleTextLeafRange.cpp b/accessible/xpcom/xpcAccessibleTextLeafRange.cpp
new file mode 100644
index 0000000000..8f493deea5
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleTextLeafRange.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 "xpcAccessibleTextLeafRange.h"
+
+#include "nsIAccessible.h"
+#include "TextLeafRange.h"
+#include "xpcAccessibleDocument.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+// xpcAccessibleTextLeafPoint
+
+NS_IMPL_ADDREF(xpcAccessibleTextLeafPoint)
+NS_IMPL_RELEASE(xpcAccessibleTextLeafPoint)
+
+NS_IMPL_QUERY_INTERFACE(xpcAccessibleTextLeafPoint, nsIAccessibleTextLeafPoint)
+
+xpcAccessibleTextLeafPoint::xpcAccessibleTextLeafPoint(
+ nsIAccessible* aAccessible, int32_t aOffset)
+ : mAccessible(nullptr), mOffset(0) {
+ // When constructing a text point it will actualize the offset
+ // and adjust the accessible to the appropriate leaf. These
+ // might differ from the given constructor arguments.
+ if (aAccessible) {
+ TextLeafPoint point(aAccessible->ToInternalGeneric(), aOffset);
+ mAccessible = ToXPC(point.mAcc);
+ mOffset = point.mOffset;
+ }
+}
+
+NS_IMETHODIMP xpcAccessibleTextLeafPoint::GetAccessible(
+ nsIAccessible** aAccessible) {
+ NS_ENSURE_ARG_POINTER(aAccessible);
+ RefPtr<nsIAccessible> acc = mAccessible;
+ acc.forget(aAccessible);
+ return NS_OK;
+}
+
+NS_IMETHODIMP xpcAccessibleTextLeafPoint::SetAccessible(
+ nsIAccessible* aAccessible) {
+ mAccessible = aAccessible;
+ return NS_OK;
+}
+
+NS_IMETHODIMP xpcAccessibleTextLeafPoint::GetOffset(int32_t* aOffset) {
+ NS_ENSURE_ARG_POINTER(aOffset);
+ *aOffset = mOffset;
+ return NS_OK;
+}
+NS_IMETHODIMP xpcAccessibleTextLeafPoint::SetOffset(int32_t aOffset) {
+ mOffset = aOffset;
+ return NS_OK;
+}
+
+NS_IMETHODIMP xpcAccessibleTextLeafPoint::FindBoundary(
+ AccessibleTextBoundary aBoundaryType, uint32_t aDirection, uint32_t aFlags,
+ nsIAccessibleTextLeafPoint** aPoint) {
+ TextLeafPoint thisPoint = ToPoint();
+ if (!thisPoint) {
+ return NS_ERROR_FAILURE;
+ }
+
+ TextLeafPoint result = thisPoint.FindBoundary(
+ aBoundaryType, static_cast<nsDirection>(aDirection),
+ static_cast<TextLeafPoint::BoundaryFlags>(aFlags));
+ RefPtr<xpcAccessibleTextLeafPoint> point = new xpcAccessibleTextLeafPoint(
+ result ? ToXPC(result.mAcc) : nullptr, result ? result.mOffset : 0);
+ point.forget(aPoint);
+ return NS_OK;
+}
+
+TextLeafPoint xpcAccessibleTextLeafPoint::ToPoint() {
+ if (mAccessible) {
+ return TextLeafPoint(mAccessible->ToInternalGeneric(), mOffset);
+ }
+
+ return TextLeafPoint();
+}
diff --git a/accessible/xpcom/xpcAccessibleTextLeafRange.h b/accessible/xpcom/xpcAccessibleTextLeafRange.h
new file mode 100644
index 0000000000..8d61d2edc1
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleTextLeafRange.h
@@ -0,0 +1,45 @@
+/* -*- 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_xpcAccessibleTextLeafRange_h_
+#define mozilla_a11y_xpcAccessibleTextLeafRange_h_
+
+#include "nsIAccessibleTextLeafRange.h"
+
+namespace mozilla {
+namespace a11y {
+
+class TextLeafPoint;
+
+class xpcAccessibleTextLeafPoint final : public nsIAccessibleTextLeafPoint {
+ public:
+ xpcAccessibleTextLeafPoint(nsIAccessible* aAccessible, int32_t aOffset);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIACCESSIBLETEXTLEAFPOINT
+
+ private:
+ xpcAccessibleTextLeafPoint() = delete;
+
+ ~xpcAccessibleTextLeafPoint() {}
+
+ xpcAccessibleTextLeafPoint& operator=(const xpcAccessibleTextLeafPoint&) =
+ delete;
+
+ TextLeafPoint ToPoint();
+
+ // We can't hold a strong reference to an Accessible, but XPCOM needs strong
+ // references. Thus, instead of holding a TextLeafPoint here, we hold
+ // an nsIAccessible references and create the TextLeafPoint for each call
+ // using ToPoint().
+ RefPtr<nsIAccessible> mAccessible;
+ int32_t mOffset;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xpcom/xpcAccessibleTextRange.cpp b/accessible/xpcom/xpcAccessibleTextRange.cpp
new file mode 100644
index 0000000000..1ecfdd6c2b
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleTextRange.cpp
@@ -0,0 +1,125 @@
+/* -*- 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 "xpcAccessibleTextRange.h"
+
+#include "TextRange-inl.h"
+
+#include "nsQueryObject.h"
+#include "xpcAccessibleDocument.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+// nsISupports and cycle collection
+
+NS_INTERFACE_MAP_BEGIN(xpcAccessibleTextRange)
+ NS_INTERFACE_MAP_ENTRY(nsIAccessibleTextRange)
+ NS_INTERFACE_MAP_ENTRY(xpcAccessibleTextRange)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAccessibleTextRange)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(xpcAccessibleTextRange)
+NS_IMPL_RELEASE(xpcAccessibleTextRange)
+
+a11y::TextRange xpcAccessibleTextRange::Range() {
+ return a11y::TextRange(mRoot->ToInternalGeneric(),
+ mStartContainer->ToInternalGeneric(), mStartOffset,
+ mEndContainer->ToInternalGeneric(), mEndOffset);
+}
+
+void xpcAccessibleTextRange::SetRange(TextRange& aRange) {
+ mRoot = ToXPCText(aRange.Root());
+ mStartContainer = ToXPCText(aRange.StartContainer());
+ mStartOffset = aRange.StartOffset();
+ mEndContainer = ToXPCText(aRange.EndContainer());
+ mEndOffset = aRange.EndOffset();
+}
+
+// nsIAccessibleTextRange
+
+NS_IMETHODIMP
+xpcAccessibleTextRange::GetStartContainer(nsIAccessibleText** aAnchor) {
+ NS_ENSURE_ARG_POINTER(aAnchor);
+ NS_IF_ADDREF(*aAnchor = mStartContainer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTextRange::GetStartOffset(int32_t* aOffset) {
+ NS_ENSURE_ARG_POINTER(aOffset);
+ *aOffset = mStartOffset;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTextRange::GetEndContainer(nsIAccessibleText** aAnchor) {
+ NS_ENSURE_ARG_POINTER(aAnchor);
+ NS_IF_ADDREF(*aAnchor = mEndContainer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTextRange::GetEndOffset(int32_t* aOffset) {
+ NS_ENSURE_ARG_POINTER(aOffset);
+ *aOffset = mEndOffset;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTextRange::GetContainer(nsIAccessible** aContainer) {
+ NS_ENSURE_ARG_POINTER(aContainer);
+ NS_IF_ADDREF(*aContainer = ToXPC(Range().Container()));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTextRange::Compare(nsIAccessibleTextRange* aOtherRange,
+ bool* aResult) {
+ RefPtr<xpcAccessibleTextRange> xpcRange(do_QueryObject(aOtherRange));
+ if (!xpcRange || !aResult) return NS_ERROR_INVALID_ARG;
+
+ *aResult = (Range() == xpcRange->Range());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTextRange::CompareEndPoints(uint32_t aEndPoint,
+ nsIAccessibleTextRange* aOtherRange,
+ uint32_t aOtherRangeEndPoint,
+ int32_t* aResult) {
+ RefPtr<xpcAccessibleTextRange> xpcRange(do_QueryObject(aOtherRange));
+ if (!xpcRange || !aResult) return NS_ERROR_INVALID_ARG;
+
+ TextRange thisRange = Range();
+ TextRange otherRange = xpcRange->Range();
+ TextPoint p = (aEndPoint == EndPoint_Start) ? thisRange.StartPoint()
+ : thisRange.EndPoint();
+ TextPoint otherPoint = (aOtherRangeEndPoint == EndPoint_Start)
+ ? otherRange.StartPoint()
+ : otherRange.EndPoint();
+
+ if (p == otherPoint) {
+ *aResult = 0;
+ } else {
+ *aResult = p < otherPoint ? -1 : 1;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleTextRange::Crop(nsIAccessible* aContainer, bool* aSuccess) {
+ Accessible* container = aContainer->ToInternalGeneric();
+ NS_ENSURE_TRUE(container, NS_ERROR_INVALID_ARG);
+
+ TextRange range = Range();
+ *aSuccess = range.Crop(container);
+ if (*aSuccess) {
+ SetRange(range);
+ }
+ return NS_OK;
+}
diff --git a/accessible/xpcom/xpcAccessibleTextRange.h b/accessible/xpcom/xpcAccessibleTextRange.h
new file mode 100644
index 0000000000..01ca228ade
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleTextRange.h
@@ -0,0 +1,78 @@
+/* -*- 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_xpcAccessibleTextRange_h_
+#define mozilla_a11y_xpcAccessibleTextRange_h_
+
+#include <utility>
+
+#include "TextRange.h"
+#include "nsIAccessibleTextRange.h"
+#include "xpcAccessibleHyperText.h"
+
+namespace mozilla {
+namespace a11y {
+
+class TextRange;
+
+#define NS_ACCESSIBLETEXTRANGE_IMPL_IID \
+ { /* 133c8bf4-4913-4355-bd50-426bd1d6e1ad */ \
+ 0xb17652d9, 0x4f54, 0x4c56, { \
+ 0xbb, 0x62, 0x6d, 0x5b, 0xf1, 0xef, 0x91, 0x0c \
+ } \
+ }
+
+class xpcAccessibleTextRange final : public nsIAccessibleTextRange {
+ public:
+ explicit xpcAccessibleTextRange(TextRange& aRange) { SetRange(aRange); }
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD GetStartContainer(nsIAccessibleText** aAnchor) final;
+ NS_IMETHOD GetStartOffset(int32_t* aOffset) final;
+ NS_IMETHOD GetEndContainer(nsIAccessibleText** aAnchor) final;
+ NS_IMETHOD GetEndOffset(int32_t* aOffset) final;
+ NS_IMETHOD GetContainer(nsIAccessible** aContainer) final;
+ NS_IMETHOD Compare(nsIAccessibleTextRange* aOtherRange, bool* aResult) final;
+ NS_IMETHOD CompareEndPoints(uint32_t aEndPoint,
+ nsIAccessibleTextRange* aOtherRange,
+ uint32_t aOtherRangeEndPoint,
+ int32_t* aResult) final;
+ NS_IMETHOD Crop(nsIAccessible* aContainer, bool* aSuccess) final;
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_ACCESSIBLETEXTRANGE_IMPL_IID)
+
+ private:
+ xpcAccessibleTextRange() {}
+
+ ~xpcAccessibleTextRange() {}
+
+ friend class xpcAccessibleHyperText;
+
+ xpcAccessibleTextRange& operator=(const xpcAccessibleTextRange&) = delete;
+
+ void SetRange(TextRange& aRange);
+
+ TextRange Range();
+
+ // We can't hold a strong reference to an Accessible, but XPCOM needs strong
+ // references. Thus, instead of holding a TextRange here, we hold
+ // xpcAccessibleHyperText references and create the TextRange for each call
+ // using Range().
+ RefPtr<xpcAccessibleHyperText> mRoot;
+ RefPtr<xpcAccessibleHyperText> mStartContainer;
+ int32_t mStartOffset;
+ RefPtr<xpcAccessibleHyperText> mEndContainer;
+ int32_t mEndOffset;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(xpcAccessibleTextRange,
+ NS_ACCESSIBLETEXTRANGE_IMPL_IID)
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xpcom/xpcAccessibleValue.cpp b/accessible/xpcom/xpcAccessibleValue.cpp
new file mode 100644
index 0000000000..b409879883
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleValue.cpp
@@ -0,0 +1,99 @@
+/* -*- 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 "xpcAccessibleGeneric.h"
+#include "LocalAccessible.h"
+#include "LocalAccessible-inl.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+NS_IMETHODIMP
+xpcAccessibleValue::GetMaximumValue(double* aValue) {
+ NS_ENSURE_ARG_POINTER(aValue);
+ *aValue = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (Intl()->IsLocal() && Intl()->AsLocal()->IsDefunct()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ double value = Intl()->MaxValue();
+
+ if (!std::isnan(value)) *aValue = value;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleValue::GetMinimumValue(double* aValue) {
+ NS_ENSURE_ARG_POINTER(aValue);
+ *aValue = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (Intl()->IsLocal() && Intl()->AsLocal()->IsDefunct()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ double value = Intl()->MinValue();
+
+ if (!std::isnan(value)) *aValue = value;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleValue::GetCurrentValue(double* aValue) {
+ NS_ENSURE_ARG_POINTER(aValue);
+ *aValue = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (Intl()->IsLocal() && Intl()->AsLocal()->IsDefunct()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ double value = Intl()->CurValue();
+
+ if (!std::isnan(value)) *aValue = value;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleValue::SetCurrentValue(double aValue) {
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (Intl()->IsLocal() && Intl()->AsLocal()->IsDefunct()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!Intl()->SetCurValue(aValue)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+xpcAccessibleValue::GetMinimumIncrement(double* aValue) {
+ NS_ENSURE_ARG_POINTER(aValue);
+ *aValue = 0;
+
+ if (!Intl()) return NS_ERROR_FAILURE;
+
+ if (Intl()->IsLocal() && Intl()->AsLocal()->IsDefunct()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ double value = Intl()->Step();
+
+ if (!std::isnan(value)) *aValue = value;
+
+ return NS_OK;
+}
diff --git a/accessible/xpcom/xpcAccessibleValue.h b/accessible/xpcom/xpcAccessibleValue.h
new file mode 100644
index 0000000000..812278ab56
--- /dev/null
+++ b/accessible/xpcom/xpcAccessibleValue.h
@@ -0,0 +1,42 @@
+/* -*- 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_xpcAccessibleValue_h_
+#define mozilla_a11y_xpcAccessibleValue_h_
+
+#include "nsIAccessibleValue.h"
+
+namespace mozilla {
+namespace a11y {
+
+class LocalAccessible;
+
+/**
+ * XPCOM nsIAccessibleValue interface implementation, used by
+ * xpcAccessibleGeneric class.
+ */
+class xpcAccessibleValue : public nsIAccessibleValue {
+ public:
+ NS_IMETHOD GetMaximumValue(double* aValue) final;
+ NS_IMETHOD GetMinimumValue(double* aValue) final;
+ NS_IMETHOD GetCurrentValue(double* aValue) final;
+ NS_IMETHOD SetCurrentValue(double aValue) final;
+ NS_IMETHOD GetMinimumIncrement(double* aMinIncrement) final;
+
+ protected:
+ xpcAccessibleValue() {}
+ virtual ~xpcAccessibleValue() {}
+
+ private:
+ Accessible* Intl();
+
+ xpcAccessibleValue(const xpcAccessibleValue&) = delete;
+ xpcAccessibleValue& operator=(const xpcAccessibleValue&) = delete;
+};
+
+} // namespace a11y
+} // namespace mozilla
+#endif
diff --git a/accessible/xul/XULAlertAccessible.cpp b/accessible/xul/XULAlertAccessible.cpp
new file mode 100644
index 0000000000..64c1fe7aa0
--- /dev/null
+++ b/accessible/xul/XULAlertAccessible.cpp
@@ -0,0 +1,44 @@
+/* -*- 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 "XULAlertAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// XULAlertAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULAlertAccessible::XULAlertAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {
+ mGenericTypes |= eAlert;
+}
+
+XULAlertAccessible::~XULAlertAccessible() {}
+
+role XULAlertAccessible::NativeRole() const { return roles::ALERT; }
+
+uint64_t XULAlertAccessible::NativeState() const {
+ return LocalAccessible::NativeState() | states::ALERT;
+}
+
+ENameValueFlag XULAlertAccessible::Name(nsString& aName) const {
+ // Screen readers need to read contents of alert, not the accessible name.
+ // If we have both some screen readers will read the alert twice.
+ aName.Truncate();
+ return eNameOK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Widgets
+
+bool XULAlertAccessible::IsWidget() const { return true; }
+
+LocalAccessible* XULAlertAccessible::ContainerWidget() const { return nullptr; }
diff --git a/accessible/xul/XULAlertAccessible.h b/accessible/xul/XULAlertAccessible.h
new file mode 100644
index 0000000000..12594e987d
--- /dev/null
+++ b/accessible/xul/XULAlertAccessible.h
@@ -0,0 +1,40 @@
+/* -*- 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_XULAlertAccessible_h__
+#define mozilla_a11y_XULAlertAccessible_h__
+
+#include "AccessibleWrap.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * LocalAccessible for supporting XUL alerts.
+ */
+
+class XULAlertAccessible : public AccessibleWrap {
+ public:
+ XULAlertAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(XULAlertAccessible, AccessibleWrap)
+
+ // LocalAccessible
+ virtual mozilla::a11y::ENameValueFlag Name(nsString& aName) const override;
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+ virtual LocalAccessible* ContainerWidget() const override;
+
+ protected:
+ ~XULAlertAccessible();
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xul/XULComboboxAccessible.cpp b/accessible/xul/XULComboboxAccessible.cpp
new file mode 100644
index 0000000000..2e22a33e51
--- /dev/null
+++ b/accessible/xul/XULComboboxAccessible.cpp
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 4; 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 "XULComboboxAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "nsAccessibilityService.h"
+#include "DocAccessible.h"
+#include "nsCoreUtils.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+
+#include "mozilla/dom/Element.h"
+#include "nsIDOMXULMenuListElement.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// XULComboboxAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULComboboxAccessible::XULComboboxAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {
+ mGenericTypes |= eCombobox;
+}
+
+role XULComboboxAccessible::NativeRole() const { return roles::COMBOBOX; }
+
+uint64_t XULComboboxAccessible::NativeState() const {
+ // As a nsComboboxAccessible we can have the following states:
+ // STATE_FOCUSED
+ // STATE_FOCUSABLE
+ // STATE_HASPOPUP
+ // STATE_EXPANDED
+ // STATE_COLLAPSED
+
+ // Get focus status from base class
+ uint64_t state = LocalAccessible::NativeState();
+
+ nsCOMPtr<nsIDOMXULMenuListElement> menuList = Elm()->AsXULMenuList();
+ if (menuList) {
+ bool isOpen = false;
+ menuList->GetOpen(&isOpen);
+ if (isOpen) {
+ state |= states::EXPANDED;
+ } else {
+ state |= states::COLLAPSED;
+ }
+ }
+
+ return state | states::HASPOPUP;
+}
+
+bool XULComboboxAccessible::IsAcceptableChild(nsIContent* aContent) const {
+ return AccessibleWrap::IsAcceptableChild(aContent) && !aContent->IsText();
+}
+
+void XULComboboxAccessible::Description(nsString& aDescription) const {
+ aDescription.Truncate();
+ // Use description of currently focused option
+ nsCOMPtr<nsIDOMXULMenuListElement> menuListElm = Elm()->AsXULMenuList();
+ if (!menuListElm) return;
+
+ nsCOMPtr<dom::Element> focusedOptionItem;
+ menuListElm->GetSelectedItem(getter_AddRefs(focusedOptionItem));
+ if (focusedOptionItem && mDoc) {
+ LocalAccessible* focusedOptionAcc = mDoc->GetAccessible(focusedOptionItem);
+ if (focusedOptionAcc) focusedOptionAcc->Description(aDescription);
+ }
+}
+
+void XULComboboxAccessible::Value(nsString& aValue) const {
+ aValue.Truncate();
+
+ // The value is the option or text shown entered in the combobox.
+ nsCOMPtr<nsIDOMXULMenuListElement> menuList = Elm()->AsXULMenuList();
+ if (menuList) menuList->GetLabel(aValue);
+}
+
+bool XULComboboxAccessible::HasPrimaryAction() const { return true; }
+
+bool XULComboboxAccessible::DoAction(uint8_t aIndex) const {
+ if (aIndex != XULComboboxAccessible::eAction_Click) return false;
+
+ // Programmaticaly toggle the combo box.
+ nsCOMPtr<nsIDOMXULMenuListElement> menuList = Elm()->AsXULMenuList();
+ if (!menuList) return false;
+
+ bool isDroppedDown = false;
+ menuList->GetOpen(&isDroppedDown);
+ menuList->SetOpen(!isDroppedDown);
+ return true;
+}
+
+void XULComboboxAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ aName.Truncate();
+ if (aIndex != XULComboboxAccessible::eAction_Click) return;
+
+ nsCOMPtr<nsIDOMXULMenuListElement> menuList = Elm()->AsXULMenuList();
+ if (!menuList) return;
+
+ bool isDroppedDown = false;
+ menuList->GetOpen(&isDroppedDown);
+ if (isDroppedDown) {
+ aName.AssignLiteral("close");
+ } else {
+ aName.AssignLiteral("open");
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Widgets
+
+bool XULComboboxAccessible::IsActiveWidget() const {
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::editable,
+ nsGkAtoms::_true, eIgnoreCase)) {
+ int32_t childCount = mChildren.Length();
+ for (int32_t idx = 0; idx < childCount; idx++) {
+ LocalAccessible* child = mChildren[idx];
+ if (child->Role() == roles::ENTRY) {
+ return FocusMgr()->HasDOMFocus(child->GetContent());
+ }
+ }
+ return false;
+ }
+
+ return FocusMgr()->HasDOMFocus(mContent);
+}
+
+bool XULComboboxAccessible::AreItemsOperable() const {
+ nsCOMPtr<nsIDOMXULMenuListElement> menuListElm = Elm()->AsXULMenuList();
+ if (menuListElm) {
+ bool isOpen = false;
+ menuListElm->GetOpen(&isOpen);
+ return isOpen;
+ }
+
+ return false;
+}
diff --git a/accessible/xul/XULComboboxAccessible.h b/accessible/xul/XULComboboxAccessible.h
new file mode 100644
index 0000000000..92f909690e
--- /dev/null
+++ b/accessible/xul/XULComboboxAccessible.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 4; 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_XULComboboxAccessible_h__
+#define mozilla_a11y_XULComboboxAccessible_h__
+
+#include "XULMenuAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Used for XUL comboboxes like xul:menulist and autocomplete textbox.
+ */
+class XULComboboxAccessible : public AccessibleWrap {
+ public:
+ enum { eAction_Click = 0 };
+
+ XULComboboxAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ void Description(nsString& aDescription) const override;
+ void Value(nsString& aValue) const override;
+ a11y::role NativeRole() const override;
+ uint64_t NativeState() const override;
+ bool IsAcceptableChild(nsIContent*) const override;
+
+ // ActionAccessible
+ bool HasPrimaryAction() const override;
+ void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ bool DoAction(uint8_t aIndex) const override;
+
+ // Widgets
+ bool IsActiveWidget() const override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY bool AreItemsOperable() const override;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xul/XULElementAccessibles.cpp b/accessible/xul/XULElementAccessibles.cpp
new file mode 100644
index 0000000000..c999ffa468
--- /dev/null
+++ b/accessible/xul/XULElementAccessibles.cpp
@@ -0,0 +1,205 @@
+/* -*- 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 "XULElementAccessibles.h"
+
+#include "LocalAccessible-inl.h"
+#include "BaseAccessibles.h"
+#include "DocAccessible-inl.h"
+#include "nsAccUtils.h"
+#include "nsCoreUtils.h"
+#include "nsTextEquivUtils.h"
+#include "Relation.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+#include "TextUpdater.h"
+
+#ifdef A11Y_LOG
+# include "Logging.h"
+#endif
+
+#include "nsNameSpaceManager.h"
+#include "nsNetUtil.h"
+#include "nsString.h"
+#include "nsXULElement.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// XULLabelAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULLabelAccessible::XULLabelAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {
+ mType = eXULLabelType;
+}
+
+void XULLabelAccessible::Shutdown() {
+ mValueTextLeaf = nullptr;
+ HyperTextAccessible::Shutdown();
+}
+
+void XULLabelAccessible::DispatchClickEvent(nsIContent* aContent,
+ uint32_t aActionIndex) const {
+ // Bug 1578140: For labels inside buttons, The base implementation of
+ // DispatchClickEvent doesn't fire a command event on the button.
+ RefPtr<nsXULElement> el = nsXULElement::FromNodeOrNull(aContent);
+ if (el) {
+ el->Click(mozilla::dom::CallerType::System);
+ }
+}
+
+ENameValueFlag XULLabelAccessible::NativeName(nsString& aName) const {
+ // if the value attr doesn't exist, the screen reader must get the accessible
+ // text from the accessible text interface or from the children
+ if (mValueTextLeaf) return mValueTextLeaf->Name(aName);
+
+ return LocalAccessible::NativeName(aName);
+}
+
+role XULLabelAccessible::NativeRole() const { return roles::LABEL; }
+
+uint64_t XULLabelAccessible::NativeState() const {
+ // Labels and description have read only state
+ // They are not focusable or selectable
+ return HyperTextAccessible::NativeState() | states::READONLY;
+}
+
+Relation XULLabelAccessible::RelationByType(RelationType aType) const {
+ Relation rel = HyperTextAccessible::RelationByType(aType);
+
+ // The label for xul:groupbox is generated from the first xul:label
+ if (aType == RelationType::LABEL_FOR) {
+ LocalAccessible* parent = LocalParent();
+ if (parent && parent->Role() == roles::GROUPING &&
+ parent->LocalChildAt(0) == this) {
+ nsIContent* parentContent = parent->GetContent();
+ if (parentContent && parentContent->IsXULElement(nsGkAtoms::groupbox)) {
+ rel.AppendTarget(parent);
+ }
+ }
+ }
+
+ return rel;
+}
+
+void XULLabelAccessible::UpdateLabelValue(const nsString& aValue) {
+#ifdef A11Y_LOG
+ if (logging::IsEnabled(logging::eText)) {
+ logging::MsgBegin("TEXT", "text may be changed (xul:label @value update)");
+ logging::Node("container", mContent);
+ logging::MsgEntry("old text '%s'",
+ NS_ConvertUTF16toUTF8(mValueTextLeaf->Text()).get());
+ logging::MsgEntry("new text: '%s'", NS_ConvertUTF16toUTF8(aValue).get());
+ logging::MsgEnd();
+ }
+#endif
+
+ TextUpdater::Run(mDoc, mValueTextLeaf, aValue);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULLabelTextLeafAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role XULLabelTextLeafAccessible::NativeRole() const { return roles::TEXT_LEAF; }
+
+uint64_t XULLabelTextLeafAccessible::NativeState() const {
+ return TextLeafAccessible::NativeState() | states::READONLY;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTooltipAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULTooltipAccessible::XULTooltipAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {
+ mType = eXULTooltipType;
+}
+
+uint64_t XULTooltipAccessible::NativeState() const {
+ return LeafAccessible::NativeState() | states::READONLY;
+}
+
+role XULTooltipAccessible::NativeRole() const { return roles::TOOLTIP; }
+
+////////////////////////////////////////////////////////////////////////////////
+// XULLinkAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULLinkAccessible::XULLinkAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : XULLabelAccessible(aContent, aDoc) {}
+
+XULLinkAccessible::~XULLinkAccessible() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULLinkAccessible: LocalAccessible
+
+void XULLinkAccessible::Value(nsString& aValue) const {
+ aValue.Truncate();
+
+ mContent->AsElement()->GetAttr(nsGkAtoms::href, aValue);
+}
+
+ENameValueFlag XULLinkAccessible::NativeName(nsString& aName) const {
+ mContent->AsElement()->GetAttr(nsGkAtoms::value, aName);
+ if (!aName.IsEmpty()) return eNameOK;
+
+ nsTextEquivUtils::GetNameFromSubtree(this, aName);
+ return aName.IsEmpty() ? eNameOK : eNameFromSubtree;
+}
+
+role XULLinkAccessible::NativeRole() const { return roles::LINK; }
+
+uint64_t XULLinkAccessible::NativeLinkState() const { return states::LINKED; }
+
+bool XULLinkAccessible::HasPrimaryAction() const { return true; }
+
+void XULLinkAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ aName.Truncate();
+
+ if (aIndex == eAction_Jump) aName.AssignLiteral("jump");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULLinkAccessible: HyperLinkAccessible
+
+bool XULLinkAccessible::IsLink() const {
+ // Expose HyperLinkAccessible unconditionally.
+ return true;
+}
+
+uint32_t XULLinkAccessible::StartOffset() {
+ // If XUL link accessible is not contained by hypertext accessible then
+ // start offset matches index in parent because the parent doesn't contains
+ // a text.
+ // XXX: accessible parent of XUL link accessible should be a hypertext
+ // accessible.
+ if (LocalAccessible::IsLink()) return LocalAccessible::StartOffset();
+ return IndexInParent();
+}
+
+uint32_t XULLinkAccessible::EndOffset() {
+ if (LocalAccessible::IsLink()) return LocalAccessible::EndOffset();
+ return IndexInParent() + 1;
+}
+
+already_AddRefed<nsIURI> XULLinkAccessible::AnchorURIAt(
+ uint32_t aAnchorIndex) const {
+ if (aAnchorIndex != 0) return nullptr;
+
+ nsAutoString href;
+ mContent->AsElement()->GetAttr(nsGkAtoms::href, href);
+
+ dom::Document* document = mContent->OwnerDoc();
+
+ nsCOMPtr<nsIURI> anchorURI;
+ NS_NewURI(getter_AddRefs(anchorURI), href,
+ document->GetDocumentCharacterSet(), mContent->GetBaseURI());
+
+ return anchorURI.forget();
+}
diff --git a/accessible/xul/XULElementAccessibles.h b/accessible/xul/XULElementAccessibles.h
new file mode 100644
index 0000000000..a51b7c3951
--- /dev/null
+++ b/accessible/xul/XULElementAccessibles.h
@@ -0,0 +1,108 @@
+/* -*- 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_XULElementAccessibles_h__
+#define mozilla_a11y_XULElementAccessibles_h__
+
+#include "HyperTextAccessible.h"
+#include "TextLeafAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+class XULLabelTextLeafAccessible;
+
+/**
+ * Used for XUL description and label elements.
+ */
+class XULLabelAccessible : public HyperTextAccessible {
+ public:
+ XULLabelAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ void UpdateLabelValue(const nsString& aValue);
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+ virtual void DispatchClickEvent(nsIContent* aContent,
+ uint32_t aActionIndex) const override;
+
+ private:
+ RefPtr<XULLabelTextLeafAccessible> mValueTextLeaf;
+};
+
+inline XULLabelAccessible* LocalAccessible::AsXULLabel() {
+ return IsXULLabel() ? static_cast<XULLabelAccessible*>(this) : nullptr;
+}
+
+/**
+ * Used to implement text interface on XUL label accessible in case when text
+ * is provided by @value attribute (no underlying text frame).
+ */
+class XULLabelTextLeafAccessible final : public TextLeafAccessible {
+ public:
+ XULLabelTextLeafAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : TextLeafAccessible(aContent, aDoc) {
+ mStateFlags |= eSharedNode;
+ }
+
+ virtual ~XULLabelTextLeafAccessible() {}
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+};
+
+/**
+ * Used for XUL tooltip element.
+ */
+class XULTooltipAccessible : public LeafAccessible {
+ public:
+ XULTooltipAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+};
+
+class XULLinkAccessible : public XULLabelAccessible {
+ public:
+ XULLinkAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual void Value(nsString& aValue) const override;
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeLinkState() const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+
+ // HyperLinkAccessible
+ virtual bool IsLink() const override;
+ virtual uint32_t StartOffset() override;
+ virtual uint32_t EndOffset() override;
+ virtual already_AddRefed<nsIURI> AnchorURIAt(
+ uint32_t aAnchorIndex) const override;
+
+ protected:
+ virtual ~XULLinkAccessible();
+
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+
+ enum { eAction_Jump = 0 };
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xul/XULFormControlAccessible.cpp b/accessible/xul/XULFormControlAccessible.cpp
new file mode 100644
index 0000000000..038f93dd68
--- /dev/null
+++ b/accessible/xul/XULFormControlAccessible.cpp
@@ -0,0 +1,446 @@
+/* -*- 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 "XULFormControlAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "HTMLFormControlAccessible.h"
+#include "nsAccUtils.h"
+#include "DocAccessible.h"
+#include "Relation.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+#include "TreeWalker.h"
+#include "XULMenuAccessible.h"
+
+#include "nsIDOMXULButtonElement.h"
+#include "nsIDOMXULMenuListElement.h"
+#include "nsIDOMXULRadioGroupElement.h"
+#include "nsIDOMXULSelectCntrlItemEl.h"
+#include "nsIFrame.h"
+#include "nsITextControlFrame.h"
+#include "nsMenuPopupFrame.h"
+#include "nsNameSpaceManager.h"
+#include "mozilla/dom/Element.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// XULButtonAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULButtonAccessible::XULButtonAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {
+ if (ContainsMenu()) {
+ mGenericTypes |= eMenuButton;
+ } else {
+ mGenericTypes |= eButton;
+ }
+}
+
+XULButtonAccessible::~XULButtonAccessible() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULButtonAccessible: nsISupports
+
+////////////////////////////////////////////////////////////////////////////////
+// XULButtonAccessible: nsIAccessible
+
+bool XULButtonAccessible::HasPrimaryAction() const { return true; }
+
+void XULButtonAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == eAction_Click) aName.AssignLiteral("press");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULButtonAccessible: LocalAccessible
+
+role XULButtonAccessible::NativeRole() const {
+ // Buttons can be checked; they simply appear pressed in rather than checked.
+ // In this case, we must expose them as toggle buttons.
+ nsCOMPtr<nsIDOMXULButtonElement> xulButtonElement = Elm()->AsXULButton();
+ if (xulButtonElement) {
+ nsAutoString type;
+ xulButtonElement->GetType(type);
+ if (type.EqualsLiteral("checkbox") || type.EqualsLiteral("radio")) {
+ return roles::TOGGLE_BUTTON;
+ }
+ }
+ return roles::PUSHBUTTON;
+}
+
+uint64_t XULButtonAccessible::NativeState() const {
+ // Possible states: focused, focusable, unavailable(disabled).
+
+ // get focus and disable status from base class
+ uint64_t state = LocalAccessible::NativeState();
+
+ nsCOMPtr<nsIDOMXULButtonElement> xulButtonElement = Elm()->AsXULButton();
+ if (xulButtonElement) {
+ // Some buttons can have their checked state set without being of type
+ // checkbox or radio. Expose the pressed state unconditionally.
+ bool checked = false;
+ xulButtonElement->GetChecked(&checked);
+ if (checked) {
+ state |= states::PRESSED;
+ }
+ }
+
+ if (ContainsMenu()) state |= states::HASPOPUP;
+
+ if (mContent->AsElement()->HasAttr(nsGkAtoms::_default)) {
+ state |= states::DEFAULT;
+ }
+
+ return state;
+}
+
+bool XULButtonAccessible::AttributeChangesState(nsAtom* aAttribute) {
+ if (aAttribute == nsGkAtoms::checked) {
+ return true;
+ }
+ return AccessibleWrap::AttributeChangesState(aAttribute);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULButtonAccessible: Widgets
+
+bool XULButtonAccessible::IsWidget() const { return true; }
+
+bool XULButtonAccessible::IsActiveWidget() const {
+ return FocusMgr()->HasDOMFocus(mContent);
+}
+
+bool XULButtonAccessible::AreItemsOperable() const {
+ if (IsMenuButton()) {
+ LocalAccessible* menuPopup = mChildren.SafeElementAt(0, nullptr);
+ if (menuPopup) {
+ nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(menuPopup->GetFrame());
+ return menuPopupFrame->IsOpen();
+ }
+ }
+ return false; // no items
+}
+
+bool XULButtonAccessible::IsAcceptableChild(nsIContent* aEl) const {
+ // In general XUL buttons should not have accessible children. However:
+ return
+ // menu buttons can have popup accessibles (@type="menu" or
+ // columnpicker).
+ aEl->IsXULElement(nsGkAtoms::menupopup) ||
+ // A XUL button can be labelled by a direct child text node, so we need to
+ // allow that as a child so it will be picked up when computing name from
+ // subtree.
+ (aEl->IsText() && aEl->GetParent() == mContent);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULButtonAccessible protected
+
+bool XULButtonAccessible::ContainsMenu() const {
+ return mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::menu, eCaseMatters);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULDropmarkerAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULDropmarkerAccessible::XULDropmarkerAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {}
+
+bool XULDropmarkerAccessible::HasPrimaryAction() const { return true; }
+
+bool XULDropmarkerAccessible::DropmarkerOpen(bool aToggleOpen) const {
+ bool isOpen = false;
+
+ nsIContent* parent = mContent->GetFlattenedTreeParent();
+
+ while (parent) {
+ nsCOMPtr<nsIDOMXULButtonElement> parentButtonElement =
+ parent->AsElement()->AsXULButton();
+ if (parentButtonElement) {
+ parentButtonElement->GetOpen(&isOpen);
+ if (aToggleOpen) parentButtonElement->SetOpen(!isOpen);
+ return isOpen;
+ }
+
+ nsCOMPtr<nsIDOMXULMenuListElement> parentMenuListElement =
+ parent->AsElement()->AsXULMenuList();
+ if (parentMenuListElement) {
+ parentMenuListElement->GetOpen(&isOpen);
+ if (aToggleOpen) parentMenuListElement->SetOpen(!isOpen);
+ return isOpen;
+ }
+ parent = parent->GetFlattenedTreeParent();
+ }
+
+ return isOpen;
+}
+
+void XULDropmarkerAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ aName.Truncate();
+ if (aIndex == eAction_Click) {
+ if (DropmarkerOpen(false)) {
+ aName.AssignLiteral("close");
+ } else {
+ aName.AssignLiteral("open");
+ }
+ }
+}
+
+bool XULDropmarkerAccessible::DoAction(uint8_t index) const {
+ if (index == eAction_Click) {
+ DropmarkerOpen(true); // Reverse the open attribute
+ return true;
+ }
+ return false;
+}
+
+role XULDropmarkerAccessible::NativeRole() const { return roles::PUSHBUTTON; }
+
+uint64_t XULDropmarkerAccessible::NativeState() const {
+ return DropmarkerOpen(false) ? states::PRESSED : 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULGroupboxAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULGroupboxAccessible::XULGroupboxAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {}
+
+role XULGroupboxAccessible::NativeRole() const { return roles::GROUPING; }
+
+ENameValueFlag XULGroupboxAccessible::NativeName(nsString& aName) const {
+ // XXX: we use the first related accessible only.
+ LocalAccessible* label =
+ RelationByType(RelationType::LABELLED_BY).LocalNext();
+ if (label) return label->Name(aName);
+
+ return eNameOK;
+}
+
+Relation XULGroupboxAccessible::RelationByType(RelationType aType) const {
+ Relation rel = AccessibleWrap::RelationByType(aType);
+
+ // The label for xul:groupbox is generated from the first xul:label
+ if (aType == RelationType::LABELLED_BY && ChildCount() > 0) {
+ LocalAccessible* childAcc = LocalChildAt(0);
+ if (childAcc->Role() == roles::LABEL &&
+ childAcc->GetContent()->IsXULElement(nsGkAtoms::label)) {
+ rel.AppendTarget(childAcc);
+ }
+ }
+
+ return rel;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULRadioButtonAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULRadioButtonAccessible::XULRadioButtonAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : RadioButtonAccessible(aContent, aDoc) {}
+
+uint64_t XULRadioButtonAccessible::NativeState() const {
+ uint64_t state = LeafAccessible::NativeState();
+ state |= states::CHECKABLE;
+
+ nsCOMPtr<nsIDOMXULSelectControlItemElement> radioButton =
+ Elm()->AsXULSelectControlItem();
+ if (radioButton) {
+ bool selected = false; // Radio buttons can be selected
+ radioButton->GetSelected(&selected);
+ if (selected) {
+ state |= states::CHECKED;
+ }
+ }
+
+ return state;
+}
+
+uint64_t XULRadioButtonAccessible::NativeInteractiveState() const {
+ return NativelyUnavailable() ? states::UNAVAILABLE : states::FOCUSABLE;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULRadioButtonAccessible: Widgets
+
+LocalAccessible* XULRadioButtonAccessible::ContainerWidget() const {
+ return mParent;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULRadioGroupAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * XUL Radio Group
+ * The Radio Group proxies for the Radio Buttons themselves. The Group gets
+ * focus whereas the Buttons do not. So we only have an accessible object for
+ * this for the purpose of getting the proper RadioButton. Need this here to
+ * avoid circular reference problems when navigating the accessible tree and
+ * for getting to the radiobuttons.
+ */
+
+XULRadioGroupAccessible::XULRadioGroupAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : XULSelectControlAccessible(aContent, aDoc) {}
+
+role XULRadioGroupAccessible::NativeRole() const { return roles::RADIO_GROUP; }
+
+uint64_t XULRadioGroupAccessible::NativeInteractiveState() const {
+ // The radio group is not focusable. Sometimes the focus controller will
+ // report that it is focused. That means that the actual selected radio button
+ // should be considered focused.
+ return NativelyUnavailable() ? states::UNAVAILABLE : 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULRadioGroupAccessible: Widgets
+
+bool XULRadioGroupAccessible::IsWidget() const { return true; }
+
+bool XULRadioGroupAccessible::IsActiveWidget() const {
+ return FocusMgr()->HasDOMFocus(mContent);
+}
+
+bool XULRadioGroupAccessible::AreItemsOperable() const { return true; }
+
+LocalAccessible* XULRadioGroupAccessible::CurrentItem() const {
+ if (!mSelectControl) {
+ return nullptr;
+ }
+
+ RefPtr<dom::Element> currentItemElm;
+ nsCOMPtr<nsIDOMXULRadioGroupElement> group =
+ mSelectControl->AsXULRadioGroup();
+ if (group) {
+ group->GetFocusedItem(getter_AddRefs(currentItemElm));
+ }
+
+ if (currentItemElm) {
+ DocAccessible* document = Document();
+ if (document) {
+ return document->GetAccessible(currentItemElm);
+ }
+ }
+
+ return nullptr;
+}
+
+void XULRadioGroupAccessible::SetCurrentItem(const LocalAccessible* aItem) {
+ if (!mSelectControl) {
+ return;
+ }
+
+ nsCOMPtr<dom::Element> itemElm = aItem->Elm();
+ nsCOMPtr<nsIDOMXULRadioGroupElement> group =
+ mSelectControl->AsXULRadioGroup();
+ if (group) {
+ group->SetFocusedItem(itemElm);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULStatusBarAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULStatusBarAccessible::XULStatusBarAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {}
+
+role XULStatusBarAccessible::NativeRole() const { return roles::STATUSBAR; }
+
+////////////////////////////////////////////////////////////////////////////////
+// XULToolbarButtonAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULToolbarButtonAccessible::XULToolbarButtonAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : XULButtonAccessible(aContent, aDoc) {}
+
+void XULToolbarButtonAccessible::GetPositionAndSetSize(int32_t* aPosInSet,
+ int32_t* aSetSize) {
+ int32_t setSize = 0;
+ int32_t posInSet = 0;
+
+ LocalAccessible* parent = LocalParent();
+ if (!parent) return;
+
+ uint32_t childCount = parent->ChildCount();
+ for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
+ LocalAccessible* child = parent->LocalChildAt(childIdx);
+ if (IsSeparator(child)) { // end of a group of buttons
+ if (posInSet) break; // we've found our group, so we're done
+
+ setSize = 0; // not our group, so start a new group
+
+ } else {
+ setSize++; // another button in the group
+
+ if (child == this) posInSet = setSize; // we've found our button
+ }
+ }
+
+ *aPosInSet = posInSet;
+ *aSetSize = setSize;
+}
+
+bool XULToolbarButtonAccessible::IsSeparator(LocalAccessible* aAccessible) {
+ nsIContent* content = aAccessible->GetContent();
+ return content && content->IsAnyOfXULElements(nsGkAtoms::toolbarseparator,
+ nsGkAtoms::toolbarspacer,
+ nsGkAtoms::toolbarspring);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULToolbarButtonAccessible: Widgets
+
+bool XULToolbarButtonAccessible::IsAcceptableChild(nsIContent* aEl) const {
+ return XULButtonAccessible::IsAcceptableChild(aEl) ||
+ // In addition to the children allowed by buttons, toolbarbuttons can
+ // have labels as children, but only if the label attribute is not
+ // present.
+ (aEl->IsXULElement(nsGkAtoms::label) &&
+ !mContent->AsElement()->HasAttr(nsGkAtoms::label));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULToolbarAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULToolbarAccessible::XULToolbarAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {}
+
+role XULToolbarAccessible::NativeRole() const { return roles::TOOLBAR; }
+
+ENameValueFlag XULToolbarAccessible::NativeName(nsString& aName) const {
+ if (mContent->AsElement()->GetAttr(nsGkAtoms::toolbarname, aName)) {
+ aName.CompressWhitespace();
+ }
+
+ return eNameOK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULToolbarAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULToolbarSeparatorAccessible::XULToolbarSeparatorAccessible(
+ nsIContent* aContent, DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {}
+
+role XULToolbarSeparatorAccessible::NativeRole() const {
+ return roles::SEPARATOR;
+}
+
+uint64_t XULToolbarSeparatorAccessible::NativeState() const { return 0; }
diff --git a/accessible/xul/XULFormControlAccessible.h b/accessible/xul/XULFormControlAccessible.h
new file mode 100644
index 0000000000..4eda16c7e9
--- /dev/null
+++ b/accessible/xul/XULFormControlAccessible.h
@@ -0,0 +1,186 @@
+/* -*- 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_XULFormControlAccessible_H_
+#define MOZILLA_A11Y_XULFormControlAccessible_H_
+
+// NOTE: alphabetically ordered
+#include "AccessibleWrap.h"
+#include "FormControlAccessible.h"
+#include "HyperTextAccessible.h"
+#include "XULSelectControlAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Used for XUL button.
+ *
+ * @note Don't inherit from LeafAccessible - it doesn't allow children
+ * and a button can have a dropmarker child.
+ */
+class XULButtonAccessible : public AccessibleWrap {
+ public:
+ enum { eAction_Click = 0 };
+ XULButtonAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // nsISupports
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(XULButtonAccessible, AccessibleWrap)
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual bool AttributeChangesState(nsAtom* aAttribute) override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+ virtual bool IsActiveWidget() const override;
+ virtual bool AreItemsOperable() const override;
+
+ virtual bool IsAcceptableChild(nsIContent* aEl) const override;
+
+ protected:
+ virtual ~XULButtonAccessible();
+
+ // XULButtonAccessible
+ bool ContainsMenu() const;
+};
+
+/**
+ * Used for XUL dropmarker element.
+ */
+class XULDropmarkerAccessible : public LeafAccessible {
+ public:
+ enum { eAction_Click = 0 };
+ XULDropmarkerAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual bool DoAction(uint8_t aIndex) const override;
+
+ private:
+ bool DropmarkerOpen(bool aToggleOpen) const;
+};
+
+/**
+ * Used for XUL groupbox element.
+ */
+class XULGroupboxAccessible final : public AccessibleWrap {
+ public:
+ XULGroupboxAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+};
+
+/**
+ * Used for XUL radio element (radio button).
+ */
+class XULRadioButtonAccessible : public RadioButtonAccessible {
+ public:
+ XULRadioButtonAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual uint64_t NativeState() const override;
+ virtual uint64_t NativeInteractiveState() const override;
+
+ // Widgets
+ virtual LocalAccessible* ContainerWidget() const override;
+};
+
+/**
+ * Used for XUL radiogroup element.
+ */
+class XULRadioGroupAccessible : public XULSelectControlAccessible {
+ public:
+ XULRadioGroupAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t NativeInteractiveState() const override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+ virtual bool IsActiveWidget() const override;
+ virtual bool AreItemsOperable() const override;
+ virtual LocalAccessible* CurrentItem() const override;
+ virtual void SetCurrentItem(const LocalAccessible* aItem) override;
+};
+
+/**
+ * Used for XUL statusbar element.
+ */
+class XULStatusBarAccessible : public AccessibleWrap {
+ public:
+ XULStatusBarAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+};
+
+/**
+ * Used for XUL toolbarbutton element.
+ */
+class XULToolbarButtonAccessible : public XULButtonAccessible {
+ public:
+ XULToolbarButtonAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // nsXULToolbarButtonAccessible
+ static bool IsSeparator(LocalAccessible* aAccessible);
+
+ // Widgets
+ virtual bool IsAcceptableChild(nsIContent* aEl) const override;
+
+ protected:
+ // LocalAccessible
+ virtual void GetPositionAndSetSize(int32_t* aPosInSet,
+ int32_t* aSetSize) override;
+};
+
+/**
+ * Used for XUL toolbar element.
+ */
+class XULToolbarAccessible : public AccessibleWrap {
+ public:
+ XULToolbarAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+};
+
+/**
+ * Used for XUL toolbarseparator element.
+ */
+class XULToolbarSeparatorAccessible : public LeafAccessible {
+ public:
+ XULToolbarSeparatorAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual mozilla::a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xul/XULListboxAccessible.cpp b/accessible/xul/XULListboxAccessible.cpp
new file mode 100644
index 0000000000..d80bac1ca0
--- /dev/null
+++ b/accessible/xul/XULListboxAccessible.cpp
@@ -0,0 +1,456 @@
+/* -*- Mode: C++; tab-width: 4; 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 "XULListboxAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "nsAccessibilityService.h"
+#include "nsAccUtils.h"
+#include "DocAccessible.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsIAutoCompletePopup.h"
+#include "nsIDOMXULMenuListElement.h"
+#include "nsIDOMXULMultSelectCntrlEl.h"
+#include "nsIDOMXULSelectCntrlItemEl.h"
+#include "nsINodeList.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// XULColumAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULColumAccessible::XULColumAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {}
+
+role XULColumAccessible::NativeRole() const { return roles::LIST; }
+
+uint64_t XULColumAccessible::NativeState() const { return states::READONLY; }
+
+////////////////////////////////////////////////////////////////////////////////
+// XULColumnItemAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULColumnItemAccessible::XULColumnItemAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : LeafAccessible(aContent, aDoc) {}
+
+role XULColumnItemAccessible::NativeRole() const { return roles::COLUMNHEADER; }
+
+uint64_t XULColumnItemAccessible::NativeState() const {
+ return states::READONLY;
+}
+
+bool XULColumnItemAccessible::HasPrimaryAction() const { return true; }
+
+void XULColumnItemAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == eAction_Click) aName.AssignLiteral("click");
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULListboxAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULListboxAccessible::XULListboxAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : XULSelectControlAccessible(aContent, aDoc) {
+ dom::Element* parentEl = mContent->GetParentElement();
+ if (parentEl) {
+ nsCOMPtr<nsIAutoCompletePopup> autoCompletePopupElm =
+ parentEl->AsAutoCompletePopup();
+ if (autoCompletePopupElm) mGenericTypes |= eAutoCompletePopup;
+ }
+
+ if (IsMulticolumn()) mGenericTypes |= eTable;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULListboxAccessible: LocalAccessible
+
+uint64_t XULListboxAccessible::NativeState() const {
+ // As a XULListboxAccessible we can have the following states:
+ // FOCUSED, READONLY, FOCUSABLE
+
+ // Get focus status from base class
+ uint64_t states = LocalAccessible::NativeState();
+
+ // see if we are multiple select if so set ourselves as such
+
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::seltype,
+ nsGkAtoms::multiple, eCaseMatters)) {
+ states |= states::MULTISELECTABLE | states::EXTSELECTABLE;
+ }
+
+ return states;
+}
+
+role XULListboxAccessible::NativeRole() const {
+ // A richlistbox is used with the new autocomplete URL bar, and has a parent
+ // popup <panel>.
+ if (mContent->GetParent() &&
+ mContent->GetParent()->IsXULElement(nsGkAtoms::panel)) {
+ return roles::COMBOBOX_LIST;
+ }
+
+ return IsMulticolumn() ? roles::TABLE : roles::LISTBOX;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULListboxAccessible: Table
+
+uint32_t XULListboxAccessible::ColCount() const { return 0; }
+
+uint32_t XULListboxAccessible::RowCount() {
+ nsCOMPtr<nsIDOMXULSelectControlElement> element = Elm()->AsXULSelectControl();
+
+ uint32_t itemCount = 0;
+ if (element) element->GetItemCount(&itemCount);
+
+ return itemCount;
+}
+
+LocalAccessible* XULListboxAccessible::CellAt(uint32_t aRowIndex,
+ uint32_t aColumnIndex) {
+ nsCOMPtr<nsIDOMXULSelectControlElement> control = Elm()->AsXULSelectControl();
+ NS_ENSURE_TRUE(control, nullptr);
+
+ RefPtr<dom::Element> element;
+ control->GetItemAtIndex(aRowIndex, getter_AddRefs(element));
+ if (!element) return nullptr;
+
+ LocalAccessible* row = mDoc->GetAccessible(element);
+ NS_ENSURE_TRUE(row, nullptr);
+
+ return row->LocalChildAt(aColumnIndex);
+}
+
+bool XULListboxAccessible::IsColSelected(uint32_t aColIdx) {
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> control =
+ Elm()->AsXULMultiSelectControl();
+ NS_ASSERTION(control,
+ "Doesn't implement nsIDOMXULMultiSelectControlElement.");
+
+ int32_t selectedrowCount = 0;
+ nsresult rv = control->GetSelectedCount(&selectedrowCount);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ return selectedrowCount == static_cast<int32_t>(RowCount());
+}
+
+bool XULListboxAccessible::IsRowSelected(uint32_t aRowIdx) {
+ nsCOMPtr<nsIDOMXULSelectControlElement> control = Elm()->AsXULSelectControl();
+ NS_ASSERTION(control, "Doesn't implement nsIDOMXULSelectControlElement.");
+
+ RefPtr<dom::Element> element;
+ nsresult rv = control->GetItemAtIndex(aRowIdx, getter_AddRefs(element));
+ NS_ENSURE_SUCCESS(rv, false);
+ if (!element) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDOMXULSelectControlItemElement> item =
+ element->AsXULSelectControlItem();
+
+ bool isSelected = false;
+ item->GetSelected(&isSelected);
+ return isSelected;
+}
+
+bool XULListboxAccessible::IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) {
+ return IsRowSelected(aRowIdx);
+}
+
+uint32_t XULListboxAccessible::SelectedCellCount() {
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> control =
+ Elm()->AsXULMultiSelectControl();
+ NS_ASSERTION(control,
+ "Doesn't implement nsIDOMXULMultiSelectControlElement.");
+
+ nsCOMPtr<nsINodeList> selectedItems;
+ control->GetSelectedItems(getter_AddRefs(selectedItems));
+ if (!selectedItems) return 0;
+
+ uint32_t selectedItemsCount = selectedItems->Length();
+
+ return selectedItemsCount * ColCount();
+}
+
+uint32_t XULListboxAccessible::SelectedColCount() {
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> control =
+ Elm()->AsXULMultiSelectControl();
+ NS_ASSERTION(control,
+ "Doesn't implement nsIDOMXULMultiSelectControlElement.");
+
+ int32_t selectedRowCount = 0;
+ nsresult rv = control->GetSelectedCount(&selectedRowCount);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ return selectedRowCount > 0 &&
+ selectedRowCount == static_cast<int32_t>(RowCount())
+ ? ColCount()
+ : 0;
+}
+
+uint32_t XULListboxAccessible::SelectedRowCount() {
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> control =
+ Elm()->AsXULMultiSelectControl();
+ NS_ASSERTION(control,
+ "Doesn't implement nsIDOMXULMultiSelectControlElement.");
+
+ int32_t selectedRowCount = 0;
+ nsresult rv = control->GetSelectedCount(&selectedRowCount);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ return selectedRowCount >= 0 ? selectedRowCount : 0;
+}
+
+void XULListboxAccessible::SelectedCells(nsTArray<Accessible*>* aCells) {
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> control =
+ Elm()->AsXULMultiSelectControl();
+ NS_ASSERTION(control,
+ "Doesn't implement nsIDOMXULMultiSelectControlElement.");
+
+ nsCOMPtr<nsINodeList> selectedItems;
+ control->GetSelectedItems(getter_AddRefs(selectedItems));
+ if (!selectedItems) return;
+
+ uint32_t selectedItemsCount = selectedItems->Length();
+
+ for (uint32_t index = 0; index < selectedItemsCount; index++) {
+ nsIContent* itemContent = selectedItems->Item(index);
+ LocalAccessible* item = mDoc->GetAccessible(itemContent);
+
+ if (item) {
+ uint32_t cellCount = item->ChildCount();
+ for (uint32_t cellIdx = 0; cellIdx < cellCount; cellIdx++) {
+ LocalAccessible* cell = mChildren[cellIdx];
+ if (cell->Role() == roles::CELL) aCells->AppendElement(cell);
+ }
+ }
+ }
+}
+
+void XULListboxAccessible::SelectedCellIndices(nsTArray<uint32_t>* aCells) {
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> control =
+ Elm()->AsXULMultiSelectControl();
+ NS_ASSERTION(control,
+ "Doesn't implement nsIDOMXULMultiSelectControlElement.");
+
+ nsCOMPtr<nsINodeList> selectedItems;
+ control->GetSelectedItems(getter_AddRefs(selectedItems));
+ if (!selectedItems) return;
+
+ uint32_t selectedItemsCount = selectedItems->Length();
+
+ uint32_t colCount = ColCount();
+ aCells->SetCapacity(selectedItemsCount * colCount);
+ aCells->AppendElements(selectedItemsCount * colCount);
+
+ for (uint32_t selItemsIdx = 0, cellsIdx = 0; selItemsIdx < selectedItemsCount;
+ selItemsIdx++) {
+ nsIContent* itemContent = selectedItems->Item(selItemsIdx);
+
+ nsCOMPtr<nsIDOMXULSelectControlItemElement> item =
+ itemContent->AsElement()->AsXULSelectControlItem();
+ if (item) {
+ int32_t itemIdx = -1;
+ control->GetIndexOfItem(item, &itemIdx);
+ if (itemIdx >= 0) {
+ for (uint32_t colIdx = 0; colIdx < colCount; colIdx++, cellsIdx++) {
+ aCells->ElementAt(cellsIdx) = itemIdx * colCount + colIdx;
+ }
+ }
+ }
+ }
+}
+
+void XULListboxAccessible::SelectedColIndices(nsTArray<uint32_t>* aCols) {
+ uint32_t selColCount = SelectedColCount();
+ aCols->SetCapacity(selColCount);
+
+ for (uint32_t colIdx = 0; colIdx < selColCount; colIdx++) {
+ aCols->AppendElement(colIdx);
+ }
+}
+
+void XULListboxAccessible::SelectedRowIndices(nsTArray<uint32_t>* aRows) {
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> control =
+ Elm()->AsXULMultiSelectControl();
+ NS_ASSERTION(control,
+ "Doesn't implement nsIDOMXULMultiSelectControlElement.");
+
+ nsCOMPtr<nsINodeList> selectedItems;
+ control->GetSelectedItems(getter_AddRefs(selectedItems));
+ if (!selectedItems) return;
+
+ uint32_t rowCount = selectedItems->Length();
+
+ if (!rowCount) return;
+
+ aRows->SetCapacity(rowCount);
+ aRows->AppendElements(rowCount);
+
+ for (uint32_t rowIdx = 0; rowIdx < rowCount; rowIdx++) {
+ nsIContent* itemContent = selectedItems->Item(rowIdx);
+ nsCOMPtr<nsIDOMXULSelectControlItemElement> item =
+ itemContent->AsElement()->AsXULSelectControlItem();
+
+ if (item) {
+ int32_t itemIdx = -1;
+ control->GetIndexOfItem(item, &itemIdx);
+ if (itemIdx >= 0) aRows->ElementAt(rowIdx) = itemIdx;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULListboxAccessible: Widgets
+
+bool XULListboxAccessible::IsWidget() const { return true; }
+
+bool XULListboxAccessible::IsActiveWidget() const {
+ if (IsAutoCompletePopup()) {
+ nsIContent* parentContent = mContent->GetParent();
+ if (parentContent) {
+ nsCOMPtr<nsIAutoCompletePopup> autoCompletePopupElm =
+ parentContent->AsElement()->AsAutoCompletePopup();
+ if (autoCompletePopupElm) {
+ bool isOpen = false;
+ autoCompletePopupElm->GetPopupOpen(&isOpen);
+ return isOpen;
+ }
+ }
+ }
+ return FocusMgr()->HasDOMFocus(mContent);
+}
+
+bool XULListboxAccessible::AreItemsOperable() const {
+ if (IsAutoCompletePopup()) {
+ nsIContent* parentContent = mContent->GetParent();
+ if (parentContent) {
+ nsCOMPtr<nsIAutoCompletePopup> autoCompletePopupElm =
+ parentContent->AsElement()->AsAutoCompletePopup();
+ if (autoCompletePopupElm) {
+ bool isOpen = false;
+ autoCompletePopupElm->GetPopupOpen(&isOpen);
+ return isOpen;
+ }
+ }
+ }
+ return true;
+}
+
+LocalAccessible* XULListboxAccessible::ContainerWidget() const {
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULListitemAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULListitemAccessible::XULListitemAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : XULMenuitemAccessible(aContent, aDoc) {
+ mIsCheckbox = mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::checkbox, eCaseMatters);
+ mType = eXULListItemType;
+}
+
+XULListitemAccessible::~XULListitemAccessible() {}
+
+LocalAccessible* XULListitemAccessible::GetListAccessible() const {
+ if (IsDefunct()) return nullptr;
+
+ nsCOMPtr<nsIDOMXULSelectControlItemElement> listItem =
+ Elm()->AsXULSelectControlItem();
+ if (!listItem) return nullptr;
+
+ RefPtr<dom::Element> listElement;
+ listItem->GetControl(getter_AddRefs(listElement));
+ if (!listElement) return nullptr;
+
+ return mDoc->GetAccessible(listElement);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULListitemAccessible LocalAccessible
+
+void XULListitemAccessible::Description(nsString& aDesc) const {
+ AccessibleWrap::Description(aDesc);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULListitemAccessible: LocalAccessible
+
+/**
+ * Get the name from GetXULName.
+ */
+ENameValueFlag XULListitemAccessible::NativeName(nsString& aName) const {
+ return LocalAccessible::NativeName(aName);
+}
+
+role XULListitemAccessible::NativeRole() const {
+ LocalAccessible* list = GetListAccessible();
+ if (!list) {
+ NS_ERROR("No list accessible for listitem accessible!");
+ return roles::NOTHING;
+ }
+
+ if (list->Role() == roles::TABLE) return roles::ROW;
+
+ if (mIsCheckbox) return roles::CHECK_RICH_OPTION;
+
+ if (mParent && mParent->Role() == roles::COMBOBOX_LIST) {
+ return roles::COMBOBOX_OPTION;
+ }
+
+ return roles::RICH_OPTION;
+}
+
+uint64_t XULListitemAccessible::NativeState() const {
+ if (mIsCheckbox) return XULMenuitemAccessible::NativeState();
+
+ uint64_t states = NativeInteractiveState();
+
+ nsCOMPtr<nsIDOMXULSelectControlItemElement> listItem =
+ Elm()->AsXULSelectControlItem();
+ if (listItem) {
+ bool isSelected;
+ listItem->GetSelected(&isSelected);
+ if (isSelected) states |= states::SELECTED;
+
+ if (FocusMgr()->IsFocused(this)) states |= states::FOCUSED;
+ }
+
+ return states;
+}
+
+uint64_t XULListitemAccessible::NativeInteractiveState() const {
+ return NativelyUnavailable() || (mParent && mParent->NativelyUnavailable())
+ ? states::UNAVAILABLE
+ : states::FOCUSABLE | states::SELECTABLE;
+}
+
+void XULListitemAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == eAction_Click && mIsCheckbox) {
+ uint64_t states = NativeState();
+ if (states & states::CHECKED) {
+ aName.AssignLiteral("uncheck");
+ } else {
+ aName.AssignLiteral("check");
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULListitemAccessible: Widgets
+
+LocalAccessible* XULListitemAccessible::ContainerWidget() const {
+ return LocalParent();
+}
diff --git a/accessible/xul/XULListboxAccessible.h b/accessible/xul/XULListboxAccessible.h
new file mode 100644
index 0000000000..2745b323d0
--- /dev/null
+++ b/accessible/xul/XULListboxAccessible.h
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 4; 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_XULListboxAccessible_h__
+#define mozilla_a11y_XULListboxAccessible_h__
+
+#include "BaseAccessibles.h"
+#include "mozilla/a11y/TableAccessible.h"
+#include "XULMenuAccessible.h"
+#include "XULSelectControlAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * XULColumAccessible are accessible for list and tree columns elements
+ * (xul:treecols and xul:listheader).
+ */
+class XULColumAccessible : public AccessibleWrap {
+ public:
+ XULColumAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+};
+
+/**
+ * XULColumnItemAccessible are accessibles for list and tree column elements
+ * (xul:treecol).
+ */
+class XULColumnItemAccessible : public LeafAccessible {
+ public:
+ XULColumnItemAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+
+ enum { eAction_Click = 0 };
+};
+
+/*
+ * A class the represents the XUL Listbox widget.
+ */
+class XULListboxAccessible : public XULSelectControlAccessible,
+ public TableAccessible {
+ public:
+ XULListboxAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // TableAccessible
+ virtual uint32_t ColCount() const override;
+ virtual uint32_t RowCount() override;
+ virtual LocalAccessible* CellAt(uint32_t aRowIndex,
+ uint32_t aColumnIndex) override;
+ virtual bool IsColSelected(uint32_t aColIdx) override;
+ virtual bool IsRowSelected(uint32_t aRowIdx) override;
+ virtual bool IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) override;
+ virtual uint32_t SelectedCellCount() override;
+ virtual uint32_t SelectedColCount() override;
+ virtual uint32_t SelectedRowCount() override;
+ virtual void SelectedCells(nsTArray<Accessible*>* aCells) override;
+ virtual void SelectedCellIndices(nsTArray<uint32_t>* aCells) override;
+ virtual void SelectedColIndices(nsTArray<uint32_t>* aCols) override;
+ virtual void SelectedRowIndices(nsTArray<uint32_t>* aRows) override;
+ virtual LocalAccessible* AsAccessible() override { return this; }
+
+ // LocalAccessible
+ virtual TableAccessible* AsTable() override { return this; }
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+ virtual bool IsActiveWidget() const override;
+ virtual bool AreItemsOperable() const override;
+
+ virtual LocalAccessible* ContainerWidget() const override;
+
+ protected:
+ virtual ~XULListboxAccessible() {}
+
+ bool IsMulticolumn() const { return ColCount() > 1; }
+};
+
+/**
+ * Listitems -- used in listboxes
+ */
+class XULListitemAccessible : public XULMenuitemAccessible {
+ public:
+ enum { eAction_Click = 0 };
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(XULListitemAccessible,
+ XULMenuitemAccessible)
+
+ XULListitemAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual void Description(nsString& aDesc) const override;
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual uint64_t NativeInteractiveState() const override;
+
+ // Actions
+ virtual void ActionNameAt(uint8_t index, nsAString& aName) override;
+
+ // Widgets
+ virtual LocalAccessible* ContainerWidget() const override;
+
+ protected:
+ virtual ~XULListitemAccessible();
+
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+
+ // XULListitemAccessible
+
+ /**
+ * Return listbox accessible for the listitem.
+ */
+ LocalAccessible* GetListAccessible() const;
+
+ private:
+ bool mIsCheckbox;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xul/XULMenuAccessible.cpp b/accessible/xul/XULMenuAccessible.cpp
new file mode 100644
index 0000000000..6628304a9e
--- /dev/null
+++ b/accessible/xul/XULMenuAccessible.cpp
@@ -0,0 +1,477 @@
+/* -*- 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 "XULMenuAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "XULMenuBarElement.h"
+#include "XULMenuParentElement.h"
+#include "XULPopupElement.h"
+#include "mozilla/Assertions.h"
+#include "nsAccessibilityService.h"
+#include "nsAccUtils.h"
+#include "DocAccessible.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+#include "XULFormControlAccessible.h"
+
+#include "nsIContentInlines.h"
+#include "nsIDOMXULContainerElement.h"
+#include "nsIDOMXULSelectCntrlEl.h"
+#include "nsIDOMXULSelectCntrlItemEl.h"
+#include "nsIContent.h"
+#include "nsMenuPopupFrame.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/XULButtonElement.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// XULMenuitemAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULMenuitemAccessible::XULMenuitemAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {}
+
+uint64_t XULMenuitemAccessible::NativeState() const {
+ uint64_t state = LocalAccessible::NativeState();
+
+ // Has Popup?
+ if (mContent->NodeInfo()->Equals(nsGkAtoms::menu, kNameSpaceID_XUL)) {
+ state |= states::HASPOPUP;
+ if (mContent->AsElement()->HasAttr(nsGkAtoms::open)) {
+ state |= states::EXPANDED;
+ } else {
+ state |= states::COLLAPSED;
+ }
+ }
+
+ // Checkable/checked?
+ static dom::Element::AttrValuesArray strings[] = {
+ nsGkAtoms::radio, nsGkAtoms::checkbox, nullptr};
+
+ if (mContent->AsElement()->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type,
+ strings, eCaseMatters) >= 0) {
+ // Checkable?
+ state |= states::CHECKABLE;
+
+ // Checked?
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::checked, nsGkAtoms::_true,
+ eCaseMatters)) {
+ state |= states::CHECKED;
+ }
+ }
+
+ // Combo box listitem
+ bool isComboboxOption = (Role() == roles::COMBOBOX_OPTION);
+ if (isComboboxOption) {
+ // Is selected?
+ bool isSelected = false;
+ nsCOMPtr<nsIDOMXULSelectControlItemElement> item =
+ Elm()->AsXULSelectControlItem();
+ NS_ENSURE_TRUE(item, state);
+ item->GetSelected(&isSelected);
+
+ // Is collapsed?
+ bool isCollapsed = false;
+ LocalAccessible* parent = LocalParent();
+ if (parent && parent->State() & states::INVISIBLE) isCollapsed = true;
+
+ if (isSelected) {
+ state |= states::SELECTED;
+
+ // Selected and collapsed?
+ if (isCollapsed) {
+ // Set selected option offscreen/invisible according to combobox state
+ LocalAccessible* grandParent = parent->LocalParent();
+ if (!grandParent) return state;
+ NS_ASSERTION(grandParent->IsCombobox(),
+ "grandparent of combobox listitem is not combobox");
+ uint64_t grandParentState = grandParent->State();
+ state &= ~(states::OFFSCREEN | states::INVISIBLE);
+ state |= (grandParentState & states::OFFSCREEN) |
+ (grandParentState & states::INVISIBLE) |
+ (grandParentState & states::OPAQUE1);
+ } // isCollapsed
+ } // isSelected
+ } // ROLE_COMBOBOX_OPTION
+
+ return state;
+}
+
+uint64_t XULMenuitemAccessible::NativeInteractiveState() const {
+ if (NativelyUnavailable()) {
+ // Note: keep in sinc with nsXULPopupManager::IsValidMenuItem() logic.
+ auto* button = dom::XULButtonElement::FromNode(GetContent());
+ bool skipNavigatingDisabledMenuItem = true;
+ if (!button || !button->IsOnMenuBar()) {
+ skipNavigatingDisabledMenuItem = LookAndFeel::GetInt(
+ LookAndFeel::IntID::SkipNavigatingDisabledMenuItem);
+ }
+
+ if (skipNavigatingDisabledMenuItem) return states::UNAVAILABLE;
+
+ return states::UNAVAILABLE | states::FOCUSABLE | states::SELECTABLE;
+ }
+
+ return states::FOCUSABLE | states::SELECTABLE;
+}
+
+ENameValueFlag XULMenuitemAccessible::NativeName(nsString& aName) const {
+ mContent->AsElement()->GetAttr(nsGkAtoms::label, aName);
+ return eNameOK;
+}
+
+void XULMenuitemAccessible::Description(nsString& aDescription) const {
+ mContent->AsElement()->GetAttr(nsGkAtoms::description, aDescription);
+}
+
+KeyBinding XULMenuitemAccessible::AccessKey() const {
+ // Return menu accesskey: N or Alt+F.
+ static int32_t gMenuAccesskeyModifier =
+ -1; // magic value of -1 indicates unitialized state
+
+ // We do not use nsCoreUtils::GetAccesskeyFor() because accesskeys for
+ // menu are't registered by EventStateManager.
+ nsAutoString accesskey;
+ mContent->AsElement()->GetAttr(nsGkAtoms::accesskey, accesskey);
+ if (accesskey.IsEmpty()) return KeyBinding();
+
+ uint32_t modifierKey = 0;
+
+ LocalAccessible* parentAcc = LocalParent();
+ if (parentAcc) {
+ if (parentAcc->NativeRole() == roles::MENUBAR) {
+ // If top level menu item, add Alt+ or whatever modifier text to string
+ // No need to cache pref service, this happens rarely
+ if (gMenuAccesskeyModifier == -1) {
+ // Need to initialize cached global accesskey pref
+ gMenuAccesskeyModifier = Preferences::GetInt("ui.key.menuAccessKey", 0);
+ }
+
+ switch (gMenuAccesskeyModifier) {
+ case dom::KeyboardEvent_Binding::DOM_VK_CONTROL:
+ modifierKey = KeyBinding::kControl;
+ break;
+ case dom::KeyboardEvent_Binding::DOM_VK_ALT:
+ modifierKey = KeyBinding::kAlt;
+ break;
+ case dom::KeyboardEvent_Binding::DOM_VK_META:
+ case dom::KeyboardEvent_Binding::DOM_VK_WIN:
+ modifierKey = KeyBinding::kMeta;
+ break;
+ }
+ }
+ }
+
+ return KeyBinding(accesskey[0], modifierKey);
+}
+
+KeyBinding XULMenuitemAccessible::KeyboardShortcut() const {
+ nsAutoString keyElmId;
+ mContent->AsElement()->GetAttr(nsGkAtoms::key, keyElmId);
+ if (keyElmId.IsEmpty()) return KeyBinding();
+
+ dom::Element* keyElm = mContent->OwnerDoc()->GetElementById(keyElmId);
+ if (!keyElm) return KeyBinding();
+
+ uint32_t key = 0;
+
+ nsAutoString keyStr;
+ keyElm->GetAttr(nsGkAtoms::key, keyStr);
+ if (keyStr.IsEmpty()) {
+ nsAutoString keyCodeStr;
+ keyElm->GetAttr(nsGkAtoms::keycode, keyCodeStr);
+ nsresult errorCode;
+ key = keyStr.ToInteger(&errorCode, /* aRadix = */ 10);
+ if (NS_FAILED(errorCode)) {
+ key = keyStr.ToInteger(&errorCode, /* aRadix = */ 16);
+ }
+ } else {
+ key = keyStr[0];
+ }
+
+ nsAutoString modifiersStr;
+ keyElm->GetAttr(nsGkAtoms::modifiers, modifiersStr);
+
+ uint32_t modifierMask = 0;
+ if (modifiersStr.Find(u"shift") != -1) modifierMask |= KeyBinding::kShift;
+ if (modifiersStr.Find(u"alt") != -1) modifierMask |= KeyBinding::kAlt;
+ if (modifiersStr.Find(u"meta") != -1) modifierMask |= KeyBinding::kMeta;
+ if (modifiersStr.Find(u"control") != -1) modifierMask |= KeyBinding::kControl;
+ if (modifiersStr.Find(u"accel") != -1) {
+ modifierMask |= KeyBinding::AccelModifier();
+ }
+
+ return KeyBinding(key, modifierMask);
+}
+
+role XULMenuitemAccessible::NativeRole() const {
+ nsCOMPtr<nsIDOMXULContainerElement> xulContainer = Elm()->AsXULContainer();
+ if (xulContainer) return roles::PARENT_MENUITEM;
+
+ LocalAccessible* widget = ContainerWidget();
+ if (widget && widget->Role() == roles::COMBOBOX_LIST) {
+ return roles::COMBOBOX_OPTION;
+ }
+
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::radio, eCaseMatters)) {
+ return roles::RADIO_MENU_ITEM;
+ }
+
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::checkbox, eCaseMatters)) {
+ return roles::CHECK_MENU_ITEM;
+ }
+
+ return roles::MENUITEM;
+}
+
+int32_t XULMenuitemAccessible::GetLevel(bool aFast) const {
+ return nsAccUtils::GetLevelForXULContainerItem(mContent);
+}
+
+void XULMenuitemAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == eAction_Click) aName.AssignLiteral("click");
+}
+
+bool XULMenuitemAccessible::HasPrimaryAction() const { return true; }
+
+////////////////////////////////////////////////////////////////////////////////
+// XULMenuitemAccessible: Widgets
+
+bool XULMenuitemAccessible::IsActiveWidget() const {
+ // Parent menu item is a widget, it's active when its popup is open.
+ // Typically the <menupopup> is included in the document markup, and
+ // <menu> prepends content in front of it.
+ nsIContent* menuPopupContent = mContent->GetLastChild();
+ if (menuPopupContent) {
+ nsMenuPopupFrame* menuPopupFrame =
+ do_QueryFrame(menuPopupContent->GetPrimaryFrame());
+ return menuPopupFrame && menuPopupFrame->IsOpen();
+ }
+ return false;
+}
+
+bool XULMenuitemAccessible::AreItemsOperable() const {
+ // Parent menu item is a widget, its items are operable when its popup is
+ // open.
+ nsIContent* menuPopupContent = mContent->GetLastChild();
+ if (menuPopupContent) {
+ nsMenuPopupFrame* menuPopupFrame =
+ do_QueryFrame(menuPopupContent->GetPrimaryFrame());
+ return menuPopupFrame && menuPopupFrame->IsOpen();
+ }
+ return false;
+}
+
+LocalAccessible* XULMenuitemAccessible::ContainerWidget() const {
+ if (auto* button = dom::XULButtonElement::FromNode(GetContent())) {
+ if (auto* popup = button->GetMenuParent()) {
+ // We use GetAccessibleOrContainer instead of just GetAccessible because
+ // we strip menupopups from the tree for ATK.
+ return mDoc->GetAccessibleOrContainer(popup);
+ }
+ }
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULMenuSeparatorAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULMenuSeparatorAccessible::XULMenuSeparatorAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : XULMenuitemAccessible(aContent, aDoc) {}
+
+uint64_t XULMenuSeparatorAccessible::NativeState() const {
+ // Isn't focusable, but can be offscreen/invisible -- only copy those states
+ return XULMenuitemAccessible::NativeState() &
+ (states::OFFSCREEN | states::INVISIBLE);
+}
+
+ENameValueFlag XULMenuSeparatorAccessible::NativeName(nsString& aName) const {
+ return eNameOK;
+}
+
+role XULMenuSeparatorAccessible::NativeRole() const { return roles::SEPARATOR; }
+
+bool XULMenuSeparatorAccessible::HasPrimaryAction() const { return false; }
+
+////////////////////////////////////////////////////////////////////////////////
+// XULMenupopupAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULMenupopupAccessible::XULMenupopupAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : XULSelectControlAccessible(aContent, aDoc) {
+ if (nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame())) {
+ if (menuPopupFrame->GetPopupType() == widget::PopupType::Menu) {
+ mType = eMenuPopupType;
+ }
+ }
+
+ // May be the anonymous <menupopup> inside <menulist> (a combobox)
+ auto* parent = mContent->GetParentElement();
+ nsCOMPtr<nsIDOMXULSelectControlElement> selectControl =
+ parent ? parent->AsXULSelectControl() : nullptr;
+ if (selectControl) {
+ mSelectControl = parent;
+ } else {
+ mSelectControl = nullptr;
+ mGenericTypes &= ~eSelect;
+ }
+}
+
+uint64_t XULMenupopupAccessible::NativeState() const {
+ uint64_t state = LocalAccessible::NativeState();
+
+#ifdef DEBUG
+ // We are onscreen if our parent is active
+ bool isActive = mContent->AsElement()->HasAttr(nsGkAtoms::menuactive);
+ if (!isActive) {
+ LocalAccessible* parent = LocalParent();
+ if (parent) {
+ nsIContent* parentContent = parent->GetContent();
+ if (parentContent && parentContent->IsElement())
+ isActive = parentContent->AsElement()->HasAttr(nsGkAtoms::open);
+ }
+ }
+
+ NS_ASSERTION(isActive || (state & states::INVISIBLE),
+ "XULMenupopup doesn't have INVISIBLE when it's inactive");
+#endif
+
+ if (state & states::INVISIBLE) state |= states::OFFSCREEN | states::COLLAPSED;
+
+ return state;
+}
+
+ENameValueFlag XULMenupopupAccessible::NativeName(nsString& aName) const {
+ nsIContent* content = mContent;
+ while (content && aName.IsEmpty()) {
+ if (content->IsElement()) {
+ content->AsElement()->GetAttr(nsGkAtoms::label, aName);
+ }
+ content = content->GetFlattenedTreeParent();
+ }
+
+ return eNameOK;
+}
+
+role XULMenupopupAccessible::NativeRole() const {
+ nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame());
+ if (menuPopupFrame && menuPopupFrame->IsContextMenu()) {
+ return roles::MENUPOPUP;
+ }
+
+ if (mParent) {
+ if (mParent->IsCombobox()) {
+ return roles::COMBOBOX_LIST;
+ }
+ }
+
+ // If accessible is not bound to the tree (this happens while children are
+ // cached) return general role.
+ return roles::MENUPOPUP;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULMenupopupAccessible: Widgets
+
+bool XULMenupopupAccessible::IsWidget() const { return true; }
+
+bool XULMenupopupAccessible::IsActiveWidget() const {
+ // If menupopup is a widget (the case of context menus) then active when open.
+ nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame());
+ return menuPopupFrame && menuPopupFrame->IsOpen();
+}
+
+bool XULMenupopupAccessible::AreItemsOperable() const {
+ nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame());
+ return menuPopupFrame && menuPopupFrame->IsOpen();
+}
+
+LocalAccessible* XULMenupopupAccessible::ContainerWidget() const {
+ DocAccessible* document = Document();
+
+ nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(GetFrame());
+ MOZ_ASSERT(menuPopupFrame);
+ if (!menuPopupFrame) {
+ return nullptr;
+ }
+
+ auto* cur = dom::XULPopupElement::FromNode(GetContent());
+ while (cur) {
+ auto* menu = cur->GetContainingMenu();
+ if (!menu) {
+ // <panel> / <tooltip> / etc.
+ return nullptr;
+ }
+ dom::XULMenuParentElement* parent = menu->GetMenuParent();
+ if (!parent) {
+ LocalAccessible* menuPopup = document->GetAccessible(cur);
+ MOZ_ASSERT(menuPopup);
+ return menuPopup ? menuPopup->LocalParent() : nullptr;
+ }
+
+ if (parent->IsMenuBar()) {
+ return document->GetAccessible(parent);
+ }
+
+ cur = dom::XULPopupElement::FromNode(parent);
+ MOZ_ASSERT(cur, "Should be a popup");
+ }
+
+ MOZ_ASSERT_UNREACHABLE("How did we get out of the loop without returning?");
+ return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULMenubarAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULMenubarAccessible::XULMenubarAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {}
+
+ENameValueFlag XULMenubarAccessible::NativeName(nsString& aName) const {
+ aName.AssignLiteral("Application");
+ return eNameOK;
+}
+
+role XULMenubarAccessible::NativeRole() const { return roles::MENUBAR; }
+
+////////////////////////////////////////////////////////////////////////////////
+// XULMenubarAccessible: Widgets
+
+bool XULMenubarAccessible::IsActiveWidget() const {
+ auto* menuBar = dom::XULMenuBarElement::FromNode(GetContent());
+ return menuBar && menuBar->IsActive();
+}
+
+bool XULMenubarAccessible::AreItemsOperable() const { return true; }
+
+LocalAccessible* XULMenubarAccessible::CurrentItem() const {
+ auto* content = dom::XULMenuParentElement::FromNode(GetContent());
+ MOZ_ASSERT(content);
+ if (!content || !content->GetActiveMenuChild()) {
+ return nullptr;
+ }
+ return mDoc->GetAccessible(content->GetActiveMenuChild());
+}
+
+void XULMenubarAccessible::SetCurrentItem(const LocalAccessible* aItem) {
+ NS_ERROR("XULMenubarAccessible::SetCurrentItem not implemented");
+}
diff --git a/accessible/xul/XULMenuAccessible.h b/accessible/xul/XULMenuAccessible.h
new file mode 100644
index 0000000000..43d165d798
--- /dev/null
+++ b/accessible/xul/XULMenuAccessible.h
@@ -0,0 +1,113 @@
+/* -*- 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_XULMenuAccessible_h__
+#define mozilla_a11y_XULMenuAccessible_h__
+
+#include "AccessibleWrap.h"
+#include "XULSelectControlAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * Used for XUL menu, menuitem elements.
+ */
+class XULMenuitemAccessible : public AccessibleWrap {
+ public:
+ enum { eAction_Click = 0 };
+
+ XULMenuitemAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual void Description(nsString& aDescription) const override;
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual uint64_t NativeInteractiveState() const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual KeyBinding AccessKey() const override;
+ virtual KeyBinding KeyboardShortcut() const override;
+
+ // Widgets
+ virtual bool IsActiveWidget() const override;
+ virtual bool AreItemsOperable() const override;
+ virtual LocalAccessible* ContainerWidget() const override;
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+ virtual int32_t GetLevel(bool aFast) const override;
+};
+
+/**
+ * Used for XUL menuseparator element.
+ */
+class XULMenuSeparatorAccessible : public XULMenuitemAccessible {
+ public:
+ XULMenuSeparatorAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+};
+
+/**
+ * Used for XUL menupopup and panel.
+ */
+class XULMenupopupAccessible : public XULSelectControlAccessible {
+ public:
+ XULMenupopupAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+ virtual bool IsActiveWidget() const override;
+ virtual bool AreItemsOperable() const override;
+
+ virtual LocalAccessible* ContainerWidget() const override;
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+};
+
+/**
+ * Used for XUL menubar element.
+ */
+class XULMenubarAccessible : public AccessibleWrap {
+ public:
+ XULMenubarAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+
+ // Widget
+ virtual bool IsActiveWidget() const override;
+ virtual bool AreItemsOperable() const override;
+ virtual LocalAccessible* CurrentItem() const override;
+ virtual void SetCurrentItem(const LocalAccessible* aItem) override;
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xul/XULSelectControlAccessible.cpp b/accessible/xul/XULSelectControlAccessible.cpp
new file mode 100644
index 0000000000..006d1daf89
--- /dev/null
+++ b/accessible/xul/XULSelectControlAccessible.cpp
@@ -0,0 +1,253 @@
+/* -*- 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 "XULSelectControlAccessible.h"
+
+#include "nsAccessibilityService.h"
+#include "DocAccessible.h"
+
+#include "nsIDOMXULSelectCntrlItemEl.h"
+#include "nsIDOMXULMultSelectCntrlEl.h"
+
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+
+using namespace mozilla;
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// XULSelectControlAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULSelectControlAccessible::XULSelectControlAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {
+ mGenericTypes |= eSelect;
+ mSelectControl = aContent->AsElement();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULSelectControlAccessible: LocalAccessible
+
+void XULSelectControlAccessible::Shutdown() {
+ mSelectControl = nullptr;
+ AccessibleWrap::Shutdown();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULSelectControlAccessible: SelectAccessible
+
+void XULSelectControlAccessible::SelectedItems(nsTArray<Accessible*>* aItems) {
+ // For XUL multi-select control
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> xulMultiSelect =
+ mSelectControl->AsXULMultiSelectControl();
+ if (xulMultiSelect) {
+ int32_t length = 0;
+ xulMultiSelect->GetSelectedCount(&length);
+ for (int32_t index = 0; index < length; index++) {
+ RefPtr<dom::Element> element;
+ xulMultiSelect->MultiGetSelectedItem(index, getter_AddRefs(element));
+ LocalAccessible* item = mDoc->GetAccessible(element);
+ if (item) aItems->AppendElement(item);
+ }
+ } else { // Single select?
+ nsCOMPtr<nsIDOMXULSelectControlElement> selectControl =
+ mSelectControl->AsXULSelectControl();
+ RefPtr<dom::Element> element;
+ selectControl->GetSelectedItem(getter_AddRefs(element));
+ if (element) {
+ LocalAccessible* item = mDoc->GetAccessible(element);
+ if (item) aItems->AppendElement(item);
+ }
+ }
+}
+
+Accessible* XULSelectControlAccessible::GetSelectedItem(uint32_t aIndex) {
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> multiSelectControl =
+ mSelectControl->AsXULMultiSelectControl();
+
+ RefPtr<dom::Element> element;
+ if (multiSelectControl) {
+ multiSelectControl->MultiGetSelectedItem(aIndex, getter_AddRefs(element));
+ } else if (aIndex == 0) {
+ nsCOMPtr<nsIDOMXULSelectControlElement> selectControl =
+ mSelectControl->AsXULSelectControl();
+ if (selectControl) {
+ selectControl->GetSelectedItem(getter_AddRefs(element));
+ }
+ }
+
+ return element && mDoc ? mDoc->GetAccessible(element) : nullptr;
+}
+
+uint32_t XULSelectControlAccessible::SelectedItemCount() {
+ // For XUL multi-select control
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> multiSelectControl =
+ mSelectControl->AsXULMultiSelectControl();
+ if (multiSelectControl) {
+ int32_t count = 0;
+ multiSelectControl->GetSelectedCount(&count);
+ return count;
+ }
+
+ // For XUL single-select control/menulist
+ nsCOMPtr<nsIDOMXULSelectControlElement> selectControl =
+ mSelectControl->AsXULSelectControl();
+ if (selectControl) {
+ int32_t index = -1;
+ selectControl->GetSelectedIndex(&index);
+ return (index >= 0) ? 1 : 0;
+ }
+
+ return 0;
+}
+
+bool XULSelectControlAccessible::AddItemToSelection(uint32_t aIndex) {
+ LocalAccessible* item = LocalChildAt(aIndex);
+ if (!item || !item->GetContent()) return false;
+
+ nsCOMPtr<nsIDOMXULSelectControlItemElement> itemElm =
+ item->GetContent()->AsElement()->AsXULSelectControlItem();
+ if (!itemElm) return false;
+
+ bool isItemSelected = false;
+ itemElm->GetSelected(&isItemSelected);
+ if (isItemSelected) return true;
+
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> multiSelectControl =
+ mSelectControl->AsXULMultiSelectControl();
+
+ if (multiSelectControl) {
+ multiSelectControl->AddItemToSelection(itemElm);
+ } else {
+ nsCOMPtr<nsIDOMXULSelectControlElement> selectControl =
+ mSelectControl->AsXULSelectControl();
+ if (selectControl) {
+ selectControl->SetSelectedItem(item->Elm());
+ }
+ }
+
+ return true;
+}
+
+bool XULSelectControlAccessible::RemoveItemFromSelection(uint32_t aIndex) {
+ LocalAccessible* item = LocalChildAt(aIndex);
+ if (!item || !item->GetContent()) return false;
+
+ nsCOMPtr<nsIDOMXULSelectControlItemElement> itemElm =
+ item->GetContent()->AsElement()->AsXULSelectControlItem();
+ if (!itemElm) return false;
+
+ bool isItemSelected = false;
+ itemElm->GetSelected(&isItemSelected);
+ if (!isItemSelected) return true;
+
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> multiSelectControl =
+ mSelectControl->AsXULMultiSelectControl();
+
+ if (multiSelectControl) {
+ multiSelectControl->RemoveItemFromSelection(itemElm);
+ } else {
+ nsCOMPtr<nsIDOMXULSelectControlElement> selectControl =
+ mSelectControl->AsXULSelectControl();
+ if (selectControl) {
+ selectControl->SetSelectedItem(nullptr);
+ }
+ }
+
+ return true;
+}
+
+bool XULSelectControlAccessible::IsItemSelected(uint32_t aIndex) {
+ LocalAccessible* item = LocalChildAt(aIndex);
+ if (!item || !item->GetContent()) return false;
+
+ nsCOMPtr<nsIDOMXULSelectControlItemElement> itemElm =
+ item->GetContent()->AsElement()->AsXULSelectControlItem();
+ if (!itemElm) return false;
+
+ bool isItemSelected = false;
+ itemElm->GetSelected(&isItemSelected);
+ return isItemSelected;
+}
+
+bool XULSelectControlAccessible::UnselectAll() {
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> multiSelectControl =
+ mSelectControl->AsXULMultiSelectControl();
+ if (multiSelectControl) {
+ multiSelectControl->ClearSelection();
+ } else {
+ nsCOMPtr<nsIDOMXULSelectControlElement> selectControl =
+ mSelectControl->AsXULSelectControl();
+ if (selectControl) {
+ selectControl->SetSelectedIndex(-1);
+ }
+ }
+
+ return true;
+}
+
+bool XULSelectControlAccessible::SelectAll() {
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> multiSelectControl =
+ mSelectControl->AsXULMultiSelectControl();
+ if (multiSelectControl) {
+ multiSelectControl->SelectAll();
+ return true;
+ }
+
+ // otherwise, don't support this method
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULSelectControlAccessible: Widgets
+
+LocalAccessible* XULSelectControlAccessible::CurrentItem() const {
+ // aria-activedescendant should override.
+ LocalAccessible* current = AccessibleWrap::CurrentItem();
+ if (current) {
+ return current;
+ }
+
+ if (!mSelectControl) return nullptr;
+
+ RefPtr<dom::Element> currentItemElm;
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> multiSelectControl =
+ mSelectControl->AsXULMultiSelectControl();
+ if (multiSelectControl) {
+ multiSelectControl->GetCurrentItem(getter_AddRefs(currentItemElm));
+ } else {
+ nsCOMPtr<nsIDOMXULSelectControlElement> selectControl =
+ mSelectControl->AsXULSelectControl();
+ if (selectControl) {
+ selectControl->GetSelectedItem(getter_AddRefs(currentItemElm));
+ }
+ }
+
+ if (currentItemElm) {
+ DocAccessible* document = Document();
+ if (document) return document->GetAccessible(currentItemElm);
+ }
+
+ return nullptr;
+}
+
+void XULSelectControlAccessible::SetCurrentItem(const LocalAccessible* aItem) {
+ if (!mSelectControl) return;
+
+ nsCOMPtr<dom::Element> itemElm = aItem->Elm();
+ nsCOMPtr<nsIDOMXULMultiSelectControlElement> multiSelectControl =
+ itemElm->AsXULMultiSelectControl();
+ if (multiSelectControl) {
+ multiSelectControl->SetCurrentItem(itemElm);
+ } else {
+ nsCOMPtr<nsIDOMXULSelectControlElement> selectControl =
+ mSelectControl->AsXULSelectControl();
+ if (selectControl) {
+ selectControl->SetSelectedItem(itemElm);
+ }
+ }
+}
diff --git a/accessible/xul/XULSelectControlAccessible.h b/accessible/xul/XULSelectControlAccessible.h
new file mode 100644
index 0000000000..ae201c74b0
--- /dev/null
+++ b/accessible/xul/XULSelectControlAccessible.h
@@ -0,0 +1,47 @@
+/* -*- 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_XULSelectControlAccessible_h__
+#define mozilla_a11y_XULSelectControlAccessible_h__
+
+#include "AccessibleWrap.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * The basic implementation of accessible selection for XUL select controls.
+ */
+class XULSelectControlAccessible : public AccessibleWrap {
+ public:
+ XULSelectControlAccessible(nsIContent* aContent, DocAccessible* aDoc);
+ virtual ~XULSelectControlAccessible() {}
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+
+ // SelectAccessible
+ virtual void SelectedItems(nsTArray<Accessible*>* aItems) override;
+ virtual uint32_t SelectedItemCount() override;
+ virtual Accessible* GetSelectedItem(uint32_t aIndex) override;
+ virtual bool IsItemSelected(uint32_t aIndex) override;
+ virtual bool AddItemToSelection(uint32_t aIndex) override;
+ virtual bool RemoveItemFromSelection(uint32_t aIndex) override;
+ virtual bool SelectAll() override;
+ virtual bool UnselectAll() override;
+
+ // Widgets
+ virtual LocalAccessible* CurrentItem() const override;
+ virtual void SetCurrentItem(const LocalAccessible* aItem) override;
+
+ protected:
+ RefPtr<dom::Element> mSelectControl;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xul/XULTabAccessible.cpp b/accessible/xul/XULTabAccessible.cpp
new file mode 100644
index 0000000000..1e2281487a
--- /dev/null
+++ b/accessible/xul/XULTabAccessible.cpp
@@ -0,0 +1,217 @@
+/* -*- 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 "XULTabAccessible.h"
+
+#include "ARIAMap.h"
+#include "nsAccUtils.h"
+#include "Relation.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+
+// NOTE: alphabetically ordered
+#include "mozilla/dom/Document.h"
+#include "nsIDOMXULSelectCntrlItemEl.h"
+#include "nsIDOMXULRelatedElement.h"
+#include "nsXULElement.h"
+
+#include "mozilla/dom/BindingDeclarations.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTabAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULTabAccessible::XULTabAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : HyperTextAccessible(aContent, aDoc) {}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTabAccessible: LocalAccessible
+
+bool XULTabAccessible::HasPrimaryAction() const { return true; }
+
+void XULTabAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == eAction_Switch) aName.AssignLiteral("switch");
+}
+
+bool XULTabAccessible::DoAction(uint8_t index) const {
+ if (index == eAction_Switch) {
+ // XXXbz Could this just FromContent?
+ RefPtr<nsXULElement> tab = nsXULElement::FromNodeOrNull(mContent);
+ if (tab) {
+ tab->Click(mozilla::dom::CallerType::System);
+ return true;
+ }
+ }
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTabAccessible: LocalAccessible
+
+role XULTabAccessible::NativeRole() const { return roles::PAGETAB; }
+
+uint64_t XULTabAccessible::NativeState() const {
+ // Possible states: focused, focusable, unavailable(disabled), offscreen.
+
+ // get focus and disable status from base class
+ uint64_t state = AccessibleWrap::NativeState();
+
+ // Check whether the tab is selected and/or pinned
+ nsCOMPtr<nsIDOMXULSelectControlItemElement> tab =
+ Elm()->AsXULSelectControlItem();
+ if (tab) {
+ bool selected = false;
+ if (NS_SUCCEEDED(tab->GetSelected(&selected)) && selected) {
+ state |= states::SELECTED;
+ }
+
+ if (mContent->AsElement()->HasAttr(kNameSpaceID_None, nsGkAtoms::pinned)) {
+ state |= states::PINNED;
+ }
+ }
+
+ return state;
+}
+
+uint64_t XULTabAccessible::NativeInteractiveState() const {
+ uint64_t state = LocalAccessible::NativeInteractiveState();
+ return (state & states::UNAVAILABLE) ? state : state | states::SELECTABLE;
+}
+
+Relation XULTabAccessible::RelationByType(RelationType aType) const {
+ Relation rel = AccessibleWrap::RelationByType(aType);
+ if (aType != RelationType::LABEL_FOR) return rel;
+
+ // Expose 'LABEL_FOR' relation on tab accessible for tabpanel accessible.
+ ErrorResult rv;
+ nsIContent* parent = mContent->AsElement()->Closest("tabs"_ns, rv);
+ if (!parent) return rel;
+
+ nsCOMPtr<nsIDOMXULRelatedElement> tabsElm =
+ parent->AsElement()->AsXULRelated();
+ if (!tabsElm) return rel;
+
+ RefPtr<mozilla::dom::Element> tabpanelElement;
+ tabsElm->GetRelatedElement(GetNode(), getter_AddRefs(tabpanelElement));
+ if (!tabpanelElement) return rel;
+
+ rel.AppendTarget(mDoc, tabpanelElement);
+ return rel;
+}
+
+void XULTabAccessible::ApplyARIAState(uint64_t* aState) const {
+ HyperTextAccessible::ApplyARIAState(aState);
+ // XUL tab has an implicit ARIA role of tab, so support aria-selected.
+ // Don't use aria::MapToState because that will set the SELECTABLE state
+ // even if the tab is disabled.
+ if (nsAccUtils::IsARIASelected(this)) {
+ *aState |= states::SELECTED;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTabsAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULTabsAccessible::XULTabsAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : XULSelectControlAccessible(aContent, aDoc) {}
+
+role XULTabsAccessible::NativeRole() const { return roles::PAGETABLIST; }
+
+bool XULTabsAccessible::HasPrimaryAction() const { return false; }
+
+void XULTabsAccessible::Value(nsString& aValue) const { aValue.Truncate(); }
+
+ENameValueFlag XULTabsAccessible::NativeName(nsString& aName) const {
+ // no name
+ return eNameOK;
+}
+
+void XULTabsAccessible::ApplyARIAState(uint64_t* aState) const {
+ XULSelectControlAccessible::ApplyARIAState(aState);
+ // XUL tabs has an implicit ARIA role of tablist, so support
+ // aria-multiselectable.
+ MOZ_ASSERT(Elm());
+ aria::MapToState(aria::eARIAMultiSelectable, Elm(), aState);
+}
+
+// XUL tabs is a single selection control and doesn't allow ARIA selection.
+// However, if aria-multiselectable is used, it becomes a multiselectable
+// control, where both native and ARIA markup are used to set selection.
+// Therefore, if aria-multiselectable is set, use the base implementation of
+// the selection retrieval methods in order to support ARIA selection.
+// We don't bother overriding the selection setting methods because
+// current front-end code using XUL tabs doesn't support setting of
+// aria-selected by the a11y engine and we still want to be able to set the
+// primary selected item according to XUL.
+
+void XULTabsAccessible::SelectedItems(nsTArray<Accessible*>* aItems) {
+ if (nsAccUtils::IsARIAMultiSelectable(this)) {
+ AccessibleWrap::SelectedItems(aItems);
+ } else {
+ XULSelectControlAccessible::SelectedItems(aItems);
+ }
+}
+
+Accessible* XULTabsAccessible::GetSelectedItem(uint32_t aIndex) {
+ if (nsAccUtils::IsARIAMultiSelectable(this)) {
+ return AccessibleWrap::GetSelectedItem(aIndex);
+ }
+
+ return XULSelectControlAccessible::GetSelectedItem(aIndex);
+}
+
+uint32_t XULTabsAccessible::SelectedItemCount() {
+ if (nsAccUtils::IsARIAMultiSelectable(this)) {
+ return AccessibleWrap::SelectedItemCount();
+ }
+
+ return XULSelectControlAccessible::SelectedItemCount();
+}
+
+bool XULTabsAccessible::IsItemSelected(uint32_t aIndex) {
+ if (nsAccUtils::IsARIAMultiSelectable(this)) {
+ return AccessibleWrap::IsItemSelected(aIndex);
+ }
+
+ return XULSelectControlAccessible::IsItemSelected(aIndex);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTabpanelsAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+role XULTabpanelsAccessible::NativeRole() const { return roles::PANE; }
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTabpanelAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULTabpanelAccessible::XULTabpanelAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {}
+
+role XULTabpanelAccessible::NativeRole() const { return roles::PROPERTYPAGE; }
+
+Relation XULTabpanelAccessible::RelationByType(RelationType aType) const {
+ Relation rel = AccessibleWrap::RelationByType(aType);
+ if (aType != RelationType::LABELLED_BY) return rel;
+
+ // Expose 'LABELLED_BY' relation on tabpanel accessible for tab accessible.
+ if (!mContent->GetParent()) return rel;
+
+ nsCOMPtr<nsIDOMXULRelatedElement> tabpanelsElm =
+ mContent->GetParent()->AsElement()->AsXULRelated();
+ if (!tabpanelsElm) return rel;
+
+ RefPtr<mozilla::dom::Element> tabElement;
+ tabpanelsElm->GetRelatedElement(GetNode(), getter_AddRefs(tabElement));
+ if (!tabElement) return rel;
+
+ rel.AppendTarget(mDoc, tabElement);
+ return rel;
+}
diff --git a/accessible/xul/XULTabAccessible.h b/accessible/xul/XULTabAccessible.h
new file mode 100644
index 0000000000..66ffc2613f
--- /dev/null
+++ b/accessible/xul/XULTabAccessible.h
@@ -0,0 +1,98 @@
+/* -*- 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_XULTabAccessible_h__
+#define mozilla_a11y_XULTabAccessible_h__
+
+// NOTE: alphabetically ordered
+#include "HyperTextAccessible.h"
+#include "XULMenuAccessible.h"
+#include "XULSelectControlAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * An individual tab, xul:tab element.
+ */
+class XULTabAccessible : public HyperTextAccessible {
+ public:
+ enum { eAction_Switch = 0 };
+
+ XULTabAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual uint64_t NativeInteractiveState() const override;
+ virtual Relation RelationByType(RelationType aType) const override;
+ virtual void ApplyARIAState(uint64_t* aState) const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual bool DoAction(uint8_t aIndex) const override;
+};
+
+/**
+ * A container of tab objects, xul:tabs element.
+ */
+class XULTabsAccessible : public XULSelectControlAccessible {
+ public:
+ XULTabsAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual void Value(nsString& aValue) const override;
+ virtual a11y::role NativeRole() const override;
+ virtual void ApplyARIAState(uint64_t* aState) const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+
+ // SelectAccessible
+ virtual void SelectedItems(nsTArray<Accessible*>* aItems) override;
+ virtual uint32_t SelectedItemCount() override;
+ virtual Accessible* GetSelectedItem(uint32_t aIndex) override;
+ virtual bool IsItemSelected(uint32_t aIndex) override;
+
+ protected:
+ // LocalAccessible
+ virtual ENameValueFlag NativeName(nsString& aName) const override;
+};
+
+/**
+ * A container of tab panels, xul:tabpanels element.
+ */
+class XULTabpanelsAccessible : public AccessibleWrap {
+ public:
+ XULTabpanelsAccessible(nsIContent* aContent, DocAccessible* aDoc)
+ : AccessibleWrap(aContent, aDoc) {
+ mType = eXULTabpanelsType;
+ }
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+};
+
+/**
+ * A tabpanel object, child elements of xul:tabpanels element.
+ *
+ * XXX: we need to move the class logic into generic class since
+ * for example we do not create instance of this class for XUL textbox used as
+ * a tabpanel.
+ */
+class XULTabpanelAccessible : public AccessibleWrap {
+ public:
+ XULTabpanelAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ // LocalAccessible
+ virtual a11y::role NativeRole() const override;
+ virtual Relation RelationByType(RelationType aType) const override;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xul/XULTreeAccessible.cpp b/accessible/xul/XULTreeAccessible.cpp
new file mode 100644
index 0000000000..818e1f8e3e
--- /dev/null
+++ b/accessible/xul/XULTreeAccessible.cpp
@@ -0,0 +1,995 @@
+/* -*- 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 "XULTreeAccessible.h"
+
+#include "LocalAccessible-inl.h"
+#include "DocAccessible-inl.h"
+#include "nsAccCache.h"
+#include "nsAccUtils.h"
+#include "nsCoreUtils.h"
+#include "nsEventShell.h"
+#include "DocAccessible.h"
+#include "Relation.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+#include "XULTreeGridAccessible.h"
+#include "nsQueryObject.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsIAutoCompletePopup.h"
+#include "nsIDOMXULMenuListElement.h"
+#include "nsITreeSelection.h"
+#include "nsTreeBodyFrame.h"
+#include "nsTreeColumns.h"
+#include "nsTreeUtils.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/XULTreeElementBinding.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULTreeAccessible::XULTreeAccessible(nsIContent* aContent, DocAccessible* aDoc,
+ nsTreeBodyFrame* aTreeFrame)
+ : AccessibleWrap(aContent, aDoc),
+ mAccessibleCache(kDefaultTreeCacheLength) {
+ mType = eXULTreeType;
+ mGenericTypes |= eSelect;
+
+ nsCOMPtr<nsITreeView> view = aTreeFrame->GetExistingView();
+ mTreeView = view;
+
+ mTree = nsCoreUtils::GetTree(aContent);
+ NS_ASSERTION(mTree, "Can't get mTree!\n");
+
+ nsIContent* parentContent = mContent->GetParent();
+ if (parentContent) {
+ nsCOMPtr<nsIAutoCompletePopup> autoCompletePopupElm =
+ do_QueryInterface(parentContent);
+ if (autoCompletePopupElm) mGenericTypes |= eAutoCompletePopup;
+ }
+}
+
+XULTreeAccessible::~XULTreeAccessible() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeAccessible: nsISupports and cycle collection implementation
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(XULTreeAccessible, LocalAccessible, mTree,
+ mAccessibleCache)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(XULTreeAccessible)
+NS_INTERFACE_MAP_END_INHERITING(LocalAccessible)
+
+NS_IMPL_ADDREF_INHERITED(XULTreeAccessible, LocalAccessible)
+NS_IMPL_RELEASE_INHERITED(XULTreeAccessible, LocalAccessible)
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeAccessible: LocalAccessible implementation
+
+uint64_t XULTreeAccessible::NativeState() const {
+ // Get focus status from base class.
+ uint64_t state = LocalAccessible::NativeState();
+
+ // readonly state
+ state |= states::READONLY;
+
+ // multiselectable state.
+ if (!mTreeView) return state;
+
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ NS_ENSURE_TRUE(selection, state);
+
+ bool isSingle = false;
+ nsresult rv = selection->GetSingle(&isSingle);
+ NS_ENSURE_SUCCESS(rv, state);
+
+ if (!isSingle) state |= states::MULTISELECTABLE;
+
+ return state;
+}
+
+void XULTreeAccessible::Value(nsString& aValue) const {
+ aValue.Truncate();
+ if (!mTreeView) return;
+
+ // Return the value is the first selected child.
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (!selection) return;
+
+ int32_t currentIndex;
+ selection->GetCurrentIndex(&currentIndex);
+ if (currentIndex >= 0) {
+ RefPtr<nsTreeColumn> keyCol;
+
+ RefPtr<nsTreeColumns> cols = mTree->GetColumns();
+ if (cols) keyCol = cols->GetKeyColumn();
+
+ mTreeView->GetCellText(currentIndex, keyCol, aValue);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeAccessible: LocalAccessible implementation
+
+void XULTreeAccessible::Shutdown() {
+ if (mDoc && !mDoc->IsDefunct()) {
+ UnbindCacheEntriesFromDocument(mAccessibleCache);
+ }
+
+ mTree = nullptr;
+ mTreeView = nullptr;
+
+ AccessibleWrap::Shutdown();
+}
+
+role XULTreeAccessible::NativeRole() const {
+ // No primary column means we're in a list. In fact, history and mail turn off
+ // the primary flag when switching to a flat view.
+
+ nsIContent* child =
+ nsTreeUtils::GetDescendantChild(mContent, nsGkAtoms::treechildren);
+ NS_ASSERTION(child, "tree without treechildren!");
+ nsTreeBodyFrame* treeFrame = do_QueryFrame(child->GetPrimaryFrame());
+ NS_ASSERTION(treeFrame, "xul tree accessible for tree without a frame!");
+ if (!treeFrame) return roles::LIST;
+
+ RefPtr<nsTreeColumns> cols = treeFrame->Columns();
+ nsTreeColumn* primaryCol = cols->GetPrimaryColumn();
+
+ return primaryCol ? roles::OUTLINE : roles::LIST;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeAccessible: LocalAccessible implementation (DON'T put methods here)
+
+LocalAccessible* XULTreeAccessible::LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) {
+ nsIFrame* frame = GetFrame();
+ if (!frame) return nullptr;
+
+ nsPresContext* presContext = frame->PresContext();
+ PresShell* presShell = presContext->PresShell();
+
+ nsIFrame* rootFrame = presShell->GetRootFrame();
+ NS_ENSURE_TRUE(rootFrame, nullptr);
+
+ CSSIntRect rootRect = rootFrame->GetScreenRect();
+
+ int32_t clientX = presContext->DevPixelsToIntCSSPixels(aX) - rootRect.X();
+ int32_t clientY = presContext->DevPixelsToIntCSSPixels(aY) - rootRect.Y();
+
+ ErrorResult rv;
+ dom::TreeCellInfo cellInfo;
+ mTree->GetCellAt(clientX, clientY, cellInfo, rv);
+
+ // If we failed to find tree cell for the given point then it might be
+ // tree columns.
+ if (cellInfo.mRow == -1 || !cellInfo.mCol) {
+ return AccessibleWrap::LocalChildAtPoint(aX, aY, aWhichChild);
+ }
+
+ XULTreeItemAccessibleBase* child = GetTreeItemAccessible(cellInfo.mRow);
+ if (aWhichChild == EWhichChildAtPoint::DeepestChild && child) {
+ LocalAccessible* cell = child->GetCellAccessible(cellInfo.mCol);
+ if (cell) {
+ return cell;
+ }
+ }
+
+ return child;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeAccessible: SelectAccessible
+
+LocalAccessible* XULTreeAccessible::CurrentItem() const {
+ if (!mTreeView) return nullptr;
+
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (selection) {
+ int32_t currentIndex = -1;
+ selection->GetCurrentIndex(&currentIndex);
+ if (currentIndex >= 0) return GetTreeItemAccessible(currentIndex);
+ }
+
+ return nullptr;
+}
+
+void XULTreeAccessible::SetCurrentItem(const LocalAccessible* aItem) {
+ NS_ERROR("XULTreeAccessible::SetCurrentItem not implemented");
+}
+
+void XULTreeAccessible::SelectedItems(nsTArray<Accessible*>* aItems) {
+ if (!mTreeView) return;
+
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (!selection) return;
+
+ int32_t rangeCount = 0;
+ selection->GetRangeCount(&rangeCount);
+ for (int32_t rangeIdx = 0; rangeIdx < rangeCount; rangeIdx++) {
+ int32_t firstIdx = 0, lastIdx = -1;
+ selection->GetRangeAt(rangeIdx, &firstIdx, &lastIdx);
+ for (int32_t rowIdx = firstIdx; rowIdx <= lastIdx; rowIdx++) {
+ LocalAccessible* item = GetTreeItemAccessible(rowIdx);
+ if (item) aItems->AppendElement(item);
+ }
+ }
+}
+
+uint32_t XULTreeAccessible::SelectedItemCount() {
+ if (!mTreeView) return 0;
+
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (selection) {
+ int32_t count = 0;
+ selection->GetCount(&count);
+ return count;
+ }
+
+ return 0;
+}
+
+bool XULTreeAccessible::AddItemToSelection(uint32_t aIndex) {
+ if (!mTreeView) return false;
+
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (selection) {
+ bool isSelected = false;
+ selection->IsSelected(aIndex, &isSelected);
+ if (!isSelected) selection->ToggleSelect(aIndex);
+
+ return true;
+ }
+ return false;
+}
+
+bool XULTreeAccessible::RemoveItemFromSelection(uint32_t aIndex) {
+ if (!mTreeView) return false;
+
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (selection) {
+ bool isSelected = false;
+ selection->IsSelected(aIndex, &isSelected);
+ if (isSelected) selection->ToggleSelect(aIndex);
+
+ return true;
+ }
+ return false;
+}
+
+bool XULTreeAccessible::IsItemSelected(uint32_t aIndex) {
+ if (!mTreeView) return false;
+
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (selection) {
+ bool isSelected = false;
+ selection->IsSelected(aIndex, &isSelected);
+ return isSelected;
+ }
+ return false;
+}
+
+bool XULTreeAccessible::UnselectAll() {
+ if (!mTreeView) return false;
+
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (!selection) return false;
+
+ selection->ClearSelection();
+ return true;
+}
+
+Accessible* XULTreeAccessible::GetSelectedItem(uint32_t aIndex) {
+ if (!mTreeView) return nullptr;
+
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (!selection) return nullptr;
+
+ uint32_t selCount = 0;
+ int32_t rangeCount = 0;
+ selection->GetRangeCount(&rangeCount);
+ for (int32_t rangeIdx = 0; rangeIdx < rangeCount; rangeIdx++) {
+ int32_t firstIdx = 0, lastIdx = -1;
+ selection->GetRangeAt(rangeIdx, &firstIdx, &lastIdx);
+ for (int32_t rowIdx = firstIdx; rowIdx <= lastIdx; rowIdx++) {
+ if (selCount == aIndex) return GetTreeItemAccessible(rowIdx);
+
+ selCount++;
+ }
+ }
+
+ return nullptr;
+}
+
+bool XULTreeAccessible::SelectAll() {
+ // see if we are multiple select if so set ourselves as such
+ if (!mTreeView) return false;
+
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (selection) {
+ bool single = false;
+ selection->GetSingle(&single);
+ if (!single) {
+ selection->SelectAll();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeAccessible: LocalAccessible implementation
+
+LocalAccessible* XULTreeAccessible::LocalChildAt(uint32_t aIndex) const {
+ uint32_t childCount = LocalAccessible::ChildCount();
+ if (aIndex < childCount) {
+ return LocalAccessible::LocalChildAt(aIndex);
+ }
+
+ return GetTreeItemAccessible(aIndex - childCount);
+}
+
+uint32_t XULTreeAccessible::ChildCount() const {
+ // Tree's children count is row count + treecols count.
+ uint32_t childCount = LocalAccessible::ChildCount();
+ if (!mTreeView) return childCount;
+
+ int32_t rowCount = 0;
+ mTreeView->GetRowCount(&rowCount);
+ childCount += rowCount;
+
+ return childCount;
+}
+
+Relation XULTreeAccessible::RelationByType(RelationType aType) const {
+ if (aType == RelationType::NODE_PARENT_OF) {
+ if (mTreeView) {
+ return Relation(new XULTreeItemIterator(this, mTreeView, -1));
+ }
+
+ return Relation();
+ }
+
+ return LocalAccessible::RelationByType(aType);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeAccessible: Widgets
+
+bool XULTreeAccessible::IsWidget() const { return true; }
+
+bool XULTreeAccessible::IsActiveWidget() const {
+ if (IsAutoCompletePopup()) {
+ nsCOMPtr<nsIAutoCompletePopup> autoCompletePopupElm =
+ do_QueryInterface(mContent->GetParent());
+
+ if (autoCompletePopupElm) {
+ bool isOpen = false;
+ autoCompletePopupElm->GetPopupOpen(&isOpen);
+ return isOpen;
+ }
+ }
+ return FocusMgr()->HasDOMFocus(mContent);
+}
+
+bool XULTreeAccessible::AreItemsOperable() const {
+ if (IsAutoCompletePopup()) {
+ nsCOMPtr<nsIAutoCompletePopup> autoCompletePopupElm =
+ do_QueryInterface(mContent->GetParent());
+
+ if (autoCompletePopupElm) {
+ bool isOpen = false;
+ autoCompletePopupElm->GetPopupOpen(&isOpen);
+ return isOpen;
+ }
+ }
+ return true;
+}
+
+LocalAccessible* XULTreeAccessible::ContainerWidget() const { return nullptr; }
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeAccessible: public implementation
+
+XULTreeItemAccessibleBase* XULTreeAccessible::GetTreeItemAccessible(
+ int32_t aRow) const {
+ if (aRow < 0 || IsDefunct() || !mTreeView) return nullptr;
+
+ int32_t rowCount = 0;
+ nsresult rv = mTreeView->GetRowCount(&rowCount);
+ if (NS_FAILED(rv) || aRow >= rowCount) return nullptr;
+
+ void* key = reinterpret_cast<void*>(intptr_t(aRow));
+ return mAccessibleCache.WithEntryHandle(
+ key, [&](auto&& entry) -> XULTreeItemAccessibleBase* {
+ if (entry) {
+ return entry->get();
+ }
+
+ RefPtr<XULTreeItemAccessibleBase> treeItem =
+ CreateTreeItemAccessible(aRow);
+ if (treeItem) {
+ entry.Insert(RefPtr{treeItem});
+ Document()->BindToDocument(treeItem, nullptr);
+ return treeItem.get();
+ }
+
+ return nullptr;
+ });
+}
+
+void XULTreeAccessible::InvalidateCache(int32_t aRow, int32_t aCount) {
+ if (IsDefunct()) return;
+
+ if (!mTreeView) {
+ UnbindCacheEntriesFromDocument(mAccessibleCache);
+ return;
+ }
+
+ // Do not invalidate the cache if rows have been inserted.
+ if (aCount > 0) return;
+
+ DocAccessible* document = Document();
+
+ // Fire destroy event for removed tree items and delete them from caches.
+ for (int32_t rowIdx = aRow; rowIdx < aRow - aCount; rowIdx++) {
+ void* key = reinterpret_cast<void*>(intptr_t(rowIdx));
+ XULTreeItemAccessibleBase* treeItem = mAccessibleCache.GetWeak(key);
+
+ if (treeItem) {
+ RefPtr<AccEvent> event =
+ new AccEvent(nsIAccessibleEvent::EVENT_HIDE, treeItem);
+ nsEventShell::FireEvent(event);
+
+ // Unbind from document, shutdown and remove from tree cache.
+ document->UnbindFromDocument(treeItem);
+ mAccessibleCache.Remove(key);
+ }
+ }
+
+ // We dealt with removed tree items already however we may keep tree items
+ // having row indexes greater than row count. We should remove these dead tree
+ // items silently from caches.
+ int32_t newRowCount = 0;
+ nsresult rv = mTreeView->GetRowCount(&newRowCount);
+ if (NS_FAILED(rv)) return;
+
+ int32_t oldRowCount = newRowCount - aCount;
+
+ for (int32_t rowIdx = newRowCount; rowIdx < oldRowCount; ++rowIdx) {
+ void* key = reinterpret_cast<void*>(intptr_t(rowIdx));
+ XULTreeItemAccessibleBase* treeItem = mAccessibleCache.GetWeak(key);
+
+ if (treeItem) {
+ // Unbind from document, shutdown and remove from tree cache.
+ document->UnbindFromDocument(treeItem);
+ mAccessibleCache.Remove(key);
+ }
+ }
+}
+
+void XULTreeAccessible::TreeViewInvalidated(int32_t aStartRow, int32_t aEndRow,
+ int32_t aStartCol,
+ int32_t aEndCol) {
+ if (IsDefunct()) return;
+
+ if (!mTreeView) {
+ UnbindCacheEntriesFromDocument(mAccessibleCache);
+ return;
+ }
+
+ int32_t endRow = aEndRow;
+
+ nsresult rv;
+ if (endRow == -1) {
+ int32_t rowCount = 0;
+ rv = mTreeView->GetRowCount(&rowCount);
+ if (NS_FAILED(rv)) return;
+
+ endRow = rowCount - 1;
+ }
+
+ RefPtr<nsTreeColumns> treeColumns = mTree->GetColumns();
+ if (!treeColumns) return;
+
+ int32_t endCol = aEndCol;
+
+ if (endCol == -1) {
+ // We need to make sure to cast to int32_t before we do the subtraction, in
+ // case the column count is 0.
+ endCol = static_cast<int32_t>(treeColumns->Count()) - 1;
+ }
+
+ for (int32_t rowIdx = aStartRow; rowIdx <= endRow; ++rowIdx) {
+ void* key = reinterpret_cast<void*>(intptr_t(rowIdx));
+ XULTreeItemAccessibleBase* treeitemAcc = mAccessibleCache.GetWeak(key);
+
+ if (treeitemAcc) {
+ treeitemAcc->RowInvalidated(aStartCol, endCol);
+ }
+ }
+}
+
+void XULTreeAccessible::TreeViewChanged(nsITreeView* aView) {
+ if (IsDefunct()) return;
+
+ // Fire reorder event on tree accessible on accessible tree (do not fire
+ // show/hide events on tree items because it can be expensive to fire them for
+ // each tree item.
+ RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(this);
+ Document()->FireDelayedEvent(reorderEvent);
+
+ // Clear cache.
+ UnbindCacheEntriesFromDocument(mAccessibleCache);
+
+ mTreeView = aView;
+ LocalAccessible* item = CurrentItem();
+ if (item) {
+ FocusMgr()->ActiveItemChanged(item, true);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeAccessible: protected implementation
+
+already_AddRefed<XULTreeItemAccessibleBase>
+XULTreeAccessible::CreateTreeItemAccessible(int32_t aRow) const {
+ RefPtr<XULTreeItemAccessibleBase> accessible = new XULTreeItemAccessible(
+ mContent, mDoc, const_cast<XULTreeAccessible*>(this), mTree, mTreeView,
+ aRow);
+
+ return accessible.forget();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeItemAccessibleBase
+////////////////////////////////////////////////////////////////////////////////
+
+XULTreeItemAccessibleBase::XULTreeItemAccessibleBase(
+ nsIContent* aContent, DocAccessible* aDoc, LocalAccessible* aParent,
+ dom::XULTreeElement* aTree, nsITreeView* aTreeView, int32_t aRow)
+ : AccessibleWrap(aContent, aDoc),
+ mTree(aTree),
+ mTreeView(aTreeView),
+ mRow(aRow) {
+ mParent = aParent;
+ mStateFlags |= eSharedNode;
+}
+
+XULTreeItemAccessibleBase::~XULTreeItemAccessibleBase() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeItemAccessibleBase: nsISupports implementation
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(XULTreeItemAccessibleBase, LocalAccessible,
+ mTree)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(XULTreeItemAccessibleBase,
+ LocalAccessible)
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeItemAccessibleBase: LocalAccessible
+
+Accessible* XULTreeItemAccessibleBase::FocusedChild() {
+ return FocusMgr()->FocusedLocalAccessible() == this ? this : nullptr;
+}
+
+nsIntRect XULTreeItemAccessibleBase::BoundsInCSSPixels() const {
+ // Get x coordinate and width from treechildren element, get y coordinate and
+ // height from tree cell.
+
+ RefPtr<nsTreeColumn> column = nsCoreUtils::GetFirstSensibleColumn(mTree);
+
+ nsresult rv;
+ nsIntRect rect = mTree->GetCoordsForCellItem(mRow, column, u"cell"_ns, rv);
+ if (NS_FAILED(rv)) {
+ return nsIntRect();
+ }
+
+ RefPtr<dom::Element> bodyElement = mTree->GetTreeBody();
+ if (!bodyElement || !bodyElement->IsXULElement()) {
+ return nsIntRect();
+ }
+
+ rect.width = bodyElement->ClientWidth();
+
+ nsIFrame* bodyFrame = bodyElement->GetPrimaryFrame();
+ if (!bodyFrame) {
+ return nsIntRect();
+ }
+
+ CSSIntRect screenRect = bodyFrame->GetScreenRect();
+ rect.x = screenRect.x;
+ rect.y += screenRect.y;
+ return rect;
+}
+
+nsRect XULTreeItemAccessibleBase::BoundsInAppUnits() const {
+ nsIntRect bounds = BoundsInCSSPixels();
+ nsPresContext* presContext = mDoc->PresContext();
+ return nsRect(presContext->CSSPixelsToAppUnits(bounds.X()),
+ presContext->CSSPixelsToAppUnits(bounds.Y()),
+ presContext->CSSPixelsToAppUnits(bounds.Width()),
+ presContext->CSSPixelsToAppUnits(bounds.Height()));
+}
+
+void XULTreeItemAccessibleBase::SetSelected(bool aSelect) {
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (selection) {
+ bool isSelected = false;
+ selection->IsSelected(mRow, &isSelected);
+ if (isSelected != aSelect) selection->ToggleSelect(mRow);
+ }
+}
+
+void XULTreeItemAccessibleBase::TakeFocus() const {
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (selection) selection->SetCurrentIndex(mRow);
+
+ // focus event will be fired here
+ LocalAccessible::TakeFocus();
+}
+
+Relation XULTreeItemAccessibleBase::RelationByType(RelationType aType) const {
+ switch (aType) {
+ case RelationType::NODE_CHILD_OF: {
+ int32_t parentIndex = -1;
+ if (!NS_SUCCEEDED(mTreeView->GetParentIndex(mRow, &parentIndex))) {
+ return Relation();
+ }
+
+ if (parentIndex == -1) return Relation(mParent);
+
+ XULTreeAccessible* treeAcc = mParent->AsXULTree();
+ return Relation(treeAcc->GetTreeItemAccessible(parentIndex));
+ }
+
+ case RelationType::NODE_PARENT_OF: {
+ bool isTrue = false;
+ if (NS_FAILED(mTreeView->IsContainerEmpty(mRow, &isTrue)) || isTrue) {
+ return Relation();
+ }
+
+ if (NS_FAILED(mTreeView->IsContainerOpen(mRow, &isTrue)) || !isTrue) {
+ return Relation();
+ }
+
+ XULTreeAccessible* tree = mParent->AsXULTree();
+ return Relation(new XULTreeItemIterator(tree, mTreeView, mRow));
+ }
+
+ default:
+ return Relation();
+ }
+}
+
+bool XULTreeItemAccessibleBase::HasPrimaryAction() const { return true; }
+
+uint8_t XULTreeItemAccessibleBase::ActionCount() const {
+ // "activate" action is available for all treeitems, "expand/collapse" action
+ // is avaible for treeitem which is container.
+ return IsExpandable() ? 2 : 1;
+}
+
+void XULTreeItemAccessibleBase::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ if (aIndex == eAction_Click) {
+ aName.AssignLiteral("activate");
+ return;
+ }
+
+ if (aIndex == eAction_Expand && IsExpandable()) {
+ bool isContainerOpen = false;
+ mTreeView->IsContainerOpen(mRow, &isContainerOpen);
+ if (isContainerOpen) {
+ aName.AssignLiteral("collapse");
+ } else {
+ aName.AssignLiteral("expand");
+ }
+ }
+}
+
+bool XULTreeItemAccessibleBase::DoAction(uint8_t aIndex) const {
+ if (aIndex != eAction_Click &&
+ (aIndex != eAction_Expand || !IsExpandable())) {
+ return false;
+ }
+
+ DoCommand(nullptr, aIndex);
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeItemAccessibleBase: LocalAccessible implementation
+
+void XULTreeItemAccessibleBase::Shutdown() {
+ mTree = nullptr;
+ mTreeView = nullptr;
+ mRow = -1;
+ mParent = nullptr; // null-out to prevent base class's shutdown ops
+
+ AccessibleWrap::Shutdown();
+}
+
+GroupPos XULTreeItemAccessibleBase::GroupPosition() {
+ GroupPos groupPos;
+
+ int32_t level;
+ nsresult rv = mTreeView->GetLevel(mRow, &level);
+ NS_ENSURE_SUCCESS(rv, groupPos);
+
+ int32_t topCount = 1;
+ for (int32_t index = mRow - 1; index >= 0; index--) {
+ int32_t lvl = -1;
+ if (NS_SUCCEEDED(mTreeView->GetLevel(index, &lvl))) {
+ if (lvl < level) break;
+
+ if (lvl == level) topCount++;
+ }
+ }
+
+ int32_t rowCount = 0;
+ rv = mTreeView->GetRowCount(&rowCount);
+ NS_ENSURE_SUCCESS(rv, groupPos);
+
+ int32_t bottomCount = 0;
+ for (int32_t index = mRow + 1; index < rowCount; index++) {
+ int32_t lvl = -1;
+ if (NS_SUCCEEDED(mTreeView->GetLevel(index, &lvl))) {
+ if (lvl < level) break;
+
+ if (lvl == level) bottomCount++;
+ }
+ }
+
+ groupPos.level = level + 1;
+ groupPos.setSize = topCount + bottomCount;
+ groupPos.posInSet = topCount;
+
+ return groupPos;
+}
+
+uint64_t XULTreeItemAccessibleBase::NativeState() const {
+ // focusable and selectable states
+ uint64_t state = NativeInteractiveState();
+
+ // expanded/collapsed state
+ if (IsExpandable()) {
+ bool isContainerOpen;
+ mTreeView->IsContainerOpen(mRow, &isContainerOpen);
+ state |= isContainerOpen ? states::EXPANDED : states::COLLAPSED;
+ }
+
+ // selected state
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (selection) {
+ bool isSelected;
+ selection->IsSelected(mRow, &isSelected);
+ if (isSelected) state |= states::SELECTED;
+ }
+
+ // focused state
+ if (FocusMgr()->IsFocused(this)) state |= states::FOCUSED;
+
+ // invisible state
+ int32_t firstVisibleRow = mTree->GetFirstVisibleRow();
+ int32_t lastVisibleRow = mTree->GetLastVisibleRow();
+ if (mRow < firstVisibleRow || mRow > lastVisibleRow) {
+ state |= states::INVISIBLE;
+ }
+
+ return state;
+}
+
+uint64_t XULTreeItemAccessibleBase::NativeInteractiveState() const {
+ return states::FOCUSABLE | states::SELECTABLE;
+}
+
+int32_t XULTreeItemAccessibleBase::IndexInParent() const {
+ return mParent ? mParent->ContentChildCount() + mRow : -1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeItemAccessibleBase: Widgets
+
+LocalAccessible* XULTreeItemAccessibleBase::ContainerWidget() const {
+ return mParent;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeItemAccessibleBase: LocalAccessible protected methods
+
+void XULTreeItemAccessibleBase::DispatchClickEvent(
+ nsIContent* aContent, uint32_t aActionIndex) const {
+ if (IsDefunct()) return;
+
+ RefPtr<nsTreeColumns> columns = mTree->GetColumns();
+ if (!columns) return;
+
+ // Get column and pseudo element.
+ RefPtr<nsTreeColumn> column;
+ nsAutoString pseudoElm;
+
+ if (aActionIndex == eAction_Click) {
+ // Key column is visible and clickable.
+ column = columns->GetKeyColumn();
+ } else {
+ // Primary column contains a twisty we should click on.
+ column = columns->GetPrimaryColumn();
+ pseudoElm = u"twisty"_ns;
+ }
+
+ if (column) {
+ RefPtr<dom::XULTreeElement> tree = mTree;
+ nsCoreUtils::DispatchClickEvent(tree, mRow, column, pseudoElm);
+ }
+}
+
+LocalAccessible* XULTreeItemAccessibleBase::GetSiblingAtOffset(
+ int32_t aOffset, nsresult* aError) const {
+ if (aError) *aError = NS_OK; // fail peacefully
+
+ return mParent->LocalChildAt(IndexInParent() + aOffset);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeItemAccessibleBase: protected implementation
+
+bool XULTreeItemAccessibleBase::IsExpandable() const {
+ bool isContainer = false;
+ mTreeView->IsContainer(mRow, &isContainer);
+ if (isContainer) {
+ bool isEmpty = false;
+ mTreeView->IsContainerEmpty(mRow, &isEmpty);
+ if (!isEmpty) {
+ RefPtr<nsTreeColumns> columns = mTree->GetColumns();
+ if (columns) {
+ nsTreeColumn* primaryColumn = columns->GetPrimaryColumn();
+ if (primaryColumn && !nsCoreUtils::IsColumnHidden(primaryColumn)) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+void XULTreeItemAccessibleBase::GetCellName(nsTreeColumn* aColumn,
+ nsAString& aName) const {
+ mTreeView->GetCellText(mRow, aColumn, aName);
+
+ // If there is still no name try the cell value:
+ // This is for graphical cells. We need tree/table view implementors to
+ // implement FooView::GetCellValue to return a meaningful string for cases
+ // where there is something shown in the cell (non-text) such as a star icon;
+ // in which case GetCellValue for that cell would return "starred" or
+ // "flagged" for example.
+ if (aName.IsEmpty()) mTreeView->GetCellValue(mRow, aColumn, aName);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeItemAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULTreeItemAccessible::XULTreeItemAccessible(
+ nsIContent* aContent, DocAccessible* aDoc, LocalAccessible* aParent,
+ dom::XULTreeElement* aTree, nsITreeView* aTreeView, int32_t aRow)
+ : XULTreeItemAccessibleBase(aContent, aDoc, aParent, aTree, aTreeView,
+ aRow) {
+ mStateFlags |= eNoKidsFromDOM;
+ mColumn = nsCoreUtils::GetFirstSensibleColumn(mTree, FlushType::None);
+ GetCellName(mColumn, mCachedName);
+}
+
+XULTreeItemAccessible::~XULTreeItemAccessible() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeItemAccessible: nsISupports implementation
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(XULTreeItemAccessible,
+ XULTreeItemAccessibleBase, mColumn)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(XULTreeItemAccessible)
+NS_INTERFACE_MAP_END_INHERITING(XULTreeItemAccessibleBase)
+NS_IMPL_ADDREF_INHERITED(XULTreeItemAccessible, XULTreeItemAccessibleBase)
+NS_IMPL_RELEASE_INHERITED(XULTreeItemAccessible, XULTreeItemAccessibleBase)
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeItemAccessible: nsIAccessible implementation
+
+ENameValueFlag XULTreeItemAccessible::Name(nsString& aName) const {
+ aName.Truncate();
+
+ GetCellName(mColumn, aName);
+ return eNameOK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeItemAccessible: LocalAccessible implementation
+
+void XULTreeItemAccessible::Shutdown() {
+ mColumn = nullptr;
+ XULTreeItemAccessibleBase::Shutdown();
+}
+
+role XULTreeItemAccessible::NativeRole() const {
+ RefPtr<nsTreeColumns> columns = mTree->GetColumns();
+ if (!columns) {
+ NS_ERROR("No tree columns object in the tree!");
+ return roles::NOTHING;
+ }
+
+ nsTreeColumn* primaryColumn = columns->GetPrimaryColumn();
+
+ return primaryColumn ? roles::OUTLINEITEM : roles::LISTITEM;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeItemAccessible: XULTreeItemAccessibleBase implementation
+
+void XULTreeItemAccessible::RowInvalidated(int32_t aStartColIdx,
+ int32_t aEndColIdx) {
+ nsAutoString name;
+ Name(name);
+
+ if (name != mCachedName) {
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ mCachedName = name;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeColumAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULTreeColumAccessible::XULTreeColumAccessible(nsIContent* aContent,
+ DocAccessible* aDoc)
+ : XULColumAccessible(aContent, aDoc) {}
+
+LocalAccessible* XULTreeColumAccessible::GetSiblingAtOffset(
+ int32_t aOffset, nsresult* aError) const {
+ if (aOffset < 0) {
+ return XULColumAccessible::GetSiblingAtOffset(aOffset, aError);
+ }
+
+ if (aError) *aError = NS_OK; // fail peacefully
+
+ RefPtr<dom::XULTreeElement> tree = nsCoreUtils::GetTree(mContent);
+ if (tree) {
+ nsCOMPtr<nsITreeView> treeView = tree->GetView(FlushType::None);
+ if (treeView) {
+ int32_t rowCount = 0;
+ treeView->GetRowCount(&rowCount);
+ if (rowCount > 0 && aOffset <= rowCount) {
+ XULTreeAccessible* treeAcc = LocalParent()->AsXULTree();
+
+ if (treeAcc) return treeAcc->GetTreeItemAccessible(aOffset - 1);
+ }
+ }
+ }
+
+ return nullptr;
+}
diff --git a/accessible/xul/XULTreeAccessible.h b/accessible/xul/XULTreeAccessible.h
new file mode 100644
index 0000000000..7abc53df42
--- /dev/null
+++ b/accessible/xul/XULTreeAccessible.h
@@ -0,0 +1,265 @@
+/* -*- 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_XULTreeAccessible_h__
+#define mozilla_a11y_XULTreeAccessible_h__
+
+#include "nsITreeView.h"
+#include "XULListboxAccessible.h"
+#include "mozilla/dom/XULTreeElement.h"
+
+class nsTreeBodyFrame;
+class nsTreeColumn;
+
+namespace mozilla {
+namespace a11y {
+
+class XULTreeGridCellAccessible;
+class XULTreeItemAccessibleBase;
+
+/*
+ * A class the represents the XUL Tree widget.
+ */
+const uint32_t kMaxTreeColumns = 100;
+const uint32_t kDefaultTreeCacheLength = 128;
+
+/**
+ * LocalAccessible class for XUL tree element.
+ */
+
+class XULTreeAccessible : public AccessibleWrap {
+ public:
+ XULTreeAccessible(nsIContent* aContent, DocAccessible* aDoc,
+ nsTreeBodyFrame* aTreeframe);
+
+ // nsISupports and cycle collection
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULTreeAccessible, LocalAccessible)
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual void Value(nsString& aValue) const override;
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual LocalAccessible* LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override;
+
+ virtual LocalAccessible* LocalChildAt(uint32_t aIndex) const override;
+ virtual uint32_t ChildCount() const override;
+ virtual Relation RelationByType(RelationType aType) const override;
+
+ // SelectAccessible
+ virtual void SelectedItems(nsTArray<Accessible*>* aItems) override;
+ virtual uint32_t SelectedItemCount() override;
+ virtual Accessible* GetSelectedItem(uint32_t aIndex) override;
+ virtual bool IsItemSelected(uint32_t aIndex) override;
+ virtual bool AddItemToSelection(uint32_t aIndex) override;
+ virtual bool RemoveItemFromSelection(uint32_t aIndex) override;
+ virtual bool SelectAll() override;
+ virtual bool UnselectAll() override;
+
+ // Widgets
+ virtual bool IsWidget() const override;
+ virtual bool IsActiveWidget() const override;
+ virtual bool AreItemsOperable() const override;
+ virtual LocalAccessible* CurrentItem() const override;
+ virtual void SetCurrentItem(const LocalAccessible* aItem) override;
+
+ virtual LocalAccessible* ContainerWidget() const override;
+
+ // XULTreeAccessible
+
+ /**
+ * Return tree item accessible at the givem row. If accessible doesn't exist
+ * in the cache then create and cache it.
+ *
+ * @param aRow [in] the given row index
+ */
+ XULTreeItemAccessibleBase* GetTreeItemAccessible(int32_t aRow) const;
+
+ /**
+ * Invalidates the number of cached treeitem accessibles.
+ *
+ * @param aRow [in] row index the invalidation starts from
+ * @param aCount [in] the number of treeitem accessibles to invalidate,
+ * the number sign specifies whether rows have been
+ * inserted (plus) or removed (minus)
+ */
+ void InvalidateCache(int32_t aRow, int32_t aCount);
+
+ /**
+ * Fires name change events for invalidated area of tree.
+ *
+ * @param aStartRow [in] row index invalidation starts from
+ * @param aEndRow [in] row index invalidation ends, -1 means last row index
+ * @param aStartCol [in] column index invalidation starts from
+ * @param aEndCol [in] column index invalidation ends, -1 mens last column
+ * index
+ */
+ void TreeViewInvalidated(int32_t aStartRow, int32_t aEndRow,
+ int32_t aStartCol, int32_t aEndCol);
+
+ /**
+ * Invalidates children created for previous tree view.
+ */
+ void TreeViewChanged(nsITreeView* aView);
+
+ protected:
+ virtual ~XULTreeAccessible();
+
+ /**
+ * Creates tree item accessible for the given row index.
+ */
+ virtual already_AddRefed<XULTreeItemAccessibleBase> CreateTreeItemAccessible(
+ int32_t aRow) const;
+
+ RefPtr<dom::XULTreeElement> mTree;
+ nsITreeView* mTreeView;
+ mutable nsRefPtrHashtable<nsPtrHashKey<const void>, XULTreeItemAccessibleBase>
+ mAccessibleCache;
+};
+
+/**
+ * Base class for tree item accessibles.
+ */
+
+class XULTreeItemAccessibleBase : public AccessibleWrap {
+ public:
+ XULTreeItemAccessibleBase(nsIContent* aContent, DocAccessible* aDoc,
+ LocalAccessible* aParent,
+ dom::XULTreeElement* aTree, nsITreeView* aTreeView,
+ int32_t aRow);
+
+ // nsISupports and cycle collection
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULTreeItemAccessibleBase,
+ AccessibleWrap)
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual nsRect BoundsInAppUnits() const override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual nsIntRect BoundsInCSSPixels() const override;
+ virtual GroupPos GroupPosition() override;
+ virtual uint64_t NativeState() const override;
+ virtual uint64_t NativeInteractiveState() const override;
+ virtual int32_t IndexInParent() const override;
+ virtual Relation RelationByType(RelationType aType) const override;
+ virtual Accessible* FocusedChild() override;
+ virtual void SetSelected(bool aSelect) override;
+ virtual void TakeFocus() const override;
+
+ // ActionAccessible
+ virtual uint8_t ActionCount() const override;
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+ virtual bool DoAction(uint8_t aIndex) const override;
+
+ // Widgets
+ virtual LocalAccessible* ContainerWidget() const override;
+
+ /**
+ * Return row index associated with the accessible.
+ */
+ int32_t GetRowIndex() const { return mRow; }
+
+ /**
+ * Return cell accessible for the given column. If XUL tree accessible is not
+ * accessible table then return null.
+ */
+ virtual XULTreeGridCellAccessible* GetCellAccessible(
+ nsTreeColumn* aColumn) const {
+ return nullptr;
+ }
+
+ /**
+ * Proccess row invalidation. Used to fires name change events.
+ */
+ virtual void RowInvalidated(int32_t aStartColIdx, int32_t aEndColIdx) = 0;
+
+ protected:
+ virtual ~XULTreeItemAccessibleBase();
+
+ enum { eAction_Click = 0, eAction_Expand = 1 };
+
+ // LocalAccessible
+ MOZ_CAN_RUN_SCRIPT
+ virtual void DispatchClickEvent(nsIContent* aContent,
+ uint32_t aActionIndex) const override;
+ virtual LocalAccessible* GetSiblingAtOffset(
+ int32_t aOffset, nsresult* aError = nullptr) const override;
+
+ // XULTreeItemAccessibleBase
+
+ /**
+ * Return true if the tree item accessible is expandable (contains subrows).
+ */
+ bool IsExpandable() const;
+
+ /**
+ * Return name for cell at the given column.
+ */
+ void GetCellName(nsTreeColumn* aColumn, nsAString& aName) const;
+
+ RefPtr<dom::XULTreeElement> mTree;
+ nsITreeView* mTreeView;
+ int32_t mRow;
+};
+
+/**
+ * LocalAccessible class for items for XUL tree.
+ */
+class XULTreeItemAccessible : public XULTreeItemAccessibleBase {
+ public:
+ XULTreeItemAccessible(nsIContent* aContent, DocAccessible* aDoc,
+ LocalAccessible* aParent, dom::XULTreeElement* aTree,
+ nsITreeView* aTreeView, int32_t aRow);
+
+ // nsISupports and cycle collection
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULTreeItemAccessible,
+ XULTreeItemAccessibleBase)
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual ENameValueFlag Name(nsString& aName) const override;
+ virtual a11y::role NativeRole() const override;
+
+ // XULTreeItemAccessibleBase
+ virtual void RowInvalidated(int32_t aStartColIdx,
+ int32_t aEndColIdx) override;
+
+ protected:
+ virtual ~XULTreeItemAccessible();
+
+ // XULTreeItemAccessible
+ RefPtr<nsTreeColumn> mColumn;
+ nsString mCachedName;
+};
+
+/**
+ * LocalAccessible class for columns element of XUL tree.
+ */
+class XULTreeColumAccessible : public XULColumAccessible {
+ public:
+ XULTreeColumAccessible(nsIContent* aContent, DocAccessible* aDoc);
+
+ protected:
+ // LocalAccessible
+ virtual LocalAccessible* GetSiblingAtOffset(
+ int32_t aOffset, nsresult* aError = nullptr) const override;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// LocalAccessible downcasting method
+
+inline XULTreeAccessible* LocalAccessible::AsXULTree() {
+ return IsXULTree() ? static_cast<XULTreeAccessible*>(this) : nullptr;
+}
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xul/XULTreeGridAccessible.cpp b/accessible/xul/XULTreeGridAccessible.cpp
new file mode 100644
index 0000000000..9b2e0cec33
--- /dev/null
+++ b/accessible/xul/XULTreeGridAccessible.cpp
@@ -0,0 +1,666 @@
+/* -*- 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 "XULTreeGridAccessible.h"
+
+#include <stdint.h>
+#include "AccAttributes.h"
+#include "LocalAccessible-inl.h"
+#include "nsAccCache.h"
+#include "nsAccessibilityService.h"
+#include "nsAccUtils.h"
+#include "DocAccessible.h"
+#include "nsEventShell.h"
+#include "Relation.h"
+#include "mozilla/a11y/Role.h"
+#include "States.h"
+#include "nsQueryObject.h"
+#include "nsTreeColumns.h"
+
+#include "nsITreeSelection.h"
+#include "nsComponentManagerUtils.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/a11y/TableAccessible.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/TreeColumnBinding.h"
+#include "mozilla/dom/XULTreeElementBinding.h"
+
+using namespace mozilla::a11y;
+using namespace mozilla;
+
+XULTreeGridAccessible::~XULTreeGridAccessible() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridAccessible: Table
+
+uint32_t XULTreeGridAccessible::ColCount() const {
+ return nsCoreUtils::GetSensibleColumnCount(mTree);
+}
+
+uint32_t XULTreeGridAccessible::RowCount() {
+ if (!mTreeView) return 0;
+
+ int32_t rowCount = 0;
+ mTreeView->GetRowCount(&rowCount);
+ return rowCount >= 0 ? rowCount : 0;
+}
+
+uint32_t XULTreeGridAccessible::SelectedCellCount() {
+ return SelectedRowCount() * ColCount();
+}
+
+uint32_t XULTreeGridAccessible::SelectedColCount() {
+ // If all the row has been selected, then all the columns are selected,
+ // because we can't select a column alone.
+
+ uint32_t selectedRowCount = SelectedItemCount();
+ return selectedRowCount > 0 && selectedRowCount == RowCount() ? ColCount()
+ : 0;
+}
+
+uint32_t XULTreeGridAccessible::SelectedRowCount() {
+ return SelectedItemCount();
+}
+
+void XULTreeGridAccessible::SelectedCells(nsTArray<Accessible*>* aCells) {
+ uint32_t colCount = ColCount(), rowCount = RowCount();
+
+ for (uint32_t rowIdx = 0; rowIdx < rowCount; rowIdx++) {
+ if (IsRowSelected(rowIdx)) {
+ for (uint32_t colIdx = 0; colIdx < colCount; colIdx++) {
+ LocalAccessible* cell = CellAt(rowIdx, colIdx);
+ aCells->AppendElement(cell);
+ }
+ }
+ }
+}
+
+void XULTreeGridAccessible::SelectedCellIndices(nsTArray<uint32_t>* aCells) {
+ uint32_t colCount = ColCount(), rowCount = RowCount();
+
+ for (uint32_t rowIdx = 0; rowIdx < rowCount; rowIdx++) {
+ if (IsRowSelected(rowIdx)) {
+ for (uint32_t colIdx = 0; colIdx < colCount; colIdx++) {
+ aCells->AppendElement(rowIdx * colCount + colIdx);
+ }
+ }
+ }
+}
+
+void XULTreeGridAccessible::SelectedColIndices(nsTArray<uint32_t>* aCols) {
+ if (RowCount() != SelectedRowCount()) return;
+
+ uint32_t colCount = ColCount();
+ aCols->SetCapacity(colCount);
+ for (uint32_t colIdx = 0; colIdx < colCount; colIdx++) {
+ aCols->AppendElement(colIdx);
+ }
+}
+
+void XULTreeGridAccessible::SelectedRowIndices(nsTArray<uint32_t>* aRows) {
+ uint32_t rowCount = RowCount();
+ for (uint32_t rowIdx = 0; rowIdx < rowCount; rowIdx++) {
+ if (IsRowSelected(rowIdx)) aRows->AppendElement(rowIdx);
+ }
+}
+
+LocalAccessible* XULTreeGridAccessible::CellAt(uint32_t aRowIndex,
+ uint32_t aColumnIndex) {
+ XULTreeItemAccessibleBase* rowAcc = GetTreeItemAccessible(aRowIndex);
+ if (!rowAcc) return nullptr;
+
+ RefPtr<nsTreeColumn> column =
+ nsCoreUtils::GetSensibleColumnAt(mTree, aColumnIndex);
+ if (!column) return nullptr;
+
+ return rowAcc->GetCellAccessible(column);
+}
+
+void XULTreeGridAccessible::ColDescription(uint32_t aColIdx,
+ nsString& aDescription) {
+ aDescription.Truncate();
+
+ LocalAccessible* treeColumns = LocalAccessible::LocalChildAt(0);
+ if (treeColumns) {
+ LocalAccessible* treeColumnItem = treeColumns->LocalChildAt(aColIdx);
+ if (treeColumnItem) treeColumnItem->Name(aDescription);
+ }
+}
+
+bool XULTreeGridAccessible::IsColSelected(uint32_t aColIdx) {
+ // If all the row has been selected, then all the columns are selected.
+ // Because we can't select a column alone.
+ return SelectedItemCount() == RowCount();
+}
+
+bool XULTreeGridAccessible::IsRowSelected(uint32_t aRowIdx) {
+ if (!mTreeView) return false;
+
+ nsCOMPtr<nsITreeSelection> selection;
+ nsresult rv = mTreeView->GetSelection(getter_AddRefs(selection));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool isSelected = false;
+ selection->IsSelected(aRowIdx, &isSelected);
+ return isSelected;
+}
+
+bool XULTreeGridAccessible::IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) {
+ return IsRowSelected(aRowIdx);
+}
+
+int32_t XULTreeGridAccessible::ColIndexAt(uint32_t aCellIdx) {
+ uint32_t colCount = ColCount();
+ if (colCount < 1 || aCellIdx >= colCount * RowCount()) {
+ return -1; // Error: column count is 0 or index out of bounds.
+ }
+
+ return static_cast<int32_t>(aCellIdx % colCount);
+}
+
+int32_t XULTreeGridAccessible::RowIndexAt(uint32_t aCellIdx) {
+ uint32_t colCount = ColCount();
+ if (colCount < 1 || aCellIdx >= colCount * RowCount()) {
+ return -1; // Error: column count is 0 or index out of bounds.
+ }
+
+ return static_cast<int32_t>(aCellIdx / colCount);
+}
+
+void XULTreeGridAccessible::RowAndColIndicesAt(uint32_t aCellIdx,
+ int32_t* aRowIdx,
+ int32_t* aColIdx) {
+ uint32_t colCount = ColCount();
+ if (colCount < 1 || aCellIdx >= colCount * RowCount()) {
+ *aRowIdx = -1;
+ *aColIdx = -1;
+ return; // Error: column count is 0 or index out of bounds.
+ }
+
+ *aRowIdx = static_cast<int32_t>(aCellIdx / colCount);
+ *aColIdx = static_cast<int32_t>(aCellIdx % colCount);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridAccessible: LocalAccessible implementation
+
+role XULTreeGridAccessible::NativeRole() const {
+ RefPtr<nsTreeColumns> treeColumns = mTree->GetColumns(FlushType::None);
+ if (!treeColumns) {
+ NS_ERROR("No treecolumns object for tree!");
+ return roles::NOTHING;
+ }
+
+ nsTreeColumn* primaryColumn = treeColumns->GetPrimaryColumn();
+
+ return primaryColumn ? roles::TREE_TABLE : roles::TABLE;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridAccessible: XULTreeAccessible implementation
+
+already_AddRefed<XULTreeItemAccessibleBase>
+XULTreeGridAccessible::CreateTreeItemAccessible(int32_t aRow) const {
+ RefPtr<XULTreeItemAccessibleBase> accessible = new XULTreeGridRowAccessible(
+ mContent, mDoc, const_cast<XULTreeGridAccessible*>(this), mTree,
+ mTreeView, aRow);
+
+ return accessible.forget();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridRowAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULTreeGridRowAccessible::XULTreeGridRowAccessible(
+ nsIContent* aContent, DocAccessible* aDoc, LocalAccessible* aTreeAcc,
+ dom::XULTreeElement* aTree, nsITreeView* aTreeView, int32_t aRow)
+ : XULTreeItemAccessibleBase(aContent, aDoc, aTreeAcc, aTree, aTreeView,
+ aRow),
+ mAccessibleCache(kDefaultTreeCacheLength) {
+ mGenericTypes |= eTableRow;
+ mStateFlags |= eNoKidsFromDOM;
+}
+
+XULTreeGridRowAccessible::~XULTreeGridRowAccessible() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridRowAccessible: nsISupports and cycle collection implementation
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(XULTreeGridRowAccessible,
+ XULTreeItemAccessibleBase, mAccessibleCache)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(XULTreeGridRowAccessible)
+NS_INTERFACE_MAP_END_INHERITING(XULTreeItemAccessibleBase)
+
+NS_IMPL_ADDREF_INHERITED(XULTreeGridRowAccessible, XULTreeItemAccessibleBase)
+NS_IMPL_RELEASE_INHERITED(XULTreeGridRowAccessible, XULTreeItemAccessibleBase)
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridRowAccessible: LocalAccessible implementation
+
+void XULTreeGridRowAccessible::Shutdown() {
+ if (mDoc && !mDoc->IsDefunct()) {
+ UnbindCacheEntriesFromDocument(mAccessibleCache);
+ }
+
+ XULTreeItemAccessibleBase::Shutdown();
+}
+
+role XULTreeGridRowAccessible::NativeRole() const { return roles::ROW; }
+
+ENameValueFlag XULTreeGridRowAccessible::Name(nsString& aName) const {
+ aName.Truncate();
+
+ // XXX: the row name sholdn't be a concatenation of cell names (bug 664384).
+ RefPtr<nsTreeColumn> column = nsCoreUtils::GetFirstSensibleColumn(mTree);
+ while (column) {
+ if (!aName.IsEmpty()) aName.Append(' ');
+
+ nsAutoString cellName;
+ GetCellName(column, cellName);
+ aName.Append(cellName);
+
+ column = nsCoreUtils::GetNextSensibleColumn(column);
+ }
+
+ return eNameOK;
+}
+
+LocalAccessible* XULTreeGridRowAccessible::LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) {
+ nsIFrame* frame = GetFrame();
+ if (!frame) return nullptr;
+
+ nsPresContext* presContext = frame->PresContext();
+ PresShell* presShell = presContext->PresShell();
+
+ nsIFrame* rootFrame = presShell->GetRootFrame();
+ NS_ENSURE_TRUE(rootFrame, nullptr);
+
+ CSSIntRect rootRect = rootFrame->GetScreenRect();
+
+ int32_t clientX = presContext->DevPixelsToIntCSSPixels(aX) - rootRect.X();
+ int32_t clientY = presContext->DevPixelsToIntCSSPixels(aY) - rootRect.Y();
+
+ ErrorResult rv;
+ dom::TreeCellInfo cellInfo;
+ mTree->GetCellAt(clientX, clientY, cellInfo, rv);
+
+ // Return if we failed to find tree cell in the row for the given point.
+ if (cellInfo.mRow != mRow || !cellInfo.mCol) return nullptr;
+
+ return GetCellAccessible(cellInfo.mCol);
+}
+
+LocalAccessible* XULTreeGridRowAccessible::LocalChildAt(uint32_t aIndex) const {
+ if (IsDefunct()) return nullptr;
+
+ RefPtr<nsTreeColumn> column = nsCoreUtils::GetSensibleColumnAt(mTree, aIndex);
+ if (!column) return nullptr;
+
+ return GetCellAccessible(column);
+}
+
+uint32_t XULTreeGridRowAccessible::ChildCount() const {
+ return nsCoreUtils::GetSensibleColumnCount(mTree);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridRowAccessible: XULTreeItemAccessibleBase implementation
+
+XULTreeGridCellAccessible* XULTreeGridRowAccessible::GetCellAccessible(
+ nsTreeColumn* aColumn) const {
+ MOZ_ASSERT(aColumn, "No tree column!");
+
+ void* key = static_cast<void*>(aColumn);
+ XULTreeGridCellAccessible* cachedCell = mAccessibleCache.GetWeak(key);
+ if (cachedCell) return cachedCell;
+
+ RefPtr<XULTreeGridCellAccessible> cell = new XULTreeGridCellAccessible(
+ mContent, mDoc, const_cast<XULTreeGridRowAccessible*>(this), mTree,
+ mTreeView, mRow, aColumn);
+ mAccessibleCache.InsertOrUpdate(key, RefPtr{cell});
+ Document()->BindToDocument(cell, nullptr);
+ return cell;
+}
+
+void XULTreeGridRowAccessible::RowInvalidated(int32_t aStartColIdx,
+ int32_t aEndColIdx) {
+ RefPtr<nsTreeColumns> treeColumns = mTree->GetColumns(FlushType::None);
+ if (!treeColumns) return;
+
+ bool nameChanged = false;
+ for (int32_t colIdx = aStartColIdx; colIdx <= aEndColIdx; ++colIdx) {
+ nsTreeColumn* column = treeColumns->GetColumnAt(colIdx);
+ if (column && !nsCoreUtils::IsColumnHidden(column)) {
+ XULTreeGridCellAccessible* cell = GetCellAccessible(column);
+ if (cell) nameChanged |= cell->CellInvalidated();
+ }
+ }
+
+ if (nameChanged) {
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridCellAccessible
+////////////////////////////////////////////////////////////////////////////////
+
+XULTreeGridCellAccessible::XULTreeGridCellAccessible(
+ nsIContent* aContent, DocAccessible* aDoc,
+ XULTreeGridRowAccessible* aRowAcc, dom::XULTreeElement* aTree,
+ nsITreeView* aTreeView, int32_t aRow, nsTreeColumn* aColumn)
+ : LeafAccessible(aContent, aDoc),
+ mTree(aTree),
+ mTreeView(aTreeView),
+ mRow(aRow),
+ mColumn(aColumn) {
+ mParent = aRowAcc;
+ mStateFlags |= eSharedNode;
+ mGenericTypes |= eTableCell;
+
+ NS_ASSERTION(mTreeView, "mTreeView is null");
+
+ if (mColumn->Type() == dom::TreeColumn_Binding::TYPE_CHECKBOX) {
+ mTreeView->GetCellValue(mRow, mColumn, mCachedTextEquiv);
+ } else {
+ mTreeView->GetCellText(mRow, mColumn, mCachedTextEquiv);
+ }
+}
+
+XULTreeGridCellAccessible::~XULTreeGridCellAccessible() {}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridCellAccessible: nsISupports implementation
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(XULTreeGridCellAccessible, LeafAccessible,
+ mTree, mColumn)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(XULTreeGridCellAccessible)
+NS_INTERFACE_MAP_END_INHERITING(LeafAccessible)
+NS_IMPL_ADDREF_INHERITED(XULTreeGridCellAccessible, LeafAccessible)
+NS_IMPL_RELEASE_INHERITED(XULTreeGridCellAccessible, LeafAccessible)
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridCellAccessible: LocalAccessible
+
+void XULTreeGridCellAccessible::Shutdown() {
+ mTree = nullptr;
+ mTreeView = nullptr;
+ mRow = -1;
+ mColumn = nullptr;
+ mParent = nullptr; // null-out to prevent base class's shutdown ops
+
+ LeafAccessible::Shutdown();
+}
+
+Accessible* XULTreeGridCellAccessible::FocusedChild() { return nullptr; }
+
+ENameValueFlag XULTreeGridCellAccessible::Name(nsString& aName) const {
+ aName.Truncate();
+
+ if (!mTreeView) return eNameOK;
+
+ mTreeView->GetCellText(mRow, mColumn, aName);
+
+ // If there is still no name try the cell value:
+ // This is for graphical cells. We need tree/table view implementors to
+ // implement FooView::GetCellValue to return a meaningful string for cases
+ // where there is something shown in the cell (non-text) such as a star icon;
+ // in which case GetCellValue for that cell would return "starred" or
+ // "flagged" for example.
+ if (aName.IsEmpty()) mTreeView->GetCellValue(mRow, mColumn, aName);
+
+ return eNameOK;
+}
+
+nsIntRect XULTreeGridCellAccessible::BoundsInCSSPixels() const {
+ // Get bounds for tree cell and add x and y of treechildren element to
+ // x and y of the cell.
+ nsresult rv;
+ nsIntRect rect = mTree->GetCoordsForCellItem(mRow, mColumn, u"cell"_ns, rv);
+ if (NS_FAILED(rv)) {
+ return nsIntRect();
+ }
+
+ RefPtr<dom::Element> bodyElement = mTree->GetTreeBody();
+ if (!bodyElement || !bodyElement->IsXULElement()) {
+ return nsIntRect();
+ }
+
+ nsIFrame* bodyFrame = bodyElement->GetPrimaryFrame();
+ if (!bodyFrame) {
+ return nsIntRect();
+ }
+
+ CSSIntRect screenRect = bodyFrame->GetScreenRect();
+ rect.x += screenRect.x;
+ rect.y += screenRect.y;
+ return rect;
+}
+
+nsRect XULTreeGridCellAccessible::BoundsInAppUnits() const {
+ nsIntRect bounds = BoundsInCSSPixels();
+ nsPresContext* presContext = mDoc->PresContext();
+ return nsRect(presContext->CSSPixelsToAppUnits(bounds.X()),
+ presContext->CSSPixelsToAppUnits(bounds.Y()),
+ presContext->CSSPixelsToAppUnits(bounds.Width()),
+ presContext->CSSPixelsToAppUnits(bounds.Height()));
+}
+
+bool XULTreeGridCellAccessible::HasPrimaryAction() const {
+ return mColumn->Cycler() ||
+ (mColumn->Type() == dom::TreeColumn_Binding::TYPE_CHECKBOX &&
+ IsEditable());
+}
+
+void XULTreeGridCellAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
+ aName.Truncate();
+
+ if (aIndex != eAction_Click || !mTreeView) return;
+
+ if (mColumn->Cycler()) {
+ aName.AssignLiteral("cycle");
+ return;
+ }
+
+ if (mColumn->Type() == dom::TreeColumn_Binding::TYPE_CHECKBOX &&
+ IsEditable()) {
+ nsAutoString value;
+ mTreeView->GetCellValue(mRow, mColumn, value);
+ if (value.EqualsLiteral("true")) {
+ aName.AssignLiteral("uncheck");
+ } else {
+ aName.AssignLiteral("check");
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridCellAccessible: TableCell
+
+TableAccessible* XULTreeGridCellAccessible::Table() const {
+ LocalAccessible* grandParent = mParent->LocalParent();
+ if (grandParent) return grandParent->AsTable();
+
+ return nullptr;
+}
+
+uint32_t XULTreeGridCellAccessible::ColIdx() const {
+ uint32_t colIdx = 0;
+ RefPtr<nsTreeColumn> column = mColumn;
+ while ((column = nsCoreUtils::GetPreviousSensibleColumn(column))) colIdx++;
+
+ return colIdx;
+}
+
+uint32_t XULTreeGridCellAccessible::RowIdx() const { return mRow; }
+
+void XULTreeGridCellAccessible::ColHeaderCells(
+ nsTArray<Accessible*>* aHeaderCells) {
+ dom::Element* columnElm = mColumn->Element();
+
+ LocalAccessible* headerCell = mDoc->GetAccessible(columnElm);
+ if (headerCell) aHeaderCells->AppendElement(headerCell);
+}
+
+bool XULTreeGridCellAccessible::Selected() {
+ nsCOMPtr<nsITreeSelection> selection;
+ nsresult rv = mTreeView->GetSelection(getter_AddRefs(selection));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool selected = false;
+ selection->IsSelected(mRow, &selected);
+ return selected;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridCellAccessible: LocalAccessible public implementation
+
+already_AddRefed<AccAttributes> XULTreeGridCellAccessible::NativeAttributes() {
+ RefPtr<AccAttributes> attributes = new AccAttributes();
+
+ // "table-cell-index" attribute
+ TableAccessible* table = Table();
+ if (!table) return attributes.forget();
+
+ attributes->SetAttribute(nsGkAtoms::tableCellIndex,
+ table->CellIndexAt(mRow, ColIdx()));
+
+ // "cycles" attribute
+ if (mColumn->Cycler()) {
+ attributes->SetAttribute(nsGkAtoms::cycles, true);
+ }
+
+ return attributes.forget();
+}
+
+role XULTreeGridCellAccessible::NativeRole() const { return roles::GRID_CELL; }
+
+uint64_t XULTreeGridCellAccessible::NativeState() const {
+ if (!mTreeView) return states::DEFUNCT;
+
+ // selectable/selected state
+ uint64_t states =
+ states::SELECTABLE; // keep in sync with NativeInteractiveState
+
+ nsCOMPtr<nsITreeSelection> selection;
+ mTreeView->GetSelection(getter_AddRefs(selection));
+ if (selection) {
+ bool isSelected = false;
+ selection->IsSelected(mRow, &isSelected);
+ if (isSelected) states |= states::SELECTED;
+ }
+
+ // checked state
+ if (mColumn->Type() == dom::TreeColumn_Binding::TYPE_CHECKBOX) {
+ states |= states::CHECKABLE;
+ nsAutoString checked;
+ mTreeView->GetCellValue(mRow, mColumn, checked);
+ if (checked.EqualsIgnoreCase("true")) states |= states::CHECKED;
+ }
+
+ return states;
+}
+
+uint64_t XULTreeGridCellAccessible::NativeInteractiveState() const {
+ return states::SELECTABLE;
+}
+
+int32_t XULTreeGridCellAccessible::IndexInParent() const { return ColIdx(); }
+
+Relation XULTreeGridCellAccessible::RelationByType(RelationType aType) const {
+ return Relation();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridCellAccessible: public implementation
+
+bool XULTreeGridCellAccessible::CellInvalidated() {
+ nsAutoString textEquiv;
+
+ if (mColumn->Type() == dom::TreeColumn_Binding::TYPE_CHECKBOX) {
+ mTreeView->GetCellValue(mRow, mColumn, textEquiv);
+ if (mCachedTextEquiv != textEquiv) {
+ bool isEnabled = textEquiv.EqualsLiteral("true");
+ RefPtr<AccEvent> accEvent =
+ new AccStateChangeEvent(this, states::CHECKED, isEnabled);
+ nsEventShell::FireEvent(accEvent);
+
+ mCachedTextEquiv = textEquiv;
+ return true;
+ }
+
+ return false;
+ }
+
+ mTreeView->GetCellText(mRow, mColumn, textEquiv);
+ if (mCachedTextEquiv != textEquiv) {
+ nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, this);
+ mCachedTextEquiv = textEquiv;
+ return true;
+ }
+
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridCellAccessible: LocalAccessible protected implementation
+
+LocalAccessible* XULTreeGridCellAccessible::GetSiblingAtOffset(
+ int32_t aOffset, nsresult* aError) const {
+ if (aError) *aError = NS_OK; // fail peacefully
+
+ RefPtr<nsTreeColumn> columnAtOffset(mColumn), column;
+ if (aOffset < 0) {
+ for (int32_t index = aOffset; index < 0 && columnAtOffset; index++) {
+ column = nsCoreUtils::GetPreviousSensibleColumn(columnAtOffset);
+ column.swap(columnAtOffset);
+ }
+ } else {
+ for (int32_t index = aOffset; index > 0 && columnAtOffset; index--) {
+ column = nsCoreUtils::GetNextSensibleColumn(columnAtOffset);
+ column.swap(columnAtOffset);
+ }
+ }
+
+ if (!columnAtOffset) return nullptr;
+
+ XULTreeItemAccessibleBase* rowAcc =
+ static_cast<XULTreeItemAccessibleBase*>(LocalParent());
+ return rowAcc->GetCellAccessible(columnAtOffset);
+}
+
+void XULTreeGridCellAccessible::DispatchClickEvent(
+ nsIContent* aContent, uint32_t aActionIndex) const {
+ if (IsDefunct()) return;
+
+ RefPtr<dom::XULTreeElement> tree = mTree;
+ RefPtr<nsTreeColumn> column = mColumn;
+ nsCoreUtils::DispatchClickEvent(tree, mRow, column);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// XULTreeGridCellAccessible: protected implementation
+
+bool XULTreeGridCellAccessible::IsEditable() const {
+ // XXX: logic corresponds to tree.xml, it's preferable to have interface
+ // method to check it.
+ bool isEditable = false;
+ nsresult rv = mTreeView->IsEditable(mRow, mColumn, &isEditable);
+ if (NS_FAILED(rv) || !isEditable) return false;
+
+ dom::Element* columnElm = mColumn->Element();
+
+ if (!columnElm->AttrValueIs(kNameSpaceID_None, nsGkAtoms::editable,
+ nsGkAtoms::_true, eCaseMatters)) {
+ return false;
+ }
+
+ return mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::editable, nsGkAtoms::_true, eCaseMatters);
+}
diff --git a/accessible/xul/XULTreeGridAccessible.h b/accessible/xul/XULTreeGridAccessible.h
new file mode 100644
index 0000000000..cfb314d30e
--- /dev/null
+++ b/accessible/xul/XULTreeGridAccessible.h
@@ -0,0 +1,193 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* -*- 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_XULTreeGridAccessible_h__
+#define mozilla_a11y_XULTreeGridAccessible_h__
+
+#include "mozilla/a11y/TableAccessible.h"
+#include "mozilla/a11y/TableCellAccessible.h"
+#include "XULTreeAccessible.h"
+
+namespace mozilla {
+namespace a11y {
+
+class XULTreeGridCellAccessible;
+
+/**
+ * Represents accessible for XUL tree in the case when it has multiple columns.
+ */
+class XULTreeGridAccessible : public XULTreeAccessible, public TableAccessible {
+ public:
+ XULTreeGridAccessible(nsIContent* aContent, DocAccessible* aDoc,
+ nsTreeBodyFrame* aTreeFrame)
+ : XULTreeAccessible(aContent, aDoc, aTreeFrame) {
+ mGenericTypes |= eTable;
+ }
+
+ // TableAccessible
+ virtual uint32_t ColCount() const override;
+ virtual uint32_t RowCount() override;
+ virtual LocalAccessible* CellAt(uint32_t aRowIndex,
+ uint32_t aColumnIndex) override;
+ virtual void ColDescription(uint32_t aColIdx,
+ nsString& aDescription) override;
+ virtual bool IsColSelected(uint32_t aColIdx) override;
+ virtual bool IsRowSelected(uint32_t aRowIdx) override;
+ virtual bool IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) override;
+ virtual uint32_t SelectedCellCount() override;
+ virtual uint32_t SelectedColCount() override;
+ virtual uint32_t SelectedRowCount() override;
+ virtual void SelectedCells(nsTArray<Accessible*>* aCells) override;
+ virtual void SelectedCellIndices(nsTArray<uint32_t>* aCells) override;
+ virtual void SelectedColIndices(nsTArray<uint32_t>* aCols) override;
+ virtual void SelectedRowIndices(nsTArray<uint32_t>* aRows) override;
+ virtual LocalAccessible* AsAccessible() override { return this; }
+
+ virtual int32_t CellIndexAt(uint32_t aRowIdx, uint32_t aColIdx) override {
+ return static_cast<int32_t>(ColCount() * aRowIdx + aColIdx);
+ }
+
+ virtual int32_t ColIndexAt(uint32_t aCellIdx) override;
+ virtual int32_t RowIndexAt(uint32_t aCellIdx) override;
+ virtual void RowAndColIndicesAt(uint32_t aCellIdx, int32_t* aRowIdx,
+ int32_t* aColIdx) override;
+
+ // LocalAccessible
+ virtual TableAccessible* AsTable() override { return this; }
+ virtual a11y::role NativeRole() const override;
+
+ protected:
+ virtual ~XULTreeGridAccessible();
+
+ // XULTreeAccessible
+ virtual already_AddRefed<XULTreeItemAccessibleBase> CreateTreeItemAccessible(
+ int32_t aRow) const override;
+};
+
+/**
+ * Represents accessible for XUL tree item in the case when XUL tree has
+ * multiple columns.
+ */
+class XULTreeGridRowAccessible final : public XULTreeItemAccessibleBase {
+ public:
+ using LocalAccessible::LocalChildAt;
+
+ XULTreeGridRowAccessible(nsIContent* aContent, DocAccessible* aDoc,
+ LocalAccessible* aParent, dom::XULTreeElement* aTree,
+ nsITreeView* aTreeView, int32_t aRow);
+
+ // nsISupports and cycle collection
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULTreeGridRowAccessible,
+ XULTreeItemAccessibleBase)
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual a11y::role NativeRole() const override;
+ virtual ENameValueFlag Name(nsString& aName) const override;
+ virtual LocalAccessible* LocalChildAtPoint(
+ int32_t aX, int32_t aY, EWhichChildAtPoint aWhichChild) override;
+
+ virtual LocalAccessible* LocalChildAt(uint32_t aIndex) const override;
+ virtual uint32_t ChildCount() const override;
+
+ // XULTreeItemAccessibleBase
+ XULTreeGridCellAccessible* GetCellAccessible(
+ nsTreeColumn* aColumn) const final;
+ virtual void RowInvalidated(int32_t aStartColIdx,
+ int32_t aEndColIdx) override;
+
+ protected:
+ virtual ~XULTreeGridRowAccessible();
+
+ // XULTreeItemAccessibleBase
+ mutable nsRefPtrHashtable<nsPtrHashKey<const void>, XULTreeGridCellAccessible>
+ mAccessibleCache;
+};
+
+/**
+ * Represents an accessible for XUL tree cell in the case when XUL tree has
+ * multiple columns.
+ */
+
+class XULTreeGridCellAccessible : public LeafAccessible,
+ public TableCellAccessible {
+ public:
+ XULTreeGridCellAccessible(nsIContent* aContent, DocAccessible* aDoc,
+ XULTreeGridRowAccessible* aRowAcc,
+ dom::XULTreeElement* aTree, nsITreeView* aTreeView,
+ int32_t aRow, nsTreeColumn* aColumn);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULTreeGridCellAccessible,
+ LeafAccessible)
+
+ // LocalAccessible
+ virtual void Shutdown() override;
+ virtual TableCellAccessible* AsTableCell() override { return this; }
+ virtual nsRect BoundsInAppUnits() const override;
+ virtual nsIntRect BoundsInCSSPixels() const override;
+ virtual ENameValueFlag Name(nsString& aName) const override;
+ virtual Accessible* FocusedChild() override;
+ virtual already_AddRefed<AccAttributes> NativeAttributes() override;
+ virtual int32_t IndexInParent() const override;
+ virtual Relation RelationByType(RelationType aType) const override;
+ virtual a11y::role NativeRole() const override;
+ virtual uint64_t NativeState() const override;
+ virtual uint64_t NativeInteractiveState() const override;
+
+ // ActionAccessible
+ virtual bool HasPrimaryAction() const override;
+ virtual void ActionNameAt(uint8_t aIndex, nsAString& aName) override;
+
+ // TableCellAccessible
+ virtual TableAccessible* Table() const override;
+ virtual uint32_t ColIdx() const override;
+ virtual uint32_t RowIdx() const override;
+ virtual void ColHeaderCells(nsTArray<Accessible*>* aHeaderCells) override;
+ virtual void RowHeaderCells(nsTArray<Accessible*>* aCells) override {}
+ virtual bool Selected() override;
+
+ /**
+ * Fire name or state change event if the accessible text or value has been
+ * changed.
+ * @return true if name has changed
+ */
+ bool CellInvalidated();
+
+ protected:
+ virtual ~XULTreeGridCellAccessible();
+
+ // LocalAccessible
+ virtual LocalAccessible* GetSiblingAtOffset(
+ int32_t aOffset, nsresult* aError = nullptr) const override;
+ MOZ_CAN_RUN_SCRIPT
+ virtual void DispatchClickEvent(nsIContent* aContent,
+ uint32_t aActionIndex) const override;
+
+ // XULTreeGridCellAccessible
+
+ /**
+ * Return true if value of cell can be modified.
+ */
+ bool IsEditable() const;
+
+ enum { eAction_Click = 0 };
+
+ RefPtr<dom::XULTreeElement> mTree;
+ nsITreeView* mTreeView;
+
+ int32_t mRow;
+ RefPtr<nsTreeColumn> mColumn;
+
+ nsString mCachedTextEquiv;
+};
+
+} // namespace a11y
+} // namespace mozilla
+
+#endif
diff --git a/accessible/xul/moz.build b/accessible/xul/moz.build
new file mode 100644
index 0000000000..4fccfff6e0
--- /dev/null
+++ b/accessible/xul/moz.build
@@ -0,0 +1,56 @@
+# -*- 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/.
+
+UNIFIED_SOURCES += [
+ "XULAlertAccessible.cpp",
+ "XULComboboxAccessible.cpp",
+ "XULElementAccessibles.cpp",
+ "XULFormControlAccessible.cpp",
+ "XULListboxAccessible.cpp",
+ "XULMenuAccessible.cpp",
+ "XULSelectControlAccessible.cpp",
+ "XULTabAccessible.cpp",
+ "XULTreeAccessible.cpp",
+ "XULTreeGridAccessible.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/accessible/base",
+ "/accessible/generic",
+ "/accessible/html",
+ "/accessible/xpcom",
+ "/dom/base",
+ "/dom/xul",
+ "/layout/generic",
+ "/layout/xul",
+ "/layout/xul/tree",
+]
+
+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"