[go: nahoru, domu]

blob: a73495638be2f66a0ae7fbe27daaa9795f5ffcc5 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/memory/raw_ptr.h"
#include "base/threading/platform_thread.h"
#include "chrome/renderer/accessibility/read_anything_app_model.h"
#include "chrome/test/base/chrome_render_view_test.h"
#include "read_anything_app_model.h"
#include "ui/accessibility/ax_enums.mojom-shared.h"
#include "ui/accessibility/ax_serializable_tree.h"
class ReadAnythingAppModelTest : public ChromeRenderViewTest {
public:
ReadAnythingAppModelTest() = default;
~ReadAnythingAppModelTest() override = default;
ReadAnythingAppModelTest(const ReadAnythingAppModelTest&) = delete;
ReadAnythingAppModelTest& operator=(const ReadAnythingAppModelTest&) = delete;
void SetUp() override {
ChromeRenderViewTest::SetUp();
model_ = new ReadAnythingAppModel();
// Create a tree id.
tree_id_ = ui::AXTreeID::CreateNewAXTreeID();
// Create simple AXTreeUpdate with a root node and 3 children.
ui::AXTreeUpdate snapshot;
ui::AXNodeData node1;
node1.id = 2;
ui::AXNodeData node2;
node2.id = 3;
ui::AXNodeData node3;
node3.id = 4;
ui::AXNodeData root;
root.id = 1;
root.child_ids = {node1.id, node2.id, node3.id};
snapshot.root_id = root.id;
snapshot.nodes = {root, node1, node2, node3};
SetUpdateTreeID(&snapshot);
AccessibilityEventReceived({snapshot});
SetActiveTreeId(tree_id_);
Reset({});
}
ui::AXTreeID SetUpPdfTrees() {
SetIsPdf(GURL("http://www.google.com/foo/bar.pdf"));
// PDF set up required for formatting checks.
ui::AXTreeID pdf_iframe_tree_id = ui::AXTreeID::CreateNewAXTreeID();
ui::AXTreeID pdf_web_contents_tree_id = ui::AXTreeID::CreateNewAXTreeID();
// Send update for main web content with child tree (pdf web contents).
ui::AXTreeUpdate main_web_contents_update;
SetUpdateTreeID(&main_web_contents_update);
ui::AXNodeData node;
node.id = 1;
node.AddChildTreeId(pdf_web_contents_tree_id);
main_web_contents_update.nodes = {node};
AccessibilityEventReceived({main_web_contents_update});
// Send update for pdf web contents with child tree (iframe).
ui::AXTreeUpdate pdf_web_contents_update;
ui::AXNodeData pdf_node;
pdf_node.id = 1;
pdf_node.AddChildTreeId(pdf_iframe_tree_id);
pdf_web_contents_update.root_id = pdf_node.id;
pdf_web_contents_update.nodes = {pdf_node};
SetUpdateTreeID(&pdf_web_contents_update, pdf_web_contents_tree_id);
AccessibilityEventReceived({pdf_web_contents_update});
return pdf_iframe_tree_id;
}
void SetUpdateTreeID(ui::AXTreeUpdate* update) {
SetUpdateTreeID(update, tree_id_);
}
void SetDistillationInProgress(bool distillation) {
model_->SetDistillationInProgress(distillation);
}
bool AreAllPendingUpdatesEmpty() {
size_t count = 0;
for (auto const& [tree_id, updates] :
model_->GetPendingUpdatesForTesting()) {
count += updates.size();
}
return count == 0;
}
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,
bool links_enabled,
SkColor foreground_color,
SkColor background_color,
int line_spacing,
int letter_spacing) {
auto line_spacing_enum =
static_cast<read_anything::mojom::LineSpacing>(line_spacing);
auto letter_spacing_enum =
static_cast<read_anything::mojom::LetterSpacing>(letter_spacing);
model_->OnThemeChanged(read_anything::mojom::ReadAnythingTheme::New(
font_name, font_size, links_enabled, foreground_color, background_color,
line_spacing_enum, letter_spacing_enum));
}
void SetLineAndLetterSpacing(
read_anything::mojom::LetterSpacing letter_spacing,
read_anything::mojom::LineSpacing line_spacing) {
model_->OnThemeChanged(read_anything::mojom::ReadAnythingTheme::New(
"Arial", 15.0, false, SkColorSetRGB(0x33, 0x40, 0x36),
SkColorSetRGB(0xDF, 0xD2, 0x63), line_spacing, letter_spacing));
}
void AccessibilityEventReceived(
const std::vector<ui::AXTreeUpdate>& updates) {
AccessibilityEventReceived(updates[0].tree_data.tree_id, updates);
}
void AccessibilityEventReceived(
const ui::AXTreeID& tree_id,
const std::vector<ui::AXTreeUpdate>& updates) {
model_->AccessibilityEventReceived(tree_id, updates, {});
}
void SetActiveTreeId(ui::AXTreeID tree_id) {
model_->SetActiveTreeId(tree_id);
}
void UnserializePendingUpdates(ui::AXTreeID tree_id) {
model_->UnserializePendingUpdates(tree_id);
}
void ClearPendingUpdates() { model_->ClearPendingUpdates(); }
std::string FontName() { return model_->font_name(); }
float FontSize() { return model_->font_size(); }
bool LinksEnabled() { return model_->links_enabled(); }
SkColor ForegroundColor() { return model_->foreground_color(); }
SkColor BackgroundColor() { return model_->background_color(); }
float LineSpacing() { return model_->line_spacing(); }
float LetterSpacing() { return model_->letter_spacing(); }
bool DistillationInProgress() { return model_->distillation_in_progress(); }
bool HasSelection() { return model_->has_selection(); }
ui::AXNodeID StartNodeId() { return model_->start_node_id(); }
ui::AXNodeID EndNodeId() { return model_->end_node_id(); }
int32_t StartOffset() { return model_->start_offset(); }
int32_t EndOffset() { return model_->end_offset(); }
bool IsNodeIgnoredForReadAnything(ui::AXNodeID ax_node_id) {
return model_->IsNodeIgnoredForReadAnything(ax_node_id);
}
size_t GetNumTrees() { return model_->GetTreesForTesting()->size(); }
bool HasTree(ui::AXTreeID tree_id) { return model_->ContainsTree(tree_id); }
void EraseTree(ui::AXTreeID tree_id) { model_->EraseTreeForTesting(tree_id); }
void AddTree(ui::AXTreeID tree_id,
std::unique_ptr<ui::AXSerializableTree> tree) {
model_->AddTree(tree_id, std::move(tree));
}
size_t GetNumPendingUpdates(ui::AXTreeID tree_id) {
return model_->GetPendingUpdatesForTesting()[tree_id].size();
}
void Reset(const std::vector<ui::AXNodeID>& content_node_ids) {
model_->Reset(content_node_ids);
}
bool ContentNodeIdsContains(ui::AXNodeID ax_node_id) {
return base::Contains(model_->content_node_ids(), ax_node_id);
}
bool DisplayNodeIdsContains(ui::AXNodeID ax_node_id) {
return base::Contains(model_->display_node_ids(), ax_node_id);
}
bool DisplayNodeIdsIsEmpty() { return model_->display_node_ids().empty(); }
bool SelectionNodeIdsContains(ui::AXNodeID ax_node_id) {
return base::Contains(model_->selection_node_ids(), ax_node_id);
}
void ProcessDisplayNodes(const std::vector<ui::AXNodeID>& content_node_ids) {
Reset(content_node_ids);
model_->ComputeDisplayNodeIdsForDistilledTree();
}
bool ProcessSelection() { return model_->PostProcessSelection(); }
bool RequiresDistillation() { return model_->requires_distillation(); }
bool RequiresPostProcessSelection() {
return model_->requires_post_process_selection();
}
void SetRequiresPostProcessSelection(bool requires_post_process_selection) {
model_->set_requires_post_process_selection(
requires_post_process_selection);
}
void SetSelectionFromAction(bool selection_from_action) {
model_->set_selection_from_action(selection_from_action);
}
void OnSelection(ax::mojom::EventFrom event_from) {
model_->OnSelection(event_from);
}
void IncreaseTextSize() { model_->IncreaseTextSize(); }
void DecreaseTextSize() { model_->DecreaseTextSize(); }
void ResetTextSize() { model_->ResetTextSize(); }
std::string DefaultLanguageCode() { return model_->default_language_code(); }
void SetLanguageCode(std::string code) {
model_->set_default_language_code(code);
}
std::vector<std::string> GetSupportedFonts() {
return model_->GetSupportedFonts();
}
bool IsPDFFormatted() { return model_->IsPDFFormatted(); }
void SetIsPdf(const GURL& url) { return model_->SetIsPdf(url); }
bool IsPdf() { return model_->is_pdf(); }
ui::AXTreeID GetPDFWebContents() { return model_->GetPDFWebContents(); }
void InitAXPosition(const ui::AXNodeID id) {
model_->InitAXPositionWithNode(id);
}
ui::AXNodePosition::AXPositionInstance GetNextNodePosition() {
ReadAnythingAppModel::ReadAloudCurrentGranularity granularity =
ReadAnythingAppModel::ReadAloudCurrentGranularity();
return model_->GetNextValidPositionFromCurrentPosition(granularity);
}
ui::AXNodePosition::AXPositionInstance GetNextNodePosition(
ReadAnythingAppModel::ReadAloudCurrentGranularity granularity) {
return model_->GetNextValidPositionFromCurrentPosition(granularity);
}
ReadAnythingAppModel::ReadAloudCurrentGranularity GetNextNodes() {
return model_->GetNextNodes();
}
size_t GetNextSentence(const std::u16string& text) {
return model_->GetNextSentence(text);
}
int GetCurrentTextStartIndex(ui::AXNodeID id) {
return model_->GetCurrentTextStartIndex(id);
}
int GetCurrentTextEndIndex(ui::AXNodeID id) {
return model_->GetCurrentTextEndIndex(id);
}
ui::AXTreeID tree_id_;
private:
// ReadAnythingAppModel constructor and destructor are private so it's
// not accessible by std::make_unique.
raw_ptr<ReadAnythingAppModel, ExperimentalRenderer> model_ = nullptr;
};
TEST_F(ReadAnythingAppModelTest, Theme) {
std::string font_name = "Roboto";
float font_size = 18.0;
bool links_enabled = false;
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, links_enabled, foreground,
background, line_spacing, letter_spacing);
EXPECT_EQ(font_name, FontName());
EXPECT_EQ(font_size, FontSize());
EXPECT_EQ(links_enabled, LinksEnabled());
EXPECT_EQ(foreground, ForegroundColor());
EXPECT_EQ(background, BackgroundColor());
EXPECT_EQ(line_spacing_value, LineSpacing());
EXPECT_EQ(letter_spacing_value, LetterSpacing());
}
TEST_F(ReadAnythingAppModelTest, IsNodeIgnoredForReadAnything) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData static_text_node;
static_text_node.id = 2;
static_text_node.role = ax::mojom::Role::kStaticText;
ui::AXNodeData combobox_node;
combobox_node.id = 3;
combobox_node.role = ax::mojom::Role::kComboBoxGrouping;
ui::AXNodeData button_node;
button_node.id = 4;
button_node.role = ax::mojom::Role::kButton;
update.nodes = {static_text_node, combobox_node, button_node};
AccessibilityEventReceived({update});
EXPECT_EQ(false, IsNodeIgnoredForReadAnything(2));
EXPECT_EQ(true, IsNodeIgnoredForReadAnything(3));
EXPECT_EQ(true, IsNodeIgnoredForReadAnything(4));
}
TEST_F(ReadAnythingAppModelTest,
IsNodeIgnoredForReadAnything_TextFieldsNotIgnored) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData tree_node;
tree_node.id = 2;
tree_node.role = ax::mojom::Role::kTree;
ui::AXNodeData textfield_with_combobox_node;
textfield_with_combobox_node.id = 3;
textfield_with_combobox_node.role = ax::mojom::Role::kTextFieldWithComboBox;
ui::AXNodeData textfield_node;
textfield_node.id = 4;
textfield_node.role = ax::mojom::Role::kTextField;
update.nodes = {tree_node, textfield_with_combobox_node, textfield_node};
AccessibilityEventReceived({update});
EXPECT_EQ(true, IsNodeIgnoredForReadAnything(2));
EXPECT_EQ(false, IsNodeIgnoredForReadAnything(3));
EXPECT_EQ(false, IsNodeIgnoredForReadAnything(4));
}
TEST_F(ReadAnythingAppModelTest,
IsNodeIgnoredForReadAnything_InaccessiblePDFPageNodes) {
ui::AXTreeID pdf_iframe_tree_id = SetUpPdfTrees();
// PDF OCR output contains kBanner and kContentInfo (each with a static text
// node child) to mark page start/end.
ui::AXTreeUpdate update;
SetUpdateTreeID(&update, pdf_iframe_tree_id);
ui::AXNodeData banner_node;
banner_node.id = 2;
banner_node.role = ax::mojom::Role::kBanner;
ui::AXNodeData static_text_start_node;
static_text_start_node.id = 3;
static_text_start_node.role = ax::mojom::Role::kStaticText;
static_text_start_node.SetNameChecked(string_constants::kPDFPageStart);
banner_node.child_ids = {static_text_start_node.id};
ui::AXNodeData content_info_node;
content_info_node.id = 4;
content_info_node.role = ax::mojom::Role::kContentInfo;
ui::AXNodeData static_text_end_node;
static_text_end_node.id = 5;
static_text_end_node.role = ax::mojom::Role::kStaticText;
static_text_end_node.SetNameChecked(string_constants::kPDFPageEnd);
content_info_node.child_ids = {static_text_end_node.id};
ui::AXNodeData root;
root.id = 1;
root.child_ids = {banner_node.id, content_info_node.id};
root.role = ax::mojom::Role::kPdfRoot;
update.root_id = root.id;
update.nodes = {root, banner_node, static_text_start_node, content_info_node,
static_text_end_node};
AccessibilityEventReceived({update});
EXPECT_EQ(true, IsNodeIgnoredForReadAnything(2));
EXPECT_EQ(true, IsNodeIgnoredForReadAnything(3));
EXPECT_EQ(false, IsNodeIgnoredForReadAnything(4));
EXPECT_EQ(true, IsNodeIgnoredForReadAnything(5));
}
TEST_F(ReadAnythingAppModelTest, ModelUpdatesTreeState) {
// Set up trees.
ui::AXTreeID tree_id_2 = ui::AXTreeID::CreateNewAXTreeID();
ui::AXTreeID tree_id_3 = ui::AXTreeID::CreateNewAXTreeID();
AddTree(tree_id_2, std::make_unique<ui::AXSerializableTree>());
AddTree(tree_id_3, std::make_unique<ui::AXSerializableTree>());
ASSERT_EQ(3u, GetNumTrees());
ASSERT_TRUE(HasTree(tree_id_2));
ASSERT_TRUE(HasTree(tree_id_3));
ASSERT_TRUE(HasTree(tree_id_));
// Remove one tree.
EraseTree(tree_id_2);
ASSERT_EQ(2u, GetNumTrees());
ASSERT_TRUE(HasTree(tree_id_3));
ASSERT_FALSE(HasTree(tree_id_2));
ASSERT_TRUE(HasTree(tree_id_));
// Remove the second tree.
EraseTree(tree_id_);
ASSERT_EQ(1u, GetNumTrees());
ASSERT_TRUE(HasTree(tree_id_3));
ASSERT_FALSE(HasTree(tree_id_2));
ASSERT_FALSE(HasTree(tree_id_));
// Remove the last tree.
EraseTree(tree_id_3);
ASSERT_EQ(0u, GetNumTrees());
ASSERT_FALSE(HasTree(tree_id_3));
ASSERT_FALSE(HasTree(tree_id_2));
ASSERT_FALSE(HasTree(tree_id_));
}
TEST_F(ReadAnythingAppModelTest, 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]);
ui::AXNodeData node;
node.id = 1;
update.nodes = {node};
update.root_id = node.id;
updates.push_back(update);
}
// Start with 1 tree (the tree created in SetUp).
ASSERT_EQ(1u, GetNumTrees());
ASSERT_TRUE(HasTree(tree_id_));
// Add the two trees.
AccessibilityEventReceived({updates[0]});
ASSERT_EQ(2u, GetNumTrees());
ASSERT_TRUE(HasTree(tree_id_));
ASSERT_TRUE(HasTree(tree_ids[0]));
AccessibilityEventReceived({updates[1]});
ASSERT_EQ(3u, GetNumTrees());
ASSERT_TRUE(HasTree(tree_id_));
ASSERT_TRUE(HasTree(tree_ids[0]));
ASSERT_TRUE(HasTree(tree_ids[1]));
// Remove all of the trees.
EraseTree(tree_id_);
ASSERT_EQ(2u, GetNumTrees());
ASSERT_TRUE(HasTree(tree_ids[0]));
ASSERT_TRUE(HasTree(tree_ids[1]));
EraseTree(tree_ids[0]);
ASSERT_EQ(1u, GetNumTrees());
ASSERT_TRUE(HasTree(tree_ids[1]));
EraseTree(tree_ids[1]);
ASSERT_EQ(0u, GetNumTrees());
}
TEST_F(ReadAnythingAppModelTest,
DistillationInProgress_TreeUpdateReceivedOnInactiveTree) {
EXPECT_EQ(0u, GetNumPendingUpdates(tree_id_));
// Create a new tree.
ui::AXTreeID tree_id_2 = ui::AXTreeID::CreateNewAXTreeID();
ui::AXTreeUpdate update_2;
SetUpdateTreeID(&update_2, tree_id_2);
ui::AXNodeData node;
node.id = 1;
update_2.root_id = node.id;
update_2.nodes = {node};
// Updates on inactive trees are processed immediately and are not marked as
// pending.
AccessibilityEventReceived({update_2});
EXPECT_EQ(0u, GetNumPendingUpdates(tree_id_));
}
TEST_F(ReadAnythingAppModelTest,
AddPendingUpdatesAfterUnserializingOnSameTree_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].SetNameChecked(base::NumberToString(id));
}
AccessibilityEventReceived({initial_update});
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;
ui::AXNodeData root;
root.id = 1;
root.child_ids = child_ids;
ui::AXNodeData node;
node.id = id;
node.role = ax::mojom::Role::kStaticText;
node.SetNameChecked(base::NumberToString(id));
update.nodes = {root, node};
updates.push_back(update);
}
// Send update 0, which starts distillation.
AccessibilityEventReceived({updates[0]});
EXPECT_EQ(0u, GetNumPendingUpdates(tree_id_));
ASSERT_TRUE(AreAllPendingUpdatesEmpty());
// Send update 1. Since distillation is in progress, this will not be
// unserialized yet.
SetDistillationInProgress(true);
AccessibilityEventReceived({updates[1]});
EXPECT_EQ(1u, GetNumPendingUpdates(tree_id_));
// Ensure that there are no crashes after an accessibility event is received
// immediately after unserializing.
UnserializePendingUpdates(tree_id_);
SetDistillationInProgress(true);
AccessibilityEventReceived({updates[2]});
EXPECT_EQ(1u, GetNumPendingUpdates(tree_id_));
ASSERT_FALSE(AreAllPendingUpdatesEmpty());
}
TEST_F(ReadAnythingAppModelTest, OnTreeErased_ClearsPendingUpdates) {
// 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].SetNameChecked(base::NumberToString(id));
}
AccessibilityEventReceived({initial_update});
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);
ui::AXNodeData root;
root.id = 1;
root.child_ids = child_ids;
ui::AXNodeData node;
node.id = id;
node.role = ax::mojom::Role::kStaticText;
node.SetNameChecked(base::NumberToString(id));
update.root_id = root.id;
update.nodes = {root, node};
updates.push_back(update);
}
// Send update 0, which starts distillation.
AccessibilityEventReceived({updates[0]});
EXPECT_EQ(0u, GetNumPendingUpdates(tree_id_));
ASSERT_TRUE(AreAllPendingUpdatesEmpty());
// Send update 1. Since distillation is in progress, this will not be
// unserialized yet.
SetDistillationInProgress(true);
AccessibilityEventReceived({updates[1]});
EXPECT_EQ(1u, GetNumPendingUpdates(tree_id_));
// Destroy the tree.
EraseTree(tree_id_);
EXPECT_EQ(0u, GetNumPendingUpdates(tree_id_));
}
TEST_F(ReadAnythingAppModelTest,
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].SetNameChecked(base::NumberToString(id));
}
AccessibilityEventReceived({initial_update});
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);
ui::AXNodeData root;
root.id = 1;
root.child_ids = child_ids;
ui::AXNodeData node;
node.id = id;
node.role = ax::mojom::Role::kStaticText;
node.SetNameChecked(base::NumberToString(id));
update.root_id = root.id;
update.nodes = {root, node};
updates.push_back(update);
}
// Send update 0, which starts distillation.
AccessibilityEventReceived({updates[0]});
EXPECT_EQ(0u, GetNumPendingUpdates(tree_id_));
ASSERT_TRUE(AreAllPendingUpdatesEmpty());
// Send update 1. Since distillation is in progress, this will not be
// unserialized yet.
SetDistillationInProgress(true);
AccessibilityEventReceived({updates[1]});
EXPECT_EQ(1u, GetNumPendingUpdates(tree_id_));
// Send update 2. This is still not unserialized yet.
AccessibilityEventReceived({updates[2]});
EXPECT_EQ(2u, GetNumPendingUpdates(tree_id_));
// Complete distillation which unserializes the pending updates and distills
// them.
UnserializePendingUpdates(tree_id_);
EXPECT_EQ(0u, GetNumPendingUpdates(tree_id_));
ASSERT_TRUE(AreAllPendingUpdatesEmpty());
}
TEST_F(ReadAnythingAppModelTest, ClearPendingUpdates_DeletesPendingUpdates) {
EXPECT_EQ(0u, GetNumPendingUpdates(tree_id_));
// 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);
ui::AXNodeData root;
root.id = 1;
root.child_ids = child_ids;
ui::AXNodeData node;
node.id = id;
node.role = ax::mojom::Role::kStaticText;
node.SetNameChecked(base::NumberToString(id));
update.root_id = root.id;
update.nodes = {root, node};
updates.push_back(update);
}
AccessibilityEventReceived({updates[0]});
EXPECT_EQ(0u, GetNumPendingUpdates(tree_id_));
SetDistillationInProgress(true);
AccessibilityEventReceived({updates[1]});
EXPECT_EQ(1u, GetNumPendingUpdates(tree_id_));
AccessibilityEventReceived({updates[2]});
EXPECT_EQ(2u, GetNumPendingUpdates(tree_id_));
// Clearing the pending updates correctly deletes the pending updates.
ClearPendingUpdates();
ASSERT_TRUE(AreAllPendingUpdatesEmpty());
}
TEST_F(ReadAnythingAppModelTest, ChangeActiveTreeWithPendingUpdates_UnknownID) {
EXPECT_EQ(0u, GetNumPendingUpdates(tree_id_));
ASSERT_TRUE(AreAllPendingUpdatesEmpty());
// 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);
ui::AXNodeData root;
root.id = 1;
root.child_ids = child_ids;
ui::AXNodeData node;
node.id = id;
node.role = ax::mojom::Role::kStaticText;
node.SetNameChecked(base::NumberToString(id));
update.root_id = root.id;
update.nodes = {root, node};
updates.push_back(update);
}
// Create an update which has no tree id.
ui::AXTreeUpdate update;
ui::AXNodeData node;
node.id = 1;
node.role = ax::mojom::Role::kGenericContainer;
update.nodes = {node};
updates.push_back(update);
// Add the three updates.
AccessibilityEventReceived({updates[0]});
EXPECT_EQ(0u, GetNumPendingUpdates(tree_id_));
ASSERT_TRUE(AreAllPendingUpdatesEmpty());
SetDistillationInProgress(true);
AccessibilityEventReceived(tree_id_, {updates[1], updates[2]});
EXPECT_EQ(2u, GetNumPendingUpdates(tree_id_));
// Switch to a new active tree. Should not crash.
SetActiveTreeId(ui::AXTreeIDUnknown());
}
TEST_F(ReadAnythingAppModelTest, DisplayNodeIdsContains_ContentNodes) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData node1;
node1.id = 5;
ui::AXNodeData node2;
node2.id = 6;
ui::AXNodeData parent_node;
parent_node.id = 4;
parent_node.child_ids = {node1.id, node2.id};
update.nodes = {parent_node, node1, node2};
// This update changes the structure of the tree. When the controller receives
// it in AccessibilityEventReceived, it will re-distill the tree.
AccessibilityEventReceived({update});
ProcessDisplayNodes({3, 4});
EXPECT_TRUE(DisplayNodeIdsContains(1));
EXPECT_FALSE(DisplayNodeIdsContains(2));
EXPECT_TRUE(DisplayNodeIdsContains(3));
EXPECT_TRUE(DisplayNodeIdsContains(4));
EXPECT_TRUE(DisplayNodeIdsContains(5));
EXPECT_TRUE(DisplayNodeIdsContains(6));
}
TEST_F(ReadAnythingAppModelTest,
DisplayNodeIdsDoesNotContain_InvisibleOrIgnoredNodes) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(3);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[1].AddState(ax::mojom::State::kInvisible);
update.nodes[2].id = 4;
update.nodes[2].AddState(ax::mojom::State::kIgnored);
AccessibilityEventReceived({update});
ProcessDisplayNodes({2, 3, 4});
EXPECT_TRUE(DisplayNodeIdsContains(1));
EXPECT_TRUE(DisplayNodeIdsContains(2));
EXPECT_FALSE(DisplayNodeIdsContains(3));
EXPECT_FALSE(DisplayNodeIdsContains(4));
}
TEST_F(ReadAnythingAppModelTest,
DisplayNodeIdsEmpty_WhenContentNodesAreAllHeadings) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
// All content nodes are heading nodes.
update.nodes.resize(3);
update.nodes[0].id = 2;
update.nodes[0].role = ax::mojom::Role::kHeading;
update.nodes[1].id = 3;
update.nodes[1].role = ax::mojom::Role::kHeading;
update.nodes[2].id = 4;
update.nodes[2].role = ax::mojom::Role::kHeading;
AccessibilityEventReceived({update});
ProcessDisplayNodes({2, 3, 4});
EXPECT_TRUE(DisplayNodeIdsIsEmpty());
// Content node is static text node with heading parent.
update.nodes.resize(3);
update.nodes[0].id = 1;
update.nodes[0].child_ids = {2};
update.nodes[1].id = 2;
update.nodes[1].role = ax::mojom::Role::kHeading;
update.nodes[1].child_ids = {3};
update.nodes[2].id = 3;
update.nodes[2].role = ax::mojom::Role::kStaticText;
AccessibilityEventReceived({update});
ProcessDisplayNodes({3});
EXPECT_TRUE(DisplayNodeIdsIsEmpty());
// Content node is inline text box with heading grandparent.
update.nodes.resize(4);
update.nodes[0].id = 1;
update.nodes[0].child_ids = {2};
update.nodes[1].id = 2;
update.nodes[1].role = ax::mojom::Role::kHeading;
update.nodes[1].child_ids = {3};
update.nodes[2].id = 3;
update.nodes[2].role = ax::mojom::Role::kStaticText;
update.nodes[2].child_ids = {4};
update.nodes[3].id = 4;
update.nodes[3].role = ax::mojom::Role::kInlineTextBox;
AccessibilityEventReceived({update});
ProcessDisplayNodes({4});
EXPECT_TRUE(DisplayNodeIdsIsEmpty());
}
TEST_F(ReadAnythingAppModelTest,
SelectionNodeIdsContains_SelectionAndNearbyNodes) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
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});
ProcessSelection();
EXPECT_TRUE(SelectionNodeIdsContains(1));
EXPECT_TRUE(SelectionNodeIdsContains(2));
EXPECT_TRUE(SelectionNodeIdsContains(3));
EXPECT_TRUE(SelectionNodeIdsContains(4));
}
TEST_F(ReadAnythingAppModelTest,
SelectionNodeIdsContains_BackwardSelectionAndNearbyNodes) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
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});
ProcessSelection();
EXPECT_TRUE(SelectionNodeIdsContains(1));
EXPECT_TRUE(SelectionNodeIdsContains(2));
EXPECT_TRUE(SelectionNodeIdsContains(3));
EXPECT_TRUE(SelectionNodeIdsContains(4));
}
TEST_F(ReadAnythingAppModelTest,
SelectionNodeIdsDoesNotContain_InvisibleOrIgnoredNodes) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.nodes.resize(3);
update.nodes[0].id = 2;
update.nodes[1].id = 3;
update.nodes[1].AddState(ax::mojom::State::kInvisible);
update.nodes[2].id = 4;
update.nodes[2].AddState(ax::mojom::State::kIgnored);
update.tree_data.sel_anchor_object_id = 2;
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});
ProcessSelection();
EXPECT_FALSE(DisplayNodeIdsContains(1));
EXPECT_FALSE(SelectionNodeIdsContains(2));
EXPECT_FALSE(SelectionNodeIdsContains(3));
EXPECT_FALSE(SelectionNodeIdsContains(4));
}
TEST_F(ReadAnythingAppModelTest, SetTheme_LineAndLetterSpacingCorrect) {
SetLineAndLetterSpacing(read_anything::mojom::LetterSpacing::kStandard,
read_anything::mojom::LineSpacing::kLoose);
ASSERT_EQ(LineSpacing(), 1.5);
ASSERT_EQ(LetterSpacing(), 0);
// Ensure the line and letter spacing are updated.
SetLineAndLetterSpacing(read_anything::mojom::LetterSpacing::kWide,
read_anything::mojom::LineSpacing::kVeryLoose);
ASSERT_EQ(LineSpacing(), 2.0);
ASSERT_EQ(LetterSpacing(), 0.05f);
}
TEST_F(ReadAnythingAppModelTest, Reset_ResetsState) {
// Initial state.
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData node1;
node1.id = 5;
ui::AXNodeData node2;
node2.id = 6;
ui::AXNodeData root;
root.id = 4;
root.child_ids = {node1.id, node2.id};
update.nodes = {root, node1, node2};
AccessibilityEventReceived({update});
ProcessDisplayNodes({3, 4});
SetDistillationInProgress(true);
// Assert initial state before resetting.
ASSERT_TRUE(DistillationInProgress());
ASSERT_TRUE(DisplayNodeIdsContains(1));
ASSERT_TRUE(DisplayNodeIdsContains(3));
ASSERT_TRUE(DisplayNodeIdsContains(4));
ASSERT_TRUE(DisplayNodeIdsContains(5));
ASSERT_TRUE(DisplayNodeIdsContains(6));
Reset({1, 2});
// Assert reset state.
ASSERT_FALSE(DistillationInProgress());
ASSERT_TRUE(ContentNodeIdsContains(1));
ASSERT_TRUE(ContentNodeIdsContains(2));
ASSERT_FALSE(DisplayNodeIdsContains(1));
ASSERT_FALSE(DisplayNodeIdsContains(3));
ASSERT_FALSE(DisplayNodeIdsContains(4));
ASSERT_FALSE(DisplayNodeIdsContains(5));
ASSERT_FALSE(DisplayNodeIdsContains(6));
// Calling reset with different content nodes updates the content nodes.
Reset({5, 4});
ASSERT_FALSE(ContentNodeIdsContains(1));
ASSERT_FALSE(ContentNodeIdsContains(2));
ASSERT_TRUE(ContentNodeIdsContains(5));
ASSERT_TRUE(ContentNodeIdsContains(4));
}
TEST_F(ReadAnythingAppModelTest, Reset_ResetsSelectionState) {
// Initial state.
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
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});
ProcessSelection();
// Assert initial selection state.
ASSERT_TRUE(SelectionNodeIdsContains(1));
ASSERT_TRUE(SelectionNodeIdsContains(2));
ASSERT_TRUE(SelectionNodeIdsContains(3));
ASSERT_TRUE(HasSelection());
ASSERT_NE(StartOffset(), -1);
ASSERT_NE(EndOffset(), -1);
ASSERT_NE(StartNodeId(), ui::kInvalidAXNodeID);
ASSERT_NE(EndNodeId(), ui::kInvalidAXNodeID);
Reset({1, 2});
// Assert reset selection state.
ASSERT_FALSE(SelectionNodeIdsContains(1));
ASSERT_FALSE(SelectionNodeIdsContains(2));
ASSERT_FALSE(SelectionNodeIdsContains(3));
ASSERT_FALSE(HasSelection());
ASSERT_EQ(StartOffset(), -1);
ASSERT_EQ(EndOffset(), -1);
ASSERT_EQ(StartNodeId(), ui::kInvalidAXNodeID);
ASSERT_EQ(EndNodeId(), ui::kInvalidAXNodeID);
}
TEST_F(ReadAnythingAppModelTest, PostProcessSelection_SelectionStateCorrect) {
// Initial state.
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
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});
SetRequiresPostProcessSelection(true);
ProcessSelection();
ASSERT_FALSE(RequiresPostProcessSelection());
ASSERT_TRUE(HasSelection());
ASSERT_TRUE(SelectionNodeIdsContains(1));
ASSERT_TRUE(SelectionNodeIdsContains(2));
ASSERT_TRUE(SelectionNodeIdsContains(3));
ASSERT_EQ(StartOffset(), 0);
ASSERT_EQ(EndOffset(), 0);
ASSERT_EQ(StartNodeId(), 2);
ASSERT_EQ(EndNodeId(), 3);
}
TEST_F(ReadAnythingAppModelTest, PostProcessSelectionFromAction_DoesNotDraw) {
// Initial state.
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
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});
ProcessDisplayNodes({2, 3});
SetSelectionFromAction(true);
ASSERT_FALSE(ProcessSelection());
}
TEST_F(ReadAnythingAppModelTest,
StartAndEndNodesHaveDifferentParents_SelectionStateCorrect) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData static_text_node1;
static_text_node1.id = 2;
static_text_node1.role = ax::mojom::Role::kStaticText;
ui::AXNodeData static_text_node2;
static_text_node2.id = 3;
static_text_node2.role = ax::mojom::Role::kStaticText;
ui::AXNodeData generic_container_node;
generic_container_node.id = 4;
generic_container_node.role = ax::mojom::Role::kGenericContainer;
ui::AXNodeData static_text_child_node1;
static_text_child_node1.id = 5;
static_text_child_node1.role = ax::mojom::Role::kStaticText;
ui::AXNodeData static_text_child_node2;
static_text_child_node2.id = 6;
static_text_child_node2.role = ax::mojom::Role::kStaticText;
ui::AXNodeData parent_node;
parent_node.id = 1;
parent_node.child_ids = {static_text_node1.id, static_text_node2.id,
generic_container_node.id};
parent_node.role = ax::mojom::Role::kStaticText;
generic_container_node.child_ids = {static_text_child_node1.id,
static_text_child_node2.id};
update.nodes = {parent_node,
static_text_node1,
static_text_node2,
generic_container_node,
static_text_child_node1,
static_text_child_node2};
AccessibilityEventReceived({update});
update.tree_data.sel_anchor_object_id = 2;
update.tree_data.sel_focus_object_id = 5;
update.tree_data.sel_anchor_offset = 0;
update.tree_data.sel_focus_offset = 0;
update.tree_data.sel_is_backward = false;
AccessibilityEventReceived({update});
ProcessSelection();
ASSERT_TRUE(HasSelection());
ASSERT_EQ(StartNodeId(), 2);
ASSERT_EQ(EndNodeId(), 5);
// 1 and 3 are ancestors, so they are included as selection nodes..
ASSERT_TRUE(SelectionNodeIdsContains(1));
ASSERT_TRUE(SelectionNodeIdsContains(3));
ASSERT_TRUE(SelectionNodeIdsContains(5));
ASSERT_TRUE(SelectionNodeIdsContains(6));
// Even though 3 is a generic container with more than one child, its sibling
// nodes are included in the selection because the start node includes it.
ASSERT_TRUE(SelectionNodeIdsContains(2));
ASSERT_TRUE(SelectionNodeIdsContains(3));
}
TEST_F(ReadAnythingAppModelTest,
SelectionParentIsLinkAndInlineBlock_SelectionStateCorrect) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData static_text_node;
static_text_node.id = 2;
static_text_node.role = ax::mojom::Role::kStaticText;
ui::AXNodeData link_node;
link_node.id = 3;
link_node.role = ax::mojom::Role::kLink;
link_node.AddStringAttribute(ax::mojom::StringAttribute::kDisplay, "block");
ui::AXNodeData inline_block_node;
inline_block_node.id = 4;
inline_block_node.role = ax::mojom::Role::kStaticText;
inline_block_node.AddStringAttribute(ax::mojom::StringAttribute::kDisplay,
"inline-block");
link_node.child_ids = {inline_block_node.id};
ui::AXNodeData root;
root.id = 1;
root.child_ids = {static_text_node.id, link_node.id};
root.role = ax::mojom::Role::kStaticText;
update.nodes = {root, static_text_node, link_node, inline_block_node};
AccessibilityEventReceived({update});
update.tree_data.sel_anchor_object_id = 4;
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});
ProcessSelection();
ASSERT_TRUE(HasSelection());
ASSERT_EQ(StartNodeId(), 4);
ASSERT_EQ(EndNodeId(), 4);
ASSERT_TRUE(SelectionNodeIdsContains(1));
ASSERT_FALSE(SelectionNodeIdsContains(2));
ASSERT_TRUE(SelectionNodeIdsContains(3));
ASSERT_TRUE(SelectionNodeIdsContains(4));
}
TEST_F(ReadAnythingAppModelTest,
SelectionParentIsListItem_SelectionStateCorrect) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData static_text_node;
static_text_node.id = 2;
static_text_node.role = ax::mojom::Role::kStaticText;
ui::AXNodeData link_node;
link_node.id = 3;
link_node.role = ax::mojom::Role::kLink;
link_node.AddStringAttribute(ax::mojom::StringAttribute::kDisplay, "block");
ui::AXNodeData static_text_list_node;
static_text_list_node.id = 4;
static_text_list_node.role = ax::mojom::Role::kStaticText;
static_text_list_node.AddStringAttribute(ax::mojom::StringAttribute::kDisplay,
"list-item");
link_node.child_ids = {static_text_list_node.id};
ui::AXNodeData parent_node;
parent_node.id = 1;
parent_node.child_ids = {static_text_node.id, link_node.id};
parent_node.role = ax::mojom::Role::kStaticText;
update.nodes = {parent_node, static_text_node, link_node,
static_text_list_node};
AccessibilityEventReceived({update});
update.tree_data.sel_anchor_object_id = 4;
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});
ProcessSelection();
ASSERT_TRUE(HasSelection());
ASSERT_EQ(StartNodeId(), 4);
ASSERT_EQ(EndNodeId(), 4);
ASSERT_TRUE(SelectionNodeIdsContains(1));
ASSERT_FALSE(SelectionNodeIdsContains(2));
ASSERT_TRUE(SelectionNodeIdsContains(3));
ASSERT_TRUE(SelectionNodeIdsContains(4));
}
TEST_F(ReadAnythingAppModelTest,
SelectionParentIsGenericContainerAndInline_SelectionStateCorrect) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData static_text_node;
static_text_node.id = 2;
static_text_node.role = ax::mojom::Role::kStaticText;
ui::AXNodeData generic_container_node;
generic_container_node.id = 3;
generic_container_node.role = ax::mojom::Role::kGenericContainer;
generic_container_node.AddStringAttribute(
ax::mojom::StringAttribute::kDisplay, "block");
ui::AXNodeData inline_node;
inline_node.id = 4;
inline_node.role = ax::mojom::Role::kStaticText;
inline_node.AddStringAttribute(ax::mojom::StringAttribute::kDisplay,
"inline");
generic_container_node.child_ids = {inline_node.id};
ui::AXNodeData parent_node;
parent_node.id = 1;
parent_node.child_ids = {static_text_node.id, generic_container_node.id};
parent_node.role = ax::mojom::Role::kStaticText;
update.nodes = {parent_node, static_text_node, generic_container_node,
inline_node};
AccessibilityEventReceived({update});
update.tree_data.sel_anchor_object_id = 4;
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 = true;
AccessibilityEventReceived({update});
ProcessSelection();
ASSERT_TRUE(HasSelection());
ASSERT_EQ(StartNodeId(), 4);
ASSERT_EQ(EndNodeId(), 4);
ASSERT_TRUE(SelectionNodeIdsContains(1));
ASSERT_FALSE(SelectionNodeIdsContains(2));
ASSERT_TRUE(SelectionNodeIdsContains(3));
ASSERT_TRUE(SelectionNodeIdsContains(4));
}
TEST_F(
ReadAnythingAppModelTest,
SelectionParentIsGenericContainerWithMultipleChildren_SelectionStateCorrect) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData static_text_node;
static_text_node.id = 2;
static_text_node.role = ax::mojom::Role::kStaticText;
ui::AXNodeData generic_container_node;
generic_container_node.role = ax::mojom::Role::kGenericContainer;
generic_container_node.id = 3;
ui::AXNodeData static_text_child_node1;
static_text_child_node1.id = 4;
static_text_child_node1.role = ax::mojom::Role::kStaticText;
ui::AXNodeData static_text_child_node2;
static_text_child_node2.id = 5;
static_text_child_node2.role = ax::mojom::Role::kStaticText;
generic_container_node.child_ids = {static_text_child_node1.id,
static_text_child_node2.id};
ui::AXNodeData parent_node;
parent_node.id = 1;
parent_node.role = ax::mojom::Role::kStaticText;
parent_node.child_ids = {static_text_node.id, generic_container_node.id};
update.nodes = {parent_node, static_text_node, generic_container_node,
static_text_child_node1, static_text_child_node2};
AccessibilityEventReceived({update});
update.tree_data.sel_anchor_object_id = 4;
update.tree_data.sel_focus_object_id = 5;
update.tree_data.sel_anchor_offset = 0;
update.tree_data.sel_focus_offset = 0;
update.tree_data.sel_is_backward = false;
AccessibilityEventReceived({update});
ProcessSelection();
ASSERT_TRUE(HasSelection());
ASSERT_EQ(StartNodeId(), 4);
ASSERT_EQ(EndNodeId(), 5);
// 1 and 3 are ancestors, so they are included as selection nodes..
ASSERT_TRUE(SelectionNodeIdsContains(1));
ASSERT_TRUE(SelectionNodeIdsContains(3));
ASSERT_TRUE(SelectionNodeIdsContains(4));
ASSERT_TRUE(SelectionNodeIdsContains(5));
// Since 3 is a generic container with more than one child, its sibling nodes
// are not included, so 2 is ignored.
ASSERT_FALSE(SelectionNodeIdsContains(2));
}
TEST_F(ReadAnythingAppModelTest, ResetTextSize_ReturnsTextSizeToDefault) {
IncreaseTextSize();
IncreaseTextSize();
IncreaseTextSize();
ASSERT_GT(FontSize(), kReadAnythingDefaultFontScale);
ResetTextSize();
ASSERT_EQ(FontSize(), kReadAnythingDefaultFontScale);
DecreaseTextSize();
DecreaseTextSize();
DecreaseTextSize();
ASSERT_LT(FontSize(), kReadAnythingDefaultFontScale);
ResetTextSize();
ASSERT_EQ(FontSize(), kReadAnythingDefaultFontScale);
}
TEST_F(ReadAnythingAppModelTest,
SupportedFonts_SetDefaultLanguageCode_ReturnsCorrectCode) {
ASSERT_EQ(DefaultLanguageCode(), "en-US");
SetLanguageCode("es");
ASSERT_EQ(DefaultLanguageCode(), "es");
}
TEST_F(ReadAnythingAppModelTest,
SupportedFonts_InvalidLanguageCode_ReturnsDefaultFonts) {
SetLanguageCode("qr");
std::vector<std::string> expectedFonts = {"Sans-serif", "Serif"};
std::vector<std::string> fonts = GetSupportedFonts();
EXPECT_EQ(fonts.size(), expectedFonts.size());
for (size_t i = 0; i < fonts.size(); i++) {
ASSERT_EQ(fonts[i], expectedFonts[i]);
}
}
TEST_F(ReadAnythingAppModelTest,
SupportedFonts_BeforeLanguageSet_ReturnsDefaultFonts) {
std::vector<std::string> expectedFonts = {"Sans-serif", "Serif"};
std::vector<std::string> fonts = GetSupportedFonts();
EXPECT_EQ(fonts.size(), expectedFonts.size());
for (size_t i = 0; i < fonts.size(); i++) {
ASSERT_EQ(fonts[i], expectedFonts[i]);
}
}
TEST_F(ReadAnythingAppModelTest,
SupportedFonts_SetDefaultLanguageCode_ReturnsExpectedDefaultFonts) {
// English
SetLanguageCode("en");
std::vector<std::string> expectedFonts = {
"Poppins", "Sans-serif", "Serif", "Comic Neue",
"Lexend Deca", "EB Garamond", "STIX Two Text", "Andika"};
std::vector<std::string> fonts = GetSupportedFonts();
EXPECT_EQ(fonts.size(), expectedFonts.size());
for (size_t i = 0; i < fonts.size(); i++) {
ASSERT_EQ(fonts[i], expectedFonts[i]);
}
// Bulgarian
SetLanguageCode("bg");
expectedFonts = {"Sans-serif", "Serif", "EB Garamond", "STIX Two Text",
"Andika"};
fonts = GetSupportedFonts();
EXPECT_EQ(fonts.size(), expectedFonts.size());
for (size_t i = 0; i < fonts.size(); i++) {
ASSERT_EQ(fonts[i], expectedFonts[i]);
}
// Hindi
SetLanguageCode("hi");
expectedFonts = {"Poppins", "Sans-serif", "Serif"};
fonts = GetSupportedFonts();
EXPECT_EQ(fonts.size(), expectedFonts.size());
for (size_t i = 0; i < fonts.size(); i++) {
ASSERT_EQ(fonts[i], expectedFonts[i]);
}
}
TEST_F(ReadAnythingAppModelTest, IsPdf) {
GURL webpage_url("http://images.google.com/foo.html");
SetIsPdf(webpage_url);
ASSERT_FALSE(IsPdf());
GURL pdf_url("http://www.google.com/foo/bar.pdf");
SetIsPdf(pdf_url);
ASSERT_TRUE(IsPdf());
}
TEST_F(ReadAnythingAppModelTest, ValidPDF) {
// Need to set is_pdf_ for DCHECK in GetPDFWebContents().
GURL pdf_url("http://www.google.com/foo/bar.pdf");
SetIsPdf(pdf_url);
ui::AXTreeID pdf_web_contents_tree_id = ui::AXTreeID::CreateNewAXTreeID();
ui::AXTreeID pdf_iframe_tree_id = ui::AXTreeID::CreateNewAXTreeID();
// Main web contents should have one child.
ui::AXTreeUpdate update;
ui::AXNodeData node;
node.id = 1;
node.AddChildTreeId(pdf_web_contents_tree_id);
update.nodes = {node};
SetUpdateTreeID(&update);
AccessibilityEventReceived({update});
// IsPDFFormatted() should return true if tree updates from the pdf web
// contents and/or the pdf iframe haven't been sent yet.
ASSERT_TRUE(IsPDFFormatted());
// Pdf web contents should have one child.
ui::AXNodeData root;
root.id = 1;
root.AddChildTreeId(pdf_iframe_tree_id);
update.root_id = root.id;
update.nodes = {root};
SetUpdateTreeID(&update, pdf_web_contents_tree_id);
AccessibilityEventReceived({update});
ASSERT_TRUE(IsPDFFormatted());
// Send pdf iframe tree to model.
ui::AXNodeData update_root;
update_root.id = 1;
update.root_id = update_root.id;
update.nodes = {update_root};
SetUpdateTreeID(&update, pdf_iframe_tree_id);
AccessibilityEventReceived({update});
ASSERT_TRUE(IsPDFFormatted());
EXPECT_EQ(pdf_web_contents_tree_id, GetPDFWebContents());
}
TEST_F(ReadAnythingAppModelTest, InvalidPDFFormat) {
// Main web contents should have one child, the pdf web contents.
ui::AXTreeID pdf_web_contents_tree_id = ui::AXTreeID::CreateNewAXTreeID();
ui::AXTreeUpdate update;
ui::AXNodeData node;
node.id = 1;
node.AddChildTreeId(pdf_web_contents_tree_id);
update.nodes = {node};
SetUpdateTreeID(&update);
AccessibilityEventReceived({update});
// This pdf web contents has no children, so this is an invalid PDF.
ui::AXTreeUpdate pdf_web_contents_update;
ui::AXNodeData empty_root;
empty_root.id = 1;
pdf_web_contents_update.root_id = empty_root.id;
pdf_web_contents_update.nodes = {empty_root};
SetUpdateTreeID(&pdf_web_contents_update, pdf_web_contents_tree_id);
AccessibilityEventReceived({pdf_web_contents_update});
ASSERT_FALSE(IsPDFFormatted());
}
TEST_F(ReadAnythingAppModelTest, PdfEvents_SetRequiresDistillation) {
SetIsPdf(GURL("http://www.google.com/foo/bar.pdf"));
ui::AXTreeUpdate initial_update;
SetUpdateTreeID(&initial_update);
initial_update.root_id = 1;
ui::AXNodeData embedded_node;
embedded_node.id = 2;
embedded_node.role = ax::mojom::Role::kEmbeddedObject;
ui::AXNodeData pdf_root_node;
pdf_root_node.id = 1;
pdf_root_node.role = ax::mojom::Role::kPdfRoot;
pdf_root_node.child_ids = {embedded_node.id};
initial_update.nodes = {pdf_root_node, embedded_node};
AccessibilityEventReceived({initial_update});
// Update with no new nodes added to the tree.
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
update.root_id = 1;
ui::AXNodeData node;
node.id = 1;
node.role = ax::mojom::Role::kPdfRoot;
node.SetNameChecked("example.pdf");
update.nodes = {node};
AccessibilityEventReceived({update});
ASSERT_FALSE(RequiresDistillation());
// Tree update with PDF contents (new nodes added).
ui::AXTreeUpdate update2;
SetUpdateTreeID(&update2);
update2.root_id = 1;
ui::AXNodeData static_text_node1;
static_text_node1.id = 1;
static_text_node1.role = ax::mojom::Role::kStaticText;
ui::AXNodeData updated_embedded_node;
updated_embedded_node.id = 2;
updated_embedded_node.role = ax::mojom::Role::kEmbeddedObject;
static_text_node1.child_ids = {updated_embedded_node.id};
ui::AXNodeData static_text_node2;
static_text_node2.id = 3;
static_text_node2.role = ax::mojom::Role::kStaticText;
updated_embedded_node.child_ids = {static_text_node2.id};
update2.nodes = {static_text_node1, updated_embedded_node, static_text_node2};
AccessibilityEventReceived({update2});
ASSERT_TRUE(RequiresDistillation());
}
TEST_F(ReadAnythingAppModelTest, PdfEvents_DontSetRequiresDistillation) {
SetIsPdf(GURL("http://www.google.com/foo/bar.pdf"));
ui::AXTreeUpdate initial_update;
SetUpdateTreeID(&initial_update);
initial_update.root_id = 1;
ui::AXNodeData node;
node.id = 1;
node.role = ax::mojom::Role::kPdfRoot;
initial_update.nodes = {node};
AccessibilityEventReceived({initial_update});
// Updates that don't create a new subtree, for example, a role change, should
// not set requires_distillation_.
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData static_text_node;
static_text_node.id = 1;
static_text_node.role = ax::mojom::Role::kStaticText;
update.root_id = static_text_node.id;
update.nodes = {static_text_node};
AccessibilityEventReceived({update});
ASSERT_FALSE(RequiresDistillation());
}
TEST_F(ReadAnythingAppModelTest, OnSelection_HandlesClickAndDragEvents) {
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
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});
ProcessSelection();
// If there is a click and drag selection (the anchor object id and offset are
// the same as the prev selection received), the event_from eventually changes
// from kUser to kPage. Post process selection should be required in either
// case.
// SetRequiresPostProcessSelection(false) is needed to reset the flag to check
// that OnSelection(...) properly sets (or doesn't set) the flag.
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 = 1;
update.tree_data.sel_is_backward = false;
AccessibilityEventReceived({update});
SetRequiresPostProcessSelection(false);
OnSelection(ax::mojom::EventFrom::kUser);
EXPECT_TRUE(RequiresPostProcessSelection());
SetRequiresPostProcessSelection(false);
OnSelection(ax::mojom::EventFrom::kPage);
EXPECT_TRUE(RequiresPostProcessSelection());
// If the user drags the selection so that it is backwards, post process
// selection should still be required.
update.tree_data.sel_anchor_object_id = 2;
update.tree_data.sel_focus_object_id = 1;
update.tree_data.sel_anchor_offset = 0;
update.tree_data.sel_focus_offset = 2;
update.tree_data.sel_is_backward = true;
AccessibilityEventReceived({update});
SetRequiresPostProcessSelection(false);
OnSelection(ax::mojom::EventFrom::kPage);
EXPECT_TRUE(RequiresPostProcessSelection());
// If the anchor changes (the user stopped dragging their cursor) and we
// receive an event with event_from kPage, post process selection should not
// be set to true.
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 = 0;
update.tree_data.sel_is_backward = false;
AccessibilityEventReceived({update});
SetRequiresPostProcessSelection(false);
OnSelection(ax::mojom::EventFrom::kPage);
EXPECT_FALSE(RequiresPostProcessSelection());
}
TEST_F(ReadAnythingAppModelTest, GetNextSentence_ReturnsCorrectIndex) {
const std::u16string first_sentence = u"This is a normal sentence. ";
const std::u16string second_sentence = u"This is a second sentence.";
const std::u16string sentence = first_sentence + second_sentence;
size_t index = GetNextSentence(sentence);
EXPECT_EQ(index, first_sentence.length());
EXPECT_EQ(sentence.substr(0, index), first_sentence);
}
TEST_F(ReadAnythingAppModelTest,
GetNextSentence_OnlyOneSentence_ReturnsCorrectIndex) {
const std::u16string sentence = u"Hello, this is a normal sentence.";
size_t index = GetNextSentence(sentence);
EXPECT_EQ(index, sentence.length());
EXPECT_EQ(sentence.substr(0, index), sentence);
}
TEST_F(ReadAnythingAppModelTest, GetNextValidPosition) {
std::u16string sentence1 = u"This is a sentence.";
std::u16string sentence2 = u"This is another sentence.";
std::u16string sentence3 = u"And this is yet another sentence.";
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData static_text1;
static_text1.id = 2;
static_text1.role = ax::mojom::Role::kStaticText;
static_text1.SetNameChecked(sentence1);
ui::AXNodeData static_text2;
static_text2.id = 3;
static_text2.role = ax::mojom::Role::kStaticText;
static_text2.SetNameChecked(sentence2);
ui::AXNodeData static_text3;
static_text3.id = 4;
static_text3.role = ax::mojom::Role::kStaticText;
static_text3.SetNameChecked(sentence3);
update.nodes = {static_text1, static_text2, static_text3};
AccessibilityEventReceived({update});
ProcessDisplayNodes({static_text1.id, static_text2.id, static_text3.id});
InitAXPosition(update.nodes[0].id);
ui::AXNodePosition::AXPositionInstance new_position = GetNextNodePosition();
EXPECT_EQ(new_position->anchor_id(), static_text2.id);
EXPECT_EQ(new_position->GetText(), sentence2);
// Getting the next node position shouldn't update the current AXPosition.
new_position = GetNextNodePosition();
EXPECT_EQ(new_position->anchor_id(), static_text2.id);
EXPECT_EQ(new_position->GetText(), sentence2);
}
TEST_F(ReadAnythingAppModelTest, GetNextValidPosition_SkipsNonTextNode) {
std::u16string sentence1 = u"This is a sentence.";
std::u16string sentence2 = u"This is another sentence.";
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData static_text1;
static_text1.id = 2;
static_text1.role = ax::mojom::Role::kStaticText;
static_text1.SetNameChecked(sentence1);
ui::AXNodeData empty_node;
empty_node.id = 3;
ui::AXNodeData static_text2;
static_text2.id = 4;
static_text2.role = ax::mojom::Role::kStaticText;
static_text2.SetNameChecked(sentence2);
update.nodes = {static_text1, empty_node, static_text2};
AccessibilityEventReceived({update});
ProcessDisplayNodes({static_text1.id, empty_node.id, static_text2.id});
InitAXPosition(update.nodes[0].id);
ui::AXNodePosition::AXPositionInstance new_position = GetNextNodePosition();
EXPECT_EQ(new_position->anchor_id(), static_text2.id);
EXPECT_EQ(new_position->GetText(), sentence2);
}
TEST_F(ReadAnythingAppModelTest, GetNextValidPosition_SkipsNonDistilledNode) {
std::u16string sentence1 = u"This is a sentence.";
std::u16string sentence2 = u"This is another sentence.";
std::u16string sentence3 = u"And this is yet another sentence.";
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData static_text1;
static_text1.id = 2;
static_text1.role = ax::mojom::Role::kStaticText;
static_text1.SetNameChecked(sentence1);
ui::AXNodeData static_text2;
static_text2.id = 3;
static_text2.role = ax::mojom::Role::kStaticText;
static_text2.SetNameChecked(sentence2);
ui::AXNodeData static_text3;
static_text3.id = 4;
static_text3.role = ax::mojom::Role::kStaticText;
static_text3.SetName(sentence3);
update.nodes = {static_text1, static_text2, static_text3};
AccessibilityEventReceived({update});
// Don't distill the node with id 3.
ProcessDisplayNodes({static_text1.id, static_text3.id});
InitAXPosition(update.nodes[0].id);
ui::AXNodePosition::AXPositionInstance new_position = GetNextNodePosition();
EXPECT_EQ(new_position->anchor_id(), static_text3.id);
EXPECT_EQ(new_position->GetText(), sentence3);
}
TEST_F(ReadAnythingAppModelTest, GetNextValidPosition_SkipsNodeWithHTMLTag) {
std::u16string sentence1 = u"This is a sentence.";
std::u16string sentence2 = u"This is another sentence.";
std::u16string sentence3 = u"And this is yet another sentence.";
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData static_text1;
static_text1.id = 2;
static_text1.role = ax::mojom::Role::kStaticText;
static_text1.SetNameChecked(sentence1);
ui::AXNodeData static_text2;
static_text2.id = 3;
static_text2.role = ax::mojom::Role::kStaticText;
static_text2.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "h1");
static_text2.SetNameChecked(sentence2);
ui::AXNodeData static_text3;
static_text3.id = 4;
static_text3.role = ax::mojom::Role::kStaticText;
static_text3.SetNameChecked(sentence3);
update.nodes = {static_text1, static_text2, static_text3};
AccessibilityEventReceived({update});
ProcessDisplayNodes({static_text1.id, static_text2.id, static_text3.id});
InitAXPosition(update.nodes[0].id);
ui::AXNodePosition::AXPositionInstance new_position = GetNextNodePosition();
EXPECT_EQ(new_position->anchor_id(), static_text3.id);
EXPECT_EQ(new_position->GetText(), sentence3);
}
TEST_F(ReadAnythingAppModelTest,
GetNextValidPosition_ReturnsNullPositionAtEndOfTree) {
std::u16string sentence1 = u"This is a sentence.";
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData static_text;
static_text.id = 2;
static_text.role = ax::mojom::Role::kStaticText;
static_text.SetNameChecked(sentence1);
ui::AXNodeData empty_node1;
empty_node1.id = 3;
ui::AXNodeData empty_node2;
empty_node2.id = 4;
update.nodes = {static_text, empty_node1, empty_node2};
AccessibilityEventReceived({update});
ProcessDisplayNodes({static_text.id, empty_node1.id, empty_node2.id});
InitAXPosition(update.nodes[0].id);
ui::AXNodePosition::AXPositionInstance new_position = GetNextNodePosition();
EXPECT_TRUE(new_position->IsNullPosition());
}
TEST_F(
ReadAnythingAppModelTest,
GetNextValidPosition_AfterGetNextNodesButBeforeGetCurrentText_UsesCurrentGranularity) {
std::u16string sentence1 = u"But from up here. The ";
std::u16string sentence2 = u"world ";
std::u16string sentence3 =
u"looks so small. And suddenly life seems so clear. And from up here. "
u"You coast past it all. The obstacles just disappear.";
ui::AXTreeUpdate update;
SetUpdateTreeID(&update);
ui::AXNodeData static_text1;
static_text1.id = 2;
static_text1.role = ax::mojom::Role::kStaticText;
static_text1.SetNameChecked(sentence1);
ui::AXNodeData static_text2;
static_text2.id = 3;
static_text2.role = ax::mojom::Role::kStaticText;
static_text2.SetNameChecked(sentence2);
ui::AXNodeData static_text3;
static_text3.id = 4;
static_text3.role = ax::mojom::Role::kStaticText;
static_text3.SetNameChecked(sentence3);
update.nodes = {static_text1, static_text2, static_text3};
AccessibilityEventReceived({update});
ProcessDisplayNodes({static_text1.id, static_text2.id, static_text3.id});
InitAXPosition(update.nodes[0].id);
ReadAnythingAppModel::ReadAloudCurrentGranularity current_granularity =
GetNextNodes();
// Expect that current_granularity contains static_text1
// Expect that the indices aren't returned correctly
// Expect that GetNextValidPosition fails without inserted the granularity.
// The first segment was returned correctly.
EXPECT_EQ((int)current_granularity.node_ids.size(), 1);
EXPECT_TRUE(base::Contains(current_granularity.node_ids, static_text1.id));
EXPECT_EQ(GetCurrentTextStartIndex(static_text1.id), -1);
EXPECT_EQ(GetCurrentTextEndIndex(static_text1.id), -1);
// Get the next position without using the current granularity. This
// simulates getting the next node position from within GetNextNode if
// the current granularity hasn't yet been added to the list processed
// granularities. This should return the ID for static_text1, even though
// it's already been used because the current granularity isn't being used.
ui::AXNodePosition::AXPositionInstance new_position = GetNextNodePosition();
EXPECT_EQ(new_position->anchor_id(), static_text1.id);
// Now get the next position using the correct current granularity. Thi
// simulates calling GetNextNodePosition from within GetNextNodes before
// the nodes have been added to the list of processed granularities. This
// should correctly return the next node in the tree.
new_position = GetNextNodePosition(current_granularity);
EXPECT_EQ(new_position->anchor_id(), static_text2.id);
}