diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/ui/accessibility/platform | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-chromium-85-based.tar.gz |
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/ui/accessibility/platform')
30 files changed, 1548 insertions, 277 deletions
diff --git a/chromium/ui/accessibility/platform/BUILD.gn b/chromium/ui/accessibility/platform/BUILD.gn new file mode 100644 index 00000000000..691a701457d --- /dev/null +++ b/chromium/ui/accessibility/platform/BUILD.gn @@ -0,0 +1,142 @@ +# Copyright 2020 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//build/config/features.gni") +import("//build/config/jumbo.gni") +import("//build/config/linux/pkg_config.gni") +import("//build/config/ui.gni") +import("//mojo/public/tools/bindings/mojom.gni") +import("//testing/libfuzzer/fuzzer_test.gni") +import("//testing/test.gni") +import("//tools/json_schema_compiler/json_schema_api.gni") +import("//ui/base/ui_features.gni") + +if (is_win) { + import("//build/toolchain/win/midl.gni") +} + +if (is_win) { + midl("ichromeaccessible") { + sources = [ "ichromeaccessible.idl" ] + } +} + +if (is_android) { + import("//build/config/android/rules.gni") +} + +source_set("platform") { + defines = [ "AX_IMPLEMENTATION" ] + + visibility = [ "//ui/accessibility" ] + + sources = [ + # Used by by browser_accessibility_state_impl.cc. + "ax_platform_node.cc", + "ax_platform_node.h", + "ax_platform_node_delegate.h", + + # Used by browser_accessibility.cc. + "ax_unique_id.cc", + "ax_unique_id.h", + + # Used by accessibility_tree_formatter_blink.cc. + "compute_attributes.cc", + "compute_attributes.h", + + # Used by //ui/accessibility:ax_assistant. + "ax_android_constants.cc", + "ax_android_constants.h", + + # Used by //ui/views/views/ax_virtual_view.h. + "ax_platform_node_base.cc", + "ax_platform_node_base.h", + "ax_platform_node_delegate_base.cc", + "ax_platform_node_delegate_base.h", + + # Used by //chrome/test/browser_tests/browser_view_browsertest.cc + "ax_platform_node_test_helper.cc", + "ax_platform_node_test_helper.h", + ] + + public_deps = [ + "//ui/accessibility:ax_base", + "//ui/display", + ] + + if (has_native_accessibility) { + sources += [ + "ax_fragment_root_delegate_win.h", + "ax_fragment_root_win.cc", + "ax_fragment_root_win.h", + "ax_platform_node_delegate_utils_win.cc", + "ax_platform_node_delegate_utils_win.h", + "ax_platform_node_textchildprovider_win.cc", + "ax_platform_node_textchildprovider_win.h", + "ax_platform_node_textprovider_win.cc", + "ax_platform_node_textprovider_win.h", + "ax_platform_node_textrangeprovider_win.cc", + "ax_platform_node_textrangeprovider_win.h", + "ax_platform_node_win.cc", + "ax_platform_node_win.h", + "ax_platform_relation_win.cc", + "ax_platform_relation_win.h", + "ax_platform_text_boundary.cc", + "ax_platform_text_boundary.h", + "ax_system_caret_win.cc", + "ax_system_caret_win.h", + "uia_registrar_win.cc", + "uia_registrar_win.h", + ] + + if (is_win) { + public_deps += [ + "//third_party/iaccessible2", + "//ui/accessibility/platform:ichromeaccessible", + ] + + libs = [ + "oleacc.lib", + "uiautomationcore.lib", + ] + } + + if (is_mac) { + sources += [ + "ax_platform_node_mac.h", + "ax_platform_node_mac.mm", + ] + + libs = [ + "AppKit.framework", + "Foundation.framework", + ] + } + + if (use_atk) { + sources += [ + "atk_util_auralinux.cc", + "atk_util_auralinux.h", + "atk_util_auralinux_gtk.cc", + "ax_platform_atk_hyperlink.cc", + "ax_platform_atk_hyperlink.h", + "ax_platform_node_auralinux.cc", + "ax_platform_node_auralinux.h", + ] + + # ax_platform_text_boundary.h includes atk.h, so ATK is needed + # as a public config to ensure anything that includes this is + # able to find atk.h. + public_configs = [ "//build/config/linux/atk" ] + + if (use_glib) { + configs += [ "//build/config/linux:glib" ] + } + + if (use_x11) { + public_deps += [ "//ui/gfx/x" ] + } + } + } +} diff --git a/chromium/ui/accessibility/platform/ax_fragment_root_win.cc b/chromium/ui/accessibility/platform/ax_fragment_root_win.cc index f1925832840..163fb8446dd 100644 --- a/chromium/ui/accessibility/platform/ax_fragment_root_win.cc +++ b/chromium/ui/accessibility/platform/ax_fragment_root_win.cc @@ -7,17 +7,21 @@ #include <unordered_map> #include "base/no_destructor.h" +#include "base/strings/string_number_conversions.h" #include "ui/accessibility/platform/ax_fragment_root_delegate_win.h" #include "ui/accessibility/platform/ax_platform_node_win.h" +#include "ui/accessibility/platform/uia_registrar_win.h" #include "ui/base/win/atl_module.h" namespace ui { class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, + public IItemContainerProvider, public IRawElementProviderFragmentRoot, public IRawElementProviderAdviseEvents { public: BEGIN_COM_MAP(AXFragmentRootPlatformNodeWin) + COM_INTERFACE_ENTRY(IItemContainerProvider) COM_INTERFACE_ENTRY(IRawElementProviderFragmentRoot) COM_INTERFACE_ENTRY(IRawElementProviderAdviseEvents) COM_INTERFACE_ENTRY_CHAIN(AXPlatformNodeWin) @@ -38,19 +42,76 @@ class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, } // + // IItemContainerProvider methods. + // + IFACEMETHODIMP FindItemByProperty( + IRawElementProviderSimple* start_after_element, + PROPERTYID property_id, + VARIANT value, + IRawElementProviderSimple** result) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ITEMCONTAINER_FINDITEMBYPROPERTY); + UIA_VALIDATE_CALL_1_ARG(result); + *result = nullptr; + + // We currently only support the custom UIA property ID for unique id and we + // ignore |start_after_element|. + if (property_id == + UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId() && + value.vt == VT_BSTR) { + // TODO: We should support the case when |start_after_element| isn't + // nullptr for unique id (https://crbug.com/1098160). + if (start_after_element) + return E_INVALIDARG; + + int32_t ax_unique_id; + if (!base::StringToInt(value.bstrVal, &ax_unique_id)) + return S_OK; + + // In the Windows accessibility platform implementation, id 0 represents + // self; a positive id represents the immediate descendants; and a + // negative id represents a unique id that can be mapped to any node. + if (AXPlatformNodeWin* node_win = + static_cast<AXPlatformNodeWin*>(GetFromUniqueId(-ax_unique_id))) { + node_win->QueryInterface(IID_PPV_ARGS(result)); + } + + return S_OK; + } + + return E_INVALIDARG; + } + + // // IRawElementProviderSimple methods. // IFACEMETHODIMP get_HostRawElementProvider( IRawElementProviderSimple** host_element_provider) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_HOST_RAW_ELEMENT_PROVIDER); UIA_VALIDATE_CALL_1_ARG(host_element_provider); HWND hwnd = GetDelegate()->GetTargetForNativeAccessibilityEvent(); return UiaHostProviderFromHwnd(hwnd, host_element_provider); } + IFACEMETHODIMP GetPatternProvider(PATTERNID pattern_id, + IUnknown** result) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_PATTERN_PROVIDER); + UIA_VALIDATE_CALL_1_ARG(result); + *result = nullptr; + + if (pattern_id == UIA_ItemContainerPatternId) { + AddRef(); + *result = static_cast<IItemContainerProvider*>(this); + return S_OK; + } + + return AXPlatformNodeWin::GetPatternProviderImpl(pattern_id, result); + } + IFACEMETHODIMP GetPropertyValue(PROPERTYID property_id, VARIANT* result) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_PROPERTY_VALUE); UIA_VALIDATE_CALL_1_ARG(result); switch (property_id) { @@ -84,6 +145,7 @@ class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, IFACEMETHODIMP get_FragmentRoot( IRawElementProviderFragmentRoot** fragment_root) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_FRAGMENTROOT); UIA_VALIDATE_CALL_1_ARG(fragment_root); QueryInterface(IID_PPV_ARGS(fragment_root)); @@ -97,6 +159,7 @@ class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, double screen_physical_pixel_x, double screen_physical_pixel_y, IRawElementProviderFragment** element_provider) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ELEMENT_PROVIDER_FROM_POINT); UIA_VALIDATE_CALL_1_ARG(element_provider); *element_provider = nullptr; @@ -124,6 +187,7 @@ class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, } IFACEMETHODIMP GetFocus(IRawElementProviderFragment** focus) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_FOCUS); UIA_VALIDATE_CALL_1_ARG(focus); *focus = nullptr; @@ -157,6 +221,7 @@ class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, // IFACEMETHODIMP AdviseEventAdded(EVENTID event_id, SAFEARRAY* property_ids) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ADVISE_EVENT_ADDED); if (event_id == UIA_LiveRegionChangedEventId) { live_region_change_listeners_++; @@ -179,6 +244,7 @@ class AXFragmentRootPlatformNodeWin : public AXPlatformNodeWin, IFACEMETHODIMP AdviseEventRemoved(EVENTID event_id, SAFEARRAY* property_ids) override { + WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_ADVISE_EVENT_REMOVED); if (event_id == UIA_LiveRegionChangedEventId) { DCHECK(live_region_change_listeners_ > 0); live_region_change_listeners_--; diff --git a/chromium/ui/accessibility/platform/ax_fragment_root_win_unittest.cc b/chromium/ui/accessibility/platform/ax_fragment_root_win_unittest.cc index 7691d12cece..e26ad0446c1 100644 --- a/chromium/ui/accessibility/platform/ax_fragment_root_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_fragment_root_win_unittest.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "ui/accessibility/platform/ax_fragment_root_win.h" +#include "ui/accessibility/accessibility_switches.h" #include "ui/accessibility/platform/ax_platform_node_win.h" #include "ui/accessibility/platform/ax_platform_node_win_unittest.h" #include "ui/accessibility/platform/test_ax_node_wrapper.h" @@ -14,11 +15,26 @@ #include "base/win/scoped_safearray.h" #include "base/win/scoped_variant.h" #include "testing/gtest/include/gtest/gtest.h" +#include "ui/accessibility/platform/uia_registrar_win.h" +using base::win::ScopedVariant; using Microsoft::WRL::ComPtr; namespace ui { +#define EXPECT_UIA_BSTR_EQ(node, property_id, expected) \ + { \ + ScopedVariant expectedVariant(expected); \ + ASSERT_EQ(VT_BSTR, expectedVariant.type()); \ + ASSERT_NE(nullptr, expectedVariant.ptr()->bstrVal); \ + ScopedVariant actual; \ + ASSERT_HRESULT_SUCCEEDED( \ + node->GetPropertyValue(property_id, actual.Receive())); \ + ASSERT_EQ(VT_BSTR, actual.type()); \ + ASSERT_NE(nullptr, actual.ptr()->bstrVal); \ + EXPECT_STREQ(expectedVariant.ptr()->bstrVal, actual.ptr()->bstrVal); \ + } + class AXFragmentRootTest : public AXPlatformNodeWinTest { public: AXFragmentRootTest() = default; @@ -27,6 +43,92 @@ class AXFragmentRootTest : public AXPlatformNodeWinTest { AXFragmentRootTest& operator=(const AXFragmentRootTest&) = delete; }; +TEST_F(AXFragmentRootTest, UIAFindItemByProperty) { + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; + root.SetName("root"); + root.child_ids = {2, 3}; + + AXNodeData text1; + text1.id = 2; + text1.role = ax::mojom::Role::kStaticText; + text1.SetName("text1"); + + AXNodeData button; + button.id = 3; + button.role = ax::mojom::Role::kButton; + button.SetName("button"); + button.child_ids = {4}; + + AXNodeData text2; + text2.id = 4; + text2.role = ax::mojom::Role::kStaticText; + text2.SetName("text2"); + + Init(root, text1, button, text2); + InitFragmentRoot(); + + ComPtr<IRawElementProviderSimple> raw_element_provider_simple; + ax_fragment_root_->GetNativeViewAccessible()->QueryInterface( + IID_PPV_ARGS(&raw_element_provider_simple)); + + ComPtr<IItemContainerProvider> item_container_provider; + EXPECT_HRESULT_SUCCEEDED(raw_element_provider_simple->GetPatternProvider( + UIA_ItemContainerPatternId, &item_container_provider)); + ASSERT_NE(nullptr, item_container_provider.Get()); + + // Fetch the AxUniqueId of "root", and verify we can retrieve its + // corresponding IRawElementProviderSimple through FindItemByProperty(). + ScopedVariant unique_id_variant; + int32_t unique_id = AXPlatformNodeFromNode(GetRootAsAXNode())->GetUniqueId(); + unique_id_variant.Set( + SysAllocString(base::NumberToString16(-unique_id).c_str())); + ComPtr<IRawElementProviderSimple> result; + EXPECT_HRESULT_SUCCEEDED(item_container_provider->FindItemByProperty( + nullptr, UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId(), + unique_id_variant, &result)); + EXPECT_UIA_BSTR_EQ(result, UIA_NamePropertyId, L"root"); + result.Reset(); + unique_id_variant.Release(); + + // Fetch the AxUniqueId of "text1", and verify we can retrieve its + // corresponding IRawElementProviderSimple through FindItemByProperty(). + unique_id = + AXPlatformNodeFromNode(GetRootAsAXNode()->children()[0])->GetUniqueId(); + unique_id_variant.Set( + SysAllocString(base::NumberToString16(-unique_id).c_str())); + EXPECT_HRESULT_SUCCEEDED(item_container_provider->FindItemByProperty( + nullptr, UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId(), + unique_id_variant, &result)); + EXPECT_UIA_BSTR_EQ(result, UIA_NamePropertyId, L"text1"); + result.Reset(); + unique_id_variant.Release(); + + // Fetch the AxUniqueId of "button", and verify we can retrieve its + // corresponding IRawElementProviderSimple through FindItemByProperty(). + AXNode* button_node = GetRootAsAXNode()->children()[1]; + unique_id = AXPlatformNodeFromNode(button_node)->GetUniqueId(); + unique_id_variant.Set( + SysAllocString(base::NumberToString16(-unique_id).c_str())); + EXPECT_HRESULT_SUCCEEDED(item_container_provider->FindItemByProperty( + nullptr, UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId(), + unique_id_variant, &result)); + EXPECT_UIA_BSTR_EQ(result, UIA_NamePropertyId, L"button"); + result.Reset(); + unique_id_variant.Release(); + + // Fetch the AxUniqueId of "text2", and verify we can retrieve its + // corresponding IRawElementProviderSimple through FindItemByProperty(). + unique_id = AXPlatformNodeFromNode(button_node->children()[0])->GetUniqueId(); + unique_id_variant.Set( + SysAllocString(base::NumberToString16(-unique_id).c_str())); + EXPECT_HRESULT_SUCCEEDED(item_container_provider->FindItemByProperty( + nullptr, UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId(), + unique_id_variant, &result)); + EXPECT_UIA_BSTR_EQ(result, UIA_NamePropertyId, L"text2"); +} + TEST_F(AXFragmentRootTest, TestUIAGetFragmentRoot) { AXNodeData root; Init(root); @@ -227,7 +329,7 @@ TEST_F(AXFragmentRootTest, TestGetPropertyValue) { // IsControlElement and IsContentElement should follow the setting on the // fragment root delegate. test_fragment_root_delegate_->is_control_element_ = true; - base::win::ScopedVariant result; + ScopedVariant result; EXPECT_HRESULT_SUCCEEDED(root_provider->GetPropertyValue( UIA_IsControlElementPropertyId, result.Receive())); EXPECT_EQ(result.type(), VT_BOOL); diff --git a/chromium/ui/accessibility/platform/ax_platform_node.h b/chromium/ui/accessibility/platform/ax_platform_node.h index c5198fc792e..9621fcb9a5c 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node.h +++ b/chromium/ui/accessibility/platform/ax_platform_node.h @@ -92,7 +92,7 @@ class AX_EXPORT AXPlatformNode { // Return true if this object is equal to or a descendant of |ancestor|. virtual bool IsDescendantOf(AXPlatformNode* ancestor) const = 0; - // Return the unique ID + // Return the unique ID. int32_t GetUniqueId() const; // Creates a string representation of this node's data. diff --git a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc index 89309020096..a4554aaac25 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc @@ -17,6 +17,7 @@ #include "base/command_line.h" #include "base/compiler_specific.h" #include "base/debug/leak_annotations.h" +#include "base/metrics/histogram_macros.h" #include "base/no_destructor.h" #include "base/numerics/ranges.h" #include "base/optional.h" @@ -73,6 +74,29 @@ namespace ui { namespace { +// IMPORTANT! +// These values are written to logs. Do not renumber or delete +// existing items; add new entries to the end of the list. +enum class UmaAtkApi { + kGetName = 0, + kGetDescription = 1, + kGetNChildren = 2, + kRefChild = 3, + kGetIndexInParent = 4, + kGetParent = 5, + kRefRelationSet = 6, + kGetAttributes = 7, + kGetRole = 8, + kRefStateSet = 9, + // This must always be the last enum. It's okay for its value to + // increase, but none of the other enum values may change. + kMaxValue = kRefStateSet, +}; + +void RecordAccessibilityAtkApi(UmaAtkApi enum_value) { + UMA_HISTOGRAM_ENUMERATION("Accessibility.ATK-APIs", enum_value); +} + // When accepting input from clients calling the API, an ATK character offset // of -1 can often represent the length of the string. static const int kStringLengthOffset = -1; @@ -186,13 +210,17 @@ void SetIntPointerValueIfNotNull(int* pointer, int value) { *pointer = value; } +#if defined(ATK_230) bool SupportsAtkComponentScrollingInterface() { return dlsym(RTLD_DEFAULT, "atk_component_scroll_to_point"); } +#endif +#if defined(ATK_232) bool SupportsAtkTextScrollingInterface() { return dlsym(RTLD_DEFAULT, "atk_text_scroll_substring_to_point"); } +#endif AtkObject* FindAtkObjectParentFrame(AtkObject* atk_object) { AXPlatformNodeAuraLinux* node = @@ -1246,6 +1274,7 @@ char* GetStringAtOffset(AtkText* atk_text, } #endif +#if defined(ATK_230) gfx::Rect GetUnclippedParentHypertextRangeBoundsRect( AXPlatformNodeDelegate* ax_platform_node_delegate, const int start_offset, @@ -1269,6 +1298,7 @@ gfx::Rect GetUnclippedParentHypertextRangeBoundsRect( AXClippingBehavior::kClipped) .OffsetFromOrigin(); } +#endif void GetCharacterExtents(AtkText* atk_text, int offset, @@ -2028,6 +2058,7 @@ const gchar* GetName(AtkObject* atk_object) { } const gchar* AtkGetName(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetName); AXPlatformNodeAuraLinux::EnableAXMode(); return GetName(atk_object); } @@ -2045,6 +2076,7 @@ const gchar* GetDescription(AtkObject* atk_object) { } const gchar* AtkGetDescription(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetDescription); AXPlatformNodeAuraLinux::EnableAXMode(); return GetDescription(atk_object); } @@ -2061,6 +2093,7 @@ gint GetNChildren(AtkObject* atk_object) { } gint AtkGetNChildren(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetNChildren); AXPlatformNodeAuraLinux::EnableAXMode(); return GetNChildren(atk_object); } @@ -2083,6 +2116,7 @@ AtkObject* RefChild(AtkObject* atk_object, gint index) { } AtkObject* AtkRefChild(AtkObject* atk_object, gint index) { + RecordAccessibilityAtkApi(UmaAtkApi::kRefChild); AXPlatformNodeAuraLinux::EnableAXMode(); return RefChild(atk_object, index); } @@ -2099,6 +2133,7 @@ gint GetIndexInParent(AtkObject* atk_object) { } gint AtkGetIndexInParent(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetIndexInParent); AXPlatformNodeAuraLinux::EnableAXMode(); return GetIndexInParent(atk_object); } @@ -2115,6 +2150,7 @@ AtkObject* GetParent(AtkObject* atk_object) { } AtkObject* AtkGetParent(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetParent); AXPlatformNodeAuraLinux::EnableAXMode(); return GetParent(atk_object); } @@ -2130,6 +2166,7 @@ AtkRelationSet* RefRelationSet(AtkObject* atk_object) { } AtkRelationSet* AtkRefRelationSet(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kRefRelationSet); AXPlatformNodeAuraLinux::EnableAXMode(); return RefRelationSet(atk_object); } @@ -2146,6 +2183,7 @@ AtkAttributeSet* GetAttributes(AtkObject* atk_object) { } AtkAttributeSet* AtkGetAttributes(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetAttributes); AXPlatformNodeAuraLinux::EnableAXMode(); return GetAttributes(atk_object); } @@ -2161,6 +2199,7 @@ AtkRole GetRole(AtkObject* atk_object) { } AtkRole AtkGetRole(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kGetRole); AXPlatformNodeAuraLinux::EnableAXMode(); return GetRole(atk_object); } @@ -2183,6 +2222,7 @@ AtkStateSet* RefStateSet(AtkObject* atk_object) { } AtkStateSet* AtkRefStateSet(AtkObject* atk_object) { + RecordAccessibilityAtkApi(UmaAtkApi::kRefStateSet); AXPlatformNodeAuraLinux::EnableAXMode(); return RefStateSet(atk_object); } @@ -2468,6 +2508,8 @@ AtkObject* AXPlatformNodeAuraLinux::CreateAtkObject() { if (GetData().role != ax::mojom::Role::kApplication && !GetAccessibilityMode().has_mode(AXMode::kNativeAPIs)) return nullptr; + if (GetDelegate()->IsChildOfLeaf()) + return nullptr; EnsureGTypeInit(); interface_mask_ = GetGTypeInterfaceMask(GetData()); GType type = GetAccessibilityGType(); @@ -3000,10 +3042,8 @@ void AXPlatformNodeAuraLinux::GetAtkState(AtkStateSet* atk_state_set) { static_cast<int32_t>(ax::mojom::InvalidState::kFalse)) atk_state_set_add_state(atk_state_set, ATK_STATE_INVALID_ENTRY); #if defined(ATK_216) - if (data.HasIntAttribute(ax::mojom::IntAttribute::kCheckedState) && - data.role != ax::mojom::Role::kToggleButton) { + if (IsPlatformCheckable()) atk_state_set_add_state(atk_state_set, ATK_STATE_CHECKABLE); - } if (data.HasIntAttribute(ax::mojom::IntAttribute::kHasPopup)) atk_state_set_add_state(atk_state_set, ATK_STATE_HAS_POPUP); #endif @@ -3217,6 +3257,13 @@ void AXPlatformNodeAuraLinux::Init(AXPlatformNodeDelegate* delegate) { GetOrCreateAtkObject(); } +bool AXPlatformNodeAuraLinux::IsPlatformCheckable() const { + if (GetData().role == ax::mojom::Role::kToggleButton) + return false; + + return AXPlatformNodeBase::IsPlatformCheckable(); +} + void AXPlatformNodeAuraLinux::EnsureAtkObjectIsValid() { if (atk_object_) { // If the object's role changes and that causes its @@ -3343,7 +3390,7 @@ void AXPlatformNodeAuraLinux::OnMenuPopupStart() { atk_object_notify_state_change(parent_frame, ATK_STATE_ACTIVE, TRUE); } -void AXPlatformNodeAuraLinux::OnMenuPopupHide() { +void AXPlatformNodeAuraLinux::OnMenuPopupEnd() { AtkObject* atk_object = GetOrCreateAtkObject(); AtkObject* parent_frame = FindAtkObjectParentFrame(atk_object); if (!parent_frame) @@ -3354,35 +3401,24 @@ void AXPlatformNodeAuraLinux::OnMenuPopupHide() { // kMenuPopupHide may be called multiple times for the same menu, so only // remove it if our parent frame matches the most recently opened menu. std::vector<AtkObject*>& active_menus = GetActiveMenus(); - if (active_menus.empty()) - return; - - // When multiple levels of menu are closed at once, they may be hidden out - // of order. When this happens, we just remove the open menu from the stack. - if (active_menus.back() != atk_object) { - auto it = std::find(active_menus.rbegin(), active_menus.rend(), atk_object); - if (it != active_menus.rend()) { - // We used a reverse iterator, so we need to convert it into a normal - // iterator to use it for std::vector::erase(...). - auto to_remove = --(it.base()); - active_menus.erase(to_remove); - } - return; - } + DCHECK(!active_menus.empty()) + << "Asymmetrical menupopupend events -- too many"; active_menus.pop_back(); - - // We exit early if the newly activated menu has the same AtkWindow as the - // previous one. AtkObject* new_active_item = ComputeActiveTopLevelFrame(); - if (new_active_item == parent_frame) - return; - g_signal_emit_by_name(parent_frame, "deactivate"); - atk_object_notify_state_change(parent_frame, ATK_STATE_ACTIVE, FALSE); - if (new_active_item) { - g_signal_emit_by_name(new_active_item, "activate"); - atk_object_notify_state_change(new_active_item, ATK_STATE_ACTIVE, TRUE); + if (new_active_item != parent_frame) { + // Newly activated menu has the different AtkWindow as the previous one. + g_signal_emit_by_name(parent_frame, "deactivate"); + atk_object_notify_state_change(parent_frame, ATK_STATE_ACTIVE, FALSE); + if (new_active_item) { + g_signal_emit_by_name(new_active_item, "activate"); + atk_object_notify_state_change(new_active_item, ATK_STATE_ACTIVE, TRUE); + } } + + // All menus are closed. + if (active_menus.empty()) + OnAllMenusEnded(); } void AXPlatformNodeAuraLinux::ResendFocusSignalsForCurrentlyFocusedNode() { @@ -3398,7 +3434,8 @@ void AXPlatformNodeAuraLinux::ResendFocusSignalsForCurrentlyFocusedNode() { atk_object_notify_state_change(focused_node, ATK_STATE_FOCUSED, true); } -void AXPlatformNodeAuraLinux::OnMenuPopupEnd() { +// All menus have closed. +void AXPlatformNodeAuraLinux::OnAllMenusEnded() { if (!GetActiveMenus().empty() && g_active_top_level_frame && ComputeActiveTopLevelFrame() != g_active_top_level_frame) { g_signal_emit_by_name(g_active_top_level_frame, "activate"); @@ -3406,8 +3443,8 @@ void AXPlatformNodeAuraLinux::OnMenuPopupEnd() { TRUE); } - ResendFocusSignalsForCurrentlyFocusedNode(); GetActiveMenus().clear(); + ResendFocusSignalsForCurrentlyFocusedNode(); } void AXPlatformNodeAuraLinux::OnWindowActivated() { @@ -3512,16 +3549,25 @@ void AXPlatformNodeAuraLinux::OnFocused() { SetActiveViewsDialog(); - if (g_current_focused) { - g_signal_emit_by_name(g_current_focused, "focus-event", false); - atk_object_notify_state_change(ATK_OBJECT(g_current_focused), + AtkObject* old_effective_focus = g_current_active_descendant + ? g_current_active_descendant + : g_current_focused; + if (old_effective_focus) { + g_signal_emit_by_name(old_effective_focus, "focus-event", false); + atk_object_notify_state_change(ATK_OBJECT(old_effective_focus), ATK_STATE_FOCUSED, false); } SetWeakGPtrToAtkObject(&g_current_focused, atk_object); - g_signal_emit_by_name(atk_object, "focus-event", true); - atk_object_notify_state_change(ATK_OBJECT(atk_object), ATK_STATE_FOCUSED, - true); + AtkObject* descendant = GetActiveDescendantOfCurrentFocused(); + SetWeakGPtrToAtkObject(&g_current_active_descendant, descendant); + + AtkObject* new_effective_focus = g_current_active_descendant + ? g_current_active_descendant + : g_current_focused; + g_signal_emit_by_name(new_effective_focus, "focus-event", true); + atk_object_notify_state_change(ATK_OBJECT(new_effective_focus), + ATK_STATE_FOCUSED, true); } void AXPlatformNodeAuraLinux::OnSelected() { @@ -3874,21 +3920,15 @@ void AXPlatformNodeAuraLinux::NotifyAccessibilityEvent( return; AXPlatformNodeBase::NotifyAccessibilityEvent(event_type); switch (event_type) { - // There are three types of messages that we receive for popup menus. Each - // time a popup menu is shown, we get a kMenuPopupStart message. This - // includes if the menu is hidden and then re-shown. When a menu is hidden - // we receive the kMenuPopupHide message. Finally, when the entire menu is - // closed we receive the kMenuPopupEnd message for the parent menu and all - // of the submenus that were opened when navigating through the menu. - case ax::mojom::Event::kMenuPopupEnd: - OnMenuPopupEnd(); - break; - case ax::mojom::Event::kMenuPopupHide: - OnMenuPopupHide(); - break; + // kMenuStart/kMenuEnd: the menu system has started / stopped. + // kMenuPopupStart/kMenuPopupEnd: an individual menu/submenu has + // opened/closed. case ax::mojom::Event::kMenuPopupStart: OnMenuPopupStart(); break; + case ax::mojom::Event::kMenuPopupEnd: + OnMenuPopupEnd(); + break; case ax::mojom::Event::kActiveDescendantChanged: OnActiveDescendantChanged(); break; @@ -4176,7 +4216,30 @@ gfx::NativeViewAccessible AXPlatformNodeAuraLinux::HitTestSync(gint x, gint y, AtkCoordType coord_type) { gfx::Point scroll_to(x, y); scroll_to = ConvertPointToScreenCoordinates(scroll_to, coord_type); - return delegate_->HitTestSync(scroll_to.x(), scroll_to.y()); + + AXPlatformNode* current_result = this; + while (true) { + gfx::NativeViewAccessible hit_child = + current_result->GetDelegate()->HitTestSync(scroll_to.x(), + scroll_to.y()); + if (!hit_child) + return nullptr; + AXPlatformNode* hit_child_node = + AXPlatformNode::FromNativeViewAccessible(hit_child); + if (!hit_child_node || !hit_child_node->IsDescendantOf(current_result)) + break; + + // If we get the same node, we're done. + if (hit_child_node == current_result) + break; + + // Continue to check recursively. That's because HitTestSync may have + // returned the best result within a particular accessibility tree, + // but we might need to recurse further in a tree of a different type + // (for example, from Views to Web). + current_result = hit_child_node; + } + return current_result->GetNativeViewAccessible(); } bool AXPlatformNodeAuraLinux::GrabFocus() { @@ -4303,9 +4366,9 @@ AtkAttributeSet* AXPlatformNodeAuraLinux::GetAtkAttributes() { AtkStateType AXPlatformNodeAuraLinux::GetAtkStateTypeForCheckableNode() { if (GetData().GetCheckedState() == ax::mojom::CheckedState::kMixed) return ATK_STATE_INDETERMINATE; - if (GetData().role == ax::mojom::Role::kToggleButton) - return ATK_STATE_PRESSED; - return ATK_STATE_CHECKED; + if (IsPlatformCheckable()) + return ATK_STATE_CHECKED; + return ATK_STATE_PRESSED; } // AtkDocumentHelpers diff --git a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h index 6fde338d9e1..610b2041b97 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h @@ -206,8 +206,8 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase { void OnWindowActivated(); void OnWindowDeactivated(); void OnMenuPopupStart(); - void OnMenuPopupHide(); void OnMenuPopupEnd(); + void OnAllMenusEnded(); void OnSelected(); void OnSelectedChildrenChanged(); void OnTextSelectionChanged(); @@ -237,6 +237,7 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase { // AXPlatformNodeBase overrides. void Init(AXPlatformNodeDelegate* delegate) override; + bool IsPlatformCheckable() const override; bool IsNameExposed(); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc index b120f746734..706471c0a84 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc @@ -201,6 +201,7 @@ static bool AtkObjectHasState(AtkObject* atk_object, AtkStateType state) { TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectDetachedObject) { AXNodeData root; root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; root.SetName("Name"); Init(root); @@ -231,6 +232,7 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectDetachedObject) { TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkObjectName) { AXNodeData root; root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; root.SetName("Name"); Init(root); @@ -659,12 +661,14 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkComponentRefAtPoint) { AXNodeData node1; node1.id = 2; + node1.role = ax::mojom::Role::kGenericContainer; node1.relative_bounds.bounds = gfx::RectF(0, 0, 10, 10); node1.SetName("Name1"); root.child_ids.push_back(node1.id); AXNodeData node2; node2.id = 3; + node2.role = ax::mojom::Role::kGenericContainer; node2.relative_bounds.bounds = gfx::RectF(20, 20, 10, 10); node2.SetName("Name2"); root.child_ids.push_back(node2.id); @@ -1820,23 +1824,10 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkPopupWindowActive) { { ActivationTester tester(menu_atk_node); GetPlatformNode(menu_node)->NotifyAccessibilityEvent( - ax::mojom::Event::kMenuPopupHide); - EXPECT_FALSE(tester.saw_activate_); - EXPECT_TRUE(tester.saw_deactivate_); - EXPECT_FALSE(tester.IsActivatedInStateSet()); - EXPECT_EQ(focus_events_on_original_node, 0); - } - - { - ActivationTester tester(menu_atk_node); - GetPlatformNode(menu_node)->NotifyAccessibilityEvent( ax::mojom::Event::kMenuPopupEnd); EXPECT_FALSE(tester.saw_activate_); - EXPECT_FALSE(tester.saw_deactivate_); + EXPECT_TRUE(tester.saw_deactivate_); EXPECT_FALSE(tester.IsActivatedInStateSet()); - - // The menu has closed so the original node should have received focus - // again. EXPECT_EQ(focus_events_on_original_node, 1); } diff --git a/chromium/ui/accessibility/platform/ax_platform_node_base.cc b/chromium/ui/accessibility/platform/ax_platform_node_base.cc index 99de6603989..36081b70c14 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_base.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_base.cc @@ -4,6 +4,9 @@ #include "ui/accessibility/platform/ax_platform_node_base.h" +#include <algorithm> +#include <limits> +#include <set> #include <string> #include <unordered_map> #include <utility> @@ -26,6 +29,7 @@ namespace ui { namespace { + // A function to call when focus changes, for testing only. base::LazyInstance<std::map<ax::mojom::Event, base::RepeatingClosure>>:: DestructorAtExit g_on_notify_event_for_testing; @@ -54,6 +58,7 @@ bool FindDescendantRoleWithMaxDepth(AXPlatformNodeBase* node, return false; } + } // namespace const base::char16 AXPlatformNodeBase::kEmbeddedCharacter = L'\xfffc'; @@ -139,7 +144,7 @@ gfx::NativeViewAccessible AXPlatformNodeBase::ChildAtIndex(int index) const { std::string AXPlatformNodeBase::GetName() const { if (delegate_) return delegate_->GetName(); - return base::EmptyString(); + return std::string(); } base::string16 AXPlatformNodeBase::GetNameAsString16() const { @@ -495,11 +500,20 @@ bool AXPlatformNodeBase::IsDocument() const { } bool AXPlatformNodeBase::IsTextOnlyObject() const { + if (!delegate_) + return false; + + // In Legacy Layout, a list marker has no children and is thus represented on + // all platforms as a leaf node that exposes the marker itself, i.e., it forms + // part of the AX tree's text representation. In contrast, in Layout NG, a + // list marker has a static text child. + if (GetData().role == ax::mojom::Role::kListMarker) + return !GetChildCount(); return ui::IsText(GetData().role); } bool AXPlatformNodeBase::IsTextField() const { - return IsPlainTextField() || IsRichTextField(); + return GetData().IsTextField(); } bool AXPlatformNodeBase::IsPlainTextField() const { @@ -507,16 +521,18 @@ bool AXPlatformNodeBase::IsPlainTextField() const { } bool AXPlatformNodeBase::IsRichTextField() const { - return GetBoolAttribute(ax::mojom::BoolAttribute::kEditableRoot) && - GetData().HasState(ax::mojom::State::kRichlyEditable); + return GetData().IsRichTextField(); } base::string16 AXPlatformNodeBase::GetHypertext() const { + if (!delegate_) + return base::string16(); + // Hypertext of platform leaves, which internally are composite objects, are // represented with the inner text of the internal composite object. These // don't exist on non-web content. if (IsChildOfLeaf()) - return GetDelegate()->GetInnerText(); + return GetInnerText(); if (hypertext_.needs_update) UpdateComputedHypertext(); @@ -524,38 +540,9 @@ base::string16 AXPlatformNodeBase::GetHypertext() const { } base::string16 AXPlatformNodeBase::GetInnerText() const { - // In order to get the inner text for web content, we potentially need access - // to nodes that are not exposed to platform APIs, i.e. they are only visible - // in the internal accessibility tree. For example, nodes representing the - // shadow DOM inside a native text field. - if (GetDelegate()->IsWebContent()) - return GetDelegate()->GetInnerText(); - - // Allows us to get text even in non-web content, e.g. in the browser's UI - // (AKA Views). - // - // Unlike in web content The "kValue" attribute takes precedence, because the - // accessibility of Views controls are carefully crafted by hand, in contrast - // to HTML pages, where any content that might be present in the shadow DOM - // (i.e. in the internal accessibility tree) is actually used by the renderer. - base::string16 value = - GetString16Attribute(ax::mojom::StringAttribute::kValue); - if (!value.empty()) - return value; - - // TODO(https://crbug.com/1030703): The check for IsInvisibleOrIgnored() - // should not be needed. ChildAtIndex() and GetChildCount() are already - // supposed to skip over nodes that are invisible or ignored, but - // ViewAXPlatformNodeDelegate does not currently implement this behavior. - if (!GetChildCount() && !IsInvisibleOrIgnored()) - return GetNameAsString16(); - - base::string16 text; - for (auto child_iter = AXPlatformNodeChildrenBegin(); - child_iter != AXPlatformNodeChildrenEnd(); ++child_iter) { - text += child_iter->GetInnerText(); - } - return text; + if (!delegate_) + return base::string16(); + return delegate_->GetInnerText(); } bool AXPlatformNodeBase::IsSelectionItemSupported() const { @@ -847,48 +834,15 @@ bool AXPlatformNodeBase::HasCaret( } bool AXPlatformNodeBase::IsLeaf() const { - if (!GetChildCount()) - return true; - - // These types of objects may have children that we use as internal - // implementation details, but we want to expose them as leaves to platform - // accessibility APIs because screen readers might be confused if they find - // any children. - if (IsPlainTextField() || IsTextOnlyObject()) - return true; - - // Roles whose children are only presentational according to the ARIA and - // HTML5 Specs should be hidden from screen readers. - // (Note that whilst ARIA buttons can have only presentational children, HTML5 - // buttons are allowed to have content.) - switch (GetData().role) { - case ax::mojom::Role::kImage: - case ax::mojom::Role::kMeter: - case ax::mojom::Role::kScrollBar: - case ax::mojom::Role::kSlider: - case ax::mojom::Role::kSplitter: - case ax::mojom::Role::kProgressIndicator: - return true; - default: - return false; - } + return delegate_ && delegate_->IsLeaf(); } bool AXPlatformNodeBase::IsChildOfLeaf() const { - AXPlatformNodeBase* ancestor = FromNativeViewAccessible(GetParent()); - - while (ancestor) { - if (ancestor->IsLeaf()) - return true; - ancestor = FromNativeViewAccessible(ancestor->GetParent()); - } - - return false; + return delegate_ && delegate_->IsChildOfLeaf(); } bool AXPlatformNodeBase::IsInvisibleOrIgnored() const { - const AXNodeData& data = GetData(); - return data.HasState(ax::mojom::State::kInvisible) || data.IsIgnored(); + return GetData().IsInvisibleOrIgnored(); } bool AXPlatformNodeBase::IsScrollable() const { @@ -981,7 +935,7 @@ void AXPlatformNodeBase::ComputeAttributes(PlatformAttributeList* attributes) { AddAttributeToList(ax::mojom::IntAttribute::kPosInSet, "posinset", attributes); - if (HasIntAttribute(ax::mojom::IntAttribute::kCheckedState)) + if (IsPlatformCheckable()) AddAttributeToList("checkable", "true", attributes); if (IsInvisibleOrIgnored()) // Note: NVDA prefers this over INVISIBLE state. @@ -1280,6 +1234,8 @@ AXHypertext::AXHypertext(const AXHypertext& other) = default; AXHypertext& AXHypertext::operator=(const AXHypertext& other) = default; void AXPlatformNodeBase::UpdateComputedHypertext() const { + if (!delegate_) + return; hypertext_ = AXHypertext(); if (IsLeaf()) { @@ -1728,6 +1684,10 @@ bool AXPlatformNodeBase::IsText(const base::string16& text, return ch != kEmbeddedCharacter; } +bool AXPlatformNodeBase::IsPlatformCheckable() const { + return delegate_ && GetData().HasCheckedState(); +} + void AXPlatformNodeBase::ComputeHypertextRemovedAndInserted( const AXHypertext& old_hypertext, size_t* start, diff --git a/chromium/ui/accessibility/platform/ax_platform_node_base.h b/chromium/ui/accessibility/platform/ax_platform_node_base.h index 90fca62bb3f..c14c8c7e698 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_base.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_base.h @@ -216,19 +216,13 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { // Optionally accepts an unignored selection to avoid redundant computation. bool HasCaret(const AXTree::Selection* unignored_selection = nullptr); - // Returns true if an ancestor of this node (not including itself) is a - // leaf node, meaning that this node is not actually exposed to the - // platform. + // See AXPlatformNodeDelegate::IsChildOfLeaf(). bool IsChildOfLeaf() const; - // Returns true if this is a leaf node on this platform, meaning any - // children should not be exposed to this platform's native accessibility - // layer. Each platform subclass should implement this itself. - // The definition of a leaf may vary depending on the platform, - // but a leaf node should never have children that are focusable or - // that might send notifications. + // See AXPlatformNodeDelegate::IsLeaf(). bool IsLeaf() const; + // See AXPlatformNodeDelegate::IsInvisibleOrIgnored(). bool IsInvisibleOrIgnored() const; // Returns true if this node can be scrolled either in the horizontal or the @@ -241,34 +235,41 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { // Returns true if this node can be scrolled in the vertical direction. bool IsVerticallyScrollable() const; - // Returns true if this node has role of StaticText, LineBreak, or + // Returns true if this node has a role of StaticText, LineBreak, or // InlineTextBox bool IsTextOnlyObject() const; - // A text field is any widget in which the user should be able to enter and - // edit text. - // - // Examples include <input type="text">, <input type="password">, <textarea>, - // <div contenteditable="true">, <div role="textbox">, <div role="searchbox"> - // and <div role="combobox">. Note that when an ARIA role that indicates that - // the widget is editable is used, such as "role=textbox", the element doesn't - // need to be contenteditable for this method to return true, as in theory - // JavaScript could be used to implement editing functionality. In practice, - // this situation should be rare. + // See AXNodeData::IsTextField(). bool IsTextField() const; - // Returns true if the node is an editable text field. + // See AXNodeData::IsPlainTextField(). bool IsPlainTextField() const; + // See AXNodeData::IsRichTextField(). + bool IsRichTextField() const; + + // Determines whether an element should be exposed with checkable state, and + // possibly the checked state. Examples are check box and radio button. + // Objects that are exposed as toggle buttons use the platform pressed state + // in some platform APIs, and should not be exposed as checkable. They don't + // expose the platform equivalent of the internal checked state. + virtual bool IsPlatformCheckable() const; + bool HasFocus(); - // If this node is a leaf, returns the text of this node, otherwise represents - // each child node with a special "embedded object" character. This is how - // text is represented in ATK and IA2 APIs. + // If this node is a leaf, returns the visible accessible name of this node. + // Otherwise represents every non-leaf child node with a special "embedded + // object character", and every leaf child node with its visible accessible + // name. This is how displayed text and embedded objects are represented in + // ATK and IA2 APIs. base::string16 GetHypertext() const; // Returns the text of this node and all descendant nodes; including text // found in embedded objects. + // + // Only text displayed on screen is included. Text from ARIA and HTML + // attributes that is either not displayed on screen, or outside this node, + // e.g. aria-label and HTML title, is not returned. base::string16 GetInnerText() const; virtual base::string16 GetValue() const; @@ -344,11 +345,10 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { // // Delegate. This is a weak reference which owns |this|. // - AXPlatformNodeDelegate* delegate_; + AXPlatformNodeDelegate* delegate_ = nullptr; protected: bool IsDocument() const; - bool IsRichTextField() const; bool IsSelectionItemSupported() const; // Get the range value text, which might come from aria-valuetext or @@ -491,7 +491,7 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { mutable AXHypertext hypertext_; private: - // Return true if the index represents a text character. + // Returns true if the index represents a text character. bool IsText(const base::string16& text, size_t index, bool is_indexed_from_end = false); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_delegate.h b/chromium/ui/accessibility/platform/ax_platform_node_delegate.h index c6064c6e248..e8ff6876672 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_delegate.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_delegate.h @@ -78,6 +78,14 @@ class AX_EXPORT AXPlatformNodeDelegate { // Get the accessibility tree data for this node. virtual const AXTreeData& GetTreeData() const = 0; + // Returns the text of this node and all descendant nodes; including text + // found in embedded objects. + // + // Only text displayed on screen is included. Text from ARIA and HTML + // attributes that is either not displayed on screen, or outside this node, + // e.g. aria-label and HTML title, is not returned. + virtual base::string16 GetInnerText() const = 0; + // Get the unignored selection from the tree virtual const AXTree::Selection GetUnignoredSelection() const = 0; @@ -121,12 +129,22 @@ class AX_EXPORT AXPlatformNodeDelegate { virtual gfx::NativeViewAccessible GetPreviousSibling() = 0; // Returns true if an ancestor of this node (not including itself) is a - // leaf node, meaning that this node is not actually exposed to the - // platform. + // leaf node, meaning that this node is not actually exposed to any + // platform's accessibility layer. virtual bool IsChildOfLeaf() const = 0; - // If this object is exposed to the platform, returns this object. Otherwise, - // returns the platform leaf under which this object is found. + // Returns true if this current node is editable and the root editable node is + // a plain text field. + virtual bool IsChildOfPlainTextField() const = 0; + + // Returns true if this is a leaf node, meaning all its + // children should not be exposed to any platform's native accessibility + // layer. + virtual bool IsLeaf() const = 0; + + // If this object is exposed to the platform's accessibility layer, returns + // this object. Otherwise, returns the platform leaf under which this object + // is found. virtual gfx::NativeViewAccessible GetClosestPlatformObject() const = 0; class ChildIterator { @@ -172,10 +190,6 @@ class AX_EXPORT AXPlatformNodeDelegate { // implementations. virtual std::string GetInheritedFontFamilyName() const = 0; - // Returns the text of this node and all descendant nodes; including text - // found in embedded objects. - virtual base::string16 GetInnerText() const = 0; - // Return the bounds of this node in the coordinate system indicated. If the // clipping behavior is set to clipped, clipping is applied. If an offscreen // result address is provided, it will be populated depending on whether the diff --git a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc index 8abc5a9300e..39d55498dbd 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc @@ -31,6 +31,38 @@ const AXTreeData& AXPlatformNodeDelegateBase::GetTreeData() const { return *empty_data; } +base::string16 AXPlatformNodeDelegateBase::GetInnerText() const { + // Unlike in web content The "kValue" attribute always takes precedence, + // because we assume that users of this base class, such as Views controls, + // are carefully crafted by hand, in contrast to HTML pages, where any content + // that might be present in the shadow DOM (AKA in the internal accessibility + // tree) is actually used by the renderer when assigning the "kValue" + // attribute, including any redundant white space. + base::string16 value = + GetData().GetString16Attribute(ax::mojom::StringAttribute::kValue); + if (!value.empty()) + return value; + + // TODO(https://crbug.com/1030703): The check for IsInvisibleOrIgnored() + // should not be needed. ChildAtIndex() and GetChildCount() are already + // supposed to skip over nodes that are invisible or ignored, but + // ViewAXPlatformNodeDelegate does not currently implement this behavior. + if (IsLeaf() && !GetData().IsInvisibleOrIgnored()) + return GetData().GetString16Attribute(ax::mojom::StringAttribute::kName); + + base::string16 inner_text; + for (int i = 0; i < GetChildCount(); ++i) { + // TODO(nektar): Add const to all tree traversal methods and remove + // const_cast. + const AXPlatformNode* child = AXPlatformNode::FromNativeViewAccessible( + const_cast<AXPlatformNodeDelegateBase*>(this)->ChildAtIndex(i)); + if (!child || !child->GetDelegate()) + continue; + inner_text += child->GetDelegate()->GetInnerText(); + } + return inner_text; +} + const AXTree::Selection AXPlatformNodeDelegateBase::GetUnignoredSelection() const { return AXTree::Selection{-1, -1, -1, ax::mojom::TextAffinity::kDownstream}; @@ -95,6 +127,21 @@ gfx::NativeViewAccessible AXPlatformNodeDelegateBase::GetPreviousSibling() { } bool AXPlatformNodeDelegateBase::IsChildOfLeaf() const { + // TODO(nektar): Make all tree traversal methods const and remove const_cast. + const AXPlatformNodeDelegate* parent = + const_cast<AXPlatformNodeDelegateBase*>(this)->GetParentDelegate(); + if (!parent) + return false; + if (parent->IsLeaf()) + return true; + return parent->IsChildOfLeaf(); +} + +bool AXPlatformNodeDelegateBase::IsLeaf() const { + return !GetChildCount(); +} + +bool AXPlatformNodeDelegateBase::IsChildOfPlainTextField() const { return false; } @@ -200,10 +247,6 @@ bool AXPlatformNodeDelegateBase::SetHypertextSelection(int start_offset, return AccessibilityPerformAction(action_data); } -base::string16 AXPlatformNodeDelegateBase::GetInnerText() const { - return base::string16(); -} - gfx::Rect AXPlatformNodeDelegateBase::GetBoundsRect( const AXCoordinateSystem coordinate_system, const AXClippingBehavior clipping_behavior, @@ -533,6 +576,11 @@ AXPlatformNodeDelegateBase::GetTargetNodesForRelation( std::set<AXPlatformNode*> AXPlatformNodeDelegateBase::GetReverseRelations( ax::mojom::IntAttribute attr) { + // TODO(accessibility) Implement these if views ever use relations more + // widely. The use so far has been for the Omnibox to the suggestion popup. + // If this is ever implemented, then the "popup for" to "controlled by" + // mapping in AXPlatformRelationWin can be removed, as it would be + // redundant with setting the controls relationship. return std::set<AXPlatformNode*>(); } diff --git a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h index 350b9f6a44b..7dd68b928d7 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h @@ -35,7 +35,7 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { // Get the accessibility tree data for this node. const AXTreeData& GetTreeData() const override; - // Get the unignored selection from the tree + base::string16 GetInnerText() const override; const AXTree::Selection GetUnignoredSelection() const override; // Creates a text position rooted at this object. @@ -67,13 +67,9 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { gfx::NativeViewAccessible GetNextSibling() override; gfx::NativeViewAccessible GetPreviousSibling() override; - // Returns true if an ancestor of this node (not including itself) is a - // leaf node, meaning that this node is not actually exposed to the - // platform. bool IsChildOfLeaf() const override; - - // If this object is exposed to the platform, returns this object. Otherwise, - // returns the platform leaf under which this object is found. + bool IsChildOfPlainTextField() const override; + bool IsLeaf() const override; gfx::NativeViewAccessible GetClosestPlatformObject() const override; class ChildIteratorBase : public ChildIterator { @@ -107,8 +103,6 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { const TextAttributeList& default_attributes) const override; std::string GetInheritedFontFamilyName() const override; - base::string16 GetInnerText() const override; - gfx::Rect GetBoundsRect(const AXCoordinateSystem coordinate_system, const AXClippingBehavior clipping_behavior, AXOffscreenResult* offscreen_result) const override; diff --git a/chromium/ui/accessibility/platform/ax_platform_node_mac.h b/chromium/ui/accessibility/platform/ax_platform_node_mac.h index 920f0a05363..c28d1a9fdc5 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_mac.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_mac.h @@ -27,6 +27,7 @@ class AXPlatformNodeMac : public AXPlatformNodeBase { // AXPlatformNodeBase. void Destroy() override; + bool IsPlatformCheckable() const override; protected: void AddAttributeToList(const char* name, diff --git a/chromium/ui/accessibility/platform/ax_platform_node_mac.mm b/chromium/ui/accessibility/platform/ax_platform_node_mac.mm index 9a8f6f6d60a..911344e83a4 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_mac.mm +++ b/chromium/ui/accessibility/platform/ax_platform_node_mac.mm @@ -497,6 +497,8 @@ bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) { return nil; for (id child in [[self AXChildren] reverseObjectEnumerator]) { + if (!NSPointInRect(point, [child accessibilityFrame])) + continue; if (id foundChild = [child accessibilityHitTest:point]) return foundChild; } @@ -744,7 +746,7 @@ bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) { if (ui::IsNameExposedInAXValueForRole(role)) return [self getName]; - if (_node->HasIntAttribute(ax::mojom::IntAttribute::kCheckedState)) { + if (_node->IsPlatformCheckable()) { // Mixed checkbox state not currently supported in views, but could be. // See browser_accessibility_cocoa.mm for details. const auto checkedState = static_cast<ax::mojom::CheckedState>( @@ -844,8 +846,10 @@ bool AlsoUseShowMenuActionForDefaultAction(const ui::AXNodeData& data) { - (NSValue*)AXSelectedTextRange { // Selection might not be supported. Return (NSRange){0,0} in that case. int start = 0, end = 0; - _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart, &start); - _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, &end); + if (_node->IsPlainTextField()) { + start = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart); + end = _node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd); + } // NSRange cannot represent the direction the text was selected in. return [NSValue valueWithRange:{std::min(start, end), abs(end - start)}]; @@ -1209,6 +1213,17 @@ void AXPlatformNodeMac::Destroy() { AXPlatformNodeBase::Destroy(); } +// On Mac, the checked state is mapped to AXValue. +bool AXPlatformNodeMac::IsPlatformCheckable() const { + if (GetData().role == ax::mojom::Role::kTab) { + // On Mac, tabs are exposed as radio buttons, and are treated as checkable. + // Also, the internal State::kSelected is be mapped to checked via AXValue. + return true; + } + + return AXPlatformNodeBase::IsPlatformCheckable(); +} + gfx::NativeViewAccessible AXPlatformNodeMac::GetNativeViewAccessible() { if (!native_node_) native_node_.reset([[AXPlatformNodeCocoa alloc] initWithNode:this]); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textchildprovider_win_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_textchildprovider_win_unittest.cc index 167e1af7f8e..ec5adb620d6 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textchildprovider_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_textchildprovider_win_unittest.cc @@ -60,7 +60,7 @@ class AXPlatformNodeTextChildProviderTest : public AXPlatformNodeWinTest { ui::AXNodeData text_child_of_text; text_child_of_text.id = 6; - text_child_of_text.role = ax::mojom::Role::kStaticText; + text_child_of_text.role = ax::mojom::Role::kInlineTextBox; text_child_of_text.SetName("text child of text."); text_child_of_root.child_ids.push_back(text_child_of_text.id); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textprovider_win_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_textprovider_win_unittest.cc index ec472458f9f..d86ee466120 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textprovider_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_textprovider_win_unittest.cc @@ -7,6 +7,8 @@ #include <UIAutomationClient.h> #include <UIAutomationCoreApi.h> +#include <vector> + #include "base/win/scoped_bstr.h" #include "base/win/scoped_safearray.h" #include "ui/accessibility/ax_action_data.h" @@ -107,8 +109,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderRangeFromChild) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); root_data.child_ids.push_back(3); @@ -196,8 +198,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ui::AXNodeData root; root.id = ROOT_ID; - root.SetName("Document"); root.role = ax::mojom::Role::kRootWebArea; + root.SetName("Document"); root.child_ids = {DIALOG_ID}; ui::AXNodeData dialog; @@ -343,8 +345,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderDocumentRange) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); Init(root_data, text_data); @@ -374,8 +376,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderDocumentRangeNested) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); Init(root_data, paragraph_data, text_data); @@ -400,8 +402,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderSupportedSelection) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); Init(root_data, text_data); @@ -433,8 +435,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderGetSelection) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); root_data.child_ids.push_back(3); @@ -608,8 +610,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderGetActiveComposition) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); ui::AXTreeUpdate update; @@ -668,8 +670,8 @@ TEST_F(AXPlatformNodeTextProviderTest, ITextProviderGetConversionTarget) { ui::AXNodeData root_data; root_data.id = 1; - root_data.SetName("Document"); root_data.role = ax::mojom::Role::kRootWebArea; + root_data.SetName("Document"); root_data.child_ids.push_back(2); ui::AXTreeUpdate update; diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc index ee4026570b1..379fa000578 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc @@ -172,6 +172,11 @@ HRESULT AXPlatformNodeTextRangeProviderWin::CompareEndpoints( HRESULT AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnit( TextUnit unit) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_EXPANDTOENCLOSINGUNIT); + return ExpandToEnclosingUnitImpl(unit); +} + +HRESULT AXPlatformNodeTextRangeProviderWin::ExpandToEnclosingUnitImpl( + TextUnit unit) { UIA_VALIDATE_TEXTRANGEPROVIDER_CALL(); NormalizeTextRange(); @@ -627,8 +632,8 @@ HRESULT AXPlatformNodeTextRangeProviderWin::Move(TextUnit unit, // Move the start of the text range forward or backward in the document by the // requested number of text unit boundaries. int start_units_moved = 0; - HRESULT hr = MoveEndpointByUnit(TextPatternRangeEndpoint_Start, unit, count, - &start_units_moved); + HRESULT hr = MoveEndpointByUnitImpl(TextPatternRangeEndpoint_Start, unit, + count, &start_units_moved); bool succeeded_move = SUCCEEDED(hr) && start_units_moved != 0; if (succeeded_move) { @@ -640,8 +645,8 @@ HRESULT AXPlatformNodeTextRangeProviderWin::Move(TextUnit unit, // by one text unit to expand the text range from the degenerate range // state. int current_start_units_moved = 0; - hr = MoveEndpointByUnit(TextPatternRangeEndpoint_Start, unit, -1, - ¤t_start_units_moved); + hr = MoveEndpointByUnitImpl(TextPatternRangeEndpoint_Start, unit, -1, + ¤t_start_units_moved); start_units_moved -= 1; succeeded_move = SUCCEEDED(hr) && current_start_units_moved == -1 && start_units_moved > 0; @@ -650,8 +655,8 @@ HRESULT AXPlatformNodeTextRangeProviderWin::Move(TextUnit unit, // forward by one text unit to expand the text range from the degenerate // state. int end_units_moved = 0; - hr = MoveEndpointByUnit(TextPatternRangeEndpoint_End, unit, 1, - &end_units_moved); + hr = MoveEndpointByUnitImpl(TextPatternRangeEndpoint_End, unit, 1, + &end_units_moved); succeeded_move = SUCCEEDED(hr) && end_units_moved == 1; } @@ -660,7 +665,7 @@ HRESULT AXPlatformNodeTextRangeProviderWin::Move(TextUnit unit, // sure to bring back the end endpoint to the end of the start's anchor. if (start_->anchor_id() != end_->anchor_id() && (unit == TextUnit_Character || unit == TextUnit_Word)) { - ExpandToEnclosingUnit(unit); + ExpandToEnclosingUnitImpl(unit); } } } @@ -683,6 +688,14 @@ HRESULT AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnit( int count, int* units_moved) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_MOVEENDPOINTBYUNIT); + return MoveEndpointByUnitImpl(endpoint, unit, count, units_moved); +} + +HRESULT AXPlatformNodeTextRangeProviderWin::MoveEndpointByUnitImpl( + TextPatternRangeEndpoint endpoint, + TextUnit unit, + int count, + int* units_moved) { UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_OUT(units_moved); // Per MSDN, MoveEndpointByUnit with zero count has no effect. @@ -1124,12 +1137,22 @@ void AXPlatformNodeTextRangeProviderWin::NormalizeTextRange() { NormalizeAsUnignoredTextRange(); // Do not normalize text ranges when a cursor or selection is visible. ATs - // may depend on the specific position that the caret or selection is at. + // may depend on the specific position that the caret or selection is at. This + // condition fixes issues when the caret is inside a plain text field, but + // causes more issues when used inside of a rich text field. For this reason, + // if we have a caret or a selection inside of an editable node, restrict this + // to a plain text field as we gain nothing from using it in a rich text + // field. AXPlatformNodeDelegate* start_delegate = GetDelegate(start_.get()); AXPlatformNodeDelegate* end_delegate = GetDelegate(end_.get()); - if ((start_delegate && start_delegate->HasVisibleCaretOrSelection()) || - (start_delegate && end_delegate->HasVisibleCaretOrSelection())) + if ((start_delegate && start_delegate->HasVisibleCaretOrSelection() && + (!start_delegate->GetData().HasState(ax::mojom::State::kEditable) || + start_delegate->IsChildOfPlainTextField())) || + (end_delegate && end_delegate->HasVisibleCaretOrSelection() && + (!end_delegate->GetData().HasState(ax::mojom::State::kEditable) || + end_delegate->IsChildOfPlainTextField()))) { return; + } AXPositionInstance normalized_start = start_->AsLeafTextPositionBeforeCharacter(); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h index db595fdf424..c967b191124 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h @@ -99,6 +99,16 @@ class AX_EXPORT __declspec(uuid("3071e40d-a10d-45ff-a59f-6e8e1138e2c1")) AXBoundaryBehavior boundary_behavior, ax::mojom::MoveDirection boundary_direction); + // Prefer these *Impl methods when functionality is needed internally. We + // should avoid calling external APIs internally as it will cause the + // histograms to become innaccurate. + HRESULT MoveEndpointByUnitImpl(TextPatternRangeEndpoint endpoint, + TextUnit unit, + int count, + int* units_moved); + + IFACEMETHODIMP ExpandToEnclosingUnitImpl(TextUnit unit); + base::string16 GetString(int max_count, size_t* appended_newlines_count = nullptr); AXPlatformNodeWin* owner() const; diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc index 13617fa8a8c..a698ca3c021 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc @@ -7,6 +7,9 @@ #include <UIAutomationClient.h> #include <UIAutomationCoreApi.h> +#include <memory> +#include <utility> + #include "base/win/atl.h" #include "base/win/scoped_bstr.h" #include "base/win/scoped_safearray.h" @@ -3074,24 +3077,12 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, update.tree_data = tree_data; update.has_tree_data = true; update.root_id = root_data.id; - update.nodes.push_back(root_data); - update.nodes.push_back(paragraph_data); - update.nodes.push_back(static_text_data1); - update.nodes.push_back(inline_text_data1); - update.nodes.push_back(link_data); - update.nodes.push_back(static_text_data2); - update.nodes.push_back(inline_text_data2); - update.nodes.push_back(link_data2); - update.nodes.push_back(list_data); - update.nodes.push_back(list_item_data); - update.nodes.push_back(static_text_data3); - update.nodes.push_back(inline_text_data3); - update.nodes.push_back(search_box); - update.nodes.push_back(search_text); - update.nodes.push_back(pdf_highlight_data); - update.nodes.push_back(static_text_data4); - update.nodes.push_back(inline_text_data4); - + update.nodes = {root_data, paragraph_data, static_text_data1, + inline_text_data1, link_data, static_text_data2, + inline_text_data2, link_data2, list_data, + list_item_data, static_text_data3, inline_text_data3, + search_box, search_text, pdf_highlight_data, + static_text_data4, inline_text_data4}; Init(update); // Set up variables from the tree for testing. diff --git a/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc index 480a7233253..1cc881f5af8 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc @@ -359,31 +359,31 @@ AXTreeUpdate AXPlatformNodeTest::BuildListBox( const std::vector<ax::mojom::State>& additional_state) { AXNodeData listbox; listbox.id = 1; - listbox.SetName("ListBox"); listbox.role = ax::mojom::Role::kListBox; + listbox.SetName("ListBox"); for (auto state : additional_state) listbox.AddState(state); AXNodeData option_1; option_1.id = 2; - option_1.SetName("Option1"); option_1.role = ax::mojom::Role::kListBoxOption; + option_1.SetName("Option1"); if (option_1_is_selected) option_1.AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, true); listbox.child_ids.push_back(option_1.id); AXNodeData option_2; option_2.id = 3; - option_2.SetName("Option2"); option_2.role = ax::mojom::Role::kListBoxOption; + option_2.SetName("Option2"); if (option_2_is_selected) option_2.AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, true); listbox.child_ids.push_back(option_2.id); AXNodeData option_3; option_3.id = 4; - option_3.SetName("Option3"); option_3.role = ax::mojom::Role::kListBoxOption; + option_3.SetName("Option3"); if (option_3_is_selected) option_3.AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, true); listbox.child_ids.push_back(option_3.id); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win.cc b/chromium/ui/accessibility/platform/ax_platform_node_win.cc index b9fbc55ae57..d0fbf20bcf0 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_win.cc @@ -15,11 +15,14 @@ #include <utility> #include <vector> +#include "base/json/json_writer.h" #include "base/lazy_instance.h" #include "base/metrics/histogram_functions.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/values.h" #include "base/win/enum_variant.h" #include "base/win/scoped_bstr.h" #include "base/win/scoped_safearray.h" @@ -27,6 +30,7 @@ #include "skia/ext/skia_utils_win.h" #include "third_party/iaccessible2/ia2_api_all.h" #include "third_party/skia/include/core/SkColor.h" +#include "ui/accessibility/accessibility_features.h" #include "ui/accessibility/accessibility_switches.h" #include "ui/accessibility/ax_action_data.h" #include "ui/accessibility/ax_active_popup.h" @@ -43,6 +47,7 @@ #include "ui/accessibility/platform/ax_platform_node_textchildprovider_win.h" #include "ui/accessibility/platform/ax_platform_node_textprovider_win.h" #include "ui/accessibility/platform/ax_platform_relation_win.h" +#include "ui/accessibility/platform/uia_registrar_win.h" #include "ui/base/win/atl_module.h" #include "ui/display/win/screen_win.h" #include "ui/gfx/geometry/rect_conversions.h" @@ -297,7 +302,7 @@ AXPlatformNode* AXPlatformNode::FromNativeViewAccessible( // AXPlatformNodeWin // -AXPlatformNodeWin::AXPlatformNodeWin() : force_new_hypertext_(false) {} +AXPlatformNodeWin::AXPlatformNodeWin() {} AXPlatformNodeWin::~AXPlatformNodeWin() { ClearOwnRelations(); @@ -305,7 +310,6 @@ AXPlatformNodeWin::~AXPlatformNodeWin() { void AXPlatformNodeWin::Init(AXPlatformNodeDelegate* delegate) { AXPlatformNodeBase::Init(delegate); - force_new_hypertext_ = false; } void AXPlatformNodeWin::ClearOwnRelations() { @@ -314,10 +318,6 @@ void AXPlatformNodeWin::ClearOwnRelations() { relations_.clear(); } -void AXPlatformNodeWin::ForceNewHypertext() { - force_new_hypertext_ = true; -} - // Static void AXPlatformNodeWin::SanitizeStringAttributeForUIAAriaProperty( const base::string16& input, @@ -645,7 +645,7 @@ void AXPlatformNodeWin::NotifyAccessibilityEvent(ax::mojom::Event event_type) { ::VariantInit(old_value.Receive()); base::win::ScopedVariant new_value; ::VariantInit(new_value.Receive()); - GetPropertyValue((*uia_property), new_value.Receive()); + GetPropertyValueImpl((*uia_property), new_value.Receive()); ::UiaRaiseAutomationPropertyChangedEvent(this, (*uia_property), old_value, new_value); } @@ -1263,6 +1263,14 @@ IFACEMETHODIMP AXPlatformNodeWin::get_states(AccessibleStates* states) { IFACEMETHODIMP AXPlatformNodeWin::get_uniqueID(LONG* id) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_UNIQUE_ID); COM_OBJECT_VALIDATE_1_ARG(id); + // We want to negate the unique id for it to be consistent across different + // Windows accessiblity APIs. The negative unique id convention originated + // from ::NotifyWinEvent() takes an hwnd and a child id. A 0 child id means + // self, and a positive child id means child #n. In order to fire an event for + // an arbitrary descendant of the window, Firefox started the practice of + // using a negative unique id. We follow the same negative unique id + // convention here and when we fire events via + // ::NotifyWinEvent(). *id = -GetUniqueId(); return S_OK; } @@ -2075,7 +2083,23 @@ HRESULT AXPlatformNodeWin::ISelectionItemProviderSetSelected( return UIA_E_ELEMENTNOTENABLED; } - if (selected == ISelectionItemProviderIsSelected()) + // The platform implements selection follows focus for single-selection + // container elements. Focus action can change a node's accessibility selected + // state, but does not cause the actual control to be selected. + // https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_selection_follows_focus + // https://www.w3.org/TR/core-aam-1.2/#mapping_events_selection + // + // We don't want to perform |Action::kDoDefault| for an ax node that has + // |kSelected=true| and |kSelectedFromFocus=false|, because perform + // |Action::kDoDefault| may cause the control to be unselected. However, if an + // ax node is selected due to focus, i.e. |kSelectedFromFocus=true|, we need + // to perform |Action::kDoDefault| on the ax node, since focus action only + // changes an ax node's accessibility selected state to |kSelected=true| and + // no |Action::kDoDefault| was performed on that node yet. So we need to + // perform |Action::kDoDefault| on the ax node to cause its associated control + // to be selected. + if (selected == ISelectionItemProviderIsSelected() && + !GetBoolAttribute(ax::mojom::BoolAttribute::kSelectedFromFocus)) return S_OK; AXActionData data; @@ -3971,6 +3995,11 @@ IFACEMETHODIMP AXPlatformNodeWin::get_FragmentRoot( IFACEMETHODIMP AXPlatformNodeWin::GetPatternProvider(PATTERNID pattern_id, IUnknown** result) { WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_GET_PATTERN_PROVIDER); + return GetPatternProviderImpl(pattern_id, result); +} + +HRESULT AXPlatformNodeWin::GetPatternProviderImpl(PATTERNID pattern_id, + IUnknown** result) { UIA_VALIDATE_CALL_1_ARG(result); *result = nullptr; @@ -3997,7 +4026,11 @@ IFACEMETHODIMP AXPlatformNodeWin::GetPropertyValue(PROPERTYID property_id, // Collapse all unknown property IDs into a single bucket. base::UmaHistogramSparse("Accessibility.WinAPIs.GetPropertyValue", 0); } + return GetPropertyValueImpl(property_id, result); +} +HRESULT AXPlatformNodeWin::GetPropertyValueImpl(PROPERTYID property_id, + VARIANT* result) { UIA_VALIDATE_CALL_1_ARG(result); result->vt = VT_EMPTY; @@ -4005,6 +4038,7 @@ IFACEMETHODIMP AXPlatformNodeWin::GetPropertyValue(PROPERTYID property_id, int int_attribute; const AXNodeData& data = GetData(); + // Default UIA Property Ids. switch (property_id) { case UIA_AriaPropertiesPropertyId: result->vt = VT_BSTR; @@ -4044,9 +4078,7 @@ IFACEMETHODIMP AXPlatformNodeWin::GetPropertyValue(PROPERTYID property_id, break; case UIA_CulturePropertyId: - result->vt = VT_BSTR; - GetStringAttributeAsBstr(ax::mojom::StringAttribute::kLanguage, - &result->bstrVal); + return GetCultureAttributeAsVariant(result); break; case UIA_DescribedByPropertyId: @@ -4366,6 +4398,21 @@ IFACEMETHODIMP AXPlatformNodeWin::GetPropertyValue(PROPERTYID property_id, case UIA_ProviderDescriptionPropertyId: case UIA_RuntimeIdPropertyId: break; + } // End of default UIA property ids. + + // Custom UIA Property Ids. + if (property_id == + UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId()) { + // We want to negate the unique id for it to be consistent across different + // Windows accessiblity APIs. The negative unique id convention originated + // from ::NotifyWinEvent() takes an hwnd and a child id. A 0 child id means + // self, and a positive child id means child #n. In order to fire an event + // for an arbitrary descendant of the window, Firefox started the practice + // of using a negative unique id. We follow the same negative unique id + // convention here and when we fire events via ::NotifyWinEvent(). + result->vt = VT_BSTR; + result->bstrVal = + SysAllocString(base::NumberToString16(-GetUniqueId()).c_str()); } return S_OK; @@ -4405,6 +4452,71 @@ IFACEMETHODIMP AXPlatformNodeWin::ShowContextMenu() { } // +// IChromeAccessible implementation. +// + +void SendBulkFetchResponse( + Microsoft::WRL::ComPtr<IChromeAccessibleDelegate> delegate, + LONG request_id, + std::string json_result) { + base::string16 json_result_utf16 = base::UTF8ToUTF16(json_result); + delegate->put_bulkFetchResult(request_id, + SysAllocString(json_result_utf16.c_str())); +} + +IFACEMETHODIMP AXPlatformNodeWin::get_bulkFetch( + BSTR input_json, + LONG request_id, + IChromeAccessibleDelegate* delegate) { + COM_OBJECT_VALIDATE(); + if (!delegate) + return E_INVALIDARG; + + // TODO(crbug.com/1083834): if parsing |input_json|, use + // DataDecoder because the json is untrusted. For now, this is just + // a stub that calls PostTask so that it's async, but it doesn't + // actually parse the input. + + base::Value result(base::Value::Type::DICTIONARY); + result.SetKey("role", base::Value(ui::ToString(GetData().role))); + + gfx::Rect bounds = GetDelegate()->GetBoundsRect( + AXCoordinateSystem::kScreenDIPs, AXClippingBehavior::kUnclipped); + result.SetKey("x", base::Value(bounds.x())); + result.SetKey("y", base::Value(bounds.y())); + result.SetKey("width", base::Value(bounds.width())); + result.SetKey("height", base::Value(bounds.height())); + std::string json_result; + base::JSONWriter::Write(result, &json_result); + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce( + &SendBulkFetchResponse, + Microsoft::WRL::ComPtr<IChromeAccessibleDelegate>(delegate), + request_id, json_result)); + return S_OK; +} + +IFACEMETHODIMP AXPlatformNodeWin::get_hitTest( + LONG screen_physical_pixel_x, + LONG screen_physical_pixel_y, + LONG request_id, + IChromeAccessibleDelegate* delegate) { + COM_OBJECT_VALIDATE(); + + if (!delegate) + return E_INVALIDARG; + + // TODO(crbug.com/1083834): Plumb through an actual async hit test. + AXPlatformNodeWin* hit_child = static_cast<AXPlatformNodeWin*>( + FromNativeViewAccessible(GetDelegate()->HitTestSync( + screen_physical_pixel_x, screen_physical_pixel_y))); + + delegate->put_hitTestResult(request_id, static_cast<IAccessible*>(hit_child)); + return S_OK; +} + +// // IServiceProvider implementation. // @@ -4430,6 +4542,12 @@ IFACEMETHODIMP AXPlatformNodeWin::QueryService(REFGUID guidService, return QueryInterface(riid, object); } + if (guidService == IID_IChromeAccessible) { + if (features::IsIChromeAccessibleEnabled()) { + return QueryInterface(riid, object); + } + } + // TODO(suproteem): Include IAccessibleEx in the list, potentially checking // for version. @@ -4468,6 +4586,10 @@ STDMETHODIMP AXPlatformNodeWin::InternalQueryInterface( if (!accessible->GetData().IsRangeValueSupported()) { return E_NOINTERFACE; } + } else if (riid == IID_IChromeAccessible) { + if (!features::IsIChromeAccessibleEnabled()) { + return E_NOINTERFACE; + } } return CComObjectRootBase::InternalQueryInterface(this_ptr, entries, riid, @@ -5254,7 +5376,7 @@ int32_t AXPlatformNodeWin::ComputeIA2State() { const AXNodeData& data = GetData(); int32_t ia2_state = IA2_STATE_OPAQUE; - if (HasIntAttribute(ax::mojom::IntAttribute::kCheckedState)) + if (IsPlatformCheckable()) ia2_state |= IA2_STATE_CHECKABLE; if (HasIntAttribute(ax::mojom::IntAttribute::kInvalidState) && @@ -6800,7 +6922,12 @@ bool AXPlatformNodeWin::IsUIAControl() const { // UIA provides multiple "views": raw, content and control. We only want to // populate the content and control views with items that make sense to // traverse over. + if (GetDelegate()->IsWebContent()) { + // Invisible or ignored elements should not show up in control view at all. + if (IsInvisibleOrIgnored()) + return false; + if (IsTextOnlyObject()) { // A text leaf can be a UIAControl, but text inside of a heading, link, // button, etc. where the role allows the name to be generated from the @@ -6840,7 +6967,8 @@ bool AXPlatformNodeWin::IsUIAControl() const { } parent = FromNativeViewAccessible(parent->GetParent()); } - } + } // end of text only case. + const AXNodeData& data = GetData(); // https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-treeoverview#control-view // The control view also includes noninteractive UI items that contribute @@ -6892,9 +7020,10 @@ bool AXPlatformNodeWin::IsUIAControl() const { !data.HasState(ax::mojom::State::kFocusable) && !data.IsClickable()) { return false; } + return true; - } - // non web-content case. + } // end of web-content only case. + const AXNodeData& data = GetData(); return !((IsReadOnlySupported(data.role) && data.IsReadOnlyOrDisabled()) || data.HasState(ax::mojom::State::kInvisible) || @@ -6959,8 +7088,10 @@ bool AXPlatformNodeWin::IsInaccessibleDueToAncestor() const { } bool AXPlatformNodeWin::ShouldHideChildrenForUIA() const { - auto role = GetData().role; + if (IsPlainTextField()) + return true; + auto role = GetData().role; if (HasPresentationalChildren(role)) return true; @@ -6982,7 +7113,6 @@ bool AXPlatformNodeWin::ShouldHideChildrenForUIA() const { return only_child && only_child->IsTextOnlyObject(); } return false; - case ax::mojom::Role::kTextField: case ax::mojom::Role::kPdfActionableHighlight: return true; default: @@ -7004,6 +7134,13 @@ base::string16 AXPlatformNodeWin::GetValue() const { return value; } +bool AXPlatformNodeWin::IsPlatformCheckable() const { + if (GetData().role == ax::mojom::Role::kToggleButton) + return false; + + return AXPlatformNodeBase::IsPlatformCheckable(); +} + bool AXPlatformNodeWin::ShouldNodeHaveFocusableState( const AXNodeData& data) const { switch (data.role) { @@ -7212,6 +7349,8 @@ base::Optional<DWORD> AXPlatformNodeWin::MojoEventToMSAAEvent( switch (event) { case ax::mojom::Event::kAlert: return EVENT_SYSTEM_ALERT; + case ax::mojom::Event::kActiveDescendantChanged: + return IA2_EVENT_ACTIVE_DESCENDANT_CHANGED; case ax::mojom::Event::kCheckedStateChanged: case ax::mojom::Event::kExpandedChanged: case ax::mojom::Event::kStateChanged: diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win.h b/chromium/ui/accessibility/platform/ax_platform_node_win.h index 1055422d59c..9f4367393bc 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_win.h @@ -28,6 +28,7 @@ #include "ui/accessibility/ax_text_utils.h" #include "ui/accessibility/platform/ax_platform_node_base.h" #include "ui/accessibility/platform/ax_platform_text_boundary.h" +#include "ui/accessibility/platform/ichromeaccessible.h" #include "ui/gfx/range/range.h" // IMPORTANT! @@ -280,6 +281,11 @@ enum { UMA_API_WINDOW_GET_WINDOWVISUALSTATE = 243, UMA_API_WINDOW_GET_WINDOWINTERACTIONSTATE = 244, UMA_API_WINDOW_GET_ISTOPMOST = 245, + UMA_API_ELEMENT_PROVIDER_FROM_POINT = 246, + UMA_API_GET_FOCUS = 247, + UMA_API_ADVISE_EVENT_ADDED = 248, + UMA_API_ADVISE_EVENT_REMOVED = 249, + UMA_API_ITEMCONTAINER_FINDITEMBYPROPERTY = 250, // This must always be the last enum. It's okay for its value to // increase, but none of the other enum values may change. @@ -358,6 +364,7 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) public IToggleProvider, public IValueProvider, public IWindowProvider, + public IChromeAccessible, public AXPlatformNodeBase { using IDispatchImpl::Invoke; @@ -381,6 +388,7 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) COM_INTERFACE_ENTRY(IAccessibleTable2) COM_INTERFACE_ENTRY(IAccessibleTableCell) COM_INTERFACE_ENTRY(IAccessibleValue) + COM_INTERFACE_ENTRY(IChromeAccessible) COM_INTERFACE_ENTRY(IExpandCollapseProvider) COM_INTERFACE_ENTRY(IGridItemProvider) COM_INTERFACE_ENTRY(IGridProvider) @@ -408,8 +416,6 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) // Clear any AXPlatformRelationWin nodes owned by this node. void ClearOwnRelations(); - void ForceNewHypertext(); - // AXPlatformNode overrides. gfx::NativeViewAccessible GetNativeViewAccessible() override; void NotifyAccessibilityEvent(ax::mojom::Event event_type) override; @@ -417,6 +423,7 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) // AXPlatformNodeBase overrides. void Destroy() override; base::string16 GetValue() const override; + bool IsPlatformCheckable() const override; // // IAccessible methods. @@ -1023,6 +1030,19 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) IFACEMETHODIMP ShowContextMenu() override; // + // IChromeAccessible methods. + // + + IFACEMETHODIMP get_bulkFetch(BSTR input_json, + LONG request_id, + IChromeAccessibleDelegate* delegate) override; + + IFACEMETHODIMP get_hitTest(LONG screen_physical_pixel_x, + LONG screen_physical_pixel_y, + LONG request_id, + IChromeAccessibleDelegate* delegate) override; + + // // IServiceProvider methods. // @@ -1046,6 +1066,16 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) // IRawElementProviderSimple support method. bool IsPatternProviderSupported(PATTERNID pattern_id); + // Prefer GetPatternProviderImpl when calling internally. We should avoid + // calling external APIs internally as it will cause the histograms to become + // innaccurate. + HRESULT GetPatternProviderImpl(PATTERNID pattern_id, IUnknown** result); + + // Prefer GetPropertyValueImpl when calling internally. We should avoid + // calling external APIs internally as it will cause the histograms to become + // innaccurate. + HRESULT GetPropertyValueImpl(PROPERTYID property_id, VARIANT* result); + // Helper to return the runtime id (without going through a SAFEARRAY) using RuntimeIdArray = std::array<int, 2>; void GetRuntimeIdArray(RuntimeIdArray& runtime_id); @@ -1123,7 +1153,6 @@ class AX_EXPORT __declspec(uuid("26f5641a-246d-457b-a96d-07f3fae6acf2")) std::vector<Microsoft::WRL::ComPtr<AXPlatformRelationWin>> relations_; AXHypertext old_hypertext_; - bool force_new_hypertext_; // These protected methods are still used by BrowserAccessibilityComWin. At // some point post conversion, we can probably move these to be private diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc index 50b78af9642..c7fce2de057 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc @@ -11,8 +11,11 @@ #include <memory> #include "base/auto_reset.h" +#include "base/json/json_reader.h" +#include "base/run_loop.h" #include "base/stl_util.h" #include "base/test/metrics/histogram_tester.h" +#include "base/test/task_environment.h" #include "base/win/atl.h" #include "base/win/scoped_bstr.h" #include "base/win/scoped_safearray.h" @@ -20,6 +23,7 @@ #include "testing/gmock/include/gmock/gmock-matchers.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/iaccessible2/ia2_api_all.h" +#include "ui/accessibility/accessibility_features.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/platform/ax_fragment_root_win.h" @@ -219,7 +223,10 @@ ScopedVariant SELF(CHILDID_SELF); testing::UnorderedElementsAreArray(expected_property_values)); \ } -AXPlatformNodeWinTest::AXPlatformNodeWinTest() {} +AXPlatformNodeWinTest::AXPlatformNodeWinTest() { + scoped_feature_list_.InitAndEnableFeature(features::kIChromeAccessible); +} + AXPlatformNodeWinTest::~AXPlatformNodeWinTest() {} void AXPlatformNodeWinTest::SetUp() { @@ -451,6 +458,7 @@ bool TestFragmentRootDelegate::IsAXFragmentRootAControlElement() { TEST_F(AXPlatformNodeWinTest, IAccessibleDetachedObject) { AXNodeData root; root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; root.SetName("Name"); Init(root); @@ -472,12 +480,14 @@ TEST_F(AXPlatformNodeWinTest, IAccessibleHitTest) { AXNodeData node1; node1.id = 2; + node1.role = ax::mojom::Role::kGenericContainer; node1.relative_bounds.bounds = gfx::RectF(0, 0, 10, 10); node1.SetName("Name1"); root.child_ids.push_back(node1.id); AXNodeData node2; node2.id = 3; + node2.role = ax::mojom::Role::kGenericContainer; node2.relative_bounds.bounds = gfx::RectF(20, 20, 20, 20); node2.SetName("Name2"); root.child_ids.push_back(node2.id); @@ -512,6 +522,7 @@ TEST_F(AXPlatformNodeWinTest, IAccessibleHitTestDoesNotLoopForever) { AXNodeData node1; node1.id = 2; + node1.role = ax::mojom::Role::kGenericContainer; node1.relative_bounds.bounds = gfx::RectF(0, 0, 10, 10); node1.SetName("Name1"); root.child_ids.push_back(node1.id); @@ -535,6 +546,7 @@ TEST_F(AXPlatformNodeWinTest, IAccessibleHitTestDoesNotLoopForever) { TEST_F(AXPlatformNodeWinTest, IAccessibleName) { AXNodeData root; root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; root.SetName("Name"); Init(root); @@ -1928,6 +1940,103 @@ TEST_F(AXPlatformNodeWinTest, IAccessible2GetNRelations) { // TODO(dougt): Try adding one more relation. } +TEST_F(AXPlatformNodeWinTest, + IAccessible2TestPopupForRelationMapsToControlledByRelation) { + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; + + AXNodeData child1; + child1.id = 2; + child1.role = ax::mojom::Role::kTextField; + child1.AddIntListAttribute(ax::mojom::IntListAttribute::kControlsIds, {3}); + root.child_ids.push_back(2); + + // Add listbox that is popup for the textfield. + AXNodeData child2; + child2.id = 3; + child2.role = ax::mojom::Role::kListBox; + child2.AddIntAttribute(ax::mojom::IntAttribute::kPopupForId, 2); + root.child_ids.push_back(3); + + Init(root, child1, child2); + ComPtr<IAccessible> root_iaccessible(GetRootIAccessible()); + ComPtr<IAccessible2> root_iaccessible2 = ToIAccessible2(root_iaccessible); + + ComPtr<IDispatch> result; + EXPECT_EQ(S_OK, root_iaccessible2->get_accChild(ScopedVariant(1), &result)); + ComPtr<IAccessible2> ax_child1; + EXPECT_EQ(S_OK, result.As(&ax_child1)); + result.Reset(); + + EXPECT_EQ(S_OK, root_iaccessible2->get_accChild(ScopedVariant(2), &result)); + ComPtr<IAccessible2> ax_child2; + EXPECT_EQ(S_OK, result.As(&ax_child2)); + result.Reset(); + + LONG n_relations = 0; + LONG n_targets = 0; + ScopedBstr relation_type; + ComPtr<IAccessibleRelation> controls_relation; + ComPtr<IAccessibleRelation> controlled_by_relation; + ComPtr<IUnknown> target; + + EXPECT_HRESULT_SUCCEEDED(ax_child1->get_nRelations(&n_relations)); + EXPECT_EQ(1, n_relations); + + EXPECT_HRESULT_SUCCEEDED(ax_child1->get_relation(0, &controls_relation)); + + EXPECT_HRESULT_SUCCEEDED( + controls_relation->get_relationType(relation_type.Receive())); + EXPECT_EQ(L"controllerFor", base::string16(relation_type.Get())); + + relation_type.Reset(); + + EXPECT_HRESULT_SUCCEEDED(controls_relation->get_nTargets(&n_targets)); + EXPECT_EQ(1, n_targets); + + EXPECT_HRESULT_SUCCEEDED(controls_relation->get_target(0, &target)); + target.Reset(); + + controls_relation.Reset(); + + // Test the controlled by relation, mapped from the popup for relation. + EXPECT_HRESULT_SUCCEEDED(ax_child2->get_nRelations(&n_relations)); + // The test is currently outsmarting us, and automatically mapping the + // reverse relation in addition to mapping the popup for -> controlled by. + // Therefore, the same relation will exist twice in this test, which + // actually shows that the popup for -> controlled by relation is working. + // As a result, both relations should have the same result in this test. + EXPECT_EQ(2, n_relations); + + // Both relations should have the same result in this test. + EXPECT_HRESULT_SUCCEEDED(ax_child2->get_relation(0, &controlled_by_relation)); + EXPECT_HRESULT_SUCCEEDED( + controlled_by_relation->get_relationType(relation_type.Receive())); + EXPECT_EQ(L"controlledBy", base::string16(relation_type.Get())); + relation_type.Reset(); + + EXPECT_HRESULT_SUCCEEDED(controlled_by_relation->get_nTargets(&n_targets)); + EXPECT_EQ(1, n_targets); + + EXPECT_HRESULT_SUCCEEDED(controlled_by_relation->get_target(0, &target)); + target.Reset(); + controlled_by_relation.Reset(); + + // Both relations should have the same result in this test. + EXPECT_HRESULT_SUCCEEDED(ax_child2->get_relation(1, &controlled_by_relation)); + EXPECT_HRESULT_SUCCEEDED( + controlled_by_relation->get_relationType(relation_type.Receive())); + EXPECT_EQ(L"controlledBy", base::string16(relation_type.Get())); + relation_type.Reset(); + + EXPECT_HRESULT_SUCCEEDED(controlled_by_relation->get_nTargets(&n_targets)); + EXPECT_EQ(1, n_targets); + + EXPECT_HRESULT_SUCCEEDED(controlled_by_relation->get_target(0, &target)); + target.Reset(); +} + TEST_F(AXPlatformNodeWinTest, DISABLED_TestRelationTargetsOfType) { AXNodeData root; root.id = 1; @@ -3474,6 +3583,124 @@ TEST_F(AXPlatformNodeWinTest, ITableProviderGetColumnHeaders) { EXPECT_EQ(nullptr, safearray.Get()); } +TEST_F(AXPlatformNodeWinTest, ITableProviderGetColumnHeadersMultipleHeaders) { + // Build a table like this: + // header_r1c1 | header_r1c2 | header_r1c3 + // cell_r2c1 | cell_r2c2 | cell_r2c3 + // cell_r3c1 | header_r3c2 | + + // <table> + // <tr aria-label="row1"> + // <th>header_r1c1</th> + // <th>header_r1c2</th> + // <th>header_r1c3</th> + // </tr> + // <tr aria-label="row2"> + // <td>cell_r2c1</td> + // <td>cell_r2c2</td> + // <td>cell_r2c3</td> + // </tr> + // <tr aria-label="row3"> + // <td>cell_r3c1</td> + // <th>header_r3c2</th> + // </tr> + // </table> + + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kTable; + + AXNodeData row1; + row1.id = 2; + row1.role = ax::mojom::Role::kRow; + root.child_ids.push_back(row1.id); + + AXNodeData row2; + row2.id = 3; + row2.role = ax::mojom::Role::kRow; + root.child_ids.push_back(row2.id); + + AXNodeData row3; + row3.id = 4; + row3.role = ax::mojom::Role::kRow; + root.child_ids.push_back(row3.id); + + // <tr aria-label="row1"> + // <th>header_r1c1</th> <th>header_r1c2</th> <th>header_r1c3</th> + // </tr> + AXNodeData header_r1c1; + header_r1c1.id = 5; + header_r1c1.role = ax::mojom::Role::kColumnHeader; + header_r1c1.SetName(L"header_r1c1"); + row1.child_ids.push_back(header_r1c1.id); + + AXNodeData header_r1c2; + header_r1c2.id = 6; + header_r1c2.role = ax::mojom::Role::kColumnHeader; + header_r1c2.SetName(L"header_r1c2"); + row1.child_ids.push_back(header_r1c2.id); + + AXNodeData header_r1c3; + header_r1c3.id = 7; + header_r1c3.role = ax::mojom::Role::kColumnHeader; + header_r1c3.SetName(L"header_r1c3"); + row1.child_ids.push_back(header_r1c3.id); + + // <tr aria-label="row2"> + // <td>cell_r2c1</td> <td>cell_r2c2</td> <td>cell_r2c3</td> + // </tr> + AXNodeData cell_r2c1; + cell_r2c1.id = 8; + cell_r2c1.role = ax::mojom::Role::kCell; + cell_r2c1.SetName(L"cell_r2c1"); + row2.child_ids.push_back(cell_r2c1.id); + + AXNodeData cell_r2c2; + cell_r2c2.id = 9; + cell_r2c2.role = ax::mojom::Role::kCell; + cell_r2c2.SetName(L"cell_r2c2"); + row2.child_ids.push_back(cell_r2c2.id); + + AXNodeData cell_r2c3; + cell_r2c3.id = 10; + cell_r2c3.role = ax::mojom::Role::kCell; + cell_r2c3.SetName(L"cell_r2c3"); + row2.child_ids.push_back(cell_r2c3.id); + + // <tr aria-label="row3"> + // <td>cell_r3c1</td> <th>header_r3c2</th> + // </tr> + AXNodeData cell_r3c1; + cell_r3c1.id = 11; + cell_r3c1.role = ax::mojom::Role::kCell; + cell_r3c1.SetName(L"cell_r3c1"); + row3.child_ids.push_back(cell_r3c1.id); + + AXNodeData header_r3c2; + header_r3c2.id = 12; + header_r3c2.role = ax::mojom::Role::kColumnHeader; + header_r3c2.SetName(L"header_r3c2"); + row3.child_ids.push_back(header_r3c2.id); + + Init(root, row1, row2, row3, header_r1c1, header_r1c2, header_r1c3, cell_r2c1, + cell_r2c2, cell_r2c3, cell_r3c1, header_r3c2); + + ComPtr<ITableProvider> root_itableprovider( + QueryInterfaceFromNode<ITableProvider>(GetRootAsAXNode())); + + base::win::ScopedSafearray safearray; + EXPECT_HRESULT_SUCCEEDED( + root_itableprovider->GetColumnHeaders(safearray.Receive())); + EXPECT_NE(nullptr, safearray.Get()); + + // Validate that we retrieve all column headers of the table and in the order + // below. + std::vector<std::wstring> expected_names = {L"header_r1c1", L"header_r1c2", + L"header_r3c2", L"header_r1c3"}; + EXPECT_UIA_ELEMENT_ARRAY_BSTR_EQ(safearray.Get(), UIA_NamePropertyId, + expected_names); +} + TEST_F(AXPlatformNodeWinTest, ITableProviderGetRowHeaders) { AXNodeData root; root.id = 1; @@ -3690,6 +3917,7 @@ TEST_F(AXPlatformNodeWinTest, IA2GetAttribute) { TEST_F(AXPlatformNodeWinTest, UIAGetPropertySimple) { AXNodeData root; + root.role = ax::mojom::Role::kList; root.SetName("fake name"); root.AddStringAttribute(ax::mojom::StringAttribute::kAccessKey, "Ctrl+Q"); root.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "en-us"); @@ -3699,7 +3927,6 @@ TEST_F(AXPlatformNodeWinTest, UIAGetPropertySimple) { root.AddIntAttribute(ax::mojom::IntAttribute::kSetSize, 2); root.AddIntAttribute(ax::mojom::IntAttribute::kInvalidState, 1); root.id = 1; - root.role = ax::mojom::Role::kList; AXNodeData child1; child1.id = 2; @@ -3725,7 +3952,8 @@ TEST_F(AXPlatformNodeWinTest, UIAGetPropertySimple) { EXPECT_UIA_BSTR_EQ(root_node, UIA_AriaPropertiesPropertyId, L"readonly=true;expanded=false;multiline=false;" L"multiselectable=false;required=false;setsize=2"); - EXPECT_UIA_BSTR_EQ(root_node, UIA_CulturePropertyId, L"en-us"); + constexpr int en_us_lcid = 1033; + EXPECT_UIA_INT_EQ(root_node, UIA_CulturePropertyId, en_us_lcid); EXPECT_UIA_BSTR_EQ(root_node, UIA_NamePropertyId, L"fake name"); EXPECT_UIA_INT_EQ(root_node, UIA_ControlTypePropertyId, int{UIA_ListControlTypeId}); @@ -3814,6 +4042,93 @@ TEST_F(AXPlatformNodeWinTest, UIAGetPropertyValueIsDialog) { UIA_IsDialogPropertyId, true); } +TEST_F(AXPlatformNodeWinTest, + UIAGetPropertyValueIsControlElementIgnoredInvisible) { + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; + root.child_ids = {2, 3, 4, 5, 6, 7, 8}; + + AXNodeData normal_button; + normal_button.id = 2; + normal_button.role = ax::mojom::Role::kButton; + + AXNodeData ignored_button; + ignored_button.id = 3; + ignored_button.role = ax::mojom::Role::kButton; + ignored_button.AddState(ax::mojom::State::kIgnored); + + AXNodeData invisible_button; + invisible_button.id = 4; + invisible_button.role = ax::mojom::Role::kButton; + invisible_button.AddState(ax::mojom::State::kInvisible); + + AXNodeData invisible_focusable_button; + invisible_focusable_button.id = 5; + invisible_focusable_button.role = ax::mojom::Role::kButton; + invisible_focusable_button.AddState(ax::mojom::State::kInvisible); + invisible_focusable_button.AddState(ax::mojom::State::kFocusable); + + AXNodeData focusable_generic_container; + focusable_generic_container.id = 6; + focusable_generic_container.role = ax::mojom::Role::kGenericContainer; + focusable_generic_container.AddState(ax::mojom::State::kFocusable); + + AXNodeData ignored_focusable_generic_container; + ignored_focusable_generic_container.id = 7; + ignored_focusable_generic_container.role = ax::mojom::Role::kGenericContainer; + ignored_focusable_generic_container.AddState(ax::mojom::State::kIgnored); + focusable_generic_container.AddState(ax::mojom::State::kFocusable); + + AXNodeData invisible_focusable_generic_container; + invisible_focusable_generic_container.id = 8; + invisible_focusable_generic_container.role = + ax::mojom::Role::kGenericContainer; + invisible_focusable_generic_container.AddState(ax::mojom::State::kInvisible); + invisible_focusable_generic_container.AddState(ax::mojom::State::kFocusable); + + Init(root, normal_button, ignored_button, invisible_button, + invisible_focusable_button, focusable_generic_container, + ignored_focusable_generic_container, + invisible_focusable_generic_container); + + // Turn on web content mode for the AXTree. + TestAXNodeWrapper::SetGlobalIsWebContent(true); + + // Normal button (id=2), no invisible or ignored state set. Should be a + // control element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(0), + UIA_IsControlElementPropertyId, true); + + // Button with ignored state (id=3). Should not be a control element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(1), + UIA_IsControlElementPropertyId, false); + + // Button with invisible state (id=4). Should not be a control element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(2), + UIA_IsControlElementPropertyId, false); + + // Button with invisible state, but focusable (id=5). Should not be a control + // element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(3), + UIA_IsControlElementPropertyId, false); + + // Generic container, focusable (id=6). Should be a control + // element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(4), + UIA_IsControlElementPropertyId, true); + + // Generic container, ignored but focusable (id=7). Should not be a control + // element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(5), + UIA_IsControlElementPropertyId, false); + + // Generic container, invisible and ignored, but focusable (id=8). Should not + // be a control element. + EXPECT_UIA_BOOL_EQ(GetIRawElementProviderSimpleFromChildIndex(6), + UIA_IsControlElementPropertyId, false); +} + TEST_F(AXPlatformNodeWinTest, UIAGetControllerForPropertyId) { AXNodeData root; root.id = 1; @@ -6251,6 +6566,65 @@ TEST_F(AXPlatformNodeWinTest, ISelectionItemProviderGetSelectionContainer) { EXPECT_EQ(container, container_provider); } +TEST_F(AXPlatformNodeWinTest, ISelectionItemProviderSelectFollowFocus) { + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kTabList; + + AXNodeData tab1; + tab1.id = 2; + tab1.role = ax::mojom::Role::kTab; + tab1.AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, false); + tab1.SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kClick); + root.child_ids.push_back(tab1.id); + + Init(root, tab1); + + auto* tab1_node = GetRootAsAXNode()->children()[0]; + ComPtr<IRawElementProviderSimple> tab1_raw_element_provider_simple = + QueryInterfaceFromNode<IRawElementProviderSimple>(tab1_node); + ASSERT_NE(nullptr, tab1_raw_element_provider_simple.Get()); + + ComPtr<IRawElementProviderFragment> tab1_raw_element_provider_fragment = + IRawElementProviderFragmentFromNode(tab1_node); + ASSERT_NE(nullptr, tab1_raw_element_provider_fragment.Get()); + + ComPtr<ISelectionItemProvider> tab1_selection_item_provider; + EXPECT_HRESULT_SUCCEEDED(tab1_raw_element_provider_simple->GetPatternProvider( + UIA_SelectionItemPatternId, &tab1_selection_item_provider)); + ASSERT_NE(nullptr, tab1_selection_item_provider.Get()); + + BOOL is_selected; + // Before setting focus to "tab1", validate that "tab1" has selected=false. + tab1_selection_item_provider->get_IsSelected(&is_selected); + EXPECT_FALSE(is_selected); + + // Setting focus on "tab1" will result in selected=true. + tab1_raw_element_provider_fragment->SetFocus(); + tab1_selection_item_provider->get_IsSelected(&is_selected); + EXPECT_TRUE(is_selected); + + // Verify that we can still trigger action::kDoDefault through Select(). + EXPECT_HRESULT_SUCCEEDED(tab1_selection_item_provider->Select()); + tab1_selection_item_provider->get_IsSelected(&is_selected); + EXPECT_TRUE(is_selected); + EXPECT_EQ(tab1_node, TestAXNodeWrapper::GetNodeFromLastDefaultAction()); + // Verify that after Select(), "tab1" is still selected. + tab1_selection_item_provider->get_IsSelected(&is_selected); + EXPECT_TRUE(is_selected); + + // Since last Select() performed |action::kDoDefault|, which set + // |kSelectedFromFocus| to false. Calling Select() again will not perform + // |action::kDoDefault| again. + TestAXNodeWrapper::SetNodeFromLastDefaultAction(nullptr); + EXPECT_HRESULT_SUCCEEDED(tab1_selection_item_provider->Select()); + tab1_selection_item_provider->get_IsSelected(&is_selected); + EXPECT_TRUE(is_selected); + // Verify that after Select(),|action::kDoDefault| was not triggered on + // "tab1". + EXPECT_EQ(nullptr, TestAXNodeWrapper::GetNodeFromLastDefaultAction()); +} + TEST_F(AXPlatformNodeWinTest, IValueProvider_GetValue) { AXNodeData root; root.id = 1; @@ -6512,4 +6886,120 @@ TEST_F(AXPlatformNodeWinTest, SanitizeStringAttributeForIA2) { EXPECT_EQ("\\\\\\:\\=\\,\\;", output); } +// +// IChromeAccessible tests +// + +class TestIChromeAccessibleDelegate + : public CComObjectRootEx<CComMultiThreadModel>, + public IDispatchImpl<IChromeAccessibleDelegate> { + using IDispatchImpl::Invoke; + + public: + BEGIN_COM_MAP(TestIChromeAccessibleDelegate) + COM_INTERFACE_ENTRY(IChromeAccessibleDelegate) + END_COM_MAP() + + TestIChromeAccessibleDelegate() = default; + ~TestIChromeAccessibleDelegate() = default; + + std::string WaitForBulkFetchResult(LONG expected_request_id) { + if (bulk_fetch_result_.empty()) + WaitUsingRunLoop(); + CHECK_EQ(expected_request_id, request_id_); + return bulk_fetch_result_; + } + + IUnknown* WaitForHitTestResult(LONG expected_request_id) { + if (!hit_test_result_) + WaitUsingRunLoop(); + CHECK_EQ(expected_request_id, request_id_); + return hit_test_result_.Get(); + } + + private: + void WaitUsingRunLoop() { + base::RunLoop run_loop; + run_loop_quit_closure_ = run_loop.QuitClosure(); + run_loop.Run(); + } + + IFACEMETHODIMP put_bulkFetchResult(LONG request_id, BSTR result) override { + bulk_fetch_result_ = base::WideToUTF8(result); + request_id_ = request_id; + if (run_loop_quit_closure_) + run_loop_quit_closure_.Run(); + return S_OK; + } + + IFACEMETHODIMP put_hitTestResult(LONG request_id, IUnknown* result) override { + hit_test_result_ = result; + request_id_ = request_id; + if (run_loop_quit_closure_) + run_loop_quit_closure_.Run(); + return S_OK; + } + + std::string bulk_fetch_result_; + ComPtr<IUnknown> hit_test_result_; + LONG request_id_ = 0; + base::RepeatingClosure run_loop_quit_closure_; +}; + +// http://crbug.com/1087206: failing on Win7 builders. +TEST_F(AXPlatformNodeWinTest, DISABLED_BulkFetch) { + base::test::SingleThreadTaskEnvironment task_environment; + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kScrollBar; + + Init(root); + + ComPtr<IChromeAccessible> chrome_accessible = + QueryInterfaceFromNode<IChromeAccessible>(GetRootAsAXNode()); + + CComObject<TestIChromeAccessibleDelegate>* delegate = nullptr; + ASSERT_HRESULT_SUCCEEDED( + CComObject<TestIChromeAccessibleDelegate>::CreateInstance(&delegate)); + ScopedBstr input_bstr(L"Potato"); + chrome_accessible->get_bulkFetch(input_bstr.Get(), 99, delegate); + std::string response = delegate->WaitForBulkFetchResult(99); + + // Note: base::JSONReader is fine for unit tests, but production code + // that parses untrusted JSON should always use DataDecoder instead. + base::Optional<base::Value> result = + base::JSONReader::Read(response, base::JSON_ALLOW_TRAILING_COMMAS); + ASSERT_TRUE(result); + ASSERT_TRUE(result->FindKey("role")); + ASSERT_EQ("scrollBar", result->FindKey("role")->GetString()); +} + +TEST_F(AXPlatformNodeWinTest, AsyncHitTest) { + base::test::SingleThreadTaskEnvironment task_environment; + AXNodeData root; + root.id = 50; + root.role = ax::mojom::Role::kArticle; + root.relative_bounds.bounds = gfx::RectF(0, 0, 800, 600); + + Init(root); + + ComPtr<IChromeAccessible> chrome_accessible = + QueryInterfaceFromNode<IChromeAccessible>(GetRootAsAXNode()); + + CComObject<TestIChromeAccessibleDelegate>* delegate = nullptr; + ASSERT_HRESULT_SUCCEEDED( + CComObject<TestIChromeAccessibleDelegate>::CreateInstance(&delegate)); + ScopedBstr input_bstr(L"Potato"); + chrome_accessible->get_hitTest(400, 300, 12345, delegate); + ComPtr<IUnknown> result = delegate->WaitForHitTestResult(12345); + ComPtr<IAccessible2> accessible = ToIAccessible2(result); + LONG result_unique_id = 0; + ASSERT_HRESULT_SUCCEEDED(accessible->get_uniqueID(&result_unique_id)); + ComPtr<IAccessible2> root_accessible = + QueryInterfaceFromNode<IAccessible2>(GetRootAsAXNode()); + LONG root_unique_id = 0; + ASSERT_HRESULT_SUCCEEDED(root_accessible->get_uniqueID(&root_unique_id)); + ASSERT_EQ(root_unique_id, result_unique_id); +} + } // namespace ui diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.h b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.h index 400be7c18c3..1fb54910d86 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.h @@ -10,6 +10,7 @@ #include <memory> #include <unordered_set> +#include "base/test/scoped_feature_list.h" #include "ui/accessibility/platform/ax_fragment_root_delegate_win.h" #include "ui/base/win/accessibility_misc_utils.h" @@ -98,6 +99,8 @@ class AXPlatformNodeWinTest : public AXPlatformNodeTest { std::unique_ptr<AXFragmentRootWin> ax_fragment_root_; std::unique_ptr<TestFragmentRootDelegate> test_fragment_root_delegate_; + + base::test::ScopedFeatureList scoped_feature_list_; }; } // namespace ui diff --git a/chromium/ui/accessibility/platform/ax_platform_relation_win.cc b/chromium/ui/accessibility/platform/ax_platform_relation_win.cc index 5f03ec806bf..eee92e3b9cb 100644 --- a/chromium/ui/accessibility/platform/ax_platform_relation_win.cc +++ b/chromium/ui/accessibility/platform/ax_platform_relation_win.cc @@ -43,6 +43,12 @@ base::string16 GetIA2RelationFromIntAttr(ax::mojom::IntAttribute attribute) { return IA2_RELATION_MEMBER_OF; case ax::mojom::IntAttribute::kErrormessageId: return IA2_RELATION_ERROR; + case ax::mojom::IntAttribute::kPopupForId: + // Map "popup for" to "controlled by". + // Unlike ATK there is no special IA2 popup-for relationship, but it can + // be exposed via the controlled by relation, which is also computed for + // content as the reverse of the controls relationship. + return IA2_RELATION_CONTROLLED_BY; default: break; } diff --git a/chromium/ui/accessibility/platform/ichromeaccessible.idl b/chromium/ui/accessibility/platform/ichromeaccessible.idl new file mode 100644 index 00000000000..f3567d1ba74 --- /dev/null +++ b/chromium/ui/accessibility/platform/ichromeaccessible.idl @@ -0,0 +1,64 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import "objidl.idl"; +import "oaidl.idl"; + +const long DISPID_CHROME_BULK_FETCH = -1600; +const long DISPID_CHROME_ON_BULK_FETCH_RESULT = -1601; +const long DISPID_CHROME_HIT_TEST = -1602; +const long DISPID_CHROME_ON_HIT_TEST_RESULT = -1603; + +// Interface to be implemented by the client that calls IChromeAccessible. +// For every method in IChromeAccessible, there's a corresponding response +// method in IChromeAccessibleDelegate. +[object, uuid(0e3edc14-79f4-413f-b854-d3b6860d74a2), pointer_default(unique)] +interface IChromeAccessibleDelegate : IUnknown +{ + [propput, id(DISPID_CHROME_ON_BULK_FETCH_RESULT)] HRESULT bulkFetchResult( + [in] LONG requestID, + [in] BSTR resultJson + ); + + [propput, id(DISPID_CHROME_ON_HIT_TEST_RESULT)] HRESULT hitTestResult( + [in] LONG requestID, + [in] IUnknown* result + ); +}; + +// Chrome-specific interface exposed on every IAccessible object. +// +// This interface is EXPERIMENTAL and only available behind a flag. +// Run Chrome with --enable-features=IChromeAccessible to use it. +// +// Do not depend on this interface remaining stable! It's only designed +// for prototyping ideas, and anything that's stabilized should move to +// an open standard API. +[object, uuid(6175bd95-3b2e-4ebc-bc51-9cab782bec92), pointer_default(unique)] +interface IChromeAccessible : IUnknown +{ + // TODO(crbug.com/1083834): Fully document this interface. + // Fetch multiple accessibility properties of one or more accessibility + // nodes as JSON. This method is asynchronous; the result is returned + // by calling put_bulkFetchResult on |delegate|. The client can pass any + // valid LONG as requestID and the same value will be passed to + // put_bulkFetchResult to enable matching of requests and responses. + [propget, id(DISPID_CHROME_BULK_FETCH)] HRESULT bulkFetch( + [in] BSTR inputJson, + [in] LONG requestID, + [in] IChromeAccessibleDelegate* delegate + ); + + // Hit-test the given pixel in screen physical pixel coordinates. + // This method is asynchronous; the result is returned + // by calling put_hitTestResult on |delegate|. The client can pass any + // valid LONG as requestID and the same value will be passed to + // put_hitTestResult to enable matching of requests and responses. + [propget, id(DISPID_CHROME_HIT_TEST)] HRESULT hitTest( + [in] LONG screenPhysicalPixelX, + [in] LONG screenPhysicalPixelY, + [in] LONG requestID, + [in] IChromeAccessibleDelegate* delegate + ); +}; diff --git a/chromium/ui/accessibility/platform/test_ax_node_wrapper.cc b/chromium/ui/accessibility/platform/test_ax_node_wrapper.cc index 85283d36f99..873ab5c1985 100644 --- a/chromium/ui/accessibility/platform/test_ax_node_wrapper.cc +++ b/chromium/ui/accessibility/platform/test_ax_node_wrapper.cc @@ -99,6 +99,11 @@ const AXNode* TestAXNodeWrapper::GetNodeFromLastDefaultAction() { } // static +void TestAXNodeWrapper::SetNodeFromLastDefaultAction(AXNode* node) { + g_node_from_last_default_action = node; +} + +// static std::unique_ptr<base::AutoReset<float>> TestAXNodeWrapper::SetScaleFactor( float value) { return std::make_unique<base::AutoReset<float>>(&g_scale_factor, value); @@ -459,30 +464,22 @@ base::Optional<bool> TestAXNodeWrapper::GetTableHasColumnOrRowHeaderNode() return node_->GetTableHasColumnOrRowHeaderNode(); } -std::vector<int32_t> TestAXNodeWrapper::GetColHeaderNodeIds() const { - std::vector<int32_t> header_ids; - node_->GetTableCellColHeaderNodeIds(&header_ids); - return header_ids; +std::vector<AXNode::AXID> TestAXNodeWrapper::GetColHeaderNodeIds() const { + return node_->GetTableColHeaderNodeIds(); } -std::vector<int32_t> TestAXNodeWrapper::GetColHeaderNodeIds( +std::vector<AXNode::AXID> TestAXNodeWrapper::GetColHeaderNodeIds( int col_index) const { - std::vector<int32_t> header_ids; - node_->GetTableColHeaderNodeIds(col_index, &header_ids); - return header_ids; + return node_->GetTableColHeaderNodeIds(col_index); } -std::vector<int32_t> TestAXNodeWrapper::GetRowHeaderNodeIds() const { - std::vector<int32_t> header_ids; - node_->GetTableCellRowHeaderNodeIds(&header_ids); - return header_ids; +std::vector<AXNode::AXID> TestAXNodeWrapper::GetRowHeaderNodeIds() const { + return node_->GetTableCellRowHeaderNodeIds(); } -std::vector<int32_t> TestAXNodeWrapper::GetRowHeaderNodeIds( +std::vector<AXNode::AXID> TestAXNodeWrapper::GetRowHeaderNodeIds( int row_index) const { - std::vector<int32_t> header_ids; - node_->GetTableRowHeaderNodeIds(row_index, &header_ids); - return header_ids; + return node_->GetTableRowHeaderNodeIds(row_index); } bool TestAXNodeWrapper::IsTableRow() const { @@ -586,6 +583,15 @@ bool TestAXNodeWrapper::AccessibilityPerformAction( } case ax::mojom::Action::kDoDefault: { + // If a default action such as a click is performed on an element, it + // could result in a selected state change. In which case, the element's + // selected state no longer comes from focus action, so we should set + // |kSelectedFromFocus| to false. + if (GetData().HasBoolAttribute( + ax::mojom::BoolAttribute::kSelectedFromFocus)) + ReplaceBoolAttribute(ax::mojom::BoolAttribute::kSelectedFromFocus, + false); + switch (GetData().role) { case ax::mojom::Role::kListBoxOption: case ax::mojom::Role::kCell: { @@ -611,7 +617,7 @@ bool TestAXNodeWrapper::AccessibilityPerformAction( default: break; } - g_node_from_last_default_action = node_; + SetNodeFromLastDefaultAction(node_); return true; } @@ -636,9 +642,21 @@ bool TestAXNodeWrapper::AccessibilityPerformAction( return true; } - case ax::mojom::Action::kFocus: + case ax::mojom::Action::kFocus: { g_focused_node_in_tree[tree_] = node_; + + // The platform has select follows focus behavior: + // https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_selection_follows_focus + // For test purpose, we support select follows focus for all elements, and + // not just single-selection container elements. + if (SupportsSelected(GetData().role)) { + ReplaceBoolAttribute(ax::mojom::BoolAttribute::kSelected, true); + ReplaceBoolAttribute(ax::mojom::BoolAttribute::kSelectedFromFocus, + true); + } + return true; + } case ax::mojom::Action::kShowContextMenu: g_node_from_last_show_context_menu = node_; diff --git a/chromium/ui/accessibility/platform/test_ax_node_wrapper.h b/chromium/ui/accessibility/platform/test_ax_node_wrapper.h index 2f0998c7bab..4cd51d5874c 100644 --- a/chromium/ui/accessibility/platform/test_ax_node_wrapper.h +++ b/chromium/ui/accessibility/platform/test_ax_node_wrapper.h @@ -41,6 +41,10 @@ class TestAXNodeWrapper : public AXPlatformNodeDelegateBase { // called from for testing. static const AXNode* GetNodeFromLastDefaultAction(); + // Set the last node which AccessibilityPerformAction default action was + // called for testing. + static void SetNodeFromLastDefaultAction(AXNode* node); + // Set a global scale factor for testing. static std::unique_ptr<base::AutoReset<float>> SetScaleFactor(float value); diff --git a/chromium/ui/accessibility/platform/uia_registrar_win.cc b/chromium/ui/accessibility/platform/uia_registrar_win.cc new file mode 100644 index 00000000000..bd6ca8f56aa --- /dev/null +++ b/chromium/ui/accessibility/platform/uia_registrar_win.cc @@ -0,0 +1,50 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/accessibility/platform/uia_registrar_win.h" +#include <wrl/implements.h> +#include "base/stl_util.h" + +namespace ui { + +UiaRegistrarWin::UiaRegistrarWin() { + // Create the registrar object and get the IUIAutomationRegistrar + // interface pointer. + Microsoft::WRL::ComPtr<IUIAutomationRegistrar> registrar; + if (FAILED(CoCreateInstance(CLSID_CUIAutomationRegistrar, nullptr, + CLSCTX_INPROC_SERVER, IID_IUIAutomationRegistrar, + ®istrar))) + return; + + // Register the custom UIA property that represents the unique id of an UIA + // element which also matches its corresponding IA2 element's unique id. + UIAutomationPropertyInfo unique_id_property_info = { + kUiaPropertyUniqueIdGuid, L"UniqueId", UIAutomationType_String}; + registrar->RegisterProperty(&unique_id_property_info, + &uia_unique_id_property_id_); + + // Register the custom UIA event that represents the test end event for the + // UIA test suite. + UIAutomationEventInfo test_complete_event_info = { + kUiaEventTestCompleteSentinelGuid, L"kUiaTestCompleteSentinel"}; + registrar->RegisterEvent(&test_complete_event_info, + &uia_test_complete_event_id_); +} + +UiaRegistrarWin::~UiaRegistrarWin() = default; + +PROPERTYID UiaRegistrarWin::GetUiaUniqueIdPropertyId() const { + return uia_unique_id_property_id_; +} + +EVENTID UiaRegistrarWin::GetUiaTestCompleteEventId() const { + return uia_test_complete_event_id_; +} + +const UiaRegistrarWin& UiaRegistrarWin::GetInstance() { + static base::NoDestructor<UiaRegistrarWin> instance; + return *instance; +} + +} // namespace ui diff --git a/chromium/ui/accessibility/platform/uia_registrar_win.h b/chromium/ui/accessibility/platform/uia_registrar_win.h new file mode 100644 index 00000000000..53c8da4fe37 --- /dev/null +++ b/chromium/ui/accessibility/platform/uia_registrar_win.h @@ -0,0 +1,45 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UI_ACCESSIBILITY_PLATFORM_UIA_REGISTRAR_WIN_H_ +#define UI_ACCESSIBILITY_PLATFORM_UIA_REGISTRAR_WIN_H_ + +#include <objbase.h> +#include <uiautomation.h> +#include "base/macros.h" +#include "base/no_destructor.h" +#include "ui/accessibility/ax_export.h" + +namespace ui { +// {3761326A-34B2-465A-835D-7A3D8F4EFB92} +static const GUID kUiaEventTestCompleteSentinelGuid = { + 0x3761326a, + 0x34b2, + 0x465a, + {0x83, 0x5d, 0x7a, 0x3d, 0x8f, 0x4e, 0xfb, 0x92}}; + +// {cc7eeb32-4b62-4f4c-aff6-1c2e5752ad8e} +static const GUID kUiaPropertyUniqueIdGuid = { + 0xcc7eeb32, + 0x4b62, + 0x4f4c, + {0xaf, 0xf6, 0x1c, 0x2e, 0x57, 0x52, 0xad, 0x8e}}; + +class AX_EXPORT UiaRegistrarWin { + public: + UiaRegistrarWin(); + ~UiaRegistrarWin(); + PROPERTYID GetUiaUniqueIdPropertyId() const; + EVENTID GetUiaTestCompleteEventId() const; + + static const UiaRegistrarWin& GetInstance(); + + private: + PROPERTYID uia_unique_id_property_id_ = 0; + EVENTID uia_test_complete_event_id_ = 0; +}; + +} // namespace ui + +#endif // UI_ACCESSIBILITY_PLATFORM_UIA_REGISTRAR_WIN_H_ |