chromium/content/browser/accessibility/browser_accessibility_cocoa_browsertest.mm

// Copyright 2018 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_cocoa.h"

#include "base/check.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/test/accessibility_notification_waiter.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/context_menu_interceptor.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/base/data_url.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#include "ui/accessibility/platform/ax_private_webkit_constants_mac.h"
#include "ui/accessibility/platform/ax_utils_mac.h"
#include "ui/accessibility/platform/browser_accessibility.h"
#include "ui/accessibility/platform/browser_accessibility_mac.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 "url/gurl.h"

namespace {

#include <string>

// Returns HTML that creates a 60-row table.
std::string GetLargeTableHTML() {
  return R"delim(data:text/html,
    <html>
      <head>
        <title>Large Table</title>
        <script type="text/javascript">

          function CreateTable() {
          const numColumns = 5;
          const numRows = 60;

          const table = document.createElement('table');
          for (let i = 0; i < numRows; i++) {
            const row = document.createElement('tr');
            for (let j = 0; j < numColumns; j++) {
              const cell = document.createElement('td');
              cell.textContent = `row ${i+1}, column ${j+1}`;
              row.appendChild(cell);
            }
            table.appendChild(row);
          }

          document.body.appendChild(table);
        }

        </script>
      </head>
      <body onLoad="CreateTable()">

      </body>
    </html>)delim";
}

// Returns the accessibility frame assigned to `element`.
NSRect GetAXFrame(AXUIElementRef element) {
  AXValueRef value;

  CGPoint position = CGPointZero;
  if (AXUIElementCopyAttributeValue(element, kAXPositionAttribute,
                                    (CFTypeRef*)&value) == kAXErrorSuccess) {
    if (AXValueGetType(value) == kAXValueCGPointType) {
      AXValueGetValue(value, (AXValueType)kAXValueCGPointType, &position);
      CFRelease(value);
    }
  }

  CGSize size = CGSizeZero;
  if (AXUIElementCopyAttributeValue(element, kAXSizeAttribute,
                                    (CFTypeRef*)&value) == kAXErrorSuccess) {
    if (AXValueGetType(value) == kAXValueCGSizeType) {
      AXValueGetValue(value, (AXValueType)kAXValueCGSizeType, &size);
      CFRelease(value);
    }
  }

  return NSMakeRect(position.x, position.y, size.width, size.height);
}

// Returns the string associated with `element`.
NSString* GetAXUIElementStringValue(AXUIElementRef element) {
  CFTypeRef value;
  NSString* stringValue = nil;

  if (AXUIElementCopyAttributeValue(element, kAXValueAttribute, &value) ==
      kAXErrorSuccess) {
    if (CFGetTypeID(value) == CFStringGetTypeID()) {
      stringValue = [NSString stringWithString:(__bridge NSString*)value];
    }
    CFRelease(value);
  }

  return stringValue;
}

// Returns a table element within the descendants of `element`.
AXUIElementRef FindTable(AXUIElementRef element) {
  AXUIElementRef table = NULL;
  CFArrayRef children = NULL;
  CFStringRef role = NULL;

  AXUIElementCopyAttributeValue(element, kAXRoleAttribute, (CFTypeRef*)&role);
  if (role) {
    if (CFStringCompare(role, CFSTR("AXTable"), 0) == kCFCompareEqualTo) {
      table = element;
      CFRetain(table);
    }
    CFRelease(role);
  }

  if (table) {
    return table;
  }

  AXUIElementCopyAttributeValue(element, kAXChildrenAttribute,
                                (CFTypeRef*)&children);
  if (children) {
    CFIndex count = CFArrayGetCount(children);
    for (CFIndex i = 0; i < count && !table; i++) {
      AXUIElementRef child =
          (AXUIElementRef)CFArrayGetValueAtIndex(children, i);

      table = FindTable(child);
    }
    CFRelease(children);
  }

  return table;
}
}  // namespace

// A table cell as seen by an assitive technology when navigating a web page.
@interface ATTestTableCell : NSObject
@property(nonatomic) NSRect frame;
@property(nonatomic, strong) NSString* value;
- (instancetype)initFromElement:(AXUIElementRef)cellElement;
@end

@implementation ATTestTableCell
@synthesize frame = _frame;
@synthesize value = _value;

- (instancetype)initFromElement:(AXUIElementRef)cellElement {
  self = [super init];
  if (self) {
    _frame = GetAXFrame(cellElement);
    _value = GetAXUIElementStringValue(cellElement);
  }
  return self;
}
@end

// A table row as seen by an assitive technology when navigating a web page.
@interface ATTestTableRow : NSObject
@property(nonatomic) NSRect frame;
@property(nonatomic, strong) NSMutableArray<ATTestTableCell*>* cells;
- (instancetype)initFromElement:(AXUIElementRef)rowElement;
@end

@implementation ATTestTableRow
@synthesize frame = _frame;
@synthesize cells = _cells;

- (instancetype)initFromElement:(AXUIElementRef)rowElement {
  self = [super init];
  if (self) {
    _frame = GetAXFrame(rowElement);
    _cells = [NSMutableArray array];
    [self populateCellsFromRowElement:rowElement];
  }
  return self;
}

- (void)populateCellsFromRowElement:(AXUIElementRef)rowElement {
  CFArrayRef cellsArray = NULL;

  if (AXUIElementCopyAttributeValue(rowElement, kAXChildrenAttribute,
                                    (CFTypeRef*)&cellsArray) ==
          kAXErrorSuccess &&
      cellsArray) {
    CFIndex cellCount = CFArrayGetCount(cellsArray);

    for (CFIndex i = 0; i < cellCount; i++) {
      AXUIElementRef cellElement =
          (AXUIElementRef)CFArrayGetValueAtIndex(cellsArray, i);
      ATTestTableCell* cell =
          [[ATTestTableCell alloc] initFromElement:cellElement];
      [_cells addObject:cell];
    }

    CFRelease(cellsArray);
  }
}

@end

// A table as seen by an assitive technology when navigating a web page.
@interface AXTestTable : NSObject
@property(nonatomic, strong) NSMutableArray<ATTestTableRow*>* rows;
- (instancetype)initWithAXUIElement:(AXUIElementRef)tableElement;
@end

@implementation AXTestTable
@synthesize rows = _rows;

- (instancetype)initWithAXUIElement:(AXUIElementRef)tableElement {
  self = [super init];
  if (self) {
    _rows = [NSMutableArray array];
    [self populateTableFromElement:tableElement];
  }
  return self;
}

- (void)populateTableFromElement:(AXUIElementRef)tableElement {
  CFArrayRef rowsArray = NULL;

  if (AXUIElementCopyAttributeValue(tableElement, kAXRowsAttribute,
                                    (CFTypeRef*)&rowsArray) ==
          kAXErrorSuccess &&
      rowsArray) {
    CFIndex rowCount = CFArrayGetCount(rowsArray);

    for (CFIndex i = 0; i < rowCount; i++) {
      AXUIElementRef rowElement =
          (AXUIElementRef)CFArrayGetValueAtIndex(rowsArray, i);
      ATTestTableRow* row = [[ATTestTableRow alloc] initFromElement:rowElement];
      [_rows addObject:row];
    }

    CFRelease(rowsArray);
  }
}

@end

namespace content {

class BrowserAccessibilityCocoaBrowserTest : public ContentBrowserTest {
 public:
  BrowserAccessibilityCocoaBrowserTest() {}
  ~BrowserAccessibilityCocoaBrowserTest() override {}

 protected:
  BrowserAccessibility* FindNode(ax::mojom::Role role) {
    BrowserAccessibility* root = GetManager()->GetBrowserAccessibilityRoot();
    CHECK(root);
    return FindNodeInSubtree(*root, role);
  }

  BrowserAccessibilityManager* GetManager() {
    WebContentsImpl* web_contents =
        static_cast<WebContentsImpl*>(shell()->web_contents());
    return web_contents->GetRootBrowserAccessibilityManager();
  }

  // Trigger a context menu for the provided element without showing it. Returns
  // the coordinates where the context menu was invoked (calculated based on
  // the provided element). These coordinates are relative to the RenderView
  // origin.
  gfx::Point TriggerContextMenuAndGetMenuLocation(
      NSAccessibilityElement* element,
      ContextMenuInterceptor* interceptor) {
    // accessibilityPerformAction is deprecated, but it's still used internally
    // by AppKit.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    [element accessibilityPerformAction:NSAccessibilityShowMenuAction];
    interceptor->Wait();

    blink::UntrustworthyContextMenuParams context_menu_params =
        interceptor->get_params();
    return gfx::Point(context_menu_params.x, context_menu_params.y);
#pragma clang diagnostic pop
  }

  void FocusAccessibilityElementAndWaitForFocusChange(
      NSAccessibilityElement* element) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    [element accessibilitySetValue:@(1)
                      forAttribute:NSAccessibilityFocusedAttribute];
#pragma clang diagnostic pop
    WaitForAccessibilityFocusChange();
  }

  NSDictionary* GetUserInfoForSelectedTextChangedNotification() {
    auto* manager = static_cast<BrowserAccessibilityManagerMac*>(GetManager());
    return manager->GetUserInfoForSelectedTextChangedNotification();
  }

  AXTextEdit GetTextEditForNodeId(int32_t id) {
    auto* manager = static_cast<BrowserAccessibilityManagerMac*>(GetManager());
    return manager->text_edits_[id];
  }

  ui::TestAXNodeIdDelegate node_id_delegate_;

 private:
  BrowserAccessibility* FindNodeInSubtree(BrowserAccessibility& node,
                                          ax::mojom::Role role) {
    if (node.GetRole() == role)
      return &node;
    for (BrowserAccessibility::PlatformChildIterator it =
             node.PlatformChildrenBegin();
         it != node.PlatformChildrenEnd(); ++it) {
      BrowserAccessibility* result = FindNodeInSubtree(*it, role);
      if (result)
        return result;
    }
    return nullptr;
  }
};

IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                       ATCanScrollLargeTable) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));

  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
                                         ui::kAXModeComplete,
                                         ax::mojom::Event::kLoadComplete);

  // Load a large table.
  GURL url(GetLargeTableHTML());

  EXPECT_TRUE(NavigateToURL(shell(), url));
  ASSERT_TRUE(waiter.WaitForNotification());

  pid_t pid = getpid();
  AXUIElementRef axApplication = AXUIElementCreateApplication(pid);
  AXUIElementRef table = FindTable(axApplication);

  ASSERT_TRUE(table);

  AXTestTable* axTable = [[AXTestTable alloc] initWithAXUIElement:table];
  NSArray* rows = [axTable rows];

  EXPECT_TRUE(rows.count == 60);

  // A very tall table will have some number of cells hidden (clipped by its
  // container or the window). For VoiceOver and other ATs to work properly,
  // all rows and cells must have non-zero sizes, even if clipped.
  int rowNumber = 0;
  for (ATTestTableRow* row in rows) {
    EXPECT_FALSE(NSIsEmptyRect(row.frame))
        << "zero extent for row " << rowNumber;
    EXPECT_TRUE(row.cells.count);
    EXPECT_FALSE(NSIsEmptyRect(row.cells[0].frame))
        << "zero extent for row " << rowNumber << ", cell "
        << row.cells[0].value;

    rowNumber++;
  }

  CFRelease(table);
  CFRelease(axApplication);
}

IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                       AXTextMarkerForTextEdit) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));

  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
                                         ui::kAXModeComplete,
                                         ax::mojom::Event::kLoadComplete);
  GURL url(R"HTML(data:text/html,
             <input />)HTML");

  EXPECT_TRUE(NavigateToURL(shell(), url));
  ASSERT_TRUE(waiter.WaitForNotification());

  BrowserAccessibility* text_field = FindNode(ax::mojom::Role::kTextField);
  ASSERT_NE(nullptr, text_field);
  EXPECT_TRUE(ExecJs(shell()->web_contents(),
                     "document.querySelector('input').focus()"));

  SimulateKeyPress(shell()->web_contents(), ui::DomKey::FromCharacter('B'),
                   ui::DomCode::US_B, ui::VKEY_B, false, false, false, false);

  BrowserAccessibilityCocoa* cocoa_text_field =
      text_field->GetNativeViewAccessible();
  AccessibilityNotificationWaiter value_waiter(shell()->web_contents(),
                                               ui::kAXModeComplete,
                                               ax::mojom::Event::kValueChanged);
  ASSERT_TRUE(value_waiter.WaitForNotification());
  AXTextEdit text_edit = [cocoa_text_field computeTextEdit];
  EXPECT_NE(text_edit.edit_text_marker, nil);

  auto ax_position = ui::AXTextMarkerToAXPosition(text_edit.edit_text_marker);
  std::string expected_string = "TextPosition anchor_id=";
  expected_string += base::NumberToString(ax_position->anchor_id());
  expected_string += " text_offset=1 affinity=downstream annotated_text=B<>";
  EXPECT_EQ(ax_position->ToString(), expected_string);
}

IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                       AXTextMarkerForTextEditContentEditable) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));

  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
                                         ui::kAXModeComplete,
                                         ax::mojom::Event::kLoadComplete);
  GURL url(R"HTML(data:text/html,
                  <div id="editable" contenteditable="true" dir="auto">
                    <p>One</p>
                    <p>Two</p>
                    <p><br></p>
                    <p>Three</p>
                    <p>Four</p>
                    <p>Five</p>
                  </div>)HTML");

  EXPECT_TRUE(NavigateToURL(shell(), url));
  ASSERT_TRUE(waiter.WaitForNotification());

  BrowserAccessibility* content_editable =
      GetManager()->GetBrowserAccessibilityRoot()->PlatformGetChild(0);
  ASSERT_NE(nullptr, content_editable);
  EXPECT_TRUE(ExecJs(shell()->web_contents(),
                     "document.querySelector('#editable').focus()"));
  {
    SimulateKeyPress(shell()->web_contents(), ui::DomKey::FromCharacter('B'),
                     ui::DomCode::US_B, ui::VKEY_B, false, false, false, false);

    AccessibilityNotificationWaiter value_waiter(
        shell()->web_contents(), ui::kAXModeComplete,
        ax::mojom::Event::kValueChanged);
    ASSERT_TRUE(value_waiter.WaitForNotification());

    AXTextEdit text_edit = GetTextEditForNodeId(content_editable->GetId());
    EXPECT_NE(text_edit.edit_text_marker, nil);
    EXPECT_EQ(u"", text_edit.deleted_text);
    EXPECT_EQ(u"B", text_edit.inserted_text);

    auto ax_position = ui::AXTextMarkerToAXPosition(text_edit.edit_text_marker);
    std::string expected_string = "TextPosition anchor_id=";
    expected_string += base::NumberToString(ax_position->anchor_id());
    expected_string += " text_offset=0 affinity=downstream annotated_text=";
    expected_string += "<B>OneTwo\nThreeFourFive";

    EXPECT_EQ(ax_position->ToString(), expected_string);
  }

  // Move Cursor down to the fourth paragraph.
  for (int i = 0; i < 4; ++i) {
    SimulateKeyPressWithoutChar(shell()->web_contents(), ui::DomKey::ARROW_DOWN,
                                ui::DomCode::ARROW_DOWN, ui::VKEY_DOWN,
                                /*control=*/false, /*shift=*/false,
                                /*alt=*/false,
                                /*command=*/false);
  }
  {
    SimulateKeyPress(shell()->web_contents(), ui::DomKey::FromCharacter('B'),
                     ui::DomCode::US_B, ui::VKEY_B, false, false, false, false);

    AccessibilityNotificationWaiter value_waiter(
        shell()->web_contents(), ui::kAXModeComplete,
        ax::mojom::Event::kValueChanged);
    ASSERT_TRUE(value_waiter.WaitForNotification());

    AXTextEdit text_edit = GetTextEditForNodeId(content_editable->GetId());
    EXPECT_NE(text_edit.edit_text_marker, nil);
    EXPECT_EQ(u"", text_edit.deleted_text);
    EXPECT_EQ(u"B", text_edit.inserted_text);
    auto ax_position = ui::AXTextMarkerToAXPosition(text_edit.edit_text_marker);
    std::string expected_string = "TextPosition anchor_id=";
    expected_string += base::NumberToString(ax_position->anchor_id());
    expected_string += " text_offset=14 affinity=downstream annotated_text=";
    expected_string += "BOneTwo\nThreeF<B>ourFive";

    EXPECT_EQ(ax_position->ToString(), expected_string);
  }
}

IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                       AXCellForColumnAndRow) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));

  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
                                         ui::kAXModeComplete,
                                         ax::mojom::Event::kLoadComplete);
  GURL url(R"HTML(data:text/html,
             <table>
               <thead style=display:block>
                 <tr>
                   <th>Name</th>
                   <th>LDAP</th>
                 </tr>
               </thead>
               <tbody style=display:block>
                 <tr>
                   <td>John Doe</td>
                   <td>johndoe@</td>
                 </tr>
                 <tr>
                   <td>Jenny Doe</td>
                   <td>jennydoe@</td>
                 </tr>
               </tbody>
             </table>)HTML");

  EXPECT_TRUE(NavigateToURL(shell(), url));
  ASSERT_TRUE(waiter.WaitForNotification());

  BrowserAccessibility* table = FindNode(ax::mojom::Role::kTable);
  ASSERT_NE(nullptr, table);
  BrowserAccessibilityCocoa* cocoa_table = table->GetNativeViewAccessible();

  // Test AXCellForColumnAndRow for four coordinates
  for (unsigned col = 0; col < 2; col++) {
    for (unsigned row = 0; row < 2; row++) {
      BrowserAccessibilityCocoa* cell =
          [cocoa_table accessibilityCellForColumn:col row:row];

      // It should be a cell.
      EXPECT_NSEQ(@"AXCell", cell.accessibilityRole);

      // The column index and row index of the cell should match what we asked
      // for.
      EXPECT_NSEQ(NSMakeRange(col, 1), cell.accessibilityColumnIndexRange);
      EXPECT_NSEQ(NSMakeRange(row, 1), cell.accessibilityRowIndexRange);
    }
  }
}

IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                       TestCoordinatesAreInScreenSpace) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));

  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
                                         ui::kAXModeComplete,
                                         ax::mojom::Event::kLoadComplete);

  GURL url(R"HTML(data:text/html, <p>Hello, world!</p>)HTML");

  EXPECT_TRUE(NavigateToURL(shell(), url));
  ASSERT_TRUE(waiter.WaitForNotification());

  BrowserAccessibility* text = FindNode(ax::mojom::Role::kStaticText);
  ASSERT_NE(nullptr, text);

  BrowserAccessibilityCocoa* cocoa_text = text->GetNativeViewAccessible();
  ASSERT_NE(nil, cocoa_text);

  NSPoint position = cocoa_text.position.pointValue;

  NSSize size = cocoa_text.accessibilityFrame.size;
  NSRect frame = NSMakeRect(position.x, position.y, size.width, size.height);

  NSPoint p0_before = position;
  NSRect r0_before = [cocoa_text frameForRange:NSMakeRange(0, 5)];
  EXPECT_TRUE(CGRectContainsRect(frame, r0_before));

  // On macOS geometry accessibility attributes are expected to use the
  // screen coordinate system with the origin at the bottom left corner.
  // We need some creativity with testing this because it is challenging
  // to setup a text element with a precise screen position.
  //
  // Content shell's window is pinned to have an origin at (0, 0), so
  // when its height is changed the content's screen y-coordinate is
  // changed by the same amount (see below).
  //
  //      Y^      original
  //       |
  //       +--------------------------------------------+
  //       |                                            |
  //       |                                            |
  //       |                                            |
  //       |                                            |
  //     h +---------------------------+                |
  //       |      Content Shell        |                |
  //       |---------------------------|                |
  //     y |Hello, world               |                |
  //       |                           |                |
  //       |                           |          Screen|
  //       +---------------------------+----------------+-->
  //      0                                               X
  //
  //      Y^       content shell enlarged
  //       |
  //       +--------------------------------------------+
  //       |                                            |
  //       |                                            |
  //  h+dh +---------------------------+                |
  //       |      Content Shell        |                |
  //       |---------------------------|                |
  //  y+dh |Hello, world               |                |
  //       |                           |                |
  //       |                           |                |
  //       |                           |                |
  //       |                           |          Screen|
  //       +---------------------------+----------------+-->
  //      0                                               X
  //
  // This observation allows us to validate the returned
  // attribute values and catch the most glaring mistakes
  // in coordinate space handling.

  const int dh = 100;
  gfx::Size content_size = Shell::GetShellDefaultSize();
  content_size.Enlarge(0, dh);
  shell()->ResizeWebContentForTests(content_size);

  NSPoint p0_after = cocoa_text.position.pointValue;
  NSRect r0_after = [cocoa_text frameForRange:NSMakeRange(0, 5)];

  ASSERT_EQ(p0_before.y + dh, p0_after.y);
  ASSERT_EQ(r0_before.origin.y + dh, r0_after.origin.y);
  ASSERT_EQ(r0_before.size.height, r0_after.size.height);
}

IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                       TestAnnotatedImageDescription) {
  std::vector<const char*> expected_descriptions;

  ui::AXTreeUpdate tree;
  tree.root_id = 1;
  tree.nodes.resize(11);
  tree.nodes[0].id = 1;
  tree.nodes[0].child_ids = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

  // If the status is EligibleForAnnotation and there's no existing label,
  // the description should be the discoverability string.
  tree.nodes[1].id = 2;
  tree.nodes[1].role = ax::mojom::Role::kImage;
  tree.nodes[1].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
                                   "Annotation");
  tree.nodes[1].SetImageAnnotationStatus(
      ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation);
  expected_descriptions.push_back(
      "To get missing image descriptions, open the context menu.");

  // If the status is EligibleForAnnotation, the discoverability string
  // should be appended to the existing name.
  tree.nodes[2].id = 3;
  tree.nodes[2].role = ax::mojom::Role::kImage;
  tree.nodes[2].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
                                   "Annotation");
  tree.nodes[2].SetName("ExistingLabel");
  tree.nodes[2].SetImageAnnotationStatus(
      ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation);
  expected_descriptions.push_back(
      "ExistingLabel. To get missing image descriptions, open the context "
      "menu.");

  // If the status is SilentlyEligibleForAnnotation, the discoverability string
  // should not be appended to the existing name.
  tree.nodes[3].id = 4;
  tree.nodes[3].role = ax::mojom::Role::kImage;
  tree.nodes[3].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
                                   "Annotation");
  tree.nodes[3].SetName("ExistingLabel");
  tree.nodes[3].SetImageAnnotationStatus(
      ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation);
  expected_descriptions.push_back("ExistingLabel");

  // If the status is IneligibleForAnnotation, nothing should be appended.
  tree.nodes[4].id = 5;
  tree.nodes[4].role = ax::mojom::Role::kImage;
  tree.nodes[4].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
                                   "Annotation");
  tree.nodes[4].SetName("ExistingLabel");
  tree.nodes[4].SetImageAnnotationStatus(
      ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation);
  expected_descriptions.push_back("ExistingLabel");

  // If the status is AnnotationPending, pending text should be appended
  // to the name.
  tree.nodes[5].id = 6;
  tree.nodes[5].role = ax::mojom::Role::kImage;
  tree.nodes[5].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
                                   "Annotation");
  tree.nodes[5].SetName("ExistingLabel");
  tree.nodes[5].SetImageAnnotationStatus(
      ax::mojom::ImageAnnotationStatus::kAnnotationPending);
  expected_descriptions.push_back("ExistingLabel. Getting description…");

  // If the status is AnnotationSucceeded, and there's no annotation,
  // nothing should be appended. (Ideally this shouldn't happen.)
  tree.nodes[6].id = 7;
  tree.nodes[6].role = ax::mojom::Role::kImage;
  tree.nodes[6].SetName("ExistingLabel");
  tree.nodes[6].SetImageAnnotationStatus(
      ax::mojom::ImageAnnotationStatus::kAnnotationSucceeded);
  expected_descriptions.push_back("ExistingLabel");

  // If the status is AnnotationSucceeded, the annotation should be appended
  // to the existing label.
  tree.nodes[7].id = 8;
  tree.nodes[7].role = ax::mojom::Role::kImage;
  tree.nodes[7].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
                                   "Annotation");
  tree.nodes[7].SetName("ExistingLabel");
  tree.nodes[7].SetImageAnnotationStatus(
      ax::mojom::ImageAnnotationStatus::kAnnotationSucceeded);
  expected_descriptions.push_back("ExistingLabel. Annotation");

  // If the status is AnnotationEmpty, failure text should be added to the
  // name.
  tree.nodes[8].id = 9;
  tree.nodes[8].role = ax::mojom::Role::kImage;
  tree.nodes[8].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
                                   "Annotation");
  tree.nodes[8].SetName("ExistingLabel");
  tree.nodes[8].SetImageAnnotationStatus(
      ax::mojom::ImageAnnotationStatus::kAnnotationEmpty);
  expected_descriptions.push_back("ExistingLabel. No description available.");

  // If the status is AnnotationAdult, appropriate text should be appended
  // to the name.
  tree.nodes[9].id = 10;
  tree.nodes[9].role = ax::mojom::Role::kImage;
  tree.nodes[9].AddStringAttribute(ax::mojom::StringAttribute::kImageAnnotation,
                                   "Annotation");
  tree.nodes[9].SetName("ExistingLabel");
  tree.nodes[9].SetImageAnnotationStatus(
      ax::mojom::ImageAnnotationStatus::kAnnotationAdult);
  expected_descriptions.push_back("ExistingLabel. Appears to contain adult "
                                  "content. No description available.");

  // If the status is AnnotationProcessFailed, failure text should be added
  // to the name.
  tree.nodes[10].id = 11;
  tree.nodes[10].role = ax::mojom::Role::kImage;
  tree.nodes[10].AddStringAttribute(
      ax::mojom::StringAttribute::kImageAnnotation, "Annotation");
  tree.nodes[10].SetName("ExistingLabel");
  tree.nodes[10].SetImageAnnotationStatus(
      ax::mojom::ImageAnnotationStatus::kAnnotationProcessFailed);
  expected_descriptions.push_back("ExistingLabel. No description available.");

  // We should have one expected description per child of the root.
  ASSERT_EQ(expected_descriptions.size(), tree.nodes[0].child_ids.size());
  int child_count = static_cast<int>(expected_descriptions.size());

  std::unique_ptr<BrowserAccessibilityManagerMac> manager(
      new BrowserAccessibilityManagerMac(tree, node_id_delegate_, nullptr));

  for (int child_index = 0; child_index < child_count; child_index++) {
    BrowserAccessibility* child =
        manager->GetBrowserAccessibilityRoot()->PlatformGetChild(child_index);
    BrowserAccessibilityCocoa* child_obj = child->GetNativeViewAccessible();

    EXPECT_NSEQ(base::SysUTF8ToNSString(expected_descriptions[child_index]),
                child_obj.accessibilityLabel);
  }
}

IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                       TestTableGetRowNodesNestedRows) {
  // rootWebArea(#1)
  // ++grid(#2)
  // ++++row(#3)
  // ++++++columnHeader(#4)
  // ++++++columnHeader(#5)
  // ++++genericContainer(#6)
  // ++++++row(#7)
  // ++++++++cell(#8)
  // ++++++++cell(#9)
  // ++++++row(#10)
  // ++++++++cell(#11)
  // ++++++++cell(#12)

  ui::AXTreeUpdate tree;
  tree.root_id = 1;
  tree.nodes.resize(12);
  tree.nodes[0].id = 1;
  tree.nodes[0].role = ax::mojom::Role::kRootWebArea;
  tree.nodes[0].child_ids = {2};

  tree.nodes[1].id = 2;
  tree.nodes[1].role = ax::mojom::Role::kGrid;
  tree.nodes[1].child_ids = {3, 6};

  tree.nodes[2].id = 3;
  tree.nodes[2].role = ax::mojom::Role::kRow;
  tree.nodes[2].AddStringAttribute(ax::mojom::StringAttribute::kName, "row1");
  tree.nodes[2].child_ids = {4, 5};

  tree.nodes[3].id = 4;
  tree.nodes[3].role = ax::mojom::Role::kColumnHeader;
  tree.nodes[3].AddStringAttribute(ax::mojom::StringAttribute::kName,
                                   "header1");

  tree.nodes[4].id = 5;
  tree.nodes[4].role = ax::mojom::Role::kColumnHeader;
  tree.nodes[4].AddStringAttribute(ax::mojom::StringAttribute::kName,
                                   "header2");

  tree.nodes[5].id = 6;
  tree.nodes[5].role = ax::mojom::Role::kGenericContainer;
  tree.nodes[5].child_ids = {7, 10};

  tree.nodes[6].id = 7;
  tree.nodes[6].role = ax::mojom::Role::kRow;
  tree.nodes[6].AddStringAttribute(ax::mojom::StringAttribute::kName, "row2");
  tree.nodes[6].child_ids = {8, 9};

  tree.nodes[7].id = 8;
  tree.nodes[7].role = ax::mojom::Role::kCell;
  tree.nodes[7].AddStringAttribute(ax::mojom::StringAttribute::kName,
                                   "cell1_row2");

  tree.nodes[8].id = 9;
  tree.nodes[8].role = ax::mojom::Role::kCell;
  tree.nodes[8].AddStringAttribute(ax::mojom::StringAttribute::kName,
                                   "cell2_row2");

  tree.nodes[9].id = 10;
  tree.nodes[9].role = ax::mojom::Role::kRow;
  tree.nodes[9].AddStringAttribute(ax::mojom::StringAttribute::kName, "row3");
  tree.nodes[9].child_ids = {11, 12};

  tree.nodes[10].id = 11;
  tree.nodes[10].role = ax::mojom::Role::kCell;
  tree.nodes[10].AddStringAttribute(ax::mojom::StringAttribute::kName,
                                    "cell1_row3");

  tree.nodes[11].id = 12;
  tree.nodes[11].role = ax::mojom::Role::kCell;
  tree.nodes[11].AddStringAttribute(ax::mojom::StringAttribute::kName,
                                    "cell2_row3");

  std::unique_ptr<BrowserAccessibilityManagerMac> manager(
      new BrowserAccessibilityManagerMac(tree, node_id_delegate_, nullptr));

  BrowserAccessibility* table =
      manager->GetBrowserAccessibilityRoot()->PlatformGetChild(0);
  BrowserAccessibilityCocoa* table_obj = table->GetNativeViewAccessible();
  NSArray* row_nodes = table_obj.accessibilityRows;

  EXPECT_EQ(3U, row_nodes.count);
  EXPECT_NSEQ(@"AXRow", [row_nodes[0] role]);
  EXPECT_NSEQ(@"row1", [row_nodes[0] accessibilityLabel]);

  EXPECT_NSEQ(@"AXRow", [row_nodes[1] role]);
  EXPECT_NSEQ(@"row2", [row_nodes[1] accessibilityLabel]);

  EXPECT_NSEQ(@"AXRow", [row_nodes[2] role]);
  EXPECT_NSEQ(@"row3", [row_nodes[2] accessibilityLabel]);
}

IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                       TestTableGetRowNodesIndirectChildIds) {
  // rootWebArea(#1)
  // ++column(#2), indirectChildIds={3, 4}
  // ++row(#3)
  // ++row(#4)

  ui::AXTreeUpdate tree;
  tree.root_id = 1;
  tree.nodes.resize(4);

  tree.nodes[0].id = 1;
  tree.nodes[0].role = ax::mojom::Role::kRootWebArea;
  tree.nodes[0].child_ids = {2, 3, 4};

  tree.nodes[1].id = 2;
  tree.nodes[1].role = ax::mojom::Role::kColumn;
  tree.nodes[1].AddStringAttribute(ax::mojom::StringAttribute::kName,
                                   "column1");
  tree.nodes[1].AddIntListAttribute(
      ax::mojom::IntListAttribute::kIndirectChildIds,
      std::vector<int32_t>{3, 4});

  tree.nodes[2].id = 3;
  tree.nodes[2].role = ax::mojom::Role::kRow;
  tree.nodes[2].AddStringAttribute(ax::mojom::StringAttribute::kName, "row1");

  tree.nodes[3].id = 4;
  tree.nodes[3].role = ax::mojom::Role::kRow;
  tree.nodes[3].AddStringAttribute(ax::mojom::StringAttribute::kName, "row2");

  std::unique_ptr<BrowserAccessibilityManagerMac> manager(
      new BrowserAccessibilityManagerMac(tree, node_id_delegate_, nullptr));

  BrowserAccessibility* column =
      manager->GetBrowserAccessibilityRoot()->PlatformGetChild(0);
  BrowserAccessibilityCocoa* col_obj = column->GetNativeViewAccessible();
  EXPECT_NSEQ(@"AXColumn", col_obj.role);
  EXPECT_NSEQ(@"column1", col_obj.accessibilityLabel);

  NSArray* row_nodes = col_obj.accessibilityRows;
  EXPECT_NSEQ(@"AXRow", [row_nodes[0] role]);
  EXPECT_NSEQ(@"row1", [row_nodes[0] accessibilityLabel]);

  EXPECT_NSEQ(@"AXRow", [row_nodes[1] role]);
  EXPECT_NSEQ(@"row2", [row_nodes[1] accessibilityLabel]);
}

IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                       TestAXHeadersShouldOnlyIncludeColHeaders) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
                                         ui::kAXModeComplete,
                                         ax::mojom::Event::kLoadComplete);

  GURL url(
      R"HTML(data:text/html,
      <table aria-label="Population per country">
        <thead style=display:block>
          <tr>
            <th aria-label="Country">Country</th>
            <th aria-label="Population">Population</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td role=rowheader>Canada</td>
            <td>37</td>
          </tr>
          <tr>
            <td role=rowheader>USA</td>
            <td>331</td>
          </tr>
        </tbody>
      </table>
  )HTML");
  EXPECT_TRUE(NavigateToURL(shell(), url));
  ASSERT_TRUE(waiter.WaitForNotification());

  BrowserAccessibility* table = FindNode(ax::mojom::Role::kTable);
  BrowserAccessibilityCocoa* table_obj = table->GetNativeViewAccessible();

  EXPECT_NSEQ(@"AXTable", table_obj.role);
  EXPECT_NSEQ(@"Population per country", table_obj.accessibilityLabel);
  BrowserAccessibilityCocoa* table_header = table_obj.header;

  NSArray* children = table_header.children;
  EXPECT_EQ(2U, children.count);

  EXPECT_NSEQ(@"AXCell", [children[0] role]);
  EXPECT_NSEQ(@"Country", [children[0] accessibilityLabel]);

  EXPECT_NSEQ(@"AXCell", [children[1] role]);
  EXPECT_NSEQ(@"Population", [children[1] accessibilityLabel]);
}

IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                       TestTreeContextMenuEvent) {
  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
                                         ui::kAXModeComplete,
                                         ax::mojom::Event::kLoadComplete);

  GURL url(R"HTML(data:text/html,
             <div alt="tree" role="tree">
               <div tabindex="1" role="treeitem">1</div>
               <div tabindex="2" role="treeitem">2</div>
             </div>)HTML");

  ASSERT_TRUE(NavigateToURL(shell(), url));
  ASSERT_TRUE(waiter.WaitForNotification());

  BrowserAccessibility* tree = FindNode(ax::mojom::Role::kTree);
  BrowserAccessibilityCocoa* cocoa_tree = tree->GetNativeViewAccessible();

  NSArray* tree_children = cocoa_tree.children;
  ASSERT_NSEQ(@"AXRow", [tree_children[0] role]);
  ASSERT_NSEQ(@"AXRow", [tree_children[1] role]);

  auto menu_interceptor = std::make_unique<ContextMenuInterceptor>(
      shell()->web_contents()->GetPrimaryMainFrame(),
      ContextMenuInterceptor::ShowBehavior::kPreventShow);

  gfx::Point tree_point =
      TriggerContextMenuAndGetMenuLocation(cocoa_tree, menu_interceptor.get());

  menu_interceptor->Reset();
  gfx::Point item_2_point = TriggerContextMenuAndGetMenuLocation(
      tree_children[1], menu_interceptor.get());
  EXPECT_NE(tree_point, item_2_point);

  // Now focus the second child and trigger a context menu on the tree.
  ASSERT_TRUE(ExecJs(shell()->web_contents(),
                     "document.body.children[0].children[1].focus();"));
  WaitForAccessibilityFocusChange();

  // Triggering a context menu on the tree should now trigger the menu
  // on the focused child.
  menu_interceptor->Reset();
  gfx::Point new_point =
      TriggerContextMenuAndGetMenuLocation(cocoa_tree, menu_interceptor.get());
  EXPECT_EQ(new_point, item_2_point);
}

IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                       TestEventRetargetingFocus) {
  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
                                         ui::kAXModeComplete,
                                         ax::mojom::Event::kLoadComplete);

  GURL url(R"HTML(data:text/html,
             <div role="tree">
               <div tabindex="1" role="treeitem">1</div>
               <div tabindex="2" role="treeitem">2</div>
             </div>
             <div role="treegrid">
               <div tabindex="1" role="treeitem">1</div>
               <div tabindex="2" role="treeitem">2</div>
             </div>
             <div role="tablist">
               <div tabindex="1" role="tab">1</div>
               <div tabindex="2" role="tab">2</div>
             </div>
             <div role="table">
               <div tabindex="1" role="row">1</div>
               <div tabindex="2" role="row">2</div>
             </div>
             <div role="banner">
               <div tabindex="1" role="link">1</div>
               <div tabindex="2" role="link">2</div>
             </div>)HTML");

  EXPECT_TRUE(NavigateToURL(shell(), url));
  ASSERT_TRUE(waiter.WaitForNotification());

  std::pair<ax::mojom::Role, bool> tests[] = {
      std::make_pair(ax::mojom::Role::kTree, true),
      std::make_pair(ax::mojom::Role::kTreeGrid, true),
      std::make_pair(ax::mojom::Role::kTabList, true),
      std::make_pair(ax::mojom::Role::kTable, false),
      std::make_pair(ax::mojom::Role::kBanner, false),
  };

  for (auto& test : tests) {
    BrowserAccessibilityCocoa* parent =
        FindNode(test.first)->GetNativeViewAccessible();
    BrowserAccessibilityCocoa* child = parent.children[1];

    EXPECT_NE(nullptr, parent);
    EXPECT_EQ([child owner], [child actionTarget]);
    EXPECT_EQ([parent owner], [parent actionTarget]);

    FocusAccessibilityElementAndWaitForFocusChange(child);
    ASSERT_EQ(test.second, [parent actionTarget] == [child owner]);
  }
}

IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                       TestEventRetargetingActiveDescendant) {
  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
                                         ui::kAXModeComplete,
                                         ax::mojom::Event::kLoadComplete);

  GURL url(R"HTML(data:text/html,
             <div role="tree" aria-activedescendant="tree-child">
               <div tabindex="1" role="treeitem">1</div>
               <div id="tree-child" tabindex="2" role="treeitem">2</div>
             </div>
             <div role="treegrid" aria-activedescendant="treegrid-child">
               <div tabindex="1" role="treeitem">1</div>
               <div id="treegrid-child" tabindex="2" role="treeitem">2</div>
             </div>
             <div role="tablist" aria-activedescendant="tablist-child">
               <div tabindex="1" role="tab">1</div>
               <div id="tablist-child" tabindex="2" role="tab">2</div>
             </div>
             <div role="table" aria-activedescendant="table-child">
               <div tabindex="1" role="row">1</div>
               <div id="table-child" tabindex="2" role="row">2</div>
             </div>
             <div role="banner" aria-activedescendant="banner-child">
               <div tabindex="1" role="link">1</div>
               <div id="banner-child" tabindex="2" role="link">2</div>
             </div>)HTML");

  EXPECT_TRUE(NavigateToURL(shell(), url));
  ASSERT_TRUE(waiter.WaitForNotification());

  std::pair<ax::mojom::Role, bool> tests[] = {
      std::make_pair(ax::mojom::Role::kTree, true),
      std::make_pair(ax::mojom::Role::kTreeGrid, true),
      std::make_pair(ax::mojom::Role::kTabList, true),
      std::make_pair(ax::mojom::Role::kTable, false),
      std::make_pair(ax::mojom::Role::kBanner, false),
  };

  for (auto& test : tests) {
    BrowserAccessibilityCocoa* parent =
        FindNode(test.first)->GetNativeViewAccessible();
    BrowserAccessibilityCocoa* first_child = parent.children[0];
    BrowserAccessibilityCocoa* second_child = parent.children[1];

    EXPECT_NE(nullptr, parent);
    EXPECT_EQ([second_child owner], [second_child actionTarget]);
    EXPECT_EQ(test.second, [second_child owner] == [parent actionTarget]);

    // aria-activedescendant should take priority over focus for determining if
    // an object is the action target.
    FocusAccessibilityElementAndWaitForFocusChange(first_child);
    EXPECT_EQ(test.second, [second_child owner] == [parent actionTarget]);
  }
}

IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                       TestNSAccessibilityTextChangeElement) {
  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
                                         ui::kAXModeComplete,
                                         ax::mojom::Event::kLoadComplete);

  GURL url(R"HTML(data:text/html,
                  <div id="editable" contenteditable="true" dir="auto">
                    <p>One</p>
                    <p>Two</p>
                    <p><br></p>
                    <p>Three</p>
                    <p>Four</p>
                  </div>)HTML");

  ASSERT_TRUE(NavigateToURL(shell(), url));
  ASSERT_TRUE(waiter.WaitForNotification());

  BrowserAccessibilityCocoa* content_editable =
      GetManager()
          ->GetBrowserAccessibilityRoot()
          ->PlatformGetChild(0)
          ->GetNativeViewAccessible();
  EXPECT_EQ(content_editable.children.count, 5ul);

  WebContents* web_contents = shell()->web_contents();
  auto run_script_and_wait_for_selection_change =
      [web_contents](const char* script) {
        AccessibilityNotificationWaiter waiter(
            web_contents, ui::kAXModeComplete,
            ui::AXEventGenerator::Event::TEXT_SELECTION_CHANGED);
        ASSERT_TRUE(ExecJs(web_contents, script));
        ASSERT_TRUE(waiter.WaitForNotification());
      };

  FocusAccessibilityElementAndWaitForFocusChange(content_editable);

  run_script_and_wait_for_selection_change(R"script(
      let editable = document.getElementById('editable');
      const selection = window.getSelection();
      selection.collapse(editable.children[0].childNodes[0], 1);)script");

  // The focused node in the user info should be the keyboard focusable
  // ancestor.
  NSDictionary* info = GetUserInfoForSelectedTextChangedNotification();
  EXPECT_EQ(id{content_editable},
            [info objectForKey:ui::NSAccessibilityTextChangeElement]);

  AccessibilityNotificationWaiter waiter2(
      web_contents, ui::kAXModeComplete,
      ui::AXEventGenerator::Event::TEXT_SELECTION_CHANGED);
  run_script_and_wait_for_selection_change(R"script(
      let editable = document.getElementById('editable');
      const selection = window.getSelection();
      selection.collapse(editable.children[2].childNodes[0], 0);)script");

  // Even when the cursor is in the empty paragraph text node, the focused
  // object should be the keyboard focusable ancestor.
  info = GetUserInfoForSelectedTextChangedNotification();
  EXPECT_EQ(id{content_editable},
            [info objectForKey:ui::NSAccessibilityTextChangeElement]);
}

}  // namespace content