blob: f535b74b75d0b47511dbc6deaccb94a23cd88f4d [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/renderer/accessibility/read_anything_app_controller.h"
#include <string>
#include <vector>
#include "chrome/renderer/accessibility/ax_tree_distiller.h"
#include "chrome/test/base/chrome_render_view_test.h"
#include "content/public/renderer/render_frame.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_serializable_tree.h"
#include "url/gurl.h"
class MockAXTreeDistiller : public AXTreeDistiller {
public:
explicit MockAXTreeDistiller(content::RenderFrame* render_frame)
: AXTreeDistiller(render_frame, base::NullCallback()) {}
MOCK_METHOD(void,
Distill,
(const ui::AXTree& tree,
const ui::AXTreeUpdate& snapshot,
const ukm::SourceId& ukm_source_id),
(override));
};
class MockReadAnythingPageHandler
: public read_anything::mojom::UntrustedPageHandler {
public:
MockReadAnythingPageHandler() = default;
MOCK_METHOD(void,
OnLinkClicked,
(const ui::AXTreeID& target_tree_id, ui::AXNodeID target_node_id),
(override));
MOCK_METHOD(void,
OnSelectionChange,
(const ui::AXTreeID& target_tree_id,
ui::AXNodeID anchor_node_id,
int anchor_offset,
ui::AXNodeID focus_node_id,
int focus_offset),
(override));
mojo::PendingRemote<read_anything::mojom::UntrustedPageHandler>
BindNewPipeAndPassRemote() {
return receiver_.BindNewPipeAndPassRemote();
}
void FlushForTesting() { receiver_.FlushForTesting(); }
private:
mojo::Receiver<read_anything::mojom::UntrustedPageHandler> receiver_{this};
};
using testing::Mock;
class ReadAnythingAppControllerTest : public ChromeRenderViewTest {
public:
ReadAnythingAppControllerTest() = default;
~ReadAnythingAppControllerTest() override = default;
ReadAnythingAppControllerTest(const ReadAnythingAppControllerTest&) = delete;
ReadAnythingAppControllerTest& operator=(
const ReadAnythingAppControllerTest&) = delete;
void SetUp() override {
ChromeRenderViewTest::SetUp();
content::RenderFrame* render_frame =
content::RenderFrame::FromWebFrame(GetMainFrame());
controller_ = ReadAnythingAppController::Install(render_frame);
// Set the page handler for testing.
controller_->page_handler_.reset();
controller_->page_handler_.Bind(page_handler_.BindNewPipeAndPassRemote());
// Set distiller for testing.
std::unique_ptr<AXTreeDistiller> distiller =
std::make_unique<MockAXTreeDistiller>(render_frame);
controller_->distiller_ = std::move(distiller);
distiller_ =
static_cast<MockAXTreeDistiller*>(controller_->distiller_.get());
// Create a tree id.
tree_id_ = ui::AXTreeID::CreateNewAXTreeID();
// Create simple AXTreeUpdate with a root node and 3 children.
ui::AXTreeUpdate snapshot;
snapshot.root_id = 1;
snapshot.nodes.resize(4);
snapshot.nodes[0].id = 1;
snapshot.nodes[0].child_ids = {2, 3, 4};
snapshot.nodes[1].id = 2;
snapshot.nodes[2].id = 3;
snapshot.nodes[3].id = 4;
SetUpdateTreeID(&snapshot);
// Send the snapshot to the controller and set its tree ID to be the active
// tree ID. When the accessibility event is received and unserialized, the
// controller will call distiller_->Distill().
EXPECT_CALL(*distiller_, Distill).Times(1);
AccessibilityEventReceived({snapshot});
OnActiveAXTreeIDChanged(tree_id_);
OnAXTreeDistilled({});
Mock::VerifyAndClearExpectations(distiller_);
}
void SetUpdateTreeID(ui::AXTreeUpdate* update) {
SetUpdateTreeID(update, tree_id_);
}
void SetUpdateTreeID(ui::AXTreeUpdate* update, ui::AXTreeID tree_id) {
ui::AXTreeData tree_data;
tree_data.tree_id = tree_id;
update->has_tree_data = true;
update->tree_data = tree_data;
}
void SetThemeForTesting(const std::string& font_name,
float font_size,
SkColor foreground_color,
SkColor background_color,
int line_spacing,
int letter_spacing) {
controller_->SetThemeForTesting(font_name, font_size, foreground_color,
background_color, line_spacing,
letter_spacing);
}
void AccessibilityEventReceived(
const std::vector<ui::AXTreeUpdate>& updates,
const std::vector<ui::AXEvent>& events = std::vector<ui::AXEvent>()) {
AccessibilityEventReceived(updates[0].tree_data.tree_id, updates, events);
}
void AccessibilityEventReceived(
const ui::AXTreeID& tree_id,
const std::vector<ui::AXTreeUpdate>& updates,
const std::vector<ui::AXEvent>& events = std::vector<ui::AXEvent>()) {
controller_->AccessibilityEventReceived(tree_id, updates, events);
}
// Since a11y events happen asynchronously, they can come between the time
// distillation finishes and pending updates are unserialized in
// OnAXTreeDistilled. Thus we need to be able to set distillation progress
// independent of OnAXTreeDistilled.
void SetDistillationInProgress(bool in_progress) {
controller_->model_.SetDistillationInProgress(in_progress);
}
void OnActiveAXTreeIDChanged(const ui::AXTreeID& tree_id) {
OnActiveAXTreeIDChanged(tree_id, GURL::EmptyGURL());
}
void OnActiveAXTreeIDChanged(const ui::AXTreeID& tree_id, const GURL& url) {
controller_->OnActiveAXTreeIDChanged(tree_id, ukm::kInvalidSourceId, url);
}
void OnAXTreeDistilled(const std::vector<ui::AXNodeID>& content_node_ids) {
OnAXTreeDistilled(tree_id_, content_node_ids);
}
void OnAXTreeDistilled(const ui::AXTreeID& tree_id,
const std::vector<ui::AXNodeID>& content_node_ids) {
controller_->OnAXTreeDistilled(tree_id, content_node_ids);
}
void OnAXTreeDestroyed(const ui::AXTreeID& tree_id) {
controller_->OnAXTreeDestroyed(tree_id);
}
ui::AXNodeID RootId() { return controller_->RootId(); }
ui::AXNodeID StartNodeId() { return controller_->StartNodeId(); }
int StartOffset() { return controller_->StartOffset(); }
ui::AXNodeID EndNodeId() { return controller_->EndNodeId(); }
int EndOffset() { return controller_->EndOffset(); }
bool HasSelection() { return controller_->model_.has_selection(); }
bool DisplayNodeIdsContains(ui::AXNodeID ax_node_id) {
return base::Contains(controller_->model_.display_node_ids(), ax_node_id);
}
bool SelectionNodeIdsContains(ui::AXNodeID ax_node_id) {
return base::Contains(controller_->model_.selection_node_ids(), ax_node_id);
}
std::string FontName() { return controller_->FontName(); }
float FontSize() { return controller_->FontSize(); }
SkColor ForegroundColor() { return controller_->ForegroundColor(); }
SkColor BackgroundColor() { return controller_->BackgroundColor(); }
float LineSpacing() { return controller_->LineSpacing(); }
float LetterSpacing() { return controller_->LetterSpacing(); }
bool isSelectable() { return controller_->isSelectable(); }
std::vector<ui::AXNodeID> GetChildren(ui::AXNodeID ax_node_id) {
return controller_->GetChildren(ax_node_id);
}
std::string GetHtmlTag(ui::AXNodeID ax_node_id) {
return controller_->GetHtmlTag(ax_node_id);
}
std::string GetTextContent(ui::AXNodeID ax_node_id) {
return controller_->GetTextContent(ax_node_id);
}
std::string GetUrl(ui::AXNodeID ax_node_id) {
return controller_->GetUrl(ax_node_id);
}
bool ShouldBold(ui::AXNodeID ax_node_id) {
return controller_->ShouldBold(ax_node_id);
}
bool IsOverline(ui::AXNodeID ax_node_id) {
return controller_->IsOverline(ax_node_id);
}
void OnLinkClicked(ui::AXNodeID ax_node_id) {
controller_->OnLinkClicked(ax_node_id);
}
void OnSelectionChange(ui::AXNodeID anchor_node_id,
int anchor_offset,
ui::AXNodeID focus_node_id,
int focus_offset) {
controller_->OnSelectionChange(anchor_node_id, anchor_offset, focus_node_id,
focus_offset);
}
bool IsNodeIgnoredForReadAnything(ui::AXNodeID ax_node_id) {
return controller_->model_.IsNodeIgnoredForReadAnything(ax_node_id);
}
bool HasTree(ui::AXTreeID tree_id) {
return controller_->model_.ContainsTree(tree_id);
}
ui::AXTreeID ActiveTreeId() { return controller_->model_.active_tree_id(); }
ui::AXTreeID tree_id_;
MockAXTreeDistiller* distiller_ = nullptr;
testing::StrictMock<MockReadAnythingPageHandler> page_handler_;
private:
// ReadAnythingAppController constructor and destructor are private so it's
// not accessible by std::make_unique.
ReadAnythingAppController* controller_ = nullptr;
};
TEST_F(ReadAnythingAppControllerTest, Theme) {
std::string font_name = "Roboto";
float font_size = 18.0;
SkColor foreground = SkColorSetRGB(0x33, 0x36, 0x39);
SkColor background = SkColorSetRGB(0xFD, 0xE2, 0x93);
int letter_spacing =
static_cast<int>(read_anything::mojom::LetterSpacing::kDefaultValue);
float letter_spacing_value = 0.0;
int line_spacing =
static_cast<int>(read_anything::mojom::LineSpacing::kDefaultValue);
float line_spacing_value = 1.5;
SetThemeForTesting(font_name, font_size, foreground, background, line_spacing,
letter_spacing);
EXPECT_EQ(font_name, FontName());
EXPECT_EQ(font_size, FontSize());
EXPECT_EQ(foreground, ForegroundColor());
EXPECT_EQ(background, BackgroundColor());
EXPECT_EQ(line_spacing_value, LineSpacing());
EXPECT_EQ(letter_spacing_value, LetterSpacing());
}
TEST_F(ReadAnythingAppControllerTest, RootIdIsSnapshotRootId) {
OnAXTreeDistilled({1});
EXPECT_EQ(1, RootId());
OnAXTreeDistilled({2});
EXPECT_EQ(1, RootId());
OnAXTreeDistilled({3});
EXPECT_EQ(1, RootId());
OnAXTreeDistilled({4});
EXPECT_EQ(1, RootId());
}
TEST_F(ReadAnythingAppControllerTest, GetChildren_NoSelectionOrContentNodes) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(1);
update.nodes[0].id = 3;
update.nodes[0].role = ax::mojom::Role::kNone;
AccessibilityEventReceived({update});
OnAXTreeDistilled({});
EXPECT_EQ(0u, GetChildren(1).size());
EXPECT_EQ(0u, GetChildren(2).size());
EXPECT_EQ(0u, GetChildren(3).size());
EXPECT_EQ(0u, GetChildren(4).size());
}
TEST_F(ReadAnythingAppControllerTest, GetChildren_WithContentNodes) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(1);
update.nodes[0].id = 3;
update.nodes[0].role = ax::mojom::Role::kNone;
AccessibilityEventReceived({update});
OnAXTreeDistilled({1, 2, 3, 4});
EXPECT_EQ(2u, GetChildren(1).size());
EXPECT_EQ(0u, GetChildren(2).size());
EXPECT_EQ(0u, GetChildren(3).size());
EXPECT_EQ(0u, GetChildren(4).size());
EXPECT_EQ(2, GetChildren(1)[0]);
EXPECT_EQ(4, GetChildren(1)[1]);
}
TEST_F(ReadAnythingAppControllerTest,
GetChildren_WithSelection_ContainsNearbyNodes) {
// Create selection from node 3-4.
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.has_tree_data = true;
update.event_from = ax::mojom::EventFrom::kUser;
update.tree_data.sel_anchor_object_id = 3;
update.tree_data.sel_focus_object_id = 4;
update.tree_data.sel_anchor_offset = 0;
update.tree_data.sel_focus_offset = 0;
update.tree_data.sel_is_backward = false;
AccessibilityEventReceived({update});
EXPECT_EQ(3u, GetChildren(1).size());
EXPECT_EQ(0u, GetChildren(2).size());
EXPECT_EQ(0u, GetChildren(3).size());
EXPECT_EQ(0u, GetChildren(4).size());
EXPECT_EQ(2, GetChildren(1)[0]);
EXPECT_EQ(3, GetChildren(1)[1]);
EXPECT_EQ(4, GetChildren(1)[2]);
}
TEST_F(ReadAnythingAppControllerTest,
GetChildren_WithBackwardSelection_ContainsNearbyNodes) {
// Create backward selection from node 4-3.
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.has_tree_data = true;
update.event_from = ax::mojom::EventFrom::kUser;
update.tree_data.sel_anchor_object_id = 4;
update.tree_data.sel_focus_object_id = 3;
update.tree_data.sel_anchor_offset = 0;
update.tree_data.sel_focus_offset = 0;
update.tree_data.sel_is_backward = true;
AccessibilityEventReceived({update});
EXPECT_EQ(3u, GetChildren(1).size());
EXPECT_EQ(0u, GetChildren(2).size());
EXPECT_EQ(0u, GetChildren(3).size());
EXPECT_EQ(0u, GetChildren(4).size());
EXPECT_EQ(2, GetChildren(1)[0]);
EXPECT_EQ(3, GetChildren(1)[1]);
EXPECT_EQ(4, GetChildren(1)[2]);
}
TEST_F(ReadAnythingAppControllerTest, GetHtmlTag) {
std::string span = "span";
std::string h1 = "h1";
std::string ul = "ul";
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(3);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[2].id = 4;
update.nodes[0].AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag,
span);
update.nodes[1].AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, h1);
update.nodes[2].AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, ul);
AccessibilityEventReceived({update});
OnAXTreeDistilled({});
EXPECT_EQ(span, GetHtmlTag(2));
EXPECT_EQ(h1, GetHtmlTag(3));
EXPECT_EQ(ul, GetHtmlTag(4));
}
TEST_F(ReadAnythingAppControllerTest, GetHtmlTag_TextFieldReturnsDiv) {
std::string span = "span";
std::string h1 = "h1";
std::string ul = "ul";
std::string div = "div";
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(3);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[2].id = 4;
update.nodes[0].AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag,
span);
update.nodes[1].AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, h1);
update.nodes[1].role = ax::mojom::Role::kTextField;
update.nodes[2].AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, ul);
update.nodes[2].role = ax::mojom::Role::kTextFieldWithComboBox;
AccessibilityEventReceived({update});
OnAXTreeDistilled({});
EXPECT_EQ(span, GetHtmlTag(2));
EXPECT_EQ(div, GetHtmlTag(3));
EXPECT_EQ(div, GetHtmlTag(4));
}
TEST_F(ReadAnythingAppControllerTest,
GetHtmlTag_DivWithHeadingAndAriaLevelReturnsH) {
std::string h3 = "h3";
std::string div = "div";
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(3);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[2].id = 4;
update.nodes[1].role = ax::mojom::Role::kHeading;
update.nodes[1].html_attributes.emplace_back("aria-level", "3");
AccessibilityEventReceived({update});
OnAXTreeDistilled({});
EXPECT_EQ(h3, GetHtmlTag(3));
}
TEST_F(ReadAnythingAppControllerTest, GetTextContent_NoSelection) {
std::string text_content = "Hello";
std::string missing_text_content = "";
std::string more_text_content = " world";
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(3);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[2].id = 4;
update.nodes[0].role = ax::mojom::Role::kStaticText;
update.nodes[0].SetName(text_content);
update.nodes[0].SetNameFrom(ax::mojom::NameFrom::kContents);
update.nodes[1].role = ax::mojom::Role::kStaticText;
update.nodes[1].SetName(missing_text_content);
update.nodes[1].SetNameFrom(ax::mojom::NameFrom::kContents);
update.nodes[2].role = ax::mojom::Role::kStaticText;
update.nodes[2].SetName(more_text_content);
update.nodes[2].SetNameFrom(ax::mojom::NameFrom::kContents);
AccessibilityEventReceived({update});
OnAXTreeDistilled({});
EXPECT_EQ("Hello world", GetTextContent(1));
EXPECT_EQ(text_content, GetTextContent(2));
EXPECT_EQ(missing_text_content, GetTextContent(3));
EXPECT_EQ(more_text_content, GetTextContent(4));
}
TEST_F(ReadAnythingAppControllerTest, GetTextContent_WithSelection) {
std::string text_content_1 = "Hello";
std::string text_content_2 = " world";
std::string text_content_3 = " friend";
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(3);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[2].id = 4;
update.nodes[0].role = ax::mojom::Role::kStaticText;
update.nodes[0].SetName(text_content_1);
update.nodes[0].SetNameFrom(ax::mojom::NameFrom::kContents);
update.nodes[1].role = ax::mojom::Role::kStaticText;
update.nodes[1].SetName(text_content_2);
update.nodes[1].SetNameFrom(ax::mojom::NameFrom::kContents);
update.nodes[2].role = ax::mojom::Role::kStaticText;
update.nodes[2].SetName(text_content_3);
update.nodes[2].SetNameFrom(ax::mojom::NameFrom::kContents);
// Create selection from node 2-3.
update.tree_data.sel_anchor_object_id = 2;
update.tree_data.sel_focus_object_id = 3;
update.tree_data.sel_anchor_offset = 1;
update.tree_data.sel_focus_offset = 3;
update.tree_data.sel_is_backward = false;
AccessibilityEventReceived({update});
OnAXTreeDistilled({});
EXPECT_EQ("Hello world friend", GetTextContent(1));
EXPECT_EQ("Hello", GetTextContent(2));
EXPECT_EQ(" world", GetTextContent(3));
EXPECT_EQ(" friend", GetTextContent(4));
}
TEST_F(ReadAnythingAppControllerTest, GetUrl) {
std::string url = "http://www.google.com";
std::string invalid_url = "cats";
std::string missing_url = "";
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(3);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[2].id = 4;
update.nodes[0].AddStringAttribute(ax::mojom::StringAttribute::kUrl, url);
update.nodes[1].AddStringAttribute(ax::mojom::StringAttribute::kUrl,
invalid_url);
update.nodes[2].AddStringAttribute(ax::mojom::StringAttribute::kUrl,
missing_url);
AccessibilityEventReceived({update});
OnAXTreeDistilled({});
EXPECT_EQ(url, GetUrl(2));
EXPECT_EQ(invalid_url, GetUrl(3));
EXPECT_EQ(missing_url, GetUrl(4));
}
TEST_F(ReadAnythingAppControllerTest, ShouldBold) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(3);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[2].id = 4;
update.nodes[0].AddTextStyle(ax::mojom::TextStyle::kOverline);
update.nodes[1].AddTextStyle(ax::mojom::TextStyle::kUnderline);
update.nodes[2].AddTextStyle(ax::mojom::TextStyle::kItalic);
AccessibilityEventReceived({update});
OnAXTreeDistilled({});
EXPECT_EQ(false, ShouldBold(2));
EXPECT_EQ(true, ShouldBold(3));
EXPECT_EQ(true, ShouldBold(4));
}
TEST_F(ReadAnythingAppControllerTest, IsOverline) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(2);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[0].AddTextStyle(ax::mojom::TextStyle::kOverline);
update.nodes[1].AddTextStyle(ax::mojom::TextStyle::kUnderline);
AccessibilityEventReceived({update});
OnAXTreeDistilled({});
EXPECT_EQ(true, IsOverline(2));
EXPECT_EQ(false, IsOverline(3));
}
TEST_F(ReadAnythingAppControllerTest, IsNodeIgnoredForReadAnything) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(3);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[2].id = 4;
update.nodes[0].role = ax::mojom::Role::kStaticText;
update.nodes[1].role = ax::mojom::Role::kComboBoxGrouping;
update.nodes[2].role = ax::mojom::Role::kButton;
AccessibilityEventReceived({update});
OnAXTreeDistilled({});
EXPECT_EQ(false, IsNodeIgnoredForReadAnything(2));
EXPECT_EQ(true, IsNodeIgnoredForReadAnything(3));
EXPECT_EQ(true, IsNodeIgnoredForReadAnything(4));
}
TEST_F(ReadAnythingAppControllerTest,
SelectionNodeIdsContains_SelectionAndNearbyNodes) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.has_tree_data = true;
update.event_from = ax::mojom::EventFrom::kUser;
update.tree_data.sel_anchor_object_id = 2;
update.tree_data.sel_focus_object_id = 3;
update.tree_data.sel_anchor_offset = 0;
update.tree_data.sel_focus_offset = 0;
update.tree_data.sel_is_backward = false;
AccessibilityEventReceived({update});
EXPECT_TRUE(SelectionNodeIdsContains(1));
EXPECT_TRUE(SelectionNodeIdsContains(2));
EXPECT_TRUE(SelectionNodeIdsContains(3));
EXPECT_TRUE(SelectionNodeIdsContains(4));
}
TEST_F(ReadAnythingAppControllerTest,
SelectionNodeIdsContains_BackwardSelectionAndNearbyNodes) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.has_tree_data = true;
update.event_from = ax::mojom::EventFrom::kUser;
update.tree_data.sel_anchor_object_id = 3;
update.tree_data.sel_focus_object_id = 2;
update.tree_data.sel_anchor_offset = 0;
update.tree_data.sel_focus_offset = 0;
update.tree_data.sel_is_backward = true;
AccessibilityEventReceived({update});
EXPECT_TRUE(SelectionNodeIdsContains(1));
EXPECT_TRUE(SelectionNodeIdsContains(2));
EXPECT_TRUE(SelectionNodeIdsContains(3));
EXPECT_TRUE(SelectionNodeIdsContains(4));
}
TEST_F(ReadAnythingAppControllerTest, DisplayNodeIdsContains_ContentNodes) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(1);
update.nodes[0].id = 3;
// This update says the page loaded. When the controller receives it in
// AccessibilityEventReceived, it will re-distill the tree. This is an
// example of a non-generated event.
EXPECT_CALL(*distiller_, Distill).Times(1);
ui::AXEvent load_complete(0, ax::mojom::Event::kLoadComplete);
AccessibilityEventReceived({update}, {load_complete});
OnAXTreeDistilled({3});
EXPECT_TRUE(DisplayNodeIdsContains(1));
EXPECT_FALSE(DisplayNodeIdsContains(2));
EXPECT_TRUE(DisplayNodeIdsContains(3));
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest,
DisplayNodeIdsContains_NoSelectionOrContentNodes) {
OnAXTreeDistilled({});
EXPECT_FALSE(DisplayNodeIdsContains(1));
EXPECT_FALSE(DisplayNodeIdsContains(2));
EXPECT_FALSE(DisplayNodeIdsContains(3));
EXPECT_FALSE(DisplayNodeIdsContains(4));
}
TEST_F(ReadAnythingAppControllerTest, DoesNotCrashIfContentNodeNotFoundInTree) {
OnAXTreeDistilled({6});
}
TEST_F(ReadAnythingAppControllerTest, AccessibilityEventReceived) {
// Tree starts off with no text content.
EXPECT_EQ("", GetTextContent(1));
EXPECT_EQ("", GetTextContent(2));
EXPECT_EQ("", GetTextContent(3));
EXPECT_EQ("", GetTextContent(4));
// Send a new update which settings the text content of node 2.
ui::AXTreeUpdate update_1;
SetUpdateTreeID(&update_1);
update_1.nodes.resize(1);
update_1.nodes[0].id = 2;
update_1.nodes[0].role = ax::mojom::Role::kStaticText;
update_1.nodes[0].SetName("Hello world");
update_1.nodes[0].SetNameFrom(ax::mojom::NameFrom::kContents);
AccessibilityEventReceived({update_1});
EXPECT_EQ("Hello world", GetTextContent(1));
EXPECT_EQ("Hello world", GetTextContent(2));
EXPECT_EQ("", GetTextContent(3));
EXPECT_EQ("", GetTextContent(4));
// Send three updates which should be merged.
std::vector<ui::AXTreeUpdate> batch_updates;
for (int i = 2; i < 5; i++) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(1);
update.nodes[0].id = i;
update.nodes[0].role = ax::mojom::Role::kStaticText;
update.nodes[0].SetName("Node " + base::NumberToString(i));
update.nodes[0].SetNameFrom(ax::mojom::NameFrom::kContents);
batch_updates.push_back(update);
}
AccessibilityEventReceived(batch_updates);
EXPECT_EQ("Node 2Node 3Node 4", GetTextContent(1));
EXPECT_EQ("Node 2", GetTextContent(2));
EXPECT_EQ("Node 3", GetTextContent(3));
EXPECT_EQ("Node 4", GetTextContent(4));
// Clear node 1.
ui::AXTreeUpdate clear_update;
SetUpdateTreeID(&clear_update);
clear_update.root_id = 1;
clear_update.node_id_to_clear = 1;
clear_update.nodes.resize(1);
clear_update.nodes[0].id = 1;
AccessibilityEventReceived({clear_update});
EXPECT_EQ("", GetTextContent(1));
}
TEST_F(ReadAnythingAppControllerTest,
AccessibilityEventReceivedWhileDistilling) {
// Tree starts off with no text content.
EXPECT_EQ("", GetTextContent(1));
EXPECT_EQ("", GetTextContent(2));
EXPECT_EQ("", GetTextContent(3));
EXPECT_EQ("", GetTextContent(4));
// Send a new update which settings the text content of node 2.
ui::AXTreeUpdate update_1;
SetUpdateTreeID(&update_1);
update_1.nodes.resize(1);
update_1.nodes[0].id = 2;
update_1.nodes[0].role = ax::mojom::Role::kStaticText;
update_1.nodes[0].SetName("Hello world");
update_1.nodes[0].SetNameFrom(ax::mojom::NameFrom::kContents);
AccessibilityEventReceived({update_1});
EXPECT_EQ("Hello world", GetTextContent(1));
EXPECT_EQ("Hello world", GetTextContent(2));
EXPECT_EQ("", GetTextContent(3));
EXPECT_EQ("", GetTextContent(4));
// Send three updates while distilling.
SetDistillationInProgress(true);
std::vector<ui::AXTreeUpdate> batch_updates;
for (int i = 2; i < 5; i++) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(1);
update.nodes[0].id = i;
update.nodes[0].role = ax::mojom::Role::kStaticText;
update.nodes[0].SetName("Node " + base::NumberToString(i));
update.nodes[0].SetNameFrom(ax::mojom::NameFrom::kContents);
batch_updates.push_back(update);
}
AccessibilityEventReceived(batch_updates);
// The updates shouldn't be applied yet.
EXPECT_EQ("Hello world", GetTextContent(1));
EXPECT_EQ("Hello world", GetTextContent(2));
// Send another update after distillation finishes but before
// OnAXTreeDistilled would unserialize the pending updates. Since a11y events
// happen asynchronously, they can come between the time distillation finishes
// and pending updates are unserialized.
SetDistillationInProgress(false);
ui::AXTreeUpdate update_2;
SetUpdateTreeID(&update_2);
update_2.nodes.resize(1);
update_2.nodes[0].id = 2;
update_2.nodes[0].role = ax::mojom::Role::kStaticText;
update_2.nodes[0].SetName("Final update");
update_2.nodes[0].SetNameFrom(ax::mojom::NameFrom::kContents);
AccessibilityEventReceived({update_2});
EXPECT_EQ("Final updateNode 3Node 4", GetTextContent(1));
EXPECT_EQ("Final update", GetTextContent(2));
EXPECT_EQ("Node 3", GetTextContent(3));
EXPECT_EQ("Node 4", GetTextContent(4));
}
TEST_F(ReadAnythingAppControllerTest, OnActiveAXTreeIDChanged) {
// Create three AXTreeUpdates with three different tree IDs.
std::vector<ui::AXTreeID> tree_ids = {ui::AXTreeID::CreateNewAXTreeID(),
ui::AXTreeID::CreateNewAXTreeID(),
tree_id_};
std::vector<ui::AXTreeUpdate> updates;
for (int i = 0; i < 3; i++) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update, tree_ids[i]);
update.root_id = 1;
update.nodes.resize(1);
update.nodes[0].id = 1;
update.nodes[0].role = ax::mojom::Role::kStaticText;
update.nodes[0].SetName("Tree " + base::NumberToString(i));
update.nodes[0].SetNameFrom(ax::mojom::NameFrom::kContents);
updates.push_back(update);
}
// Add the three updates separately since they have different tree IDs.
// Check that changing the active tree ID changes the active tree which is
// used when using a v8 getter.
for (int i = 0; i < 3; i++) {
AccessibilityEventReceived({updates[i]});
OnAXTreeDistilled({1});
EXPECT_CALL(*distiller_, Distill).Times(1);
OnActiveAXTreeIDChanged(tree_ids[i]);
EXPECT_EQ("Tree " + base::NumberToString(i), GetTextContent(1));
Mock::VerifyAndClearExpectations(distiller_);
}
// Changing the active tree ID to the same ID does nothing.
EXPECT_CALL(*distiller_, Distill).Times(0);
OnActiveAXTreeIDChanged(tree_ids[2]);
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest,
OnActiveAXTreeIDChanged_DocsLabeledNotSelectable) {
ui::AXTreeUpdate update;
ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID();
SetUpdateTreeID(&update, id_1);
update.root_id = 1;
update.nodes.resize(1);
update.nodes[0].id = 1;
AccessibilityEventReceived({update});
OnAXTreeDistilled({1});
EXPECT_CALL(*distiller_, Distill).Times(1);
OnActiveAXTreeIDChanged(id_1, GURL("www.google.com"));
EXPECT_TRUE(isSelectable());
Mock::VerifyAndClearExpectations(distiller_);
ui::AXTreeUpdate update_1;
SetUpdateTreeID(&update_1, tree_id_);
update_1.root_id = 1;
update_1.nodes.resize(1);
update_1.nodes[0].id = 1;
AccessibilityEventReceived({update_1});
OnAXTreeDistilled({1});
EXPECT_CALL(*distiller_, Distill).Times(1);
OnActiveAXTreeIDChanged(
tree_id_, GURL("https://docs.google.com/document/d/"
"1t6x1PQaQWjE8wb9iyYmFaoK1XAEgsl8G1Hx3rzfpoKA/"
"edit?ouid=103677288878638916900&usp=docs_home&ths=true"));
EXPECT_FALSE(isSelectable());
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest, DoesNotCrashIfActiveAXTreeIDUnknown) {
EXPECT_CALL(*distiller_, Distill).Times(0);
ui::AXTreeID tree_id = ui::AXTreeIDUnknown();
OnActiveAXTreeIDChanged(tree_id);
OnAXTreeDestroyed(tree_id);
OnAXTreeDistilled({1});
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest, DoesNotCrashIfActiveAXTreeIDNotInTrees) {
ui::AXTreeID tree_id = ui::AXTreeID::CreateNewAXTreeID();
OnActiveAXTreeIDChanged(tree_id);
OnAXTreeDestroyed(tree_id);
}
TEST_F(ReadAnythingAppControllerTest, AddAndRemoveTrees) {
// Create two new trees with new tree IDs.
std::vector<ui::AXTreeID> tree_ids = {ui::AXTreeID::CreateNewAXTreeID(),
ui::AXTreeID::CreateNewAXTreeID()};
std::vector<ui::AXTreeUpdate> updates;
for (int i = 0; i < 2; i++) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update, tree_ids[i]);
update.root_id = 1;
update.nodes.resize(1);
update.nodes[0].id = 1;
updates.push_back(update);
}
// Start with 1 tree (the tree created in SetUp).
ASSERT_TRUE(HasTree(tree_id_));
// Add the two trees.
AccessibilityEventReceived({updates[0]});
ASSERT_TRUE(HasTree(tree_id_));
ASSERT_TRUE(HasTree(tree_ids[0]));
AccessibilityEventReceived({updates[1]});
ASSERT_TRUE(HasTree(tree_id_));
ASSERT_TRUE(HasTree(tree_ids[0]));
ASSERT_TRUE(HasTree(tree_ids[1]));
// Remove all of the trees.
OnAXTreeDestroyed(tree_id_);
ASSERT_FALSE(HasTree(tree_id_));
ASSERT_TRUE(HasTree(tree_ids[0]));
ASSERT_TRUE(HasTree(tree_ids[1]));
OnAXTreeDestroyed(tree_ids[0]);
ASSERT_FALSE(HasTree(tree_ids[0]));
ASSERT_TRUE(HasTree(tree_ids[1]));
OnAXTreeDestroyed(tree_ids[1]);
ASSERT_FALSE(HasTree(tree_ids[1]));
}
TEST_F(ReadAnythingAppControllerTest, OnAXTreeDestroyed_EraseTreeCalled) {
// Set the name of each node to be its id.
ui::AXTreeUpdate initial_update;
SetUpdateTreeID(&initial_update);
initial_update.root_id = 1;
initial_update.nodes.resize(3);
std::vector<int> child_ids;
for (int i = 0; i < 3; i++) {
int id = i + 2;
child_ids.push_back(id);
initial_update.nodes[i].id = id;
initial_update.nodes[i].role = ax::mojom::Role::kStaticText;
initial_update.nodes[i].SetName(base::NumberToString(id));
initial_update.nodes[i].SetNameFrom(ax::mojom::NameFrom::kContents);
}
// Since this update is just cosmetic (it changes the nodes' name but doesn't
// change the structure of the tree by adding or removing nodes), the
// controller does not distill.
EXPECT_CALL(*distiller_, Distill).Times(0);
AccessibilityEventReceived({initial_update});
EXPECT_EQ("234", GetTextContent(1));
Mock::VerifyAndClearExpectations(distiller_);
std::vector<ui::AXTreeUpdate> updates;
for (int i = 0; i < 3; i++) {
int id = i + 5;
child_ids.push_back(id);
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.root_id = 1;
update.nodes.resize(2);
update.nodes[0].id = 1;
update.nodes[0].child_ids = child_ids;
update.nodes[1].id = id;
update.nodes[1].role = ax::mojom::Role::kStaticText;
update.nodes[1].SetName(base::NumberToString(id));
update.nodes[1].SetNameFrom(ax::mojom::NameFrom::kContents);
updates.push_back(update);
}
// Send update 0.
EXPECT_CALL(*distiller_, Distill).Times(0);
AccessibilityEventReceived({updates[0]});
EXPECT_EQ("2345", GetTextContent(1));
Mock::VerifyAndClearExpectations(distiller_);
// Send update 1.
EXPECT_CALL(*distiller_, Distill).Times(0);
AccessibilityEventReceived({updates[1]});
EXPECT_EQ("23456", GetTextContent(1));
Mock::VerifyAndClearExpectations(distiller_);
// Destroy the tree.
ASSERT_TRUE(HasTree(tree_id_));
OnAXTreeDestroyed(tree_id_);
ASSERT_FALSE(HasTree(tree_id_));
}
TEST_F(ReadAnythingAppControllerTest,
DistillationInProgress_TreeUpdateReceivedOnActiveTree) {
// Set the name of each node to be its id.
ui::AXTreeUpdate initial_update;
SetUpdateTreeID(&initial_update);
initial_update.root_id = 1;
initial_update.nodes.resize(3);
std::vector<int> child_ids;
for (int i = 0; i < 3; i++) {
int id = i + 2;
child_ids.push_back(id);
initial_update.nodes[i].id = id;
initial_update.nodes[i].role = ax::mojom::Role::kStaticText;
initial_update.nodes[i].SetName(base::NumberToString(id));
initial_update.nodes[i].SetNameFrom(ax::mojom::NameFrom::kContents);
}
// No events we care about come about, so there's no distillation.
EXPECT_CALL(*distiller_, Distill).Times(0);
AccessibilityEventReceived({initial_update});
EXPECT_EQ("234", GetTextContent(1));
Mock::VerifyAndClearExpectations(distiller_);
std::vector<ui::AXTreeUpdate> updates;
for (int i = 0; i < 3; i++) {
int id = i + 5;
child_ids.push_back(id);
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.root_id = 1;
update.nodes.resize(2);
update.nodes[0].id = 1;
update.nodes[0].child_ids = child_ids;
update.nodes[1].id = id;
update.nodes[1].role = ax::mojom::Role::kStaticText;
update.nodes[1].SetName(base::NumberToString(id));
update.nodes[1].SetNameFrom(ax::mojom::NameFrom::kContents);
updates.push_back(update);
}
// Send update 0. Data gets unserialized.
EXPECT_CALL(*distiller_, Distill).Times(0);
AccessibilityEventReceived({updates[0]});
EXPECT_EQ("2345", GetTextContent(1));
Mock::VerifyAndClearExpectations(distiller_);
// Send update 1. This triggers distillation via a non-generated event. The
// data is also unserialized.
EXPECT_CALL(*distiller_, Distill).Times(1);
ui::AXEvent load_complete_1(1, ax::mojom::Event::kLoadComplete);
AccessibilityEventReceived({updates[1]}, {load_complete_1});
EXPECT_EQ("23456", GetTextContent(1));
Mock::VerifyAndClearExpectations(distiller_);
// Send update 2. Distillation is still in progress; we get a non-generated
// event. This does not result in distillation (yet). The data is not
// unserialized.
EXPECT_CALL(*distiller_, Distill).Times(0);
ui::AXEvent load_complete_2(2, ax::mojom::Event::kLoadComplete);
AccessibilityEventReceived({updates[2]}, {load_complete_2});
EXPECT_EQ("23456", GetTextContent(1));
Mock::VerifyAndClearExpectations(distiller_);
// Complete distillation. The queued up tree update gets unserialized; we also
// request distillation (deferred from above) with state
// `requires_distillation_` from the model.
EXPECT_CALL(*distiller_, Distill).Times(1);
OnAXTreeDistilled({1});
EXPECT_EQ("234567", GetTextContent(1));
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest,
AccessibilityReceivedAfterDistillingOnSameTree_DoesNotCrash) {
// Set the name of each node to be its id.
ui::AXTreeUpdate initial_update;
SetUpdateTreeID(&initial_update);
initial_update.root_id = 1;
initial_update.nodes.resize(3);
std::vector<int> child_ids;
for (int i = 0; i < 3; i++) {
int id = i + 2;
child_ids.push_back(id);
initial_update.nodes[i].id = id;
initial_update.nodes[i].role = ax::mojom::Role::kStaticText;
initial_update.nodes[i].SetName(base::NumberToString(id));
initial_update.nodes[i].SetNameFrom(ax::mojom::NameFrom::kContents);
}
// Since this update is just cosmetic (it changes the nodes' name but doesn't
// change the structure of the tree by adding or removing nodes), the
// controller does not distill.
EXPECT_CALL(*distiller_, Distill).Times(0);
AccessibilityEventReceived({initial_update});
EXPECT_EQ("234", GetTextContent(1));
Mock::VerifyAndClearExpectations(distiller_);
std::vector<ui::AXTreeUpdate> updates;
for (int i = 0; i < 3; i++) {
int id = i + 5;
child_ids.push_back(id);
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.root_id = 1;
update.nodes.resize(2);
update.nodes[0].id = 1;
update.nodes[0].child_ids = child_ids;
update.nodes[1].id = id;
update.nodes[1].role = ax::mojom::Role::kStaticText;
update.nodes[1].SetName(base::NumberToString(id));
update.nodes[1].SetNameFrom(ax::mojom::NameFrom::kContents);
updates.push_back(update);
}
// Send update 0, which starts distillation because of the load complete.
EXPECT_CALL(*distiller_, Distill).Times(1);
ui::AXEvent load_complete(1, ax::mojom::Event::kLoadComplete);
AccessibilityEventReceived({updates[0]}, {load_complete});
Mock::VerifyAndClearExpectations(distiller_);
// Send update 1. Since there's no event (generated or not) which triggers
// distllation, we have no calls.
EXPECT_CALL(*distiller_, Distill).Times(0);
AccessibilityEventReceived({updates[1]});
Mock::VerifyAndClearExpectations(distiller_);
// Ensure that there are no crashes after an accessibility event is received
// immediately after distilling.
EXPECT_CALL(*distiller_, Distill).Times(0);
OnAXTreeDistilled({1});
SetDistillationInProgress(true);
AccessibilityEventReceived({updates[2]});
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest,
DistillationInProgress_ActiveTreeIDChanges) {
// Create a couple of updates which add additional nodes to the tree.
std::vector<ui::AXTreeUpdate> updates;
std::vector<int> child_ids = {2, 3, 4};
for (int i = 0; i < 3; i++) {
int id = i + 5;
child_ids.push_back(id);
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.root_id = 1;
update.nodes.resize(2);
update.nodes[0].id = 1;
update.nodes[0].child_ids = child_ids;
update.nodes[1].id = id;
update.nodes[1].role = ax::mojom::Role::kStaticText;
update.nodes[1].SetName(base::NumberToString(id));
update.nodes[1].SetNameFrom(ax::mojom::NameFrom::kContents);
updates.push_back(update);
}
EXPECT_CALL(*distiller_, Distill).Times(0);
AccessibilityEventReceived({updates[0]});
Mock::VerifyAndClearExpectations(distiller_);
EXPECT_CALL(*distiller_, Distill).Times(1);
ui::AXEvent load_complete(1, ax::mojom::Event::kLoadComplete);
AccessibilityEventReceived({updates[1]}, {load_complete});
Mock::VerifyAndClearExpectations(distiller_);
EXPECT_CALL(*distiller_, Distill).Times(0);
AccessibilityEventReceived({updates[2]});
EXPECT_EQ("56", GetTextContent(1));
Mock::VerifyAndClearExpectations(distiller_);
// Calling OnActiveAXTreeID updates the active AXTreeID.
ui::AXTreeID tree_id_2 = ui::AXTreeID::CreateNewAXTreeID();
EXPECT_CALL(*distiller_, Distill).Times(0);
ASSERT_EQ(tree_id_, ActiveTreeId());
OnActiveAXTreeIDChanged(tree_id_2);
ASSERT_EQ(tree_id_2, ActiveTreeId());
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest,
OnAXTreeDistilledCalledWithInactiveTreeId) {
OnActiveAXTreeIDChanged(ui::AXTreeID::CreateNewAXTreeID());
// Should not crash.
OnAXTreeDistilled({});
}
TEST_F(ReadAnythingAppControllerTest,
OnAXTreeDistilledCalledWithDestroyedTreeId) {
OnAXTreeDestroyed(tree_id_);
// Should not crash.
OnAXTreeDistilled({});
}
TEST_F(ReadAnythingAppControllerTest,
OnAXTreeDistilledCalledWithUnknownActiveTreeId) {
OnActiveAXTreeIDChanged(ui::AXTreeIDUnknown());
// Should not crash.
OnAXTreeDistilled({});
}
TEST_F(ReadAnythingAppControllerTest,
OnAXTreeDistilledCalledWithUnknownTreeId) {
// Should not crash.
OnAXTreeDistilled(ui::AXTreeIDUnknown(), {});
}
TEST_F(ReadAnythingAppControllerTest,
ChangeActiveTreeWithPendingUpdates_UnknownID) {
// Create a couple of updates which add additional nodes to the tree.
std::vector<ui::AXTreeUpdate> updates;
std::vector<int> child_ids = {2, 3, 4};
for (int i = 0; i < 2; i++) {
int id = i + 5;
child_ids.push_back(id);
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.root_id = 1;
update.nodes.resize(2);
update.nodes[0].id = 1;
update.nodes[0].child_ids = child_ids;
update.nodes[1].id = id;
update.nodes[1].role = ax::mojom::Role::kStaticText;
update.nodes[1].SetName(base::NumberToString(id));
update.nodes[1].SetNameFrom(ax::mojom::NameFrom::kContents);
updates.push_back(update);
}
// Create an update which has no tree id.
ui::AXTreeUpdate update;
update.nodes.resize(1);
update.nodes[0].id = 1;
update.nodes[0].role = ax::mojom::Role::kGenericContainer;
updates.push_back(update);
// Add the three updates.
EXPECT_CALL(*distiller_, Distill).Times(0);
AccessibilityEventReceived({updates[0]});
AccessibilityEventReceived(tree_id_, {updates[1], updates[2]});
Mock::VerifyAndClearExpectations(distiller_);
// Switch to a new active tree. Should not crash.
EXPECT_CALL(*distiller_, Distill).Times(0);
OnActiveAXTreeIDChanged(ui::AXTreeIDUnknown());
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest, OnLinkClicked) {
ui::AXNodeID ax_node_id = 2;
EXPECT_CALL(page_handler_, OnLinkClicked(tree_id_, ax_node_id)).Times(1);
OnLinkClicked(ax_node_id);
page_handler_.FlushForTesting();
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest, OnLinkClicked_DistillationInProgress) {
ui::AXTreeID new_tree_id = ui::AXTreeID::CreateNewAXTreeID();
ui::AXTreeUpdate update;
SetUpdateTreeID(&update, new_tree_id);
update.root_id = 1;
update.nodes.resize(1);
update.nodes[0].id = 1;
AccessibilityEventReceived({update});
EXPECT_CALL(*distiller_, Distill).Times(1);
OnActiveAXTreeIDChanged(new_tree_id);
Mock::VerifyAndClearExpectations(distiller_);
// If distillation is in progress, OnLinkClicked should not be called.
EXPECT_CALL(page_handler_, OnLinkClicked).Times(0);
OnLinkClicked(2);
page_handler_.FlushForTesting();
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest, OnSelectionChange) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(3);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[2].id = 4;
update.nodes[0].role = ax::mojom::Role::kStaticText;
update.nodes[1].role = ax::mojom::Role::kStaticText;
update.nodes[2].role = ax::mojom::Role::kStaticText;
AccessibilityEventReceived({update});
ui::AXNodeID anchor_node_id = 2;
int anchor_offset = 0;
ui::AXNodeID focus_node_id = 3;
int focus_offset = 1;
EXPECT_CALL(page_handler_,
OnSelectionChange(tree_id_, anchor_node_id, anchor_offset,
focus_node_id, focus_offset))
.Times(1);
OnSelectionChange(anchor_node_id, anchor_offset, focus_node_id, focus_offset);
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest,
OnSelectionChange_ClickAfterClickDoesNotUpdateSelection) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(2);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[0].role = ax::mojom::Role::kStaticText;
update.nodes[1].role = ax::mojom::Role::kStaticText;
AccessibilityEventReceived({update});
ui::AXTreeUpdate selection;
SetUpdateTreeID(&selection);
selection.has_tree_data = true;
selection.event_from = ax::mojom::EventFrom::kUser;
selection.tree_data.sel_anchor_object_id = 2;
selection.tree_data.sel_focus_object_id = 2;
selection.tree_data.sel_anchor_offset = 0;
selection.tree_data.sel_focus_offset = 0;
AccessibilityEventReceived({selection});
EXPECT_CALL(page_handler_, OnSelectionChange).Times(0);
OnSelectionChange(3, 5, 3, 5);
page_handler_.FlushForTesting();
}
TEST_F(ReadAnythingAppControllerTest,
OnSelectionChange_ClickAfterSelectionClearsSelection) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(2);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[0].role = ax::mojom::Role::kStaticText;
update.nodes[1].role = ax::mojom::Role::kStaticText;
AccessibilityEventReceived({update});
ui::AXTreeUpdate selection;
SetUpdateTreeID(&selection);
selection.has_tree_data = true;
selection.event_from = ax::mojom::EventFrom::kUser;
selection.tree_data.sel_anchor_object_id = 2;
selection.tree_data.sel_focus_object_id = 3;
selection.tree_data.sel_anchor_offset = 0;
selection.tree_data.sel_focus_offset = 1;
AccessibilityEventReceived({selection});
ui::AXNodeID anchor_node_id = 3;
int anchor_offset = 5;
ui::AXNodeID focus_node_id = 3;
int focus_offset = 5;
EXPECT_CALL(page_handler_,
OnSelectionChange(tree_id_, anchor_node_id, anchor_offset,
focus_node_id, focus_offset))
.Times(1);
OnSelectionChange(anchor_node_id, anchor_offset, focus_node_id, focus_offset);
page_handler_.FlushForTesting();
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest,
OnSelectionChange_DistillationInProgress) {
ui::AXTreeID new_tree_id = ui::AXTreeID::CreateNewAXTreeID();
ui::AXTreeUpdate update;
SetUpdateTreeID(&update, new_tree_id);
update.root_id = 1;
update.nodes.resize(1);
update.nodes[0].role = ax::mojom::Role::kStaticText;
update.nodes[0].id = 1;
AccessibilityEventReceived({update});
EXPECT_CALL(*distiller_, Distill).Times(1);
OnActiveAXTreeIDChanged(new_tree_id);
Mock::VerifyAndClearExpectations(distiller_);
// If distillation is in progress, OnSelectionChange should not be called.
EXPECT_CALL(page_handler_, OnSelectionChange).Times(0);
OnSelectionChange(2, 0, 3, 1);
page_handler_.FlushForTesting();
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest,
OnSelectionChange_NonTextFieldDoesNotUpdateSelection) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(3);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[2].id = 4;
update.nodes[0].role = ax::mojom::Role::kTextField;
update.nodes[1].role = ax::mojom::Role::kGenericContainer;
update.nodes[2].role = ax::mojom::Role::kTextField;
AccessibilityEventReceived({update});
ui::AXNodeID anchor_node_id = 2;
int anchor_offset = 0;
ui::AXNodeID focus_node_id = 3;
int focus_offset = 1;
EXPECT_CALL(page_handler_,
OnSelectionChange(tree_id_, anchor_node_id, anchor_offset,
focus_node_id, focus_offset))
.Times(0);
OnSelectionChange(anchor_node_id, anchor_offset, focus_node_id, focus_offset);
page_handler_.FlushForTesting();
Mock::VerifyAndClearExpectations(distiller_);
}
TEST_F(ReadAnythingAppControllerTest, Selection_Forward) {
// Create selection from node 3-4.
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.has_tree_data = true;
update.event_from = ax::mojom::EventFrom::kUser;
update.tree_data.sel_anchor_object_id = 3;
update.tree_data.sel_focus_object_id = 4;
update.tree_data.sel_anchor_offset = 0;
update.tree_data.sel_focus_offset = 1;
update.tree_data.sel_is_backward = false;
AccessibilityEventReceived({update});
EXPECT_EQ(3, StartNodeId());
EXPECT_EQ(4, EndNodeId());
EXPECT_EQ(0, StartOffset());
EXPECT_EQ(1, EndOffset());
}
TEST_F(ReadAnythingAppControllerTest, Selection_Backward) {
// Create backward selection from node 4-3.
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.has_tree_data = true;
update.event_from = ax::mojom::EventFrom::kUser;
update.tree_data.sel_anchor_object_id = 4;
update.tree_data.sel_focus_object_id = 3;
update.tree_data.sel_anchor_offset = 1;
update.tree_data.sel_focus_offset = 0;
update.tree_data.sel_is_backward = true;
AccessibilityEventReceived({update});
EXPECT_EQ(3, StartNodeId());
EXPECT_EQ(4, EndNodeId());
EXPECT_EQ(0, StartOffset());
EXPECT_EQ(1, EndOffset());
}
TEST_F(ReadAnythingAppControllerTest, Selection_IgnoredNode) {
// Make 4 ignored and give 3 some text content.
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.root_id = 1;
update.nodes.resize(2);
update.nodes[0].id = 3;
update.nodes[1].id = 4;
update.nodes[0].role = ax::mojom::Role::kStaticText;
update.nodes[0].SetName("Hello");
update.nodes[0].SetNameFrom(ax::mojom::NameFrom::kContents);
update.nodes[1].role = ax::mojom::Role::kNone; // This node is ignored.
AccessibilityEventReceived({update});
OnAXTreeDistilled({});
// Create selection from node 2-4, where 4 is ignored.
ui::AXTreeUpdate update_2;
SetUpdateTreeID(&update_2);
update_2.tree_data.sel_anchor_object_id = 2;
update_2.tree_data.sel_focus_object_id = 4;
update_2.tree_data.sel_anchor_offset = 0;
update_2.tree_data.sel_focus_offset = 0;
update_2.tree_data.sel_is_backward = false;
AccessibilityEventReceived({update_2});
OnAXTreeDistilled({});
// We want to check that no crash occurs in this case. These node ids and
// offset are incorrect, they should be 2, 3, 0, and 5 (5 is the length of the
// world "Hello"). But because we don't have an AXTreeManager in the test,
// AXSelection::ToUnignoredSelection() exits early without calculating the
// actual ignored selection.
EXPECT_EQ(2, StartNodeId());
EXPECT_EQ(4, EndNodeId());
EXPECT_EQ(0, StartOffset());
EXPECT_EQ(0, EndOffset());
}
TEST_F(ReadAnythingAppControllerTest, Selection_IsCollapsed) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.has_tree_data = true;
update.event_from = ax::mojom::EventFrom::kUser;
update.tree_data.sel_anchor_object_id = 2;
update.tree_data.sel_focus_object_id = 2;
update.tree_data.sel_anchor_offset = 3;
update.tree_data.sel_focus_offset = 3;
AccessibilityEventReceived({update});
EXPECT_EQ(ui::kInvalidAXNodeID, StartNodeId());
EXPECT_EQ(ui::kInvalidAXNodeID, EndNodeId());
EXPECT_EQ(-1, StartOffset());
EXPECT_EQ(-1, EndOffset());
EXPECT_EQ(false, HasSelection());
}