// Copyright 2012 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/accessibility/platform/browser_accessibility_mac.h"

#include <ApplicationServices/ApplicationServices.h>
#import <Cocoa/Cocoa.h>

#include <memory>
#include <string>
#include <vector>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/ax_tree_update.h"
#include "ui/accessibility/ax_updates_and_events.h"
#include "ui/accessibility/platform/ax_private_webkit_constants_mac.h"
#include "ui/accessibility/platform/browser_accessibility_cocoa.h"
#include "ui/accessibility/platform/browser_accessibility_manager.h"
#include "ui/accessibility/platform/browser_accessibility_manager_mac.h"
#include "ui/accessibility/platform/test_ax_node_id_delegate.h"
#include "ui/accessibility/platform/test_ax_platform_tree_manager_delegate.h"
#include "ui/accessibility/test_ax_tree_update.h"
#import "ui/base/test/cocoa_helper.h"

using base::apple::CFToNSPtrCast;
using base::apple::ObjCCastStrict;

namespace ui {

namespace {

enum class TableHeaderOption {
  NoHeaders,
  RowHeaders,
  TwoRowHeaders,
  ColumnHeaders
};

void MakeRow(AXNodeData* row, int row_id) {
  row->id = row_id;
  row->role = ax::mojom::Role::kRow;
}

void MakeCell(AXNodeData* cell,
              int cell_id,
              int row_index,
              int column_index,
              int row_span = 1,
              int column_span = 1) {
  cell->id = cell_id;
  cell->role = ax::mojom::Role::kCell;
  cell->AddIntAttribute(ax::mojom::IntAttribute::kTableCellRowIndex, row_index);
  cell->AddIntAttribute(ax::mojom::IntAttribute::kTableCellColumnIndex,
                        column_index);
  if (row_span > 1) {
    cell->AddIntAttribute(ax::mojom::IntAttribute::kTableCellRowSpan, row_span);
  }
  if (column_span > 1) {
    cell->AddIntAttribute(ax::mojom::IntAttribute::kTableCellColumnSpan,
                          column_span);
  }
}

void MakeRowHeader(AXNodeData* cell,
                   int cell_id,
                   int row_index,
                   int column_index,
                   int row_span = 1,
                   int column_span = 1) {
  MakeCell(cell, cell_id, row_index, column_index, row_span, column_span);
  cell->role = ax::mojom::Role::kRowHeader;
}

void MakeColumnHeader(AXNodeData* cell,
                      int cell_id,
                      int row_index,
                      int column_index,
                      int row_span = 1,
                      int column_span = 1) {
  MakeCell(cell, cell_id, row_index, column_index, row_span, column_span);
  cell->role = ax::mojom::Role::kColumnHeader;
}

void MakeTable(AXTreeUpdate* initial_state,
               int row_count,
               int column_count,
               TableHeaderOption header_option = TableHeaderOption::NoHeaders) {
  int next_id = 1;
  initial_state->root_id = next_id++;

  // Node count is the table, plus each row, plus all cells.
  initial_state->nodes.resize(1 + row_count + row_count * column_count);
  int next_node_index = 0;

  AXNodeData* table = &initial_state->nodes[next_node_index++];
  table->id = initial_state->root_id;
  table->role = ax::mojom::Role::kTable;
  table->AddIntAttribute(ax::mojom::IntAttribute::kTableRowCount, row_count);
  table->AddIntAttribute(ax::mojom::IntAttribute::kTableColumnCount,
                         column_count);

  for (int row = 0; row < row_count; row++) {
    AXNodeData* row_node = &initial_state->nodes[next_node_index++];
    MakeRow(row_node, next_id++);
    table->child_ids.push_back(row_node->id);

    for (int column = 0; column < column_count; column++) {
      AXNodeData* cell_node = &initial_state->nodes[next_node_index++];
      if (header_option == TableHeaderOption::RowHeaders && column == 0) {
        MakeRowHeader(cell_node, next_id++, row, column);
      } else if (header_option == TableHeaderOption::TwoRowHeaders &&
                 column < 2) {
        MakeRowHeader(cell_node, next_id++, row, column);
      } else if (header_option == TableHeaderOption::ColumnHeaders &&
                 row == 0) {
        MakeColumnHeader(cell_node, next_id++, row, column);
      } else {
        MakeCell(cell_node, next_id++, row, column);
      }
      row_node->child_ids.push_back(cell_node->id);
    }
  }
}

}  // namespace

namespace {

// Lets a test choose whether a node owns a platform node. See
// BrowserAccessibility::ShouldHavePlatformNode for details.
class TogglablePlatformNodeBrowserAccessibilityMac
    : public BrowserAccessibilityMac {
 public:
  TogglablePlatformNodeBrowserAccessibilityMac(
      BrowserAccessibilityManager* manager,
      AXNode* node)
      : BrowserAccessibilityMac(manager, node) {}

  bool ShouldHavePlatformNode() const override { return should_have_; }

  void SetShouldHavePlatformNode(bool value) {
    should_have_ = value;
    UpdatePlatformNode();
  }

 private:
  bool should_have_ = true;
};

}  // namespace

class BrowserAccessibilityPlatformNodeMacTest : public CocoaTest {
 protected:
  std::unique_ptr<TogglablePlatformNodeBrowserAccessibilityMac> MakeNode() {
    AXNodeData root;
    root.id = 1;
    root.role = ax::mojom::Role::kRootWebArea;
    root.child_ids = {2};

    // Only an ignored node may go without a platform node.
    AXNodeData ignored_child;
    ignored_child.id = 2;
    ignored_child.role = ax::mojom::Role::kGenericContainer;
    ignored_child.AddState(ax::mojom::State::kIgnored);
    ignored_child.child_ids = {3};

    // A node below the ignored one keeps the root off the bottom of the tree.
    // ATK gives no object to the child of a leaf.
    AXNodeData grandchild;
    grandchild.id = 3;
    grandchild.role = ax::mojom::Role::kButton;

    manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
        MakeAXTreeUpdateForTesting(root, ignored_child, grandchild),
        node_id_delegate_, nullptr);
    auto node = std::make_unique<TogglablePlatformNodeBrowserAccessibilityMac>(
        manager_.get(), manager_->GetFromID(2)->node());
    // The manager does this for every wrapper that it creates.
    node->OnDataChanged();
    return node;
  }

  TestAXNodeIdDelegate node_id_delegate_;
  std::unique_ptr<BrowserAccessibilityManager> manager_;
  const base::test::SingleThreadTaskEnvironment task_environment_;
};

TEST_F(BrowserAccessibilityPlatformNodeMacTest,
       ANodeOwnsAPlatformNodeByDefault) {
  std::unique_ptr<TogglablePlatformNodeBrowserAccessibilityMac> node =
      MakeNode();

  EXPECT_TRUE(node->ShouldHavePlatformNode());
  EXPECT_TRUE(node->GetAXPlatformNode());
  EXPECT_TRUE(node->GetNativeViewAccessible());
}

TEST_F(BrowserAccessibilityPlatformNodeMacTest, ANodeCanOwnNoPlatformNode) {
  std::unique_ptr<TogglablePlatformNodeBrowserAccessibilityMac> node =
      MakeNode();
  node->SetShouldHavePlatformNode(false);

  EXPECT_FALSE(node->GetAXPlatformNode());
  EXPECT_FALSE(node->GetNativeViewAccessible());
}

TEST_F(BrowserAccessibilityPlatformNodeMacTest,
       EveryAccessorIsSafeWithNoPlatformNode) {
  std::unique_ptr<TogglablePlatformNodeBrowserAccessibilityMac> node =
      MakeNode();
  node->SetShouldHavePlatformNode(false);

  // Each of these reads the tree, not the platform node that is gone.
  EXPECT_EQ(1u, node->PlatformChildCount());
  EXPECT_TRUE(node->PlatformGetFirstChild());
  EXPECT_TRUE(node->PlatformGetLastChild());
  node->OnDataChanged();
}

TEST_F(BrowserAccessibilityPlatformNodeMacTest, APlatformNodeComesBack) {
  std::unique_ptr<TogglablePlatformNodeBrowserAccessibilityMac> node =
      MakeNode();
  node->SetShouldHavePlatformNode(false);
  ASSERT_FALSE(node->GetAXPlatformNode());

  node->SetShouldHavePlatformNode(true);

  EXPECT_TRUE(node->GetAXPlatformNode());
  EXPECT_TRUE(node->GetNativeViewAccessible());
}

TEST_F(BrowserAccessibilityPlatformNodeMacTest, RepeatedChangesAreSafe) {
  std::unique_ptr<TogglablePlatformNodeBrowserAccessibilityMac> node =
      MakeNode();

  for (int i = 0; i < 3; ++i) {
    node->SetShouldHavePlatformNode(false);
    ASSERT_FALSE(node->GetAXPlatformNode()) << "iteration " << i;

    node->SetShouldHavePlatformNode(true);
    ASSERT_TRUE(node->GetAXPlatformNode()) << "iteration " << i;
  }
}

TEST_F(BrowserAccessibilityPlatformNodeMacTest,
       RepeatedUpdatesKeepOnePlatformNode) {
  std::unique_ptr<TogglablePlatformNodeBrowserAccessibilityMac> node =
      MakeNode();
  AXPlatformNode* first = node->GetAXPlatformNode();

  node->UpdatePlatformNode();
  node->UpdatePlatformNode();

  EXPECT_EQ(first, node->GetAXPlatformNode());
}

class BrowserAccessibilityMacTest : public CocoaTest {
 public:
  void SetUp() override {
    CocoaTest::SetUp();
    RebuildAccessibilityTree();
  }

 protected:
  void RebuildAccessibilityTree() {
    // Clean out the existing root data in case this method is called multiple
    // times in a test.
    root_ = AXNodeData();
    root_.id = 1000;
    root_.relative_bounds.bounds.set_width(500);
    root_.relative_bounds.bounds.set_height(100);
    root_.role = ax::mojom::Role::kRootWebArea;
    root_.AddStringAttribute(ax::mojom::StringAttribute::kDescription,
                             "HelpText");
    root_.child_ids.push_back(1001);
    root_.child_ids.push_back(1002);

    AXNodeData child1;
    child1.id = 1001;
    child1.role = ax::mojom::Role::kButton;
    child1.SetName("Child1");
    child1.relative_bounds.bounds.set_width(250);
    child1.relative_bounds.bounds.set_height(100);

    AXNodeData child2;
    child2.id = 1002;
    child2.relative_bounds.bounds.set_x(250);
    child2.relative_bounds.bounds.set_width(250);
    child2.relative_bounds.bounds.set_height(100);
    child2.role = ax::mojom::Role::kHeading;

    manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
        MakeAXTreeUpdateForTesting(root_, child1, child2), node_id_delegate_,
        nullptr);
    accessibility_ = ObjCCastStrict<BrowserAccessibilityCocoa>(
        manager_->GetBrowserAccessibilityRoot()
            ->GetNativeViewAccessible()
            .Get());
  }

  void SetRootValue(std::string value) {
    if (!manager_) {
      return;
    }
    root_.SetValue(value);
    AXUpdatesAndEvents event_bundle;
    event_bundle.updates.resize(1);
    event_bundle.updates[0].nodes.push_back(root_);
    ASSERT_TRUE(manager_->OnAccessibilityEvents(event_bundle));
  }

  NSDictionary* GetUserInfoForSelectedTextChangedNotification() {
    auto* manager_mac =
        static_cast<BrowserAccessibilityManagerMac*>(manager_.get());
    return manager_mac->GetUserInfoForSelectedTextChangedNotification();
  }

  AXNodeData root_;
  BrowserAccessibilityCocoa* __strong accessibility_;
  TestAXNodeIdDelegate node_id_delegate_;
  std::unique_ptr<BrowserAccessibilityManager> manager_;

  const base::test::SingleThreadTaskEnvironment task_environment_;
};

// Standard hit test.
TEST_F(BrowserAccessibilityMacTest, HitTestTest) {
  BrowserAccessibilityCocoa* firstChild =
      [accessibility_ accessibilityHitTest:NSMakePoint(50, 50)];
  EXPECT_NSEQ(@"Child1", firstChild.accessibilityLabel);
}

// Test doing a hit test on the edge of a child.
TEST_F(BrowserAccessibilityMacTest, EdgeHitTest) {
  BrowserAccessibilityCocoa* firstChild =
      [accessibility_ accessibilityHitTest:NSZeroPoint];
  EXPECT_NSEQ(@"Child1", firstChild.accessibilityLabel);
}

// This will test a hit test with invalid coordinates.  It is assumed that
// the hit test has been narrowed down to this object or one of its children
// so it should return itself since it has no better hit result.
TEST_F(BrowserAccessibilityMacTest, InvalidHitTestCoordsTest) {
  BrowserAccessibilityCocoa* hitTestResult =
      [accessibility_ accessibilityHitTest:NSMakePoint(-50, 50)];
  EXPECT_NSEQ(accessibility_, hitTestResult);
}

// Test to ensure querying standard attributes works.
TEST_F(BrowserAccessibilityMacTest, BasicAttributeTest) {
  EXPECT_NSEQ(@"HelpText", [accessibility_ accessibilityHelp]);
}

TEST_F(BrowserAccessibilityMacTest, RetainedDetachedObjectsReturnNil) {
  // Get the first child. Hold it in a precise lifetime variable. This simulates
  // what the system might do with an accessibility object.
  NS_VALID_UNTIL_END_OF_SCOPE BrowserAccessibilityCocoa* retainedFirstChild =
      [accessibility_ accessibilityHitTest:NSMakePoint(50, 50)];
  EXPECT_NSEQ(@"Child1", retainedFirstChild.accessibilityLabel);

  // Rebuild the accessibility tree, which should detach |retainedFirstChild|.
  RebuildAccessibilityTree();

  // Now any attributes we query should return nil.
  EXPECT_NSEQ(nil, retainedFirstChild.accessibilityLabel);
}

// AppKit may retain a wrapper and key it in a hash-based collection, so
// -hash must remain stable for the lifetime of the wrapper, and distinct
// wrappers must never compare equal even if their backing nodes happen to
// share the same per-tree id.
TEST_F(BrowserAccessibilityMacTest, IdentityIsPerWrapperAndStable) {
  // Build a second, independent tree whose root re-uses the same per-tree id
  // as a node in the fixture's tree.
  AXNodeData other_root;
  other_root.id = 1000;
  other_root.role = ax::mojom::Role::kRootWebArea;
  TestAXNodeIdDelegate other_node_id_delegate;
  std::unique_ptr<BrowserAccessibilityManager> other_manager =
      std::make_unique<BrowserAccessibilityManagerMac>(
          MakeAXTreeUpdateForTesting(other_root), other_node_id_delegate,
          nullptr);
  BrowserAccessibilityCocoa* other_wrapper =
      ObjCCastStrict<BrowserAccessibilityCocoa>(
          other_manager->GetBrowserAccessibilityRoot()
              ->GetNativeViewAccessible()
              .Get());

  ASSERT_NE(accessibility_, other_wrapper);
  EXPECT_FALSE([accessibility_ isEqual:other_wrapper]);
  EXPECT_FALSE([other_wrapper isEqual:accessibility_]);

  // Hold the wrapper past detach, as the system might.
  NS_VALID_UNTIL_END_OF_SCOPE BrowserAccessibilityCocoa* retained =
      accessibility_;
  const NSUInteger hash_before = retained.hash;
  EXPECT_TRUE([retained isEqual:retained]);

  // Tearing down the manager detaches the wrapper.
  manager_.reset();
  ASSERT_FALSE([retained instanceActive]);

  EXPECT_EQ(hash_before, retained.hash);
  EXPECT_TRUE([retained isEqual:retained]);
  EXPECT_FALSE([retained isEqual:other_wrapper]);
}

TEST_F(BrowserAccessibilityMacTest, TestComputeTextEdit) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kTextField;
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  accessibility_ = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());

  // Insertion but no deletion.

  SetRootValue("text");
  AXTextEdit text_edit = [accessibility_ computeTextEdit];
  EXPECT_EQ(u"text", text_edit.inserted_text);
  EXPECT_TRUE(text_edit.deleted_text.empty());

  SetRootValue("new text");
  text_edit = [accessibility_ computeTextEdit];
  EXPECT_EQ(u"new ", text_edit.inserted_text);
  EXPECT_TRUE(text_edit.deleted_text.empty());

  SetRootValue("new text hello");
  text_edit = [accessibility_ computeTextEdit];
  EXPECT_EQ(u" hello", text_edit.inserted_text);
  EXPECT_TRUE(text_edit.deleted_text.empty());

  SetRootValue("newer text hello");
  text_edit = [accessibility_ computeTextEdit];
  EXPECT_EQ(u"er", text_edit.inserted_text);
  EXPECT_TRUE(text_edit.deleted_text.empty());

  // Deletion but no insertion.

  SetRootValue("new text hello");
  text_edit = [accessibility_ computeTextEdit];
  EXPECT_EQ(u"er", text_edit.deleted_text);
  EXPECT_TRUE(text_edit.inserted_text.empty());

  SetRootValue("new text");
  text_edit = [accessibility_ computeTextEdit];
  EXPECT_EQ(u" hello", text_edit.deleted_text);
  EXPECT_TRUE(text_edit.inserted_text.empty());

  SetRootValue("text");
  text_edit = [accessibility_ computeTextEdit];
  EXPECT_EQ(u"new ", text_edit.deleted_text);
  EXPECT_TRUE(text_edit.inserted_text.empty());

  SetRootValue("");
  text_edit = [accessibility_ computeTextEdit];
  EXPECT_EQ(u"text", text_edit.deleted_text);
  EXPECT_TRUE(text_edit.inserted_text.empty());

  // Both insertion and deletion.

  SetRootValue("new text hello");
  text_edit = [accessibility_ computeTextEdit];
  SetRootValue("new word hello");
  text_edit = [accessibility_ computeTextEdit];
  EXPECT_EQ(u"text", text_edit.deleted_text);
  EXPECT_EQ(u"word", text_edit.inserted_text);

  SetRootValue("new word there");
  text_edit = [accessibility_ computeTextEdit];
  EXPECT_EQ(u"hello", text_edit.deleted_text);
  EXPECT_EQ(u"there", text_edit.inserted_text);

  SetRootValue("old word there");
  text_edit = [accessibility_ computeTextEdit];
  EXPECT_EQ(u"new", text_edit.deleted_text);
  EXPECT_EQ(u"old", text_edit.inserted_text);
}

TEST_F(BrowserAccessibilityMacTest,
       UserInfoForSelectedTextChangedNotificationIsEditForTextEdits) {
  // Firing focus events requires a real delegate and events must not be
  // suppressed for lack of window focus.
  BrowserAccessibilityManager::NeverSuppressOrDelayEventsForTesting();
  TestAXPlatformTreeManagerDelegate delegate;
  delegate.is_root_frame_ = true;

  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kTextField;
  root_.AddState(ax::mojom::State::kEditable);
  AXTreeUpdate initial_update = MakeAXTreeUpdateForTesting(root_);
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      initial_update, node_id_delegate_, &delegate);
  accessibility_ = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());

  // Move focus onto the text field. This also syncs the manager's notion of
  // the "last focused node" with the current focus, so that a subsequent
  // selected-text-changed notification won't be attributed to a focus move.
  AXUpdatesAndEvents focus_bundle;
  focus_bundle.updates.resize(1);
  focus_bundle.updates[0].has_tree_data = true;
  focus_bundle.updates[0].tree_data = initial_update.tree_data;
  focus_bundle.updates[0].tree_data.focus_id = root_.id;
  ASSERT_TRUE(manager_->OnAccessibilityEvents(focus_bundle));

  // Focus hasn't moved and there are no pending text edits, so the change type
  // should be unknown. This may change with crbug.com/545879268.
  NSDictionary* user_info = GetUserInfoForSelectedTextChangedNotification();
  EXPECT_NSEQ(@(AXTextStateChangeTypeUnknown),
              user_info[NSAccessibilityTextStateChangeTypeKey]);

  // Simulate the user typing a character into the field. Focus doesn't move,
  // but a text edit is now pending for the focused node.
  root_.SetValue("a");
  AXUpdatesAndEvents edit_bundle;
  edit_bundle.updates.resize(1);
  edit_bundle.updates[0].nodes.push_back(root_);
  ASSERT_TRUE(manager_->OnAccessibilityEvents(edit_bundle));

  // This should not change with crbug.com/545879268, or if a change is
  // necessary, make sure it does not cause the regression reported in
  // https://issues.chromium.org/issues/512582992.
  user_info = GetUserInfoForSelectedTextChangedNotification();
  EXPECT_NSEQ(@(AXTextStateChangeTypeEdit),
              user_info[NSAccessibilityTextStateChangeTypeKey]);
}

// Test Mac-specific table APIs.
TEST_F(BrowserAccessibilityMacTest, TableAPIs) {
  AXTreeUpdate initial_state;
  const int kNumberOfRows = 2;
  const int kNumberOfColumns = 2;
  MakeTable(&initial_state, kNumberOfRows, kNumberOfColumns,
            TableHeaderOption::ColumnHeaders);

  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      initial_state, node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* ax_table =
      ObjCCastStrict<BrowserAccessibilityCocoa>(
          manager_->GetBrowserAccessibilityRoot()
              ->GetNativeViewAccessible()
              .Get());
  NSArray<BrowserAccessibilityCocoa*>* children =
      ax_table.accessibilityChildren;
  EXPECT_EQ(5U, children.count);

  EXPECT_NSEQ(CFToNSPtrCast(kAXRowRole), [children[0] role]);
  EXPECT_EQ(2U, [[children[0] accessibilityChildren] count]);

  EXPECT_NSEQ(CFToNSPtrCast(kAXRowRole), [children[1] role]);
  EXPECT_EQ(2U, [[children[1] accessibilityChildren] count]);

  EXPECT_NSEQ(CFToNSPtrCast(kAXColumnRole), [children[2] role]);
  EXPECT_EQ(2U, [[children[2] accessibilityChildren] count]);
  NSArray<BrowserAccessibilityCocoa*>* col_children =
      [children[2] accessibilityChildren];
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [col_children[0] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [col_children[1] role]);

  EXPECT_NSEQ(CFToNSPtrCast(kAXColumnRole), [children[3] role]);
  EXPECT_EQ(2U, [[children[3] accessibilityChildren] count]);
  col_children = [children[3] accessibilityChildren];
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [col_children[0] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [col_children[1] role]);

  EXPECT_NSEQ(CFToNSPtrCast(kAXGroupRole), [children[4] role]);
  EXPECT_EQ(2U, [[children[4] accessibilityChildren] count]);
  col_children = [children[4] accessibilityChildren];
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [col_children[0] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [col_children[1] role]);
}

// Test table row header support.
TEST_F(BrowserAccessibilityMacTest, TableWithRowHeaders) {
  // A non-table object should return nil for rowHeaders.
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kTextField;
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* ax_textfield =
      ObjCCastStrict<BrowserAccessibilityCocoa>(
          manager_->GetBrowserAccessibilityRoot()
              ->GetNativeViewAccessible()
              .Get());
  NSArray* row_headers = [ax_textfield rowHeaders];
  EXPECT_EQ(nil, row_headers);

  // A table with no row headers should return nil for rowHeaders.
  AXTreeUpdate headerless_table_state;
  const int kNumberOfRows = 2;
  const int kNumberOfColumns = 2;
  MakeTable(&headerless_table_state, kNumberOfRows, kNumberOfColumns);
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      headerless_table_state, node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* ax_table =
      ObjCCastStrict<BrowserAccessibilityCocoa>(
          manager_->GetBrowserAccessibilityRoot()
              ->GetNativeViewAccessible()
              .Get());
  ax_table = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  row_headers = [ax_table rowHeaders];
  EXPECT_EQ(nil, row_headers);

  // Create a table with row headers.
  AXTreeUpdate table_state;
  MakeTable(&table_state, kNumberOfRows, kNumberOfColumns,
            TableHeaderOption::RowHeaders);
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      table_state, node_id_delegate_, nullptr);
  ax_table = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());

  // Confirm the AX structure is as expected.
  NSArray<BrowserAccessibilityCocoa*>* ax_table_children =
      ax_table.accessibilityChildren;
  EXPECT_EQ(5U, ax_table_children.count);

  BrowserAccessibilityCocoa* first_row = ax_table_children[0];
  EXPECT_NSEQ(CFToNSPtrCast(kAXRowRole), [first_row role]);
  NSArray<BrowserAccessibilityCocoa*>* first_row_children =
      [first_row accessibilityChildren];
  EXPECT_EQ(2U, [first_row_children count]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [first_row_children[0] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [first_row_children[1] role]);

  BrowserAccessibilityCocoa* second_row = ax_table_children[1];
  EXPECT_NSEQ(CFToNSPtrCast(kAXRowRole), [second_row role]);
  NSArray<BrowserAccessibilityCocoa*>* second_row_children =
      [second_row accessibilityChildren];
  EXPECT_EQ(2U, [second_row_children count]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [second_row_children[0] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [second_row_children[1] role]);

  BrowserAccessibilityCocoa* first_column = ax_table_children[2];
  EXPECT_NSEQ(CFToNSPtrCast(kAXColumnRole), [first_column role]);
  NSArray<BrowserAccessibilityCocoa*>* first_column_children =
      [first_column accessibilityChildren];
  EXPECT_EQ(2U, [first_column_children count]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [first_column_children[0] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [first_column_children[1] role]);

  BrowserAccessibilityCocoa* second_column = ax_table_children[3];
  EXPECT_NSEQ(CFToNSPtrCast(kAXColumnRole), [second_column role]);
  NSArray<BrowserAccessibilityCocoa*>* second_column_children =
      [second_column accessibilityChildren];
  EXPECT_EQ(2U, [second_column_children count]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [second_column_children[0] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [second_column_children[1] role]);

  EXPECT_EQ(first_row_children[0], first_column_children[0]);
  EXPECT_EQ(first_row_children[1], second_column_children[0]);
  EXPECT_EQ(second_row_children[0], first_column_children[1]);
  EXPECT_EQ(second_row_children[1], second_column_children[1]);

  BrowserAccessibilityCocoa* table_group = ax_table_children[4];
  EXPECT_NSEQ(CFToNSPtrCast(kAXGroupRole), [table_group role]);
  EXPECT_EQ(0U, [table_group accessibilityChildren].count);

  // Confirm the table has row headers, and that they match the expected cells
  // in the table.
  row_headers = [ax_table rowHeaders];
  EXPECT_EQ(2U, [row_headers count]);
  id first_row_header_cell = row_headers[0];
  EXPECT_EQ(first_row_header_cell, first_row_children[0]);
  id second_row_header_cell = row_headers[1];
  EXPECT_EQ(second_row_header_cell, second_row_children[0]);

  // If we ask a row header cell for its rowHeaders, we should get that
  // cell back.
  row_headers = [first_row_header_cell rowHeaders];
  EXPECT_EQ(1U, [row_headers count]);
  EXPECT_EQ(first_row_header_cell, row_headers[0]);

  // A non-row-header cell should return the header for its row.
  id last_cell_second_row = second_row_children[1];
  row_headers = [last_cell_second_row rowHeaders];
  EXPECT_EQ(1U, [row_headers count]);
  EXPECT_NSEQ(second_row_header_cell, row_headers[0]);
}

// Test table with more than one row header.
TEST_F(BrowserAccessibilityMacTest, TableWithTwoRowHeaders) {
  // Create a table with two row headers per row.
  const int kNumberOfRows = 2;
  const int kNumberOfColumns = 3;
  AXTreeUpdate table_state;
  MakeTable(&table_state, kNumberOfRows, kNumberOfColumns,
            TableHeaderOption::TwoRowHeaders);
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      table_state, node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* ax_table =
      ObjCCastStrict<BrowserAccessibilityCocoa>(
          manager_->GetBrowserAccessibilityRoot()
              ->GetNativeViewAccessible()
              .Get());

  // Confirm the AX structure is as expected.
  NSArray<BrowserAccessibilityCocoa*>* ax_table_children =
      ax_table.accessibilityChildren;
  EXPECT_EQ(6U, ax_table_children.count);

  BrowserAccessibilityCocoa* first_row = ax_table_children[0];
  EXPECT_NSEQ(CFToNSPtrCast(kAXRowRole), [first_row role]);
  NSArray<BrowserAccessibilityCocoa*>* first_row_children =
      [first_row accessibilityChildren];
  EXPECT_EQ(3U, [first_row_children count]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [first_row_children[0] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [first_row_children[1] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [first_row_children[2] role]);

  BrowserAccessibilityCocoa* second_row = ax_table_children[1];
  EXPECT_NSEQ(CFToNSPtrCast(kAXRowRole), [second_row role]);
  NSArray<BrowserAccessibilityCocoa*>* second_row_children =
      [second_row accessibilityChildren];
  EXPECT_EQ(3U, [second_row_children count]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [second_row_children[0] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [second_row_children[1] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [second_row_children[2] role]);

  BrowserAccessibilityCocoa* first_column = ax_table_children[2];
  EXPECT_NSEQ(CFToNSPtrCast(kAXColumnRole), [first_column role]);
  NSArray<BrowserAccessibilityCocoa*>* first_column_children =
      [first_column accessibilityChildren];
  EXPECT_EQ(2U, [first_column_children count]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [first_column_children[0] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [first_column_children[1] role]);

  BrowserAccessibilityCocoa* second_column = ax_table_children[3];
  EXPECT_NSEQ(CFToNSPtrCast(kAXColumnRole), [second_column role]);
  NSArray<BrowserAccessibilityCocoa*>* second_column_children =
      [second_column accessibilityChildren];
  EXPECT_EQ(2U, [second_column_children count]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [second_column_children[0] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [second_column_children[1] role]);

  BrowserAccessibilityCocoa* third_column = ax_table_children[4];
  EXPECT_NSEQ(CFToNSPtrCast(kAXColumnRole), [third_column role]);
  NSArray<BrowserAccessibilityCocoa*>* third_column_children =
      [third_column accessibilityChildren];
  EXPECT_EQ(2U, [third_column_children count]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [third_column_children[0] role]);
  EXPECT_NSEQ(CFToNSPtrCast(kAXCellRole), [third_column_children[1] role]);

  EXPECT_EQ(first_row_children[0], first_column_children[0]);
  EXPECT_EQ(first_row_children[1], second_column_children[0]);
  EXPECT_EQ(first_row_children[2], third_column_children[0]);
  EXPECT_EQ(second_row_children[0], first_column_children[1]);
  EXPECT_EQ(second_row_children[1], second_column_children[1]);
  EXPECT_EQ(second_row_children[2], third_column_children[1]);

  BrowserAccessibilityCocoa* table_group = ax_table_children[5];
  EXPECT_NSEQ(CFToNSPtrCast(kAXGroupRole), [table_group role]);
  EXPECT_EQ(0U, [table_group accessibilityChildren].count);

  // Confirm the table has two row headers per row, and that they match
  // the expected cells in the table.
  NSArray<BrowserAccessibilityCocoa*>* row_headers = [ax_table rowHeaders];
  EXPECT_EQ(4U, [row_headers count]);
  BrowserAccessibilityCocoa* first_row_header_cell = row_headers[0];
  EXPECT_EQ(first_row_header_cell, first_row_children[0]);
  BrowserAccessibilityCocoa* second_row_header_cell = row_headers[1];
  EXPECT_EQ(second_row_header_cell, first_row_children[1]);
  BrowserAccessibilityCocoa* third_row_header_cell = row_headers[2];
  EXPECT_EQ(third_row_header_cell, second_row_children[0]);
  BrowserAccessibilityCocoa* fourth_row_header_cell = row_headers[3];
  EXPECT_EQ(fourth_row_header_cell, second_row_children[1]);

  // A non-row-header cell should return the headers for its row.
  BrowserAccessibilityCocoa* last_cell_second_row = second_row_children[2];
  row_headers = [last_cell_second_row rowHeaders];
  EXPECT_EQ(2U, [row_headers count]);
  EXPECT_NSEQ(third_row_header_cell, row_headers[0]);
  EXPECT_NSEQ(fourth_row_header_cell, row_headers[1]);
}

// Test Mac indirect columns and descendants.
TEST_F(BrowserAccessibilityMacTest, TableColumnsAndDescendants) {
  AXTreeUpdate initial_state;
  const int kNumberOfRows = 2;
  const int kNumberOfColumns = 2;
  MakeTable(&initial_state, kNumberOfRows, kNumberOfColumns,
            TableHeaderOption::ColumnHeaders);

  // This relation is the key to force
  // AXEventGenerator::FireRelationSourceEvents to trigger addition of an event
  // which had caused a crash below.
  initial_state.nodes[6].AddIntListAttribute(
      ax::mojom::IntListAttribute::kFlowtoIds, {1});

  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      initial_state, node_id_delegate_, nullptr);

  BrowserAccessibilityMac* root = static_cast<BrowserAccessibilityMac*>(
      manager_->GetBrowserAccessibilityRoot());

  // This triggers computation of the extra Mac table cells. 2 rows, 2 extra
  // columns, and 1 extra column header. This used to crash.
  ASSERT_EQ(root->PlatformChildCount(), 5U);
}

// Non-header cells should not support AXSortDirection, even if there's a sort
// direction in the AXNodeData.
TEST_F(BrowserAccessibilityMacTest, AXSortDirectionUnsupportedOnCell) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kCell;
  root_.AddIntAttribute(ax::mojom::IntAttribute::kSortDirection,
                        static_cast<int>(ax::mojom::SortDirection::kAscending));
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* cell = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  EXPECT_NSEQ([cell role], NSAccessibilityCellRole);
  EXPECT_EQ([cell internalRole], ax::mojom::Role::kCell);
  EXPECT_FALSE([[cell internalAccessibilityAttributeNames]
      containsObject:NSAccessibilitySortDirectionAttribute]);
  EXPECT_FALSE([cell sortDirection]);
}

// A row header whose AXNodeData lacks a sort order should not support
// AXSortDirection.
TEST_F(BrowserAccessibilityMacTest,
       AXSortDirectionUnspecifiedUnsupportedOnRowHeader) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kRowHeader;
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* cell = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  EXPECT_NSEQ([cell role], NSAccessibilityCellRole);
  EXPECT_EQ([cell internalRole], ax::mojom::Role::kRowHeader);
  EXPECT_FALSE([[cell internalAccessibilityAttributeNames]
      containsObject:NSAccessibilitySortDirectionAttribute]);
  EXPECT_FALSE([cell sortDirection]);
}

// A column header whose AXNodeData lacks a sort order should not support
// AXSortDirection.
TEST_F(BrowserAccessibilityMacTest,
       AXSortDirectionUnspecifiedUnsupportedOnColumnHeader) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kColumnHeader;
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* cell = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  EXPECT_NSEQ([cell role], NSAccessibilityCellRole);
  EXPECT_EQ([cell internalRole], ax::mojom::Role::kColumnHeader);
  EXPECT_FALSE([[cell internalAccessibilityAttributeNames]
      containsObject:NSAccessibilitySortDirectionAttribute]);
  EXPECT_FALSE([cell sortDirection]);
}

// A row header whose AXNodeData contains an "unsorted" sort order should not
// support AXSortDirection.
TEST_F(BrowserAccessibilityMacTest,
       AXSortDirectionUnsortedUnsupportedOnRowHeader) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kRowHeader;
  root_.AddIntAttribute(ax::mojom::IntAttribute::kSortDirection,
                        static_cast<int>(ax::mojom::SortDirection::kUnsorted));
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* cell = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  EXPECT_NSEQ([cell role], NSAccessibilityCellRole);
  EXPECT_EQ([cell internalRole], ax::mojom::Role::kRowHeader);
  EXPECT_FALSE([[cell internalAccessibilityAttributeNames]
      containsObject:NSAccessibilitySortDirectionAttribute]);
  EXPECT_FALSE([cell sortDirection]);
}

// A column header whose AXNodeData contains an "unsorted" sort order should not
// support AXSortDirection.
TEST_F(BrowserAccessibilityMacTest,
       AXSortDirectionUnsortedUnsupportedOnColumnHeader) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kColumnHeader;
  root_.AddIntAttribute(ax::mojom::IntAttribute::kSortDirection,
                        static_cast<int>(ax::mojom::SortDirection::kUnsorted));
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* cell = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  EXPECT_NSEQ([cell role], NSAccessibilityCellRole);
  EXPECT_EQ([cell internalRole], ax::mojom::Role::kColumnHeader);
  EXPECT_FALSE([[cell internalAccessibilityAttributeNames]
      containsObject:NSAccessibilitySortDirectionAttribute]);
  EXPECT_FALSE([cell sortDirection]);
}

// A row header whose AXNodeData contains an "ascending" sort order should
// support AXSortDirection.
TEST_F(BrowserAccessibilityMacTest,
       AXSortDirectionAscendingSupportedOnRowHeader) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kRowHeader;
  root_.AddIntAttribute(ax::mojom::IntAttribute::kSortDirection,
                        static_cast<int>(ax::mojom::SortDirection::kAscending));
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* cell = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  EXPECT_NSEQ([cell role], NSAccessibilityCellRole);
  EXPECT_EQ([cell internalRole], ax::mojom::Role::kRowHeader);
  EXPECT_TRUE([[cell internalAccessibilityAttributeNames]
      containsObject:NSAccessibilitySortDirectionAttribute]);
  EXPECT_NSEQ([cell sortDirection], NSAccessibilityAscendingSortDirectionValue);
}

// A column header whose AXNodeData contains an "ascending" sort order should
// support AXSortDirection.
TEST_F(BrowserAccessibilityMacTest,
       AXSortDirectionAscendingSupportedOnColumnHeader) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kColumnHeader;
  root_.AddIntAttribute(ax::mojom::IntAttribute::kSortDirection,
                        static_cast<int>(ax::mojom::SortDirection::kAscending));
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* cell = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  EXPECT_NSEQ([cell role], NSAccessibilityCellRole);
  EXPECT_EQ([cell internalRole], ax::mojom::Role::kColumnHeader);
  EXPECT_TRUE([[cell internalAccessibilityAttributeNames]
      containsObject:NSAccessibilitySortDirectionAttribute]);
  EXPECT_NSEQ([cell sortDirection], NSAccessibilityAscendingSortDirectionValue);
}

// A row header whose AXNodeData contains a "descending" sort order should
// support AXSortDirection.
TEST_F(BrowserAccessibilityMacTest,
       AXSortDirectionDescendingSupportedOnRowHeader) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kRowHeader;
  root_.AddIntAttribute(
      ax::mojom::IntAttribute::kSortDirection,
      static_cast<int>(ax::mojom::SortDirection::kDescending));
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* cell = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  EXPECT_NSEQ([cell role], NSAccessibilityCellRole);
  EXPECT_EQ([cell internalRole], ax::mojom::Role::kRowHeader);
  EXPECT_TRUE([[cell internalAccessibilityAttributeNames]
      containsObject:NSAccessibilitySortDirectionAttribute]);
  EXPECT_NSEQ([cell sortDirection],
              NSAccessibilityDescendingSortDirectionValue);
}

// A column header whose AXNodeData contains a "descending" sort order should
// support AXSortDirection.
TEST_F(BrowserAccessibilityMacTest,
       AXSortDirectionDescendingSupportedOnColumnHeader) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kColumnHeader;
  root_.AddIntAttribute(
      ax::mojom::IntAttribute::kSortDirection,
      static_cast<int>(ax::mojom::SortDirection::kDescending));
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* cell = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  EXPECT_NSEQ([cell role], NSAccessibilityCellRole);
  EXPECT_EQ([cell internalRole], ax::mojom::Role::kColumnHeader);
  EXPECT_TRUE([[cell internalAccessibilityAttributeNames]
      containsObject:NSAccessibilitySortDirectionAttribute]);
  EXPECT_NSEQ([cell sortDirection],
              NSAccessibilityDescendingSortDirectionValue);
}

// A row header whose AXNodeData contains an "other" sort order should support
// AXSortDirection.
TEST_F(BrowserAccessibilityMacTest, AXSortDirectionOtherSupportedOnRowHeader) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kRowHeader;
  root_.AddIntAttribute(ax::mojom::IntAttribute::kSortDirection,
                        static_cast<int>(ax::mojom::SortDirection::kOther));
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* cell = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  EXPECT_NSEQ([cell role], NSAccessibilityCellRole);
  EXPECT_EQ([cell internalRole], ax::mojom::Role::kRowHeader);
  EXPECT_TRUE([[cell internalAccessibilityAttributeNames]
      containsObject:NSAccessibilitySortDirectionAttribute]);
  EXPECT_NSEQ([cell sortDirection], NSAccessibilityUnknownSortDirectionValue);
}

// A column header whose AXNodeData contains an "other" sort order should
// support AXSortDirection.
TEST_F(BrowserAccessibilityMacTest,
       AXSortDirectionOtherSupportedOnColumnHeader) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kColumnHeader;
  root_.AddIntAttribute(ax::mojom::IntAttribute::kSortDirection,
                        static_cast<int>(ax::mojom::SortDirection::kOther));
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  BrowserAccessibilityCocoa* cell = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  EXPECT_NSEQ([cell role], NSAccessibilityCellRole);
  EXPECT_EQ([cell internalRole], ax::mojom::Role::kColumnHeader);
  EXPECT_TRUE([[cell internalAccessibilityAttributeNames]
      containsObject:NSAccessibilitySortDirectionAttribute]);
  EXPECT_NSEQ([cell sortDirection], NSAccessibilityUnknownSortDirectionValue);
}

// Test that the header container can be retrieved on a table with column
// headers.
TEST_F(BrowserAccessibilityMacTest, AXHeaderOnTableWithColumnHeaders) {
  AXTreeUpdate initial_state;
  const int kNumberOfRows = 3;
  const int kNumberOfColumns = 2;
  MakeTable(&initial_state, kNumberOfRows, kNumberOfColumns,
            TableHeaderOption::ColumnHeaders);
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      initial_state, node_id_delegate_, nullptr);

  // The native table will have six children: the three rows, the two columns,
  // and the header group.
  BrowserAccessibilityCocoa* ax_table =
      ObjCCastStrict<BrowserAccessibilityCocoa>(
          manager_->GetBrowserAccessibilityRoot()
              ->GetNativeViewAccessible()
              .Get());
  NSArray<BrowserAccessibilityCocoa*>* children =
      [ax_table accessibilityChildren];
  EXPECT_EQ(6U, [children count]);
  BrowserAccessibilityCocoa* header = children[5];
  EXPECT_NSEQ([header role], NSAccessibilityGroupRole);

  // Asking for the header directly should return that last child, which should
  // have two children, namely the header cell for each of the two columns.
  EXPECT_EQ(header, [ax_table accessibilityHeader]);
  EXPECT_EQ(2U, [[header accessibilityChildren] count]);
}

// Test that the header container can be retrieved on a table with row headers.
TEST_F(BrowserAccessibilityMacTest, AXHeaderOnTableWithRowHeaders) {
  AXTreeUpdate initial_state;
  const int kNumberOfRows = 5;
  const int kNumberOfColumns = 7;
  MakeTable(&initial_state, kNumberOfRows, kNumberOfColumns,
            TableHeaderOption::RowHeaders);
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      initial_state, node_id_delegate_, nullptr);

  // The native table will have 13 children: the five rows, the seven columns,
  // and the header group.
  BrowserAccessibilityCocoa* ax_table =
      ObjCCastStrict<BrowserAccessibilityCocoa>(
          manager_->GetBrowserAccessibilityRoot()
              ->GetNativeViewAccessible()
              .Get());
  NSArray<BrowserAccessibilityCocoa*>* children =
      [ax_table accessibilityChildren];
  EXPECT_EQ(13U, [children count]);
  BrowserAccessibilityCocoa* header = children[12];
  EXPECT_NSEQ([header role], NSAccessibilityGroupRole);

  // Asking for the header directly should return that last child, but it will
  // not contain any children because only column headers are included. See
  // the `TableWithRowHeaders` and `TableWithTwoRowHeaders` tests above.
  EXPECT_EQ(header, [ax_table accessibilityHeader]);
  EXPECT_EQ(0U, [[header accessibilityChildren] count]);
}

// Test that the column header cells can be retrieved on a table with column
// headers.
TEST_F(BrowserAccessibilityMacTest, AXHeaderOnColumnsWithColumnHeaders) {
  AXTreeUpdate initial_state;
  const int kNumberOfRows = 3;
  const int kNumberOfColumns = 2;
  MakeTable(&initial_state, kNumberOfRows, kNumberOfColumns,
            TableHeaderOption::ColumnHeaders);
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      initial_state, node_id_delegate_, nullptr);

  // The native table will have six children: the three rows, the two columns,
  // and the header group for the table itself.
  BrowserAccessibilityCocoa* ax_table =
      ObjCCastStrict<BrowserAccessibilityCocoa>(
          manager_->GetBrowserAccessibilityRoot()
              ->GetNativeViewAccessible()
              .Get());
  NSArray<BrowserAccessibilityCocoa*>* children =
      [ax_table accessibilityChildren];
  EXPECT_EQ(6U, [children count]);

  // Asking for the header for a given column should return the first child of
  // that column because the headers are in the first row.
  BrowserAccessibilityCocoa* first_column = children[3];
  EXPECT_NSEQ([first_column role], NSAccessibilityColumnRole);
  EXPECT_EQ([first_column accessibilityHeader],
            [first_column accessibilityChildren][0]);

  BrowserAccessibilityCocoa* second_column = children[4];
  EXPECT_NSEQ([second_column role], NSAccessibilityColumnRole);
  EXPECT_EQ([second_column accessibilityHeader],
            [second_column accessibilityChildren][0]);
}

// Test that the row header cells can be retrieved on a table with row headers.
TEST_F(BrowserAccessibilityMacTest, AXHeaderOnRowsWithRowHeaders) {
  AXTreeUpdate initial_state;
  const int kNumberOfRows = 3;
  const int kNumberOfColumns = 7;
  MakeTable(&initial_state, kNumberOfRows, kNumberOfColumns,
            TableHeaderOption::RowHeaders);
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      initial_state, node_id_delegate_, nullptr);

  // The native table will have 11 children: the three rows, the seven columns,
  // and the header group for the table itself.
  BrowserAccessibilityCocoa* ax_table =
      ObjCCastStrict<BrowserAccessibilityCocoa>(
          manager_->GetBrowserAccessibilityRoot()
              ->GetNativeViewAccessible()
              .Get());
  NSArray<BrowserAccessibilityCocoa*>* children =
      [ax_table accessibilityChildren];
  EXPECT_EQ(11U, [children count]);

  // Asking for the header for a given row should return the first child of
  // that row because the headers are in the first column. This fails outside
  // of blink due to the failure to set `kTableRowHeaderId` on the row node
  // in `ui::AXTableInfo`. See crbug.com/380211806 for details.
  BrowserAccessibilityCocoa* first_row = children[0];
  EXPECT_NSEQ([first_row role], NSAccessibilityRowRole);
  EXPECT_NE([first_row accessibilityHeader],
            [first_row accessibilityChildren][0])
      << "These should be equal. See crbug.com/380211806";

  BrowserAccessibilityCocoa* second_row = children[1];
  EXPECT_NSEQ([second_row role], NSAccessibilityRowRole);
  EXPECT_NE([second_row accessibilityHeader],
            [second_row accessibilityChildren][0])
      << "These should be equal. See crbug.com/380211806";

  BrowserAccessibilityCocoa* third_row = children[2];
  EXPECT_NSEQ([third_row role], NSAccessibilityRowRole);
  EXPECT_NE([third_row accessibilityHeader],
            [third_row accessibilityChildren][0])
      << "These should be equal. See crbug.com/380211806";
}

// `accessibilityNumberOfCharacters` on a text field.
TEST_F(BrowserAccessibilityMacTest,
       AccessibilityNumberOfCharactersOnTextField) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kTextField;
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  accessibility_ = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  SetRootValue("hello world");
  EXPECT_EQ([accessibility_ accessibilityNumberOfCharacters], 11);
}

// `accessibilityVisibleCharacterRange` on a text field.
TEST_F(BrowserAccessibilityMacTest,
       AccessibilityVisibleCharacterRangeOnTextField) {
  root_ = AXNodeData();
  root_.id = 1;
  root_.role = ax::mojom::Role::kTextField;
  manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
      MakeAXTreeUpdateForTesting(root_), node_id_delegate_, nullptr);
  accessibility_ = ObjCCastStrict<BrowserAccessibilityCocoa>(
      manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible().Get());
  SetRootValue("hello world");
  NSRange visibleRange = [accessibility_ accessibilityVisibleCharacterRange];
  EXPECT_EQ(visibleRange.location, 0U);
  EXPECT_EQ(visibleRange.length, 11U);
}

// A Views-sourced tree whose WebView node hosts the web content child tree.
class BrowserAccessibilityMacWebViewHostTest : public CocoaTest {
 public:
  void SetUp() override {
    CocoaTest::SetUp();

    AXNodeData child_tree_root;
    child_tree_root.id = 1;
    child_tree_root.role = ax::mojom::Role::kRootWebArea;
    child_tree_root.relative_bounds.bounds = gfx::RectF(100, 0, 100, 100);
    AXTreeUpdate child_tree_update =
        MakeAXTreeUpdateForTesting(child_tree_root);

    AXNodeData views_root;
    views_root.id = 1;
    views_root.role = ax::mojom::Role::kWindow;
    views_root.relative_bounds.bounds = gfx::RectF(0, 0, 200, 100);
    views_root.child_ids = {2, 3};

    AXNodeData toolbar;
    toolbar.id = 2;
    toolbar.role = ax::mojom::Role::kToolbar;
    toolbar.relative_bounds.bounds = gfx::RectF(0, 0, 100, 100);

    AXNodeData web_view;
    web_view.id = 3;
    web_view.role = ax::mojom::Role::kWebView;
    web_view.relative_bounds.bounds = gfx::RectF(100, 0, 100, 100);
    web_view.AddChildTreeId(child_tree_update.tree_data.tree_id);
    web_view.AddState(ax::mojom::State::kIgnored);

    AXTreeUpdate views_update =
        MakeAXTreeUpdateForTesting(views_root, toolbar, web_view);

    child_tree_update.tree_data.parent_tree_id = views_update.tree_data.tree_id;
    child_tree_update_ = child_tree_update;

    views_delegate_.is_web_content_source_ = false;
    views_manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
        views_update, node_id_delegate_, &views_delegate_);
    web_delegate_.is_root_frame_ = false;
    ConnectTheBridge();
  }

  void TearDown() override {
    web_manager_.reset();
    views_manager_.reset();
    CocoaTest::TearDown();
  }

 protected:
  BrowserAccessibility* ViewsRoot() const {
    return views_manager_->GetBrowserAccessibilityRoot();
  }
  BrowserAccessibility* WebRoot() const {
    return web_manager_->GetBrowserAccessibilityRoot();
  }
  BrowserAccessibility* Host() const { return views_manager_->GetFromID(3); }

  void SeverTheBridge() {
    AXNodeData severed = Host()->GetData();
    severed.RemoveStringAttribute(ax::mojom::StringAttribute::kChildTreeId);
    severed.RemoveState(ax::mojom::State::kIgnored);
    AXTreeUpdate update;
    update.nodes.push_back(severed);
    ASSERT_TRUE(views_manager_->ax_tree()->Unserialize(update));
  }

  void RestoreTheBridge() {
    AXNodeData restored = Host()->GetData();
    restored.AddChildTreeId(child_tree_update_.tree_data.tree_id);
    restored.AddState(ax::mojom::State::kIgnored);
    AXTreeUpdate update;
    update.nodes.push_back(restored);
    ASSERT_TRUE(views_manager_->ax_tree()->Unserialize(update));
  }

  // Brings up the hosted tree the way the renderer does: the manager is
  // created, then its first event batch establishes the parent connection and
  // tells the host about it.
  void ConnectTheBridge() {
    web_manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
        child_tree_update_, node_id_delegate_, &web_delegate_);
    AXUpdatesAndEvents bundle;
    bundle.updates.resize(1);
    bundle.updates[0].nodes.push_back(WebRoot()->GetData());
    ASSERT_TRUE(web_manager_->OnAccessibilityEvents(bundle));
  }

  BrowserAccessibilityCocoa* CocoaNode(BrowserAccessibility* node) const {
    return base::apple::ObjCCastStrict<BrowserAccessibilityCocoa>(
        node->GetNativeViewAccessible().Get());
  }

  TestAXNodeIdDelegate node_id_delegate_;
  TestAXPlatformTreeManagerDelegate views_delegate_;
  TestAXPlatformTreeManagerDelegate web_delegate_;
  AXTreeUpdate child_tree_update_;
  std::unique_ptr<BrowserAccessibilityManager> views_manager_;
  std::unique_ptr<BrowserAccessibilityManager> web_manager_;

  const base::test::SingleThreadTaskEnvironment task_environment_;
};

TEST_F(BrowserAccessibilityMacWebViewHostTest, HostIsNotInThePlatformTree) {
  EXPECT_EQ(2u, ViewsRoot()->PlatformChildCount());
  EXPECT_EQ(WebRoot(), ViewsRoot()->PlatformGetChild(1));
  EXPECT_EQ(WebRoot(), ViewsRoot()->PlatformGetLastChild());
}

TEST_F(BrowserAccessibilityMacWebViewHostTest, HostedRootFollowsTheToolbar) {
  BrowserAccessibility* toolbar = views_manager_->GetFromID(2);
  EXPECT_EQ(WebRoot(), toolbar->PlatformGetNextSibling());
}

TEST_F(BrowserAccessibilityMacWebViewHostTest,
       IgnoredHostWithNoHostedTreeIsNotExposed) {
  web_manager_.reset();

  EXPECT_EQ(1u, ViewsRoot()->PlatformChildCount());
}

TEST_F(BrowserAccessibilityMacWebViewHostTest, SeveredHostIsExposedAgain) {
  web_manager_.reset();
  SeverTheBridge();

  EXPECT_EQ(Host(), ViewsRoot()->PlatformGetChild(1));
  EXPECT_EQ(ViewsRoot(), Host()->PlatformGetParent());
}

TEST_F(BrowserAccessibilityMacWebViewHostTest,
       HostedRootWalksBackToTheToolbar) {
  BrowserAccessibility* toolbar = views_manager_->GetFromID(2);

  EXPECT_EQ(WebRoot(), toolbar->PlatformGetNextSibling());
  EXPECT_EQ(toolbar, WebRoot()->PlatformGetPreviousSibling());
}

TEST_F(BrowserAccessibilityMacWebViewHostTest,
       HostedRootTakesTheIndexOfItsHost) {
  BrowserAccessibility* toolbar = views_manager_->GetFromID(2);

  EXPECT_EQ(0u, toolbar->GetIndexInParent());
  EXPECT_EQ(1u, WebRoot()->GetIndexInParent());
}

TEST_F(BrowserAccessibilityMacWebViewHostTest,
       TreeOrderCrossesTheHostBothWays) {
  BrowserAccessibility* toolbar = views_manager_->GetFromID(2);

  EXPECT_EQ(WebRoot(), BrowserAccessibilityManager::NextInTreeOrder(toolbar));
  EXPECT_EQ(toolbar, BrowserAccessibilityManager::PreviousInTreeOrder(
                         WebRoot(), /*can_wrap_to_last_element=*/false));
}

TEST_F(BrowserAccessibilityMacWebViewHostTest, CocoaTreeSkipsTheHost) {
  BrowserAccessibilityCocoa* views_root = CocoaNode(ViewsRoot());
  BrowserAccessibilityCocoa* web_root = CocoaNode(WebRoot());

  EXPECT_NSEQ(web_root, [views_root accessibilityChildren].lastObject);
  EXPECT_EQ(2u, [[views_root accessibilityChildren] count]);
}

TEST_F(BrowserAccessibilityMacWebViewHostTest, ConnectedHostHasNoPlatformNode) {
  EXPECT_FALSE(Host()->GetAXPlatformNode());
  EXPECT_FALSE(Host()->GetNativeViewAccessible());
}

TEST_F(BrowserAccessibilityMacWebViewHostTest,
       EveryPlatformChildHasANativeObject) {
  for (size_t i = 0; i < ViewsRoot()->PlatformChildCount(); ++i) {
    EXPECT_TRUE(ViewsRoot()->PlatformGetChild(i)->GetNativeViewAccessible())
        << "platform child " << i;
  }
}

TEST_F(BrowserAccessibilityMacWebViewHostTest, SeveredHostHasAPlatformNode) {
  SeverTheBridge();

  EXPECT_TRUE(Host()->GetAXPlatformNode());
  EXPECT_TRUE(Host()->GetNativeViewAccessible());
}

TEST_F(BrowserAccessibilityMacWebViewHostTest,
       AHostWithoutAHostedTreeStillOwnsNoPlatformNode) {
  web_manager_.reset();

  EXPECT_FALSE(Host()->GetAXPlatformNode());
  EXPECT_FALSE(Host()->GetNativeViewAccessible());
}

TEST_F(BrowserAccessibilityMacWebViewHostTest,
       PlatformNodeFollowsRepeatedBridgeTransitions) {
  for (int i = 0; i < 3; ++i) {
    SeverTheBridge();
    ASSERT_TRUE(Host()->GetAXPlatformNode()) << "iteration " << i;
    ASSERT_EQ(Host(), ViewsRoot()->PlatformGetChild(1)) << "iteration " << i;

    RestoreTheBridge();
    ASSERT_FALSE(Host()->GetAXPlatformNode()) << "iteration " << i;
    ASSERT_EQ(WebRoot(), ViewsRoot()->PlatformGetChild(1)) << "iteration " << i;
  }
}

TEST_F(BrowserAccessibilityMacWebViewHostTest,
       NoPlatformNodeAcrossHostedTreeTransitions) {
  // The host keeps its child tree ID here, thus it stays ignored and owns no
  // platform node. Only the tree that takes its place comes and goes.
  for (int i = 0; i < 3; ++i) {
    web_manager_.reset();
    ASSERT_FALSE(Host()->GetAXPlatformNode()) << "iteration " << i;
    ASSERT_EQ(1u, ViewsRoot()->PlatformChildCount()) << "iteration " << i;

    ConnectTheBridge();
    ASSERT_FALSE(Host()->GetAXPlatformNode()) << "iteration " << i;
    ASSERT_EQ(WebRoot(), ViewsRoot()->PlatformGetChild(1)) << "iteration " << i;
  }
}

namespace {

constexpr char kEmptyGroupChainTree[] = R"HTML(
  ++1 kRootWebArea
  ++++2 kGenericContainer
  ++++++3 kGenericContainer
)HTML";

// Node ids in `kEmptyGroupChainTree`: outer empty wrapper and its leaf
// descendant.
constexpr int32_t kEmptyGroupSubroleOuterId = 2;
constexpr int32_t kEmptyGroupSubroleLeafId = 3;

}  // namespace

// AXEmptyGroup subrole: predicate correctness, invalidation hooks, landmark
// override.
class BrowserAccessibilityMacEmptyGroupSubroleTest
    : public BrowserAccessibilityMacTest {
 protected:
  void SetUp() override {
    CocoaTest::SetUp();
    BuildTree(kEmptyGroupChainTree);
  }

  void BuildTree(const char* tree) {
    manager_ = std::make_unique<BrowserAccessibilityManagerMac>(
        TestAXTreeUpdate(std::string(tree)), node_id_delegate_, nullptr);
  }

  BrowserAccessibilityCocoa* CocoaForId(int32_t id) {
    return ObjCCastStrict<BrowserAccessibilityCocoa>(
        manager_->GetFromID(id)->GetNativeViewAccessible().Get());
  }

  AXNodeData DataForId(int32_t id) {
    return manager_->GetFromID(id)->GetData();
  }

  void ApplyNodeDataUpdate(const AXNodeData& updated_node) {
    AXUpdatesAndEvents bundle;
    bundle.updates.resize(1);
    bundle.updates[0].nodes.push_back(updated_node);
    ASSERT_TRUE(manager_->OnAccessibilityEvents(bundle));
  }

  void ExpectSubrole(int32_t id, NSString* expected) {
    EXPECT_NSEQ(expected, [CocoaForId(id) subrole]);
  }

  void ExpectNotSubrole(int32_t id, NSString* unexpected) {
    EXPECT_NSNE(unexpected, [CocoaForId(id) subrole]);
  }

  void ExpectEmptyGroupSubrole(int32_t id) {
    ExpectSubrole(id, CFToNSPtrCast(kAXEmptyGroupSubrole));
  }

  void ExpectNotEmptyGroupSubrole(int32_t id) {
    ExpectNotSubrole(id, CFToNSPtrCast(kAXEmptyGroupSubrole));
  }

  template <typename MutateFn>
  void ExpectEmptyGroupSubroleInvalidatedOnLeafMutation(MutateFn mutate) {
    ExpectEmptyGroupSubrole(kEmptyGroupSubroleOuterId);
    AXNodeData updated_leaf = DataForId(kEmptyGroupSubroleLeafId);
    mutate(updated_leaf);
    ApplyNodeDataUpdate(updated_leaf);
    ExpectNotEmptyGroupSubrole(kEmptyGroupSubroleOuterId);
  }
};

// Empty wrapper chain: every layout-only group in the path reports
// AXEmptyGroup.
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest, EmptyGroupChain) {
  ExpectEmptyGroupSubrole(kEmptyGroupSubroleOuterId);
  ExpectEmptyGroupSubrole(kEmptyGroupSubroleLeafId);
}

// WebKit parity: a group's OWN name does not exempt it from AXEmptyGroup --
// isEmptyGroup() looks only at children, not the group's own name. A named but
// childless group is therefore AXEmptyGroup on itself (so VoiceOver walks past
// it instead of landing on a dead-end labeled container).
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest, NamedGroupOnSelfIsEmpty) {
  BuildTree(R"HTML(
    ++1 kRootWebArea
    ++++2 kGenericContainer name="Label"
  )HTML");
  ExpectEmptyGroupSubrole(2);
}

// Like the name, the root ignores the node's OWN description (a
// label/annotation on empty content is a dead-end), so a plain group whose only
// signal is a description still flattens. Live-region roles are the exception
// that keeps such a node (see LiveRegionRolesKeepTheirSubrole) -- via the role,
// not the text.
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       OwnDescriptionDoesNotPreventEmptyGroup) {
  BuildTree(R"HTML(
    ++1 kRootWebArea
    ++++2 kGenericContainer stringAttribute=kDescription,Message
  )HTML");
  ExpectEmptyGroupSubrole(2);
}

// Focusable nodes are tab stops; flattening them to AXEmptyGroup would skip
// them during VoiceOver linear navigation while they remain tab stops. A
// focusable node is never AXEmptyGroup, even when childless. (A plain childless
// group is AXEmptyGroup -- see EmptyGroupChain.)
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest, FocusableGroupIsNotEmpty) {
  BuildTree(R"HTML(
    ++1 kRootWebArea
    ++++2 kGenericContainer state=kFocusable
  )HTML");
  ExpectNotEmptyGroupSubrole(2);
}

// Live-region / status roles (alert, status, log) map to
// NSAccessibilityGroupRole but must not be flattened to AXEmptyGroup even when
// childless, or VoiceOver would skip a critical announcement region; they keep
// their native subrole. This is what protects the native form-validation bubble
// (a childless alert whose message lives in its description) -- via the role,
// not the description (cf. OwnDescriptionDoesNotPreventEmptyGroup).
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       LiveRegionRolesKeepTheirSubrole) {
  BuildTree(R"HTML(
    ++1 kRootWebArea
    ++++2 kAlert
    ++++3 kStatus
    ++++4 kLog
  )HTML");
  ExpectSubrole(2, CFToNSPtrCast(kAXApplicationAlertSubrole));
  ExpectSubrole(3, CFToNSPtrCast(kAXApplicationStatusSubrole));
  ExpectSubrole(4, CFToNSPtrCast(kAXApplicationLogSubrole));
}

// Contract #4: empty landmark wrappers report AXEmptyGroup, not the landmark
// subrole.
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       EmptyLandmarkBeatsLandmarkSubrole) {
  BuildTree(R"HTML(
    ++1 kRootWebArea
    ++++2 kBanner
    ++++++3 kGenericContainer
  )HTML");
  ExpectEmptyGroupSubrole(2);
  ExpectNotSubrole(2, CFToNSPtrCast(kAXLandmarkBannerSubrole));
}

// The flip side of WebKit's split between isEmptyGroup() (ignores own name) and
// computeIsIgnored() (keeps named nodes unignored): a named childless group is
// AXEmptyGroup on itself, yet as an unignored descendant it still keeps its
// ancestor non-empty. Removing the descendant name check would instead swallow
// named / text leaves into an AXEmptyGroup ancestor and hide them.
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       NamedDescendantGroupKeepsAncestorNonEmpty) {
  BuildTree(R"HTML(
    ++1 kRootWebArea
    ++++2 kGenericContainer
    ++++++3 kGenericContainer name="Label"
  )HTML");
  ExpectEmptyGroupSubrole(3);
  ExpectNotEmptyGroupSubrole(2);
}

// OnNodeDataChanged -> -invalidateEmptyGroupCacheUpwards (representative string
// attribute).
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       InvalidationOnDescendantNameChange) {
  ExpectEmptyGroupSubroleInvalidatedOnLeafMutation(
      [](AXNodeData& leaf) { leaf.SetName("Hello"); });
}

// Deep descendant change must flip an ancestor even though the intermediate
// group was previously cached as empty (exercises the kEmptyGroupCacheEmpty
// short-circuit in resolveEmptyGroupSubtree after invalidation).
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       InvalidationThroughCachedIntermediateGroup) {
  BuildTree(R"HTML(
    ++1 kRootWebArea
    ++++2 kGenericContainer
    ++++++3 kGenericContainer
    ++++++++4 kGenericContainer
  )HTML");
  // Prime caches: outer (2), intermediate (3) and leaf (4) all empty.
  ExpectEmptyGroupSubrole(2);
  ExpectEmptyGroupSubrole(3);
  ExpectEmptyGroupSubrole(4);

  AXNodeData named_leaf = DataForId(4);
  named_leaf.SetName("Hello");
  ApplyNodeDataUpdate(named_leaf);

  // The intermediate group's cache must have been invalidated too; otherwise
  // the outer group would short-circuit on a stale empty verdict.
  ExpectNotEmptyGroupSubrole(2);
  ExpectNotEmptyGroupSubrole(3);
}

// State diff path (not covered by the string-attribute loop in the manager
// hook).
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       InvalidationOnDescendantFocusableStateChange) {
  ExpectEmptyGroupSubroleInvalidatedOnLeafMutation(
      [](AXNodeData& leaf) { leaf.AddState(ax::mojom::State::kFocusable); });
}

// Embedded-content host short-circuit (iframe / OOPIF / plugin hosts).
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       InvalidationOnDescendantChildTreeIdChange) {
  ExpectEmptyGroupSubroleInvalidatedOnLeafMutation([](AXNodeData& leaf) {
    leaf.AddChildTreeId(ui::AXTreeID::CreateNewAXTreeID());
  });
}

// kDefaultActionVerb diff path (IsClickable without a role change).
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       InvalidationOnDescendantDefaultActionVerbChange) {
  ExpectEmptyGroupSubroleInvalidatedOnLeafMutation([](AXNodeData& leaf) {
    leaf.SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kClick);
  });
}

// Range-value descendants (meter/progress/etc.) carry announceable value via
// kValueForRange, not the string kValue attribute.
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       RangeValueDescendantSuppressesEmptyGroupOnAncestor) {
  BuildTree(R"HTML(
    ++1 kRootWebArea
    ++++2 kGenericContainer
    ++++++3 kGenericContainer
  )HTML");

  AXNodeData meter = DataForId(3);
  meter.role = ax::mojom::Role::kMeter;
  meter.AddFloatAttribute(ax::mojom::FloatAttribute::kValueForRange, 2.0f);
  ApplyNodeDataUpdate(meter);

  ExpectNotEmptyGroupSubrole(2);
}

// A leaf turning into (and back from) a range role flips the ancestor verdict.
// The invalidation is driven by the role change; the kValueForRange value is
// not an independently-watched axis (see HasNonEmptyGroupSemantics and the
// invalidation diff in BrowserAccessibilityManagerMac::OnNodeDataChanged).
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       InvalidationOnDescendantBecomingRangeRole) {
  ExpectEmptyGroupSubrole(kEmptyGroupSubroleOuterId);

  AXNodeData range_leaf = DataForId(kEmptyGroupSubroleLeafId);
  range_leaf.role = ax::mojom::Role::kProgressIndicator;
  range_leaf.AddFloatAttribute(ax::mojom::FloatAttribute::kValueForRange,
                               22.0f);
  ApplyNodeDataUpdate(range_leaf);
  ExpectNotEmptyGroupSubrole(kEmptyGroupSubroleOuterId);

  AXNodeData cleared = DataForId(kEmptyGroupSubroleLeafId);
  cleared.role = ax::mojom::Role::kGenericContainer;
  cleared.RemoveFloatAttribute(ax::mojom::FloatAttribute::kValueForRange);
  ApplyNodeDataUpdate(cleared);
  ExpectEmptyGroupSubrole(kEmptyGroupSubroleOuterId);
}

// Invalidation must allow the verdict to flip back to empty when semantics are
// removed.
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       InvalidationRestoresEmptyGroupWhenSemanticRemoved) {
  ExpectEmptyGroupSubrole(kEmptyGroupSubroleOuterId);

  AXNodeData named_leaf = DataForId(kEmptyGroupSubroleLeafId);
  named_leaf.SetName("Hello");
  ApplyNodeDataUpdate(named_leaf);
  ExpectNotEmptyGroupSubrole(kEmptyGroupSubroleOuterId);

  AXNodeData cleared_leaf = DataForId(kEmptyGroupSubroleLeafId);
  cleared_leaf.SetName("");
  ApplyNodeDataUpdate(cleared_leaf);
  ExpectEmptyGroupSubrole(kEmptyGroupSubroleOuterId);
}

// Structural changes: -childrenChanged invalidates cached empty verdicts.
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       StructuralChildAdditionInvalidates) {
  ExpectEmptyGroupSubrole(kEmptyGroupSubroleOuterId);

  AXNodeData updated_mid = DataForId(kEmptyGroupSubroleOuterId);
  updated_mid.child_ids = {kEmptyGroupSubroleLeafId, 4};

  AXNodeData new_child;
  new_child.id = 4;
  new_child.role = ax::mojom::Role::kGenericContainer;
  new_child.SetName("Hello");

  AXUpdatesAndEvents bundle;
  bundle.updates.resize(1);
  bundle.updates[0].nodes.push_back(updated_mid);
  bundle.updates[0].nodes.push_back(new_child);
  ASSERT_TRUE(manager_->OnAccessibilityEvents(bundle));

  // Harness has no delegate, so CHILDREN_CHANGED is not dispatched; call
  // directly.
  [CocoaForId(kEmptyGroupSubroleOuterId) childrenChanged];

  ExpectNotEmptyGroupSubrole(kEmptyGroupSubroleOuterId);
}

// A group whose only children are indirect (kIndirectChildIds) and carry
// content must not be AXEmptyGroup: the predicate has to walk
// -accessibilityChildren (which includes indirect children), not platform
// children alone. Mac table header containers are the real-world instance of
// this shape; the synthesis path itself is covered by
// BrowserAccessibilityMacTest.TableAPIs.
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       GroupWithIndirectContentChildIsNotEmptyGroup) {
  BuildTree(R"HTML(
    ++1 kRootWebArea
    ++++2 kGenericContainer intListAttribute=kIndirectChildIds,4
    ++++4 kStaticText name="Header"
  )HTML");
  // Node 2 has zero platform children but one content-bearing indirect child.
  ExpectNotEmptyGroupSubrole(2);
}

// Math roles keep their native subrole (e.g. AXDocumentMath), not AXEmptyGroup.
TEST_F(BrowserAccessibilityMacEmptyGroupSubroleTest,
       MathRolePreservesNativeSubrole) {
  BuildTree(R"HTML(
    ++1 kRootWebArea
    ++++2 kMathMLMath
  )HTML");
  ExpectSubrole(2, CFToNSPtrCast(kAXDocumentMathSubrole));
  ExpectNotEmptyGroupSubrole(2);
}

}  // namespace ui
