diff --git a/BUILD.gn b/BUILD.gn index d46fc48d7..05f728fbb 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -2261,6 +2261,7 @@ if (is_mac) { gypi_paths2.cefclient_sources_browser + gypi_paths2.cefclient_sources_common + gypi_paths2.shared_sources_osr_mac + + gypi_paths2.shared_sources_osr_accessibility + gypi_paths2.cefclient_sources_mac deps = [ ":libcef_dll_wrapper", @@ -2483,6 +2484,7 @@ if (is_mac) { gypi_paths2.shared_sources_mac + gypi_paths2.ceftests_sources_common + gypi_paths2.shared_sources_osr_mac + + gypi_paths2.shared_sources_osr_accessibility + gypi_paths2.ceftests_sources_mac deps = [ ":libcef_dll_wrapper", @@ -2569,6 +2571,7 @@ if (is_mac) { gypi_paths2.includes_wrapper_win + gypi_paths2.shared_sources_win + gypi_paths2.cefclient_sources_win + + gypi_paths2.shared_sources_osr_accessibility + gypi_paths2.cefclient_sources_resources_win_rc # Set /SUBSYSTEM:WINDOWS. @@ -2665,6 +2668,7 @@ if (is_mac) { gypi_paths2.cefclient_sources_common + gypi_paths2.cefclient_sources_renderer + gypi_paths2.cefclient_sources_win + + gypi_paths2.shared_sources_osr_accessibility + gypi_paths2.cefclient_sources_resources_win_rc + [ # Generated installer config resource (embedded in client DLL, # read by bootstrap.exe). diff --git a/cef_paths2.gypi b/cef_paths2.gypi index b951dc19a..6de0bd818 100644 --- a/cef_paths2.gypi +++ b/cef_paths2.gypi @@ -229,11 +229,18 @@ 'tests/shared/process_helper_mac.cc', ], 'shared_sources_osr_mac': [ + 'tests/shared/browser/osr_accessibility_node_mac.mm', 'tests/shared/browser/osr_begin_frame_timer_mac.h', 'tests/shared/browser/osr_begin_frame_timer_mac.mm', 'tests/shared/browser/osr_renderer_metal.h', 'tests/shared/browser/osr_renderer_metal.mm', ], + 'shared_sources_osr_accessibility': [ + 'tests/shared/browser/osr_accessibility_helper.cc', + 'tests/shared/browser/osr_accessibility_helper.h', + 'tests/shared/browser/osr_accessibility_node.cc', + 'tests/shared/browser/osr_accessibility_node.h', + ], 'shared_sources_win': [ 'tests/shared/browser/main_message_loop_external_pump_win.cc', 'tests/shared/browser/resource_util_win.cc', @@ -379,10 +386,6 @@ 'tests/cefclient/browser/main_context_impl_win.cc', 'tests/cefclient/browser/main_message_loop_multithreaded_win.cc', 'tests/cefclient/browser/main_message_loop_multithreaded_win.h', - 'tests/cefclient/browser/osr_accessibility_helper.cc', - 'tests/cefclient/browser/osr_accessibility_helper.h', - 'tests/cefclient/browser/osr_accessibility_node.cc', - 'tests/cefclient/browser/osr_accessibility_node.h', 'tests/cefclient/browser/osr_accessibility_node_win.cc', 'tests/cefclient/browser/osr_dragdrop_win.cc', 'tests/cefclient/browser/osr_dragdrop_win.h', @@ -421,11 +424,6 @@ 'tests/cefclient/browser/browser_window_std_mac.h', 'tests/cefclient/browser/browser_window_std_mac.mm', 'tests/cefclient/browser/main_context_impl_posix.cc', - 'tests/cefclient/browser/osr_accessibility_helper.cc', - 'tests/cefclient/browser/osr_accessibility_helper.h', - 'tests/cefclient/browser/osr_accessibility_node.cc', - 'tests/cefclient/browser/osr_accessibility_node.h', - 'tests/cefclient/browser/osr_accessibility_node_mac.mm', 'tests/cefclient/browser/root_window_mac.h', 'tests/cefclient/browser/root_window_mac.mm', 'tests/cefclient/browser/temp_window_mac.h', @@ -691,6 +689,7 @@ 'tests/ceftests/win/ceftests.rc', ], 'ceftests_sources_mac': [ + 'tests/ceftests/osr_accessibility_unittest_mac.mm', 'tests/ceftests/osr_begin_frame_unittest_mac.mm', 'tests/ceftests/osr_renderer_metal_unittest.mm', 'tests/ceftests/os_rendering_unittest_mac.h', diff --git a/tests/cefclient/CMakeLists.txt.in b/tests/cefclient/CMakeLists.txt.in index 4e87a52d3..ab2f07cde 100644 --- a/tests/cefclient/CMakeLists.txt.in +++ b/tests/cefclient/CMakeLists.txt.in @@ -53,6 +53,7 @@ 'includes': [ 'shared_sources_mac', 'shared_sources_osr_mac', + 'shared_sources_osr_accessibility', 'cefclient_sources_mac', ], }} @@ -72,6 +73,7 @@ 'set': 'CEFCLIENT_WINDOWS_SRCS', 'includes': [ 'shared_sources_win', + 'shared_sources_osr_accessibility', 'cefclient_sources_win', 'cefclient_sources_resources_win_rc', ], diff --git a/tests/cefclient/browser/browser_window_osr_mac.mm b/tests/cefclient/browser/browser_window_osr_mac.mm index c9c72d68d..20f907d3c 100644 --- a/tests/cefclient/browser/browser_window_osr_mac.mm +++ b/tests/cefclient/browser/browser_window_osr_mac.mm @@ -17,12 +17,12 @@ #include "include/wrapper/cef_closure_task.h" #include "tests/cefclient/browser/bytes_write_handler.h" #include "tests/cefclient/browser/main_context.h" -#include "tests/cefclient/browser/osr_accessibility_helper.h" -#include "tests/cefclient/browser/osr_accessibility_node.h" #include "tests/cefclient/browser/text_input_client_osr_mac.h" #include "tests/cefclient/browser/util_mac.h" #include "tests/shared/browser/geometry_util.h" #include "tests/shared/browser/main_message_loop.h" +#include "tests/shared/browser/osr_accessibility_helper.h" +#include "tests/shared/browser/osr_accessibility_node.h" #include "tests/shared/browser/osr_begin_frame_timer_mac.h" #include "tests/shared/browser/osr_renderer_metal.h" @@ -119,6 +119,7 @@ NSPoint ConvertPointFromWindowToScreen(NSWindow* window, NSPoint point) { } - (void)dealloc { + [self detach]; [[NSNotificationCenter defaultCenter] removeObserver:self]; #if !__has_feature(objc_arc) if (text_input_context_osr_mac_) { @@ -132,6 +133,10 @@ NSPoint ConvertPointFromWindowToScreen(NSWindow* window, NSPoint point) { - (void)detach { [NSObject cancelPreviousPerformRequestsWithTarget:self]; display_pending_ = false; + // Clear the view's pointer before destruction notifications can reenter it. + auto* accessibility_helper = accessibility_helper_; + accessibility_helper_ = nullptr; + delete accessibility_helper; renderer_ = nullptr; browser_window_ = nullptr; if (text_input_client_) { @@ -1108,9 +1113,11 @@ NSPoint ConvertPointFromWindowToScreen(NSWindow* window, NSPoint point) { client::OsrAXNode* node = accessibility_helper_->GetRootNode(); // Add Root as first Kid NSMutableArray* kids = [NSMutableArray arrayWithCapacity:1]; - NSObject* child = CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT( - node->GetNativeAccessibleObject(nullptr)); - [kids addObject:child]; + if (node) { + NSObject* child = CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT( + node->GetNativeAccessibleObject(nullptr)); + [kids addObject:child]; + } return NSAccessibilityUnignoredChildren(kids); } else { return [super accessibilityAttributeValue:attribute]; diff --git a/tests/cefclient/browser/osr_accessibility_node_win.cc b/tests/cefclient/browser/osr_accessibility_node_win.cc index e0db0ff0c..6a1e2e172 100644 --- a/tests/cefclient/browser/osr_accessibility_node_win.cc +++ b/tests/cefclient/browser/osr_accessibility_node_win.cc @@ -6,7 +6,7 @@ // data back and forth between MSAA clients and CefClient renderers. // Sample implementation based on ui\accessibility\ax_platform_node_win.h -#include "tests/cefclient/browser/osr_accessibility_node.h" +#include "tests/shared/browser/osr_accessibility_node.h" #if defined(CEF_USE_ATL) @@ -16,7 +16,7 @@ #include -#include "tests/cefclient/browser/osr_accessibility_helper.h" +#include "tests/shared/browser/osr_accessibility_helper.h" namespace client { @@ -234,9 +234,9 @@ struct CefIAccessible : public IAccessible { STDMETHODIMP GetTypeInfoCount(unsigned int FAR* pctinfo) override; STDMETHODIMP GetTypeInfo(unsigned int iTInfo, LCID lcid, - ITypeInfo FAR* FAR* ppTInfo) override; + ITypeInfo FAR * FAR * ppTInfo) override; STDMETHODIMP GetIDsOfNames(REFIID riid, - OLECHAR FAR* FAR* rgszNames, + OLECHAR FAR * FAR * rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgDispId) override; @@ -673,12 +673,12 @@ STDMETHODIMP CefIAccessible::GetTypeInfoCount(unsigned int FAR* pctinfo) { STDMETHODIMP CefIAccessible::GetTypeInfo(unsigned int iTInfo, LCID lcid, - ITypeInfo FAR* FAR* ppTInfo) { + ITypeInfo FAR * FAR * ppTInfo) { return E_NOTIMPL; } STDMETHODIMP CefIAccessible::GetIDsOfNames(REFIID riid, - OLECHAR FAR* FAR* rgszNames, + OLECHAR FAR * FAR * rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgDispId) { diff --git a/tests/cefclient/browser/osr_window_win.cc b/tests/cefclient/browser/osr_window_win.cc index 3ae20a8b0..a5f70c62c 100644 --- a/tests/cefclient/browser/osr_window_win.cc +++ b/tests/cefclient/browser/osr_window_win.cc @@ -15,14 +15,14 @@ #include "include/base/cef_build.h" #include "include/views/cef_display.h" #include "tests/cefclient/browser/main_context.h" -#include "tests/cefclient/browser/osr_accessibility_helper.h" -#include "tests/cefclient/browser/osr_accessibility_node.h" #include "tests/cefclient/browser/osr_ime_handler_win.h" #include "tests/cefclient/browser/osr_render_handler_win_d3d11.h" #include "tests/cefclient/browser/osr_render_handler_win_gl.h" #include "tests/cefclient/browser/resource.h" #include "tests/shared/browser/geometry_util.h" #include "tests/shared/browser/main_message_loop.h" +#include "tests/shared/browser/osr_accessibility_helper.h" +#include "tests/shared/browser/osr_accessibility_node.h" #include "tests/shared/browser/util_win.h" namespace client { diff --git a/tests/cefclient/browser/osr_window_win.h b/tests/cefclient/browser/osr_window_win.h index b3911475b..a8c2a696d 100644 --- a/tests/cefclient/browser/osr_window_win.h +++ b/tests/cefclient/browser/osr_window_win.h @@ -11,10 +11,10 @@ #include "include/wrapper/cef_closure_task.h" #include "include/wrapper/cef_helpers.h" #include "tests/cefclient/browser/client_handler_osr.h" -#include "tests/cefclient/browser/osr_accessibility_node.h" #include "tests/cefclient/browser/osr_dragdrop_win.h" #include "tests/cefclient/browser/osr_render_handler_win.h" #include "tests/cefclient/browser/osr_renderer_settings.h" +#include "tests/shared/browser/osr_accessibility_node.h" namespace client { diff --git a/tests/ceftests/CMakeLists.txt.in b/tests/ceftests/CMakeLists.txt.in index 88cbbd27a..afa08013c 100644 --- a/tests/ceftests/CMakeLists.txt.in +++ b/tests/ceftests/CMakeLists.txt.in @@ -16,6 +16,7 @@ 'shared_sources_linux:LINUX', 'shared_sources_mac:MAC', 'shared_sources_osr_mac:MAC', + 'shared_sources_osr_accessibility:MAC', 'shared_sources_renderer:WINDOWS', 'shared_sources_renderer:LINUX', 'shared_sources_win:WINDOWS', diff --git a/tests/ceftests/osr_accessibility_unittest_mac.mm b/tests/ceftests/osr_accessibility_unittest_mac.mm new file mode 100644 index 000000000..94040ff9b --- /dev/null +++ b/tests/ceftests/osr_accessibility_unittest_mac.mm @@ -0,0 +1,186 @@ +// Copyright (c) 2026 The Chromium Embedded Framework 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 +#import + +#include + +#include "include/cef_parser.h" +#include "tests/ceftests/thread_helper.h" +#include "tests/gtest/include/gtest/gtest.h" +#include "tests/shared/browser/osr_accessibility_helper.h" +#include "tests/shared/browser/osr_accessibility_node.h" + +// Exercise the legacy NSAccessibility entry points implemented by the sample. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +// Observe actual object deallocation without retaining the accessibility +// object. +@interface OsrAXLifetimeObserver : NSObject { + @public + int* destruction_count_; +} +@end + +@implementation OsrAXLifetimeObserver +- (void)dealloc { + ++*destruction_count_; +#if !__has_feature(objc_arc) + [super dealloc]; +#endif +} +@end + +namespace { + +void ObserveLifetime(NSObject* object, int* count) { + static char key; + OsrAXLifetimeObserver* observer = [[OsrAXLifetimeObserver alloc] init]; + observer->destruction_count_ = count; + objc_setAssociatedObject(object, &key, observer, OBJC_ASSOCIATION_RETAIN); +#if !__has_feature(objc_arc) + [observer release]; +#endif +} + +client::OsrAXNode* CreateNode(int id = 1) { + auto value = CefDictionaryValue::Create(); + value->SetString("role", "button"); + auto attributes = CefDictionaryValue::Create(); + attributes->SetString("name", "Test button"); + value->SetDictionary("attributes", attributes); + return client::OsrAXNode::CreateNode("test-tree", id, value, nullptr); +} + +NSObject* NativeObject(client::OsrAXNode* node, + client::OsrAXNode* parent = nullptr) { + return CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT( + node->GetNativeAccessibleObject(parent)); +} + +void NativeObjectSurvivesAutoreleasePoolsImpl() { + int destroyed = 0; + auto* node = CreateNode(); + for (int i = 0; i < 3; ++i) { + @autoreleasepool { + NSObject* object = NativeObject(node); + if (i == 0) { + ObserveLifetime(object, &destroyed); + } + EXPECT_TRUE([NSAccessibilityButtonRole + isEqual:[object accessibilityAttributeValue: + NSAccessibilityRoleAttribute]]); + EXPECT_TRUE( + [@"Test button" isEqual:[object accessibilityAttributeValue: + NSAccessibilityTitleAttribute]]); + } + // Without ownership, the first pool drain frees the cached native object. + ASSERT_EQ(0, destroyed); + } + @autoreleasepool { + node->Destroy(); + } + EXPECT_EQ(1, destroyed); +} + +void RetainedNativeObjectDetachesImpl() { + int destroyed = 0; + CFTypeRef retained; + @autoreleasepool { + auto* node = CreateNode(); + NSObject* object = NativeObject(node); + ObserveLifetime(object, &destroyed); + retained = CFRetain((__bridge CFTypeRef)object); + node->Destroy(); + } + EXPECT_EQ(0, destroyed); + @autoreleasepool { + NSObject* object = (__bridge NSObject*)retained; + EXPECT_TRUE([object accessibilityIsIgnored]); + for (NSString* attribute in [object accessibilityAttributeNames]) { + EXPECT_EQ(nil, [object accessibilityAttributeValue:attribute]); + } + EXPECT_EQ(nil, [object accessibilityFocusedUIElement]); + EXPECT_EQ(nil, [object accessibilityHitTest:NSZeroPoint]); + EXPECT_EQ(0U, [[object accessibilityActionNames] count]); + EXPECT_EQ( + nil, + [object accessibilityActionDescription:NSAccessibilityPressAction]); + [object accessibilityPerformAction:NSAccessibilityPressAction]; + [object accessibilityPerformAction:NSAccessibilityShowMenuAction]; + CFRelease(retained); + } + EXPECT_EQ(1, destroyed); +} + +void NativeParentCanChangeImpl() { + @autoreleasepool { + auto* first = CreateNode(1); + auto* second = CreateNode(2); + auto* child = CreateNode(3); + NSObject* first_object = NativeObject(first); + NSObject* second_object = NativeObject(second); + // A focus query can create the child before its parent is assigned. + NSObject* child_object = NativeObject(child); + EXPECT_EQ(child_object, NativeObject(child, first)); + EXPECT_EQ(first_object, + [child_object + accessibilityAttributeValue:NSAccessibilityParentAttribute]); + EXPECT_EQ(child_object, NativeObject(child, second)); + first->Destroy(); + EXPECT_EQ(second_object, + [child_object + accessibilityAttributeValue:NSAccessibilityParentAttribute]); + child->SetParent(nullptr); + second->Destroy(); + EXPECT_EQ(nil, + [child_object + accessibilityAttributeValue:NSAccessibilityParentAttribute]); + child->Destroy(); + } +} + +void HelperTeardownDetachesAllNodesImpl() { + // No browser is needed to verify role queries and teardown of rootless nodes. + auto tree = CefParseJSON(R"({"ax_tree_id":"test-tree","updates":[{ + "nodes":[{"id":1,"role":"button"},{"id":2,"role":"button"}] + }]})", + JSON_PARSER_RFC); + auto helper = std::make_unique(tree, nullptr); + auto* first = helper->GetNode("test-tree", 1); + auto* second = helper->GetNode("test-tree", 2); + ASSERT_NE(nullptr, first); + ASSERT_NE(nullptr, second); + int destroyed = 0; + CFTypeRef retained; + @autoreleasepool { + // Construction does not query the browser/window; parent resolution is + // lazy. + NSObject* object = NativeObject(first); + ObserveLifetime(object, &destroyed); + retained = CFRetain((__bridge CFTypeRef)object); + ObserveLifetime(NativeObject(second), &destroyed); + helper.reset(); + } + EXPECT_EQ(1, destroyed); + @autoreleasepool { + NSObject* object = (__bridge NSObject*)retained; + EXPECT_TRUE([object accessibilityIsIgnored]); + EXPECT_EQ( + nil, [object accessibilityAttributeValue:NSAccessibilityRoleAttribute]); + CFRelease(retained); + } + EXPECT_EQ(2, destroyed); +} + +} // namespace + +UI_THREAD_TEST(OsrAccessibilityTest, NativeObjectSurvivesAutoreleasePools) +UI_THREAD_TEST(OsrAccessibilityTest, RetainedNativeObjectDetaches) +UI_THREAD_TEST(OsrAccessibilityTest, NativeParentCanChange) +UI_THREAD_TEST(OsrAccessibilityTest, HelperTeardownDetachesAllNodes) + +#pragma clang diagnostic pop diff --git a/tests/cefclient/browser/osr_accessibility_helper.cc b/tests/shared/browser/osr_accessibility_helper.cc similarity index 92% rename from tests/cefclient/browser/osr_accessibility_helper.cc rename to tests/shared/browser/osr_accessibility_helper.cc index baf8c4338..2e92a49bc 100644 --- a/tests/cefclient/browser/osr_accessibility_helper.cc +++ b/tests/shared/browser/osr_accessibility_helper.cc @@ -2,9 +2,9 @@ // 2013 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 "tests/cefclient/browser/osr_accessibility_helper.h" +#include "tests/shared/browser/osr_accessibility_helper.h" -#include "tests/cefclient/browser/osr_accessibility_node.h" +#include "tests/shared/browser/osr_accessibility_node.h" namespace client { @@ -43,6 +43,10 @@ OsrAccessibilityHelper::OsrAccessibilityHelper(CefRefPtr value, UpdateAccessibilityTree(value); } +OsrAccessibilityHelper::~OsrAccessibilityHelper() { + Reset(); +} + int OsrAccessibilityHelper::CastToInt(CefRefPtr value) { if (value->GetType() == VTYPE_STRING) { const std::string& str = value->GetString(); @@ -226,10 +230,22 @@ void OsrAccessibilityHelper::UpdateFocusedNode(const CefString& treeId, } void OsrAccessibilityHelper::Reset() { - accessibility_node_map_.clear(); + // Remove nodes from lookups before destruction notifications can reenter. + decltype(accessibility_node_map_) trees; + trees.swap(accessibility_node_map_); root_tree_id_ = ""; focused_tree_id_ = ""; focused_node_id_ = -1; + for (auto& [tree_id, tree] : trees) { + for (auto& [node_id, node] : tree.node_map_) { + node->SetParent(nullptr); + } + } + for (auto& [tree_id, tree] : trees) { + for (auto& [node_id, node] : tree.node_map_) { + node->Destroy(); + } + } } void OsrAccessibilityHelper::DestroyNode(OsrAXNode* node) { diff --git a/tests/cefclient/browser/osr_accessibility_helper.h b/tests/shared/browser/osr_accessibility_helper.h similarity index 84% rename from tests/cefclient/browser/osr_accessibility_helper.h rename to tests/shared/browser/osr_accessibility_helper.h index 802996473..a68d29c42 100644 --- a/tests/cefclient/browser/osr_accessibility_helper.h +++ b/tests/shared/browser/osr_accessibility_helper.h @@ -2,8 +2,8 @@ // 2013 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 CEF_TESTS_CEFCLIENT_BROWSER_OSR_ACCESSIBILITY_HELPER_H_ -#define CEF_TESTS_CEFCLIENT_BROWSER_OSR_ACCESSIBILITY_HELPER_H_ +#ifndef CEF_TESTS_SHARED_BROWSER_OSR_ACCESSIBILITY_HELPER_H_ +#define CEF_TESTS_SHARED_BROWSER_OSR_ACCESSIBILITY_HELPER_H_ #include @@ -26,6 +26,7 @@ class OsrAXTree { void SetRootNodeId(int nodeId) { root_node_id_ = nodeId; } private: + friend class OsrAccessibilityHelper; CefString parent_tree_id_; int root_node_id_ = -1; std::map node_map_; @@ -38,6 +39,10 @@ class OsrAccessibilityHelper { public: OsrAccessibilityHelper(CefRefPtr value, CefRefPtr browser); + ~OsrAccessibilityHelper(); + + OsrAccessibilityHelper(const OsrAccessibilityHelper&) = delete; + OsrAccessibilityHelper& operator=(const OsrAccessibilityHelper&) = delete; void UpdateAccessibilityTree(CefRefPtr value); @@ -78,4 +83,4 @@ class OsrAccessibilityHelper { } // namespace client -#endif // CEF_TESTS_CEFCLIENT_BROWSER_OSR_ACCESSIBILITY_HELPER_H_ +#endif // CEF_TESTS_SHARED_BROWSER_OSR_ACCESSIBILITY_HELPER_H_ diff --git a/tests/cefclient/browser/osr_accessibility_node.cc b/tests/shared/browser/osr_accessibility_node.cc similarity index 97% rename from tests/cefclient/browser/osr_accessibility_node.cc rename to tests/shared/browser/osr_accessibility_node.cc index 83e3f1d17..d0ba4083e 100644 --- a/tests/cefclient/browser/osr_accessibility_node.cc +++ b/tests/shared/browser/osr_accessibility_node.cc @@ -5,9 +5,9 @@ // Base class implementation for CEF Acccessibility node. This is subclassed and // used by both IAccessible/NSAccessibility protocol implementation. -#include "tests/cefclient/browser/osr_accessibility_node.h" +#include "tests/shared/browser/osr_accessibility_node.h" -#include "tests/cefclient/browser/osr_accessibility_helper.h" +#include "tests/shared/browser/osr_accessibility_helper.h" namespace client { diff --git a/tests/cefclient/browser/osr_accessibility_node.h b/tests/shared/browser/osr_accessibility_node.h similarity index 94% rename from tests/cefclient/browser/osr_accessibility_node.h rename to tests/shared/browser/osr_accessibility_node.h index 5e40af1ac..b3a577be0 100644 --- a/tests/cefclient/browser/osr_accessibility_node.h +++ b/tests/shared/browser/osr_accessibility_node.h @@ -2,8 +2,8 @@ // 2013 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 CEF_TESTS_CEFCLIENT_BROWSER_OSR_ACCESSIBILITY_NODE_H_ -#define CEF_TESTS_CEFCLIENT_BROWSER_OSR_ACCESSIBILITY_NODE_H_ +#ifndef CEF_TESTS_SHARED_BROWSER_OSR_ACCESSIBILITY_NODE_H_ +#define CEF_TESTS_SHARED_BROWSER_OSR_ACCESSIBILITY_NODE_H_ #pragma once #include @@ -120,4 +120,4 @@ class OsrAXNode { } // namespace client -#endif // CEF_TESTS_CEFCLIENT_BROWSER_OSR_ACCESSIBILITY_NODE_H_ +#endif // CEF_TESTS_SHARED_BROWSER_OSR_ACCESSIBILITY_NODE_H_ diff --git a/tests/cefclient/browser/osr_accessibility_node_mac.mm b/tests/shared/browser/osr_accessibility_node_mac.mm similarity index 87% rename from tests/cefclient/browser/osr_accessibility_node_mac.mm rename to tests/shared/browser/osr_accessibility_node_mac.mm index b5d5c3811..642ecb9a6 100644 --- a/tests/cefclient/browser/osr_accessibility_node_mac.mm +++ b/tests/shared/browser/osr_accessibility_node_mac.mm @@ -5,12 +5,12 @@ // Sample implementation for the NSAccessibility protocol for interacting with // VoiceOver and other accessibility clients. -#include "tests/cefclient/browser/osr_accessibility_node.h" +#include "tests/shared/browser/osr_accessibility_node.h" #import #import -#include "tests/cefclient/browser/osr_accessibility_helper.h" +#include "tests/shared/browser/osr_accessibility_helper.h" namespace { @@ -354,60 +354,64 @@ inline int MiddleY(const CefRect& rect) { @interface OsrAXNodeObject : NSObject { // OsrAXNode* proxy object client::OsrAXNode* node_; - CefNativeAccessible* parent_; } -- (id)init:(client::OsrAXNode*)node; -+ (OsrAXNodeObject*)elementWithNode:(client::OsrAXNode*)node; +- (id)initWithNode:(client::OsrAXNode*)node; +- (void)detach; @end @implementation OsrAXNodeObject -- (id)init:(client::OsrAXNode*)node { - node_ = node; - parent_ = node_->GetParentAccessibleObject(); - if (!parent_) { - parent_ = node_->GetWindowHandle(); +- (id)initWithNode:(client::OsrAXNode*)node { + if ((self = [super init])) { + node_ = node; } return self; } -+ (OsrAXNodeObject*)elementWithNode:(client::OsrAXNode*)node { - // We manage the release ourself - return [[OsrAXNodeObject alloc] init:node]; +- (void)detach { + node_ = nullptr; } - (BOOL)isEqual:(id)object { - if ([object isKindOfClass:[OsrAXNodeObject self]]) { - OsrAXNodeObject* other = object; - return (node_ == other->node_); - } else { - return NO; - } + // There is one native wrapper per node. Detached wrappers remain distinct. + return self == object; } // Utility methods to map AX information received from renderer // to platform properties - (NSString*)axRole { // Get the Role from CefAccessibilityHelper and Map to NSRole - return AxRoleToNSAxRole(node_->AxRole()); + return node_ ? AxRoleToNSAxRole(node_->AxRole()) : nil; } - (NSString*)axDescription { + if (!node_) { + return nil; + } std::string desc = node_->AxDescription(); return [NSString stringWithUTF8String:desc.c_str()]; } - (NSString*)axName { + if (!node_) { + return nil; + } std::string desc = node_->AxName(); return [NSString stringWithUTF8String:desc.c_str()]; } - (NSString*)axValue { + if (!node_) { + return nil; + } std::string desc = node_->AxValue(); return [NSString stringWithUTF8String:desc.c_str()]; } - (void)doMouseClick:(cef_mouse_button_type_t)type { + if (!node_) { + return; + } CefRefPtr browser = node_->GetBrowser(); if (browser) { CefMouseEvent mouse_event; @@ -422,14 +426,18 @@ inline int MiddleY(const CefRect& rect) { } - (NSMutableArray*)getKids { + if (!node_) { + return nil; + } int numChilds = node_->GetChildCount(); if (numChilds > 0) { NSMutableArray* kids = [NSMutableArray arrayWithCapacity:numChilds]; for (int index = 0; index < numChilds; index++) { client::OsrAXNode* child = node_->ChildAtIndex(index); - [kids addObject:child ? CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT( - child->GetNativeAccessibleObject(node_)) - : nil]; + if (child) { + [kids addObject:CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT( + child->GetNativeAccessibleObject(node_))]; + } } return kids; } @@ -437,6 +445,9 @@ inline int MiddleY(const CefRect& rect) { } - (NSPoint)position { + if (!node_) { + return NSZeroPoint; + } CefRect cef_rect = node_->AxLocation(); NSPoint origin = NSMakePoint(cef_rect.x, cef_rect.y); NSSize size = NSMakeSize(cef_rect.width, cef_rect.height); @@ -454,6 +465,9 @@ inline int MiddleY(const CefRect& rect) { } - (NSSize)size { + if (!node_) { + return NSZeroSize; + } CefRect cef_rect = node_->AxLocation(); NSRect rect = NSMakeRect(cef_rect.x, cef_rect.y, cef_rect.width, cef_rect.height); @@ -469,7 +483,7 @@ inline int MiddleY(const CefRect& rect) { // attributes - (BOOL)accessibilityIsIgnored { - return NO; + return node_ == nullptr; } - (NSArray*)accessibilityAttributeNames { @@ -493,10 +507,16 @@ inline int MiddleY(const CefRect& rect) { } - (id)accessibilityAttributeValue:(NSString*)attribute { - NSObject* typed_parent = CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT(parent_); if (!node_) { return nil; } + // Resolve the current parent only while attached. Do not cache an unowned + // native parent pointer, which can outlive the parent or miss reparenting. + CefNativeAccessible* parent = node_->GetParentAccessibleObject(); + if (!parent) { + parent = node_->GetWindowHandle(); + } + NSObject* typed_parent = CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT(parent); if ([attribute isEqualToString:NSAccessibilityRoleAttribute]) { return [self axRole]; } else if ([attribute @@ -535,15 +555,15 @@ inline int MiddleY(const CefRect& rect) { } - (id)accessibilityHitTest:(NSPoint)point { - return NSAccessibilityUnignoredAncestor(self); + return node_ ? NSAccessibilityUnignoredAncestor(self) : nil; } - (NSArray*)accessibilityActionNames { - return [NSArray arrayWithObject:NSAccessibilityPressAction]; + return node_ ? @[ NSAccessibilityPressAction ] : @[]; } - (NSString*)accessibilityActionDescription:(NSString*)action { - return NSAccessibilityActionDescription(action); + return node_ ? NSAccessibilityActionDescription(action) : nil; } - (void)accessibilityPerformAction:(NSString*)action { @@ -557,13 +577,11 @@ inline int MiddleY(const CefRect& rect) { } - (id)accessibilityFocusedUIElement { - return NSAccessibilityUnignoredAncestor(self); + return node_ ? NSAccessibilityUnignoredAncestor(self) : nil; } - (BOOL)accessibilityNotifiesWhenDestroyed { - // Indicate that BrowserAccessibilityCocoa will post a notification when it's - // destroyed (see -detach). This allows VoiceOver to do some internal things - // more efficiently. + // OsrAXNode::Destroy posts the notification after detaching this wrapper. return YES; } @@ -590,9 +608,21 @@ void OsrAXNode::NotifyAccessibilityEvent(std::string event_type) const { void OsrAXNode::Destroy() { if (platform_accessibility_) { + // Transfer our ownership to a local before notifying clients, which may + // reenter the wrapper or keep it alive after the C++ node is deleted. +#if __has_feature(objc_arc) + OsrAXNodeObject* object = + (__bridge_transfer OsrAXNodeObject*)platform_accessibility_; +#else + OsrAXNodeObject* object = (OsrAXNodeObject*)platform_accessibility_; +#endif + platform_accessibility_ = nullptr; + [object detach]; NSAccessibilityPostNotification( - CAST_CEF_NATIVE_ACCESSIBLE_TO_NSOBJECT(platform_accessibility_), - NSAccessibilityUIElementDestroyedNotification); + object, NSAccessibilityUIElementDestroyedNotification); +#if !__has_feature(objc_arc) + [object release]; +#endif } delete this; @@ -601,11 +631,17 @@ void OsrAXNode::Destroy() { // Create and return NSAccessibility Implementation Object for Mac CefNativeAccessible* OsrAXNode::GetNativeAccessibleObject( client::OsrAXNode* parent) { - if (!platform_accessibility_) { - platform_accessibility_ = CAST_NSOBJECT_TO_CEF_NATIVE_ACCESSIBLE( - [OsrAXNodeObject elementWithNode:this]); + if (parent) { SetParent(parent); } + if (!platform_accessibility_) { + OsrAXNodeObject* object = [[OsrAXNodeObject alloc] initWithNode:this]; +#if __has_feature(objc_arc) + platform_accessibility_ = (__bridge_retained CefNativeAccessible*)object; +#else + platform_accessibility_ = object; +#endif + } return platform_accessibility_; } diff --git a/tools/distrib/bazel/tests-cefclient-BUILD.bazel.in b/tools/distrib/bazel/tests-cefclient-BUILD.bazel.in index 4faaa832c..aea98e11a 100644 --- a/tools/distrib/bazel/tests-cefclient-BUILD.bazel.in +++ b/tools/distrib/bazel/tests-cefclient-BUILD.bazel.in @@ -96,7 +96,9 @@ cc_library( # when linking libraries. filegroup( name = "SrcsWin", - srcs = srcs_common + srcs_browser + srcs_browser_win + srcs_renderer, + srcs = srcs_common + srcs_browser + srcs_browser_win + srcs_renderer + [ + "//tests/shared:OsrAccessibilitySrcsWin", + ], target_compatible_with = [ "@platforms//os:windows", ], diff --git a/tools/distrib/bazel/tests-shared-BUILD.bazel.in b/tools/distrib/bazel/tests-shared-BUILD.bazel.in index 3fbdd5568..8ed58d401 100644 --- a/tools/distrib/bazel/tests-shared-BUILD.bazel.in +++ b/tools/distrib/bazel/tests-shared-BUILD.bazel.in @@ -34,6 +34,10 @@ srcs_osr_mac = [ ${shared_sources_osr_mac} ] +srcs_osr_accessibility = [ + ${shared_sources_osr_accessibility} +] + srcs_browser_mac_ceftests = [ ${ceftests_sources_mac_browser_shared} ] @@ -91,7 +95,7 @@ declare_cc_library( declare_objc_library( name = "OsrLibMac", - srcs = srcs_osr_mac, + srcs = srcs_osr_mac + srcs_osr_accessibility, target_compatible_with = [ "@platforms//os:macos", ], @@ -133,6 +137,14 @@ declare_objc_library( # Windows targets. # +filegroup( + name = "OsrAccessibilitySrcsWin", + srcs = srcs_osr_accessibility, + target_compatible_with = [ + "@platforms//os:windows", + ], +) + # Include files directly in the declare_exe target. This simplifies the build # configuration and avoids issues with Windows discarding symbols (like WinMain) # when linking libraries. diff --git a/tools/make_distrib.py b/tools/make_distrib.py index 1a8b1e7d0..2b80a2d4a 100644 --- a/tools/make_distrib.py +++ b/tools/make_distrib.py @@ -1268,6 +1268,8 @@ def main(argv=None): # transfer shared files transfer_gypi_files(cef_dir, cef_paths2['shared_sources_win'], \ 'tests/shared/', shared_dir, options.quiet) + transfer_gypi_files(cef_dir, cef_paths2['shared_sources_osr_accessibility'], \ + 'tests/shared/', shared_dir, options.quiet) # transfer cefclient files transfer_gypi_files( @@ -1441,6 +1443,8 @@ def main(argv=None): 'tests/shared/', shared_dir, options.quiet) transfer_gypi_files(cef_dir, cef_paths2['shared_sources_osr_mac'], \ 'tests/shared/', shared_dir, options.quiet) + transfer_gypi_files(cef_dir, cef_paths2['shared_sources_osr_accessibility'], \ + 'tests/shared/', shared_dir, options.quiet) transfer_gypi_files(cef_dir, cef_paths2['shared_sources_mac_helper'], \ 'tests/shared/', shared_dir, options.quiet)