| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/base/test/ns_ax_tree_validator.h" |
| |
| #include <Cocoa/Cocoa.h> |
| |
| #include "base/logging.h" |
| #include "base/strings/sys_string_conversions.h" |
| |
| namespace { |
| |
| id<NSAccessibility> ToNSAccessibility(id obj) { |
| return [obj conformsToProtocol:@protocol(NSAccessibility)] ? obj : nil; |
| } |
| |
| void PrintNSAXTreeHelper(id<NSAccessibility> node, int depth) { |
| std::string desc; |
| for (int i = 0; i < depth; i++) { |
| desc += " "; |
| } |
| desc += base::SysNSStringToUTF8([NSString stringWithFormat:@"%@", node]); |
| LOG(INFO) << desc; |
| for (id child in node.accessibilityChildren) { |
| PrintNSAXTreeHelper(child, depth + 1); |
| } |
| } |
| } // namespace |
| |
| namespace ui { |
| |
| NSAXTreeProblemDetails::NSAXTreeProblemDetails(ProblemType type, |
| id node_a, |
| id node_b) |
| : type(type), node_a(node_a), node_b(node_b) {} |
| |
| std::string NSAXTreeProblemDetails::ToString() { |
| NSString* s; |
| switch (type) { |
| case NSAX_NOT_CHILD_OF_PARENT: |
| s = [NSString |
| stringWithFormat:@"Node %@ isn't a child of %@", node_a, node_b]; |
| break; |
| case NSAX_NOT_NSACCESSIBILITY: |
| s = [NSString stringWithFormat:@"Node %@ does not conform to" |
| " to NSAccessibility", |
| node_a]; |
| break; |
| case NSAX_PARENT_NOT_NSACCESSIBILITY: |
| s = [NSString stringWithFormat:@"Node %@'s parent %@ does not conform" |
| " to NSAccessibility", |
| node_a, node_b]; |
| break; |
| } |
| return base::SysNSStringToUTF8(s); |
| } |
| |
| std::optional<NSAXTreeProblemDetails> ValidateNSAXTree(id<NSAccessibility> node, |
| size_t* nodes_visited) { |
| if (!ToNSAccessibility(node)) { |
| return std::make_optional<NSAXTreeProblemDetails>( |
| NSAXTreeProblemDetails::NSAX_NOT_NSACCESSIBILITY, node, nil); |
| } |
| (*nodes_visited)++; |
| |
| // NSThemeWidgetZoomMenuRemoteView, a class new in macOS 14, violates |
| // invariants; its actual accessibility parent chain is [NSWindow, |
| // _NSThemeZoomWidgetCell] but when asked for its parent it returns the |
| // NSWindow, which doesn't have it as a child. This is an invariant that |
| // should hold (FB13557859). TODO(crbug.com/40935248): When FB13557859 |
| // is fixed, remove this workaround. |
| bool skip_due_to_fb13557859 = |
| node.class == NSClassFromString(@"NSThemeWidgetZoomMenuRemoteView"); |
| |
| if (node.accessibilityParent && !skip_due_to_fb13557859) { |
| id<NSAccessibility> parent = ToNSAccessibility(node.accessibilityParent); |
| if (!parent) { |
| return std::make_optional<NSAXTreeProblemDetails>( |
| NSAXTreeProblemDetails::NSAX_PARENT_NOT_NSACCESSIBILITY, node, |
| parent); |
| } |
| |
| NSArray<id<NSAccessibility>>* parent_children = |
| parent.accessibilityChildren; |
| |
| if (![parent_children containsObject:node]) { |
| return std::make_optional<NSAXTreeProblemDetails>( |
| NSAXTreeProblemDetails::NSAX_NOT_CHILD_OF_PARENT, node, parent); |
| } |
| } |
| |
| NSArray<id<NSAccessibility>>* children = node.accessibilityChildren; |
| for (id<NSAccessibility> child in children) { |
| std::optional<NSAXTreeProblemDetails> details = |
| ValidateNSAXTree(child, nodes_visited); |
| if (details.has_value()) { |
| return details; |
| } |
| } |
| |
| return std::nullopt; |
| } |
| |
| void PrintNSAXTree(id<NSAccessibility> root) { |
| PrintNSAXTreeHelper(root, 0); |
| } |
| |
| } // namespace ui |