|  | // 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 "content/browser/accessibility/browser_accessibility_mac.h" | 
|  |  | 
|  | #import <Cocoa/Cocoa.h> | 
|  |  | 
|  | #include <memory> | 
|  | #include <string> | 
|  | #include <vector> | 
|  |  | 
|  | #include "base/strings/string_util.h" | 
|  | #include "base/strings/utf_string_conversions.h" | 
|  | #include "content/browser/accessibility/browser_accessibility_cocoa.h" | 
|  | #include "content/browser/accessibility/browser_accessibility_manager.h" | 
|  | #include "content/browser/accessibility/browser_accessibility_manager_mac.h" | 
|  | #include "content/public/browser/ax_event_notification_details.h" | 
|  | #include "content/public/test/browser_task_environment.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  | #import "testing/gtest_mac.h" | 
|  | #include "ui/accessibility/ax_tree_update.h" | 
|  | #import "ui/base/test/cocoa_helper.h" | 
|  |  | 
|  | namespace content { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | void MakeTable(ui::AXNodeData* table, int id, int row_count, int col_count) { | 
|  | table->id = id; | 
|  | table->role = ax::mojom::Role::kTable; | 
|  | table->AddIntAttribute(ax::mojom::IntAttribute::kTableRowCount, row_count); | 
|  | table->AddIntAttribute(ax::mojom::IntAttribute::kTableColumnCount, col_count); | 
|  | } | 
|  |  | 
|  | void MakeRow(ui::AXNodeData* row, int id) { | 
|  | row->id = id; | 
|  | row->role = ax::mojom::Role::kRow; | 
|  | } | 
|  |  | 
|  | void MakeCell(ui::AXNodeData* cell, | 
|  | int id, | 
|  | int row_index, | 
|  | int col_index, | 
|  | int row_span = 1, | 
|  | int col_span = 1) { | 
|  | cell->id = id; | 
|  | cell->role = ax::mojom::Role::kCell; | 
|  | cell->AddIntAttribute(ax::mojom::IntAttribute::kTableCellRowIndex, row_index); | 
|  | cell->AddIntAttribute(ax::mojom::IntAttribute::kTableCellColumnIndex, | 
|  | col_index); | 
|  | if (row_span > 1) | 
|  | cell->AddIntAttribute(ax::mojom::IntAttribute::kTableCellRowSpan, row_span); | 
|  | if (col_span > 1) | 
|  | cell->AddIntAttribute(ax::mojom::IntAttribute::kTableCellColumnSpan, | 
|  | col_span); | 
|  | } | 
|  |  | 
|  | void MakeColumnHeader(ui::AXNodeData* cell, | 
|  | int id, | 
|  | int row_index, | 
|  | int col_index, | 
|  | int row_span = 1, | 
|  | int col_span = 1) { | 
|  | MakeCell(cell, id, row_index, col_index, row_span, col_span); | 
|  | cell->role = ax::mojom::Role::kColumnHeader; | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | class BrowserAccessibilityMacTest : public ui::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_ = ui::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); | 
|  |  | 
|  | ui::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); | 
|  |  | 
|  | ui::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), nullptr); | 
|  | accessibility_.reset( | 
|  | [manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible() | 
|  | retain]); | 
|  | } | 
|  |  | 
|  | void SetRootValue(std::string value) { | 
|  | if (!manager_) | 
|  | return; | 
|  | root_.SetValue(value); | 
|  | AXEventNotificationDetails event_bundle; | 
|  | event_bundle.updates.resize(1); | 
|  | event_bundle.updates[0].nodes.push_back(root_); | 
|  | ASSERT_TRUE(manager_->OnAccessibilityEvents(event_bundle)); | 
|  | } | 
|  |  | 
|  | ui::AXNodeData root_; | 
|  | base::scoped_nsobject<BrowserAccessibilityCocoa> accessibility_; | 
|  | std::unique_ptr<BrowserAccessibilityManager> manager_; | 
|  |  | 
|  | const content::BrowserTaskEnvironment 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. | 
|  | BrowserAccessibilityCocoa* retainedFirstChild = | 
|  | [accessibility_ accessibilityHitTest:NSMakePoint(50, 50)]; | 
|  | EXPECT_NSEQ(@"Child1", retainedFirstChild.accessibilityLabel); | 
|  |  | 
|  | // Retain it. This simulates what the system might do with an | 
|  | // accessibility object. | 
|  | [retainedFirstChild retain]; | 
|  |  | 
|  | // Rebuild the accessibility tree, which should detach |retainedFirstChild|. | 
|  | RebuildAccessibilityTree(); | 
|  |  | 
|  | // Now any attributes we query should return nil. | 
|  | EXPECT_NSEQ(nil, retainedFirstChild.accessibilityLabel); | 
|  |  | 
|  | // Don't leak memory in the test. | 
|  | [retainedFirstChild release]; | 
|  | } | 
|  |  | 
|  | TEST_F(BrowserAccessibilityMacTest, TestComputeTextEdit) { | 
|  | root_ = ui::AXNodeData(); | 
|  | root_.id = 1; | 
|  | root_.role = ax::mojom::Role::kTextField; | 
|  | manager_ = std::make_unique<BrowserAccessibilityManagerMac>( | 
|  | MakeAXTreeUpdateForTesting(root_), nullptr); | 
|  | accessibility_.reset( | 
|  | [manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible() | 
|  | retain]); | 
|  |  | 
|  | // 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 Mac-specific table APIs. | 
|  | TEST_F(BrowserAccessibilityMacTest, TableAPIs) { | 
|  | ui::AXTreeUpdate initial_state; | 
|  | initial_state.root_id = 1; | 
|  | initial_state.nodes.resize(7); | 
|  | MakeTable(&initial_state.nodes[0], 1, 0, 0); | 
|  | initial_state.nodes[0].child_ids = {2, 3}; | 
|  | MakeRow(&initial_state.nodes[1], 2); | 
|  | initial_state.nodes[1].child_ids = {4, 5}; | 
|  | MakeRow(&initial_state.nodes[2], 3); | 
|  | initial_state.nodes[2].child_ids = {6, 7}; | 
|  | MakeColumnHeader(&initial_state.nodes[3], 4, 0, 0); | 
|  | MakeColumnHeader(&initial_state.nodes[4], 5, 0, 1); | 
|  | MakeCell(&initial_state.nodes[5], 6, 1, 0); | 
|  | MakeCell(&initial_state.nodes[6], 7, 1, 1); | 
|  |  | 
|  | manager_ = | 
|  | std::make_unique<BrowserAccessibilityManagerMac>(initial_state, nullptr); | 
|  | base::scoped_nsobject<BrowserAccessibilityCocoa> ax_table_( | 
|  | [manager_->GetBrowserAccessibilityRoot()->GetNativeViewAccessible() | 
|  | retain]); | 
|  | id children = [ax_table_ children]; | 
|  | EXPECT_EQ(5U, [children count]); | 
|  |  | 
|  | EXPECT_NSEQ(@"AXRow", [children[0] role]); | 
|  | EXPECT_EQ(2U, [[children[0] children] count]); | 
|  |  | 
|  | EXPECT_NSEQ(@"AXRow", [children[1] role]); | 
|  | EXPECT_EQ(2U, [[children[1] children] count]); | 
|  |  | 
|  | EXPECT_NSEQ(@"AXColumn", [children[2] role]); | 
|  | EXPECT_EQ(2U, [[children[2] children] count]); | 
|  | id col_children = [children[2] children]; | 
|  | EXPECT_NSEQ(@"AXCell", [col_children[0] role]); | 
|  | EXPECT_NSEQ(@"AXCell", [col_children[1] role]); | 
|  |  | 
|  | EXPECT_NSEQ(@"AXColumn", [children[3] role]); | 
|  | EXPECT_EQ(2U, [[children[3] children] count]); | 
|  | col_children = [children[3] children]; | 
|  | EXPECT_NSEQ(@"AXCell", [col_children[0] role]); | 
|  | EXPECT_NSEQ(@"AXCell", [col_children[1] role]); | 
|  |  | 
|  | EXPECT_NSEQ(@"AXGroup", [children[4] role]); | 
|  | EXPECT_EQ(2U, [[children[4] children] count]); | 
|  | col_children = [children[4] children]; | 
|  | EXPECT_NSEQ(@"AXCell", [col_children[0] role]); | 
|  | EXPECT_NSEQ(@"AXCell", [col_children[1] role]); | 
|  | } | 
|  |  | 
|  | // Test Mac indirect columns and descendants. | 
|  | TEST_F(BrowserAccessibilityMacTest, TableColumnsAndDescendants) { | 
|  | ui::AXTreeUpdate initial_state; | 
|  | initial_state.root_id = 1; | 
|  | initial_state.nodes.resize(7); | 
|  | MakeTable(&initial_state.nodes[0], 1, 0, 0); | 
|  | initial_state.nodes[0].child_ids = {2, 3}; | 
|  | MakeRow(&initial_state.nodes[1], 2); | 
|  | initial_state.nodes[1].child_ids = {4, 5}; | 
|  | MakeRow(&initial_state.nodes[2], 3); | 
|  | initial_state.nodes[2].child_ids = {6, 7}; | 
|  | MakeColumnHeader(&initial_state.nodes[3], 4, 0, 0); | 
|  | MakeColumnHeader(&initial_state.nodes[4], 5, 0, 1); | 
|  | MakeCell(&initial_state.nodes[5], 6, 1, 0); | 
|  | MakeCell(&initial_state.nodes[6], 7, 1, 1); | 
|  |  | 
|  | // 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, 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); | 
|  | } | 
|  |  | 
|  | }  // namespace content |