chromium/ui/base/test/ns_ax_tree_validator.mm

// 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