| // 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 <cstddef> |
| #include <string> |
| #include <vector> |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "chrome/common/accessibility/read_anything_constants.h" |
| #include "chrome/renderer/accessibility/ax_tree_distiller.h" |
| #include "chrome/renderer/accessibility/read_anything_test_utils.h" |
| #include "chrome/test/base/chrome_render_view_test.h" |
| #include "content/public/renderer/render_frame.h" |
| #include "read_anything_test_utils.h" |
| #include "services/strings/grit/services_strings.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "ui/accessibility/accessibility_features.h" |
| #include "ui/accessibility/ax_node.h" |
| #include "ui/accessibility/ax_node_id_forward.h" |
| #include "ui/accessibility/ax_serializable_tree.h" |
| #include "ui/base/l10n/l10n_util.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 MockReadAnythingUntrustedPageHandler |
| : public read_anything::mojom::UntrustedPageHandler { |
| public: |
| MockReadAnythingUntrustedPageHandler() = default; |
| |
| MOCK_METHOD(void, |
| GetVoicePackInfo, |
| (const std::string& language, |
| GetVoicePackInfoCallback mojo_callback), |
| (override)); |
| MOCK_METHOD(void, |
| InstallVoicePack, |
| (const std::string& language, |
| InstallVoicePackCallback mojo_callback), |
| (override)); |
| MOCK_METHOD(void, |
| OnLinkClicked, |
| (const ui::AXTreeID& target_tree_id, ui::AXNodeID target_node_id), |
| (override)); |
| MOCK_METHOD(void, |
| ScrollToTargetNode, |
| (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)); |
| MOCK_METHOD(void, OnCollapseSelection, (), (override)); |
| MOCK_METHOD(void, OnSnapshotRequested, (), (override)); |
| MOCK_METHOD(void, OnCopy, (), (override)); |
| MOCK_METHOD(void, |
| OnLineSpaceChange, |
| (read_anything::mojom::LineSpacing line_spacing), |
| (override)); |
| MOCK_METHOD(void, |
| OnLetterSpaceChange, |
| (read_anything::mojom::LetterSpacing letter_spacing), |
| (override)); |
| MOCK_METHOD(void, OnFontChange, (const std::string& font), (override)); |
| MOCK_METHOD(void, OnFontSizeChange, (double font_size), (override)); |
| MOCK_METHOD(void, OnLinksEnabledChanged, (bool enabled), (override)); |
| MOCK_METHOD(void, OnImagesEnabledChanged, (bool enabled), (override)); |
| MOCK_METHOD(void, OnSpeechRateChange, (double rate), (override)); |
| MOCK_METHOD(void, |
| OnVoiceChange, |
| (const std::string& voice, const std::string& lang), |
| (override)); |
| MOCK_METHOD(void, |
| OnLanguagePrefChange, |
| (const std::string& lang, bool enabled), |
| (override)); |
| MOCK_METHOD(void, |
| OnColorChange, |
| (read_anything::mojom::Colors color), |
| (override)); |
| MOCK_METHOD(void, |
| OnHighlightGranularityChanged, |
| (read_anything::mojom::HighlightGranularity color), |
| (override)); |
| MOCK_METHOD(void, |
| OnImageDataRequested, |
| (const ::ui::AXTreeID& target_tree_id, int32_t target_node_id), |
| (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; |
| |
| const std::string DOCS_URL = |
| "https://docs.google.com/document/d/" |
| "1t6x1PQaQWjE8wb9iyYmFaoK1XAEgsl8G1Hx3rzfpoKA/" |
| "edit?ouid=103677288878638916900&usp=docs_home&ths=true"; |
| |
| 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. |
| std::unique_ptr<ui::AXTreeUpdate> snapshot = test::CreateInitialUpdate(); |
| SetUpdateTreeID(snapshot.get()); |
| |
| // 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 SetIsPdf() { |
| // Call OnActiveAXTreeIDChanged() to set is_pdf_ state. |
| OnActiveAXTreeIDChanged(tree_id_, true /* is_pdf */); |
| } |
| |
| void SetUpdateTreeID(ui::AXTreeUpdate* update) { |
| test::SetUpdateTreeID(update, tree_id_); |
| } |
| |
| void SendBatchUpdates() { |
| std::vector<ui::AXTreeUpdate> batch_updates; |
| for (int i = 2; i < 5; i++) { |
| ui::AXTreeUpdate update; |
| SetUpdateTreeID(&update); |
| ui::AXNodeData node = |
| test::TextNode(/* id= */ i, u"Node " + base::NumberToString16(i)); |
| update.nodes = {node}; |
| batch_updates.push_back(update); |
| } |
| |
| AccessibilityEventReceived(batch_updates); |
| } |
| |
| std::vector<int> SendSimpleUpdateAndGetChildIds() { |
| 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] = test::TextNodeWithTextFromId(id); |
| } |
| // No events we care about come about, so there's no distillation. |
| EXPECT_CALL(*distiller_, Distill).Times(0); |
| AccessibilityEventReceived({initial_update}); |
| EXPECT_EQ(u"234", GetTextContent(1)); |
| Mock::VerifyAndClearExpectations(distiller_); |
| return child_ids; |
| } |
| |
| std::vector<ui::AXTreeUpdate> CreateSimpleUpdateList( |
| std::vector<int> child_ids) { |
| return test::CreateSimpleUpdateList(child_ids, tree_id_); |
| } |
| |
| void OnSettingsRestoredFromPrefs( |
| read_anything::mojom::LineSpacing line_spacing, |
| read_anything::mojom::LetterSpacing letter_spacing, |
| const std::string& font, |
| double font_size, |
| bool links_enabled, |
| bool images_enabled, |
| read_anything::mojom::Colors color, |
| double speech_rate, |
| base::Value::Dict voices, |
| base::Value::List languages_enabled_in_pref, |
| read_anything::mojom::HighlightGranularity granularity) { |
| controller_->OnSettingsRestoredFromPrefs( |
| line_spacing, letter_spacing, font, font_size, links_enabled, |
| images_enabled, color, speech_rate, std::move(voices), |
| std::move(languages_enabled_in_pref), granularity); |
| } |
| |
| void OnLetterSpacingChange(int value) { |
| controller_->OnLetterSpacingChange(value); |
| } |
| void OnLineSpacingChange(int value) { |
| controller_->OnLineSpacingChange(value); |
| } |
| void OnThemeChange(int value) { controller_->OnThemeChange(value); } |
| |
| 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 set_distillation_in_progress(bool in_progress) { |
| controller_->model_.set_distillation_in_progress(in_progress); |
| } |
| |
| void OnSpeechPlayingStateChanged(bool is_speech_active) { |
| controller_->OnSpeechPlayingStateChanged(is_speech_active); |
| } |
| |
| void OnActiveAXTreeIDChanged(const ui::AXTreeID& tree_id, |
| bool is_pdf = false) { |
| controller_->OnActiveAXTreeIDChanged(tree_id, ukm::kInvalidSourceId, |
| is_pdf); |
| } |
| |
| void OnAXTreeDistilled(const std::vector<ui::AXNodeID>& content_node_ids) { |
| OnAXTreeDistilled(tree_id_, content_node_ids); |
| } |
| |
| void InitAXPosition(const ui::AXNodeID id) { |
| controller_->InitAXPositionWithNode(id); |
| } |
| |
| std::vector<ui::AXNodeID> MoveToNextGranularityAndGetText() { |
| MovePositionToNextGranularity(); |
| return GetCurrentText(); |
| } |
| |
| std::vector<ui::AXNodeID> GetCurrentText() { |
| return controller_->GetCurrentText(); |
| } |
| |
| void PreprocessTextForSpeech() { controller_->PreprocessTextForSpeech(); } |
| void MovePositionToNextGranularity() { |
| return controller_->MovePositionToNextGranularity(); |
| } |
| |
| std::vector<ui::AXNodeID> MoveToPreviousGranularityAndGetText() { |
| controller_->MovePositionToPreviousGranularity(); |
| return controller_->GetCurrentText(); |
| } |
| |
| int GetCurrentTextStartIndex(ui::AXNodeID id) { |
| return controller_->GetCurrentTextStartIndex(id); |
| } |
| |
| int GetCurrentTextEndIndex(ui::AXNodeID id) { |
| return controller_->GetCurrentTextEndIndex(id); |
| } |
| |
| 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(); } |
| |
| bool LinksEnabled() { return controller_->LinksEnabled(); } |
| |
| bool ImagesEnabled() { return controller_->ImagesEnabled(); } |
| |
| int LineSpacing() { return controller_->LineSpacing(); } |
| |
| int LetterSpacing() { return controller_->LetterSpacing(); } |
| |
| int ColorTheme() { return controller_->ColorTheme(); } |
| |
| void OnFontSizeReset() { controller_->OnFontSizeReset(); } |
| |
| void OnLinksEnabledToggled() { controller_->OnLinksEnabledToggled(); } |
| |
| void OnImagesEnabledToggled() { controller_->OnImagesEnabledToggled(); } |
| |
| bool IsHighlightOn() { return controller_->IsHighlightOn(); } |
| |
| void TurnedHighlightOn() { controller_->TurnedHighlightOn(); } |
| |
| void TurnedHighlightOff() { controller_->TurnedHighlightOff(); } |
| |
| std::vector<ui::AXNodeID> GetChildren(ui::AXNodeID ax_node_id) { |
| return controller_->GetChildren(ax_node_id); |
| } |
| |
| std::string GetDisplayNameForLocale(std::string locale, |
| std::string display_locale) { |
| return controller_->GetDisplayNameForLocale(locale, display_locale); |
| } |
| |
| void OnFontChange(const std::string& font) { |
| controller_->OnFontChange(font); |
| } |
| |
| void OnVoiceChange(const std::string& voice, const std::string& lang) { |
| controller_->OnVoiceChange(voice, lang); |
| } |
| |
| std::string GetStoredVoice() { return controller_->GetStoredVoice(); } |
| |
| std::string GetDataFontCss(ui::AXNodeID ax_node_id) { |
| return controller_->GetDataFontCss(ax_node_id); |
| } |
| |
| std::string GetHtmlTag(ui::AXNodeID ax_node_id) { |
| return controller_->GetHtmlTag(ax_node_id); |
| } |
| |
| std::string GetAltText(ui::AXNodeID ax_node_id) { |
| return controller_->GetAltText(ax_node_id); |
| } |
| |
| std::string GetImageDataUrl(ui::AXNodeID ax_node_id) { |
| return controller_->GetImageDataUrl(ax_node_id); |
| } |
| |
| std::u16string 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); |
| } |
| |
| bool IsGoogleDocs() { return controller_->IsGoogleDocs(); } |
| |
| bool IsUrlInformationSet(ui::AXTreeID tree_id) { |
| return controller_->model_.GetTreesForTesting() |
| ->at(tree_id) |
| ->is_url_information_set; |
| } |
| |
| bool IsReadAloudEnabled() { return controller_->IsReadAloudEnabled(); } |
| |
| bool IsLeafNode(ui::AXNodeID ax_node_id) { |
| return controller_->IsLeafNode(ax_node_id); |
| } |
| |
| void OnLinkClicked(ui::AXNodeID ax_node_id) { |
| controller_->OnLinkClicked(ax_node_id); |
| } |
| |
| void RequestImageDataUrl(ui::AXNodeID node_id) { |
| controller_->RequestImageDataUrl(node_id); |
| } |
| |
| void OnScrolledToBottom() { controller_->OnScrolledToBottom(); } |
| |
| 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); |
| } |
| |
| void OnSpeechRateChange(double rate) { |
| controller_->OnSpeechRateChange(rate); |
| } |
| |
| void OnLanguagePrefChange(const std::string& lang, bool enabled) { |
| controller_->OnLanguagePrefChange(lang, enabled); |
| } |
| |
| double SpeechRate() { return controller_->read_aloud_model_.speech_rate(); } |
| |
| int HighlightGranularity() { |
| return controller_->read_aloud_model_.highlight_granularity(); |
| } |
| |
| const base::Value::List& EnabledLanguages() { |
| return controller_->read_aloud_model_.languages_enabled_in_pref(); |
| } |
| |
| void OnCollapseSelection() { controller_->OnCollapseSelection(); } |
| |
| void ResetGranularityIndex() { controller_->ResetGranularityIndex(); } |
| |
| bool HasTree(ui::AXTreeID tree_id) { |
| return controller_->model_.ContainsTree(tree_id); |
| } |
| |
| ui::AXTreeID active_tree_id() { return controller_->model_.active_tree_id(); } |
| |
| bool RequiresTreeLang() { return controller_->model_.requires_tree_lang(); } |
| std::string LanguageCodeForSpeech() { |
| return controller_->GetLanguageCodeForSpeech(); |
| } |
| void SetLanguageCode(std::string code) { |
| controller_->SetLanguageForTesting(code); |
| } |
| |
| std::vector<std::string> GetLanguagesEnabledInPref() { |
| return controller_->GetLanguagesEnabledInPref(); |
| } |
| |
| size_t GetAccessibleBoundary(const std::u16string& text, |
| int max_text_length) { |
| return controller_->GetAccessibleBoundary(text, max_text_length); |
| } |
| |
| std::string GetValidatedFont(const std::string& font) { |
| return controller_->GetValidatedFontName(font); |
| } |
| |
| void ResetReadAloudState() { |
| controller_->read_aloud_model_.ResetReadAloudState(); |
| } |
| |
| ui::AXNodePosition::AXPositionInstance GetNextNodePosition() { |
| a11y::ReadAloudCurrentGranularity granularity = |
| a11y::ReadAloudCurrentGranularity(); |
| return GetNextNodePosition(granularity); |
| } |
| |
| ui::AXNodePosition::AXPositionInstance GetNextNodePosition( |
| a11y::ReadAloudCurrentGranularity granularity) { |
| return controller_->read_aloud_model_ |
| .GetNextValidPositionFromCurrentPosition( |
| granularity, controller_->model_.is_pdf(), |
| controller_->model_.IsDocs(), |
| &controller_->model_.display_node_ids()); |
| } |
| |
| a11y::ReadAloudCurrentGranularity GetNextNodes() { |
| return controller_->read_aloud_model_.GetNextNodes( |
| controller_->model_.is_pdf(), controller_->model_.IsDocs(), |
| &controller_->model_.display_node_ids()); |
| } |
| |
| ui::AXNodeID GetNodeIdForCurrentSegmentIndex(int index) { |
| ui::AXNodeID id = |
| controller_->read_aloud_model_.GetNodeIdForCurrentSegmentIndex(index); |
| return id; |
| } |
| |
| ui::AXNodeID GetHighlightStartIndex(ui::AXNodeID& node_id, int index) { |
| return controller_->read_aloud_model_.GetHighlightStartIndex(node_id, |
| index); |
| } |
| |
| int GetWordLength(int index) { |
| return controller_->read_aloud_model_.GetNextWordHighlightLength(index); |
| } |
| |
| std::vector<ui::AXNodeID> GetCurrentText( |
| bool is_pdf, |
| bool is_docs, |
| const std::set<ui::AXNodeID>* current_nodes) { |
| return controller_->read_aloud_model_.GetCurrentText(is_pdf, is_docs, |
| current_nodes); |
| } |
| |
| void ProcessDisplayNodes(const std::vector<ui::AXNodeID>& content_node_ids) { |
| controller_->model_.Reset(content_node_ids); |
| controller_->model_.ComputeDisplayNodeIdsForDistilledTree(); |
| } |
| |
| void Draw(bool recompute_display_nodes) { |
| controller_->Draw(recompute_display_nodes); |
| } |
| |
| void Reset(const std::vector<ui::AXNodeID>& content_node_ids) { |
| controller_->model_.Reset(content_node_ids); |
| } |
| |
| void SendUpdateWithNodes(std::vector<ui::AXNodeData> nodes) { |
| ui::AXTreeUpdate update; |
| SetUpdateTreeID(&update); |
| update.nodes = nodes; |
| AccessibilityEventReceived({update}); |
| } |
| |
| void SendUpdateAndDistillNodes(std::vector<ui::AXNodeData> nodes) { |
| SendUpdateWithNodes(nodes); |
| |
| std::vector<int> node_ids; |
| for (const ui::AXNodeData& node : nodes) { |
| node_ids.push_back(node.id); |
| } |
| |
| OnAXTreeDistilled(node_ids); |
| InitAXPosition(nodes[0].id); |
| } |
| |
| void InitializeWithAndProcessNodes(std::vector<ui::AXNodeData> nodes) { |
| SendUpdateWithNodes(nodes); |
| std::vector<int> node_ids; |
| for (const ui::AXNodeData& node : nodes) { |
| node_ids.push_back(node.id); |
| } |
| |
| ProcessDisplayNodes(node_ids); |
| InitAXPosition(nodes[0].id); |
| } |
| |
| ui::AXTreeID tree_id_; |
| raw_ptr<MockAXTreeDistiller, DanglingUntriaged> distiller_ = nullptr; |
| testing::StrictMock<MockReadAnythingUntrustedPageHandler> page_handler_; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| |
| // ReadAnythingAppController constructor and destructor are protected so it's |
| // not accessible by std::make_unique. |
| raw_ptr<ReadAnythingAppController, DanglingUntriaged> controller_ = nullptr; |
| }; |
| |
| TEST_F(ReadAnythingAppControllerTest, IsReadAloudEnabled) { |
| EXPECT_FALSE(IsReadAloudEnabled()); |
| |
| scoped_feature_list_.InitAndEnableFeature(features::kReadAnythingReadAloud); |
| EXPECT_TRUE(IsReadAloudEnabled()); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, OnLetterSpacingChange_ValidChange) { |
| OnLetterSpacingChange(2); |
| EXPECT_CALL(page_handler_, |
| OnLetterSpaceChange(read_anything::mojom::LetterSpacing::kWide)) |
| .Times(1); |
| ASSERT_EQ(LetterSpacing(), 2); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, OnLetterSpacingChange_InvalidChange) { |
| OnLetterSpacingChange(10); |
| EXPECT_CALL(page_handler_, OnLetterSpaceChange).Times(0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, OnLineSpacingChange_ValidChange) { |
| OnLineSpacingChange(3); |
| EXPECT_CALL(page_handler_, |
| OnLineSpaceChange(read_anything::mojom::LineSpacing::kVeryLoose)) |
| .Times(1); |
| ASSERT_EQ(LineSpacing(), 3); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, OnLineSpacingChange_InvalidChange) { |
| OnLineSpacingChange(10); |
| EXPECT_CALL(page_handler_, OnLineSpaceChange).Times(0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, OnThemeChange_ValidChange) { |
| OnThemeChange(3); |
| EXPECT_CALL(page_handler_, |
| OnColorChange(read_anything::mojom::Colors::kYellow)) |
| .Times(1); |
| ASSERT_EQ(ColorTheme(), 3); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, OnThemeChange_InvalidChange) { |
| OnThemeChange(10); |
| EXPECT_CALL(page_handler_, OnColorChange).Times(0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, OnFontChange_UpdatesFont) { |
| std::string expected_font = "Roboto"; |
| |
| OnFontChange(expected_font); |
| |
| EXPECT_CALL(page_handler_, OnFontChange(expected_font)).Times(1); |
| ASSERT_EQ(FontName(), expected_font); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetValidatedFontName_FontWithQuotes) { |
| std::string expected_font = "\"Lexend Deca\""; |
| std::string actual_font = GetValidatedFont("Lexend Deca"); |
| ASSERT_EQ(actual_font, expected_font); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetValidatedFontName_FontWithoutQuotes) { |
| std::string expected_font = "serif"; |
| std::string actual_font = GetValidatedFont("Serif"); |
| ASSERT_EQ(actual_font, expected_font); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetValidatedFontName_InvalidFont) { |
| std::string expected_font = string_constants::kReadAnythingDefaultFont; |
| std::string actual_font = GetValidatedFont("not a real font"); |
| ASSERT_EQ(actual_font, expected_font); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetValidatedFontName_UnsupportedFont) { |
| std::string expected_font = string_constants::kReadAnythingDefaultFont; |
| std::string actual_font = GetValidatedFont("Times New Roman"); |
| ASSERT_EQ(actual_font, expected_font); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, OnSpeechRateChange) { |
| double expected_rate = 1.5; |
| |
| OnSpeechRateChange(expected_rate); |
| |
| EXPECT_CALL(page_handler_, OnSpeechRateChange(expected_rate)).Times(1); |
| ASSERT_EQ(SpeechRate(), expected_rate); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, OnLanguagePrefChange) { |
| std::string enabled_lang = "ja-jp"; |
| std::string disabled_lang = "en-us"; |
| |
| OnLanguagePrefChange(enabled_lang, true); |
| OnLanguagePrefChange(disabled_lang, true); |
| OnLanguagePrefChange(disabled_lang, false); |
| |
| EXPECT_CALL(page_handler_, OnLanguagePrefChange).Times(3); |
| ASSERT_TRUE(base::Contains(EnabledLanguages(), enabled_lang)); |
| ASSERT_FALSE(base::Contains(EnabledLanguages(), disabled_lang)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetStoredVoice_NoAutoSwitching_ReturnsLatestVoice) { |
| std::string current_lang = "it-IT"; |
| std::string current_voice = "Italian voice 3"; |
| std::string previous_voice = "Dutch voice 1"; |
| |
| SetLanguageCode(current_lang); |
| OnVoiceChange(previous_voice, current_lang); |
| OnVoiceChange(current_voice, current_lang); |
| |
| EXPECT_CALL(page_handler_, OnVoiceChange).Times(2); |
| ASSERT_EQ(GetStoredVoice(), current_voice); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetStoredVoice_NoAutoSwitching_ReturnsLatestVoiceRegardlessOfLang) { |
| std::string current_lang = "it-IT"; |
| std::string other_lang = "de-DE"; |
| std::string current_voice = "Italian voice 3"; |
| std::string previous_voice = "Dutch voice 1"; |
| |
| SetLanguageCode(current_lang); |
| OnVoiceChange(previous_voice, current_lang); |
| OnVoiceChange(current_voice, other_lang); |
| |
| EXPECT_CALL(page_handler_, OnVoiceChange).Times(2); |
| ASSERT_EQ(GetStoredVoice(), current_voice); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetStoredVoice_NoVoices_ReturnsEmpty) { |
| scoped_feature_list_.InitWithFeatures( |
| {features::kReadAnythingReadAloud, |
| features::kReadAloudAutoVoiceSwitching}, |
| {}); |
| ASSERT_EQ(GetStoredVoice(), ""); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetStoredVoice_CurrentBaseLangStored_ReturnsExpectedVoice) { |
| scoped_feature_list_.InitWithFeatures( |
| {features::kReadAnythingReadAloud, |
| features::kReadAloudAutoVoiceSwitching}, |
| {}); |
| std::string base_lang = "fr"; |
| std::string expected_voice_name = "French voice 1"; |
| |
| OnVoiceChange(expected_voice_name, base_lang); |
| SetLanguageCode(base_lang); |
| |
| EXPECT_CALL(page_handler_, OnVoiceChange).Times(1); |
| ASSERT_EQ(GetStoredVoice(), expected_voice_name); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetStoredVoice_CurrentFullLangStored_ReturnsExpectedVoice) { |
| scoped_feature_list_.InitWithFeatures( |
| {features::kReadAnythingReadAloud, |
| features::kReadAloudAutoVoiceSwitching}, |
| {}); |
| std::string full_lang = "en-UK"; |
| std::string expected_voice_name = "British voice 45"; |
| |
| OnVoiceChange(expected_voice_name, full_lang); |
| SetLanguageCode(full_lang); |
| |
| EXPECT_CALL(page_handler_, OnVoiceChange).Times(1); |
| ASSERT_EQ(GetStoredVoice(), expected_voice_name); |
| } |
| |
| TEST_F( |
| ReadAnythingAppControllerTest, |
| GetStoredVoice_BaseLangStoredButCurrentLangIsFull_ReturnsStoredBaseLang) { |
| scoped_feature_list_.InitWithFeatures( |
| {features::kReadAnythingReadAloud, |
| features::kReadAloudAutoVoiceSwitching}, |
| {}); |
| std::string base_lang = "zh"; |
| std::string full_lang = "zh-TW"; |
| std::string expected_voice_name = "Chinese voice"; |
| |
| OnVoiceChange(expected_voice_name, base_lang); |
| SetLanguageCode(full_lang); |
| |
| EXPECT_CALL(page_handler_, OnVoiceChange).Times(1); |
| ASSERT_EQ(GetStoredVoice(), expected_voice_name); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetStoredVoice_CurrentLangNotStored_ReturnsEmpty) { |
| scoped_feature_list_.InitWithFeatures( |
| {features::kReadAnythingReadAloud, |
| features::kReadAloudAutoVoiceSwitching}, |
| {}); |
| std::string current_lang = "de-DE"; |
| std::string stored_lang = "it-IT"; |
| |
| OnVoiceChange("Italian voice 3", stored_lang); |
| SetLanguageCode(current_lang); |
| |
| EXPECT_CALL(page_handler_, OnVoiceChange).Times(1); |
| ASSERT_EQ(GetStoredVoice(), ""); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, OnSettingsRestoredFromPrefs) { |
| auto line_spacing = read_anything::mojom::LineSpacing::kVeryLoose; |
| auto letter_spacing = read_anything::mojom::LetterSpacing::kVeryWide; |
| std::string font_name = "Roboto"; |
| double font_size = 18.0; |
| bool links_enabled = false; |
| bool images_enabled = true; |
| auto color = read_anything::mojom::Colors::kDefaultValue; |
| int color_value = 0; |
| double speech_rate = 1.5; |
| std::string voice_value = "Italian voice 3"; |
| std::string language_value = "it-IT"; |
| base::Value::Dict voices = base::Value::Dict(); |
| voices.Set(language_value, voice_value); |
| base::Value::List languages_enabled_in_pref = base::Value::List(); |
| languages_enabled_in_pref.Append(language_value); |
| auto highlight_granularity = |
| read_anything::mojom::HighlightGranularity::kDefaultValue; |
| int highlight_granularity_value = 0; |
| |
| OnSettingsRestoredFromPrefs( |
| line_spacing, letter_spacing, font_name, font_size, links_enabled, |
| images_enabled, color, speech_rate, std::move(voices), |
| std::move(languages_enabled_in_pref), highlight_granularity); |
| |
| EXPECT_EQ(static_cast<int>(line_spacing), LineSpacing()); |
| EXPECT_EQ(static_cast<int>(letter_spacing), LetterSpacing()); |
| EXPECT_EQ(font_name, FontName()); |
| EXPECT_EQ(font_size, FontSize()); |
| EXPECT_EQ(links_enabled, LinksEnabled()); |
| EXPECT_EQ(color_value, ColorTheme()); |
| EXPECT_EQ(speech_rate, SpeechRate()); |
| EXPECT_EQ(voice_value, GetStoredVoice()); |
| EXPECT_EQ(1u, GetLanguagesEnabledInPref().size()); |
| EXPECT_EQ(language_value, GetLanguagesEnabledInPref()[0]); |
| EXPECT_EQ(highlight_granularity_value, HighlightGranularity()); |
| } |
| |
| 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::AXNodeData node; |
| node.id = 3; |
| node.role = ax::mojom::Role::kNone; |
| SendUpdateWithNodes({node}); |
| 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::AXNodeData node; |
| node.id = 3; |
| node.role = ax::mojom::Role::kNone; |
| SendUpdateWithNodes({node}); |
| 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::AXNodeData span_node; |
| span_node.id = 2; |
| span_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, span); |
| |
| ui::AXNodeData h1_node; |
| h1_node.id = 3; |
| h1_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, h1); |
| |
| ui::AXNodeData ul_node; |
| ul_node.id = 4; |
| ul_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, ul); |
| |
| SendUpdateWithNodes({span_node, h1_node, ul_node}); |
| |
| 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::AXNodeData span_node; |
| span_node.id = 2; |
| span_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, span); |
| |
| ui::AXNodeData h1_node; |
| h1_node.id = 3; |
| h1_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, h1); |
| h1_node.role = ax::mojom::Role::kTextField; |
| |
| ui::AXNodeData ul_node; |
| ul_node.id = 4; |
| ul_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, ul); |
| ul_node.role = ax::mojom::Role::kTextFieldWithComboBox; |
| |
| SendUpdateWithNodes({span_node, h1_node, ul_node}); |
| |
| OnAXTreeDistilled({}); |
| EXPECT_EQ(span, GetHtmlTag(2)); |
| EXPECT_EQ(div, GetHtmlTag(3)); |
| EXPECT_EQ(div, GetHtmlTag(4)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetHtmlTag_SvgReturnsDivIfGoogleDocs) { |
| std::string svg = "svg"; |
| std::string div = "div"; |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| ui::AXNodeData node; |
| node.id = 2; |
| node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, svg); |
| |
| ui::AXNodeData root = test::LinkNode(/* id= */ 1, DOCS_URL); |
| root.child_ids = {node.id}; |
| update.nodes = {root, node}; |
| update.root_id = root.id; |
| |
| AccessibilityEventReceived({update}); |
| EXPECT_TRUE(IsUrlInformationSet(id_1)); |
| OnAXTreeDistilled({}); |
| OnActiveAXTreeIDChanged(id_1); |
| EXPECT_TRUE(IsGoogleDocs()); |
| EXPECT_EQ(div, GetHtmlTag(2)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetHtmlTag_paragraphWithTagGReturnsPIfGoogleDocs) { |
| std::string g = "g"; |
| std::string p = "p"; |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| ui::AXNodeData paragraph_node; |
| paragraph_node.id = 2; |
| paragraph_node.role = ax::mojom::Role::kParagraph; |
| paragraph_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, g); |
| |
| ui::AXNodeData svg_node; |
| svg_node.id = 3; |
| svg_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, g); |
| |
| ui::AXNodeData root = test::LinkNode(/* id= */ 1, DOCS_URL); |
| root.role = ax::mojom::Role::kParagraph; |
| root.child_ids = {paragraph_node.id, svg_node.id}; |
| update.root_id = root.id; |
| update.nodes = {root, paragraph_node, svg_node}; |
| AccessibilityEventReceived({update}); |
| EXPECT_TRUE(IsUrlInformationSet(id_1)); |
| OnAXTreeDistilled({}); |
| OnActiveAXTreeIDChanged(id_1); |
| EXPECT_TRUE(IsGoogleDocs()); |
| EXPECT_EQ("", GetHtmlTag(1)); |
| EXPECT_EQ(p, GetHtmlTag(2)); |
| EXPECT_EQ(g, GetHtmlTag(3)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetHtmlTag_DivWithHeadingAndAriaLevelReturnsH) { |
| std::string h3 = "h3"; |
| std::string div = "div"; |
| ui::AXNodeData node1; |
| node1.id = 2; |
| |
| ui::AXNodeData node2; |
| node2.id = 3; |
| node2.role = ax::mojom::Role::kHeading; |
| node2.AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel, 3); |
| |
| ui::AXNodeData node3; |
| node3.id = 4; |
| SendUpdateWithNodes({node1, node2, node3}); |
| OnAXTreeDistilled({}); |
| EXPECT_EQ(h3, GetHtmlTag(3)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetHtmlTag_PDF) { |
| // Send pdf iframe update with html tags to test. |
| SetIsPdf(); |
| ui::AXTreeUpdate update; |
| test::SetUpdateTreeID(&update, tree_id_); |
| ui::AXNodeData node1; |
| node1.id = 2; |
| node1.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "h1"); |
| ui::AXNodeData node2; |
| node2.id = 3; |
| node2.role = ax::mojom::Role::kHeading; |
| node2.AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel, 2); |
| |
| ui::AXNodeData root; |
| root.id = 1; |
| root.child_ids = {node1.id, node2.id}; |
| root.role = ax::mojom::Role::kPdfRoot; |
| update.root_id = root.id; |
| update.nodes = {root, node1, node2}; |
| AccessibilityEventReceived({update}); |
| |
| OnAXTreeDistilled({}); |
| EXPECT_EQ("span", GetHtmlTag(1)); |
| EXPECT_EQ("h1", GetHtmlTag(2)); |
| EXPECT_EQ("h2", GetHtmlTag(3)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetHtmlTag_IncorrectlyFormattedPDF) { |
| SetIsPdf(); |
| |
| // Send pdf update with html tags to test. Two headings next to each |
| // other should be spans. A heading that's too long should be turned into a |
| // paragraph. |
| ui::AXTreeUpdate update; |
| test::SetUpdateTreeID(&update, tree_id_); |
| ui::AXNodeData heading_node1; |
| heading_node1.id = 2; |
| heading_node1.role = ax::mojom::Role::kHeading; |
| heading_node1.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "h1"); |
| ui::AXNodeData heading_node2; |
| heading_node2.id = 3; |
| heading_node2.role = ax::mojom::Role::kHeading; |
| heading_node2.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "h1"); |
| |
| ui::AXNodeData link_node; |
| link_node.id = 4; |
| link_node.role = ax::mojom::Role::kLink; |
| |
| ui::AXNodeData aria_node; |
| aria_node.id = 5; |
| aria_node.role = ax::mojom::Role::kHeading; |
| aria_node.html_attributes.emplace_back("aria-level", "1"); |
| aria_node.SetNameChecked( |
| "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " |
| "tempor incididunt ut labore et dolore magna aliqua."); |
| aria_node.SetNameFrom(ax::mojom::NameFrom::kContents); |
| |
| ui::AXNodeData root; |
| root.id = 1; |
| root.child_ids = {heading_node1.id, heading_node2.id, link_node.id, |
| aria_node.id}; |
| root.role = ax::mojom::Role::kPdfRoot; |
| update.root_id = root.id; |
| update.nodes = {root, heading_node1, heading_node2, link_node, aria_node}; |
| |
| AccessibilityEventReceived({update}); |
| |
| OnAXTreeDistilled({}); |
| EXPECT_EQ("span", GetHtmlTag(2)); |
| EXPECT_EQ("span", GetHtmlTag(3)); |
| EXPECT_EQ("a", GetHtmlTag(4)); |
| EXPECT_EQ("p", GetHtmlTag(5)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetHtmlTag_InaccessiblePDF) { |
| SetIsPdf(); |
| |
| // Send pdf iframe update with html tags to test. |
| ui::AXTreeUpdate update; |
| test::SetUpdateTreeID(&update, tree_id_); |
| ui::AXNodeData node; |
| node.id = 2; |
| node.role = ax::mojom::Role::kContentInfo; |
| node.SetNameChecked(l10n_util::GetStringUTF8(IDS_PDF_OCR_RESULT_END)); |
| node.SetNameFrom(ax::mojom::NameFrom::kContents); |
| |
| ui::AXNodeData root; |
| root.id = 1; |
| root.child_ids = {node.id}; |
| root.role = ax::mojom::Role::kPdfRoot; |
| update.root_id = 1; |
| update.nodes = {root, node}; |
| AccessibilityEventReceived({update}); |
| |
| OnAXTreeDistilled({}); |
| EXPECT_EQ("br", GetHtmlTag(2)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetAltText) { |
| std::string img = "img"; |
| std::string sample_alt_text = "sample_alt_text"; |
| ui::AXNodeData img_node; |
| img_node.id = 2; |
| img_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, img); |
| img_node.AddStringAttribute(ax::mojom::StringAttribute::kName, |
| sample_alt_text); |
| |
| SendUpdateWithNodes({img_node}); |
| |
| OnAXTreeDistilled({}); |
| EXPECT_EQ(img, GetHtmlTag(2)); |
| EXPECT_EQ(sample_alt_text, GetAltText(2)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetAltText_Unset) { |
| std::string img = "img"; |
| ui::AXNodeData img_node; |
| img_node.id = 2; |
| img_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, img); |
| |
| SendUpdateWithNodes({img_node}); |
| |
| OnAXTreeDistilled({}); |
| EXPECT_EQ(img, GetHtmlTag(2)); |
| EXPECT_EQ("", GetAltText(2)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetImageDataUrl) { |
| std::string img = "img"; |
| std::string img_data = |
| "data:image/" |
| "png;base64," |
| "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAADElEQVQImWNgoBMAAABpAAFE" |
| "I8ARAAAAAElFTkSuQmCC"; |
| ui::AXNodeData img_node; |
| img_node.id = 2; |
| img_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, img); |
| img_node.AddStringAttribute(ax::mojom::StringAttribute::kImageDataUrl, |
| img_data); |
| |
| SendUpdateWithNodes({img_node}); |
| |
| OnAXTreeDistilled({}); |
| EXPECT_EQ(img, GetHtmlTag(2)); |
| EXPECT_EQ(img_data, GetImageDataUrl(2)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetImageDataUrl_Unset) { |
| std::string img = "img"; |
| ui::AXNodeData img_node; |
| img_node.id = 2; |
| img_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, img); |
| |
| SendUpdateWithNodes({img_node}); |
| |
| OnAXTreeDistilled({}); |
| EXPECT_EQ(img, GetHtmlTag(2)); |
| EXPECT_EQ("", GetImageDataUrl(2)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetTextContent_NoSelection) { |
| ui::AXNodeData node1 = test::TextNode(/* id= */ 2, u"Hello"); |
| ui::AXNodeData node2 = test::ExplicitlyEmptyTextNode(/* id= */ 3); |
| ui::AXNodeData node3 = test::TextNode(/* id = */ 4, u" world"); |
| |
| SendUpdateWithNodes({node1, node2, node3}); |
| OnAXTreeDistilled({}); |
| EXPECT_EQ(u"Hello world", GetTextContent(1)); |
| EXPECT_EQ(u"Hello", GetTextContent(2)); |
| EXPECT_EQ(u"", GetTextContent(3)); |
| EXPECT_EQ(u" world", GetTextContent(4)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetTextContent_WithSelection) { |
| ui::AXTreeUpdate update; |
| SetUpdateTreeID(&update); |
| ui::AXNodeData node1 = test::TextNode(/* id= */ 2, u"Hello"); |
| ui::AXNodeData node2 = test::TextNode(/* id= */ 3, u" world"); |
| ui::AXNodeData node3 = test::TextNode(/* id= */ 4, u" friend"); |
| update.nodes = {node1, node2, node3}; |
| |
| // 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(u"Hello world friend", GetTextContent(1)); |
| EXPECT_EQ(u"Hello", GetTextContent(2)); |
| EXPECT_EQ(u" world", GetTextContent(3)); |
| EXPECT_EQ(u" friend", GetTextContent(4)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetTextContent_IgoreStaticTextIfGoogleDocs) { |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| ui::AXNodeData node1 = test::TextNode(/* id= */ 2, u"Hello"); |
| ui::AXNodeData node2 = test::ExplicitlyEmptyTextNode(/* id= */ 3); |
| |
| ui::AXNodeData root = test::LinkNode(/* id= */ 1, DOCS_URL); |
| root.child_ids = {node1.id, node2.id}; |
| root.role = ax::mojom::Role::kParagraph; |
| update.root_id = root.id; |
| update.nodes = {root, node1, node2}; |
| |
| AccessibilityEventReceived({update}); |
| EXPECT_TRUE(IsUrlInformationSet(id_1)); |
| OnAXTreeDistilled({}); |
| OnActiveAXTreeIDChanged(id_1); |
| EXPECT_TRUE(IsGoogleDocs()); |
| EXPECT_EQ(u"", GetTextContent(2)); |
| EXPECT_EQ(u"", GetTextContent(3)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetTextContent_UseNameAttributeTextIfGoogleDocs) { |
| std::u16string text_content = u"Hello"; |
| std::u16string more_text_content = u"world"; |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| ui::AXNodeData node1; |
| node1.id = 2; |
| node1.AddStringAttribute(ax::mojom::StringAttribute::kName, "Hello"); |
| |
| ui::AXNodeData node2; |
| node2.id = 3; |
| node2.AddStringAttribute(ax::mojom::StringAttribute::kName, "world"); |
| ui::AXNodeData root = test::LinkNode(/* id= */ 1, DOCS_URL); |
| root.child_ids = {node1.id, node2.id}; |
| root.role = ax::mojom::Role::kParagraph; |
| update.root_id = root.id; |
| update.nodes = {root, node1, node2}; |
| |
| AccessibilityEventReceived({update}); |
| EXPECT_TRUE(IsUrlInformationSet(id_1)); |
| OnAXTreeDistilled({}); |
| OnActiveAXTreeIDChanged(id_1); |
| EXPECT_TRUE(IsGoogleDocs()); |
| EXPECT_EQ(u"Hello world ", GetTextContent(1)); |
| EXPECT_EQ(text_content + u" ", GetTextContent(2)); |
| EXPECT_EQ(more_text_content + u" ", GetTextContent(3)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetTextContent_DoNotUseNameAttributeTextIfNotGoogleDocs) { |
| std::string text_content = "Hello"; |
| std::string more_text_content = "world"; |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| ui::AXNodeData node1; |
| node1.id = 2; |
| node1.AddStringAttribute(ax::mojom::StringAttribute::kName, text_content); |
| |
| ui::AXNodeData node2; |
| node2.id = 3; |
| node2.AddStringAttribute(ax::mojom::StringAttribute::kName, |
| more_text_content); |
| |
| ui::AXNodeData root = test::LinkNode(/* id= */ 1, "https://www.google.com"); |
| root.child_ids = {node1.id, node2.id}; |
| root.role = ax::mojom::Role::kParagraph; |
| update.root_id = root.id; |
| update.nodes = {root, node1, node2}; |
| |
| AccessibilityEventReceived({update}); |
| EXPECT_TRUE(IsUrlInformationSet(id_1)); |
| OnAXTreeDistilled({}); |
| OnActiveAXTreeIDChanged(id_1); |
| EXPECT_FALSE(IsGoogleDocs()); |
| EXPECT_EQ(u"", GetTextContent(1)); |
| EXPECT_EQ(u"", GetTextContent(2)); |
| EXPECT_EQ(u"", GetTextContent(3)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetDisplayNameForLocale) { |
| EXPECT_EQ(GetDisplayNameForLocale("en-US", "en"), "English (United States)"); |
| EXPECT_EQ(GetDisplayNameForLocale("en-US", "es"), "Inglés (Estados Unidos)"); |
| EXPECT_EQ(GetDisplayNameForLocale("en-US", "en-US"), |
| "English (United States)"); |
| EXPECT_EQ(GetDisplayNameForLocale("en-UK", "en"), "English (United Kingdom)"); |
| EXPECT_EQ(GetDisplayNameForLocale("en-UK", "foo5"), ""); |
| EXPECT_EQ(GetDisplayNameForLocale("foo", "en"), ""); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetUrl) { |
| std::string http_url = "http://www.google.com"; |
| std::string https_url = "https://www.google.com"; |
| std::string invalid_url = "cats"; |
| std::string missing_url = ""; |
| std::string js = "javascript:alert(origin)"; |
| |
| ui::AXNodeData node1 = test::LinkNode(/* id= */ 2, http_url); |
| ui::AXNodeData node2 = test::LinkNode(/*id= */ 3, https_url); |
| ui::AXNodeData node3 = test::LinkNode(/* id= */ 4, invalid_url); |
| ui::AXNodeData node4 = test::LinkNode(/* id= */ 5, missing_url); |
| ui::AXNodeData node5 = test::LinkNode(/* id= */ 6, js); |
| |
| ui::AXNodeData root; |
| root.id = 1; |
| root.child_ids = {node1.id, node2.id, node3.id, node4.id, node5.id}; |
| SendUpdateWithNodes({root, node1, node2, node3, node4, node5}); |
| |
| OnAXTreeDistilled({}); |
| EXPECT_EQ(http_url, GetUrl(2)); |
| EXPECT_EQ(https_url, GetUrl(3)); |
| EXPECT_EQ("", GetUrl(4)); |
| EXPECT_EQ("", GetUrl(5)); |
| EXPECT_EQ("", GetUrl(6)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, ShouldBold) { |
| ui::AXNodeData overline_node; |
| overline_node.id = 2; |
| overline_node.AddTextStyle(ax::mojom::TextStyle::kOverline); |
| |
| ui::AXNodeData underline_node; |
| underline_node.id = 3; |
| underline_node.AddTextStyle(ax::mojom::TextStyle::kUnderline); |
| |
| ui::AXNodeData italic_node; |
| italic_node.id = 4; |
| italic_node.AddTextStyle(ax::mojom::TextStyle::kItalic); |
| SendUpdateWithNodes({overline_node, underline_node, italic_node}); |
| |
| OnAXTreeDistilled({}); |
| EXPECT_EQ(false, ShouldBold(2)); |
| EXPECT_EQ(true, ShouldBold(3)); |
| EXPECT_EQ(true, ShouldBold(4)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetDataFontCss) { |
| std::string dataFontCss = "italic 400 14.6667px 'Courier New'"; |
| ui::AXNodeData node; |
| node.id = 2; |
| node.html_attributes.emplace_back("data-font-css", dataFontCss); |
| SendUpdateWithNodes({node}); |
| OnAXTreeDistilled({}); |
| EXPECT_EQ(dataFontCss, GetDataFontCss(2)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, IsOverline) { |
| ui::AXNodeData overline_node; |
| overline_node.id = 2; |
| overline_node.AddTextStyle(ax::mojom::TextStyle::kOverline); |
| |
| ui::AXNodeData underline_node; |
| underline_node.id = 3; |
| underline_node.AddTextStyle(ax::mojom::TextStyle::kUnderline); |
| SendUpdateWithNodes({overline_node, underline_node}); |
| |
| OnAXTreeDistilled({}); |
| EXPECT_EQ(true, IsOverline(2)); |
| EXPECT_EQ(false, IsOverline(3)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, IsLeafNode) { |
| ui::AXNodeData node1; |
| node1.id = 2; |
| |
| ui::AXNodeData node2; |
| node2.id = 3; |
| |
| ui::AXNodeData node3; |
| node3.id = 4; |
| |
| ui::AXNodeData parent; |
| parent.id = 1; |
| parent.child_ids = {node1.id, node2.id, node3.id}; |
| SendUpdateWithNodes({parent, node1, node2, node3}); |
| |
| OnAXTreeDistilled({}); |
| EXPECT_EQ(false, IsLeafNode(1)); |
| EXPECT_EQ(true, IsLeafNode(2)); |
| EXPECT_EQ(true, IsLeafNode(3)); |
| EXPECT_EQ(true, IsLeafNode(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); |
| ui::AXNodeData node; |
| node.id = 3; |
| update.nodes = {node}; |
| // 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, Draw_RecomputeDisplayNodes) { |
| ui::AXNodeData node; |
| node.id = 4; |
| |
| // This update changes the structure of the tree. When the controller receives |
| // it in AccessibilityEventReceived, it will re-distill the tree. |
| SendUpdateWithNodes({node}); |
| Reset({3, 4}); |
| Draw(/* recompute_display_nodes= */ true); |
| EXPECT_TRUE(DisplayNodeIdsContains(1)); |
| EXPECT_FALSE(DisplayNodeIdsContains(2)); |
| EXPECT_TRUE(DisplayNodeIdsContains(3)); |
| EXPECT_TRUE(DisplayNodeIdsContains(4)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, Draw_DoNotRecomputeDisplayNodesForDocs) { |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| ui::AXNodeData node; |
| node.id = 2; |
| |
| ui::AXNodeData root = test::LinkNode(/* id= */ 1, DOCS_URL); |
| root.child_ids = {node.id}; |
| update.nodes = {root, node}; |
| update.root_id = root.id; |
| |
| EXPECT_CALL(*distiller_, Distill).Times(1); |
| ui::AXEvent load_complete(0, ax::mojom::Event::kLoadComplete); |
| AccessibilityEventReceived({update}, {load_complete}); |
| OnAXTreeDistilled({3}); |
| OnActiveAXTreeIDChanged(id_1); |
| EXPECT_TRUE(IsGoogleDocs()); |
| EXPECT_TRUE(DisplayNodeIdsContains(1)); |
| EXPECT_FALSE(DisplayNodeIdsContains(2)); |
| EXPECT_TRUE(DisplayNodeIdsContains(3)); |
| Mock::VerifyAndClearExpectations(distiller_); |
| |
| ui::AXNodeData node1; |
| node1.id = 4; |
| |
| // This update changes the structure of the tree. When the controller receives |
| // it in AccessibilityEventReceived, it will re-distill the tree. |
| SendUpdateWithNodes({node1}); |
| Reset({3, 4}); |
| Draw(/* recompute_display_nodes= */ true); |
| EXPECT_FALSE(DisplayNodeIdsContains(1)); |
| EXPECT_FALSE(DisplayNodeIdsContains(2)); |
| EXPECT_FALSE(DisplayNodeIdsContains(3)); |
| EXPECT_FALSE(DisplayNodeIdsContains(4)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, AccessibilityEventReceived) { |
| // Tree starts off with no text content. |
| EXPECT_EQ(u"", GetTextContent(1)); |
| EXPECT_EQ(u"", GetTextContent(2)); |
| EXPECT_EQ(u"", GetTextContent(3)); |
| EXPECT_EQ(u"", GetTextContent(4)); |
| |
| // Send a new update which settings the text content of node 2. |
| ui::AXNodeData node = test::TextNode(/* id= */ 2, u"Hello world"); |
| SendUpdateWithNodes({node}); |
| |
| EXPECT_EQ(u"Hello world", GetTextContent(1)); |
| EXPECT_EQ(u"Hello world", GetTextContent(2)); |
| EXPECT_EQ(u"", GetTextContent(3)); |
| EXPECT_EQ(u"", GetTextContent(4)); |
| |
| // Send three updates which should be merged. |
| SendBatchUpdates(); |
| EXPECT_EQ(u"Node 2Node 3Node 4", GetTextContent(1)); |
| EXPECT_EQ(u"Node 2", GetTextContent(2)); |
| EXPECT_EQ(u"Node 3", GetTextContent(3)); |
| EXPECT_EQ(u"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; |
| ui::AXNodeData clearNode; |
| clearNode.id = 1; |
| clear_update.nodes = {clearNode}; |
| AccessibilityEventReceived({clear_update}); |
| EXPECT_EQ(u"", GetTextContent(1)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| AccessibilityEventReceivedWhileDistilling) { |
| // Tree starts off with no text content. |
| EXPECT_EQ(u"", GetTextContent(1)); |
| EXPECT_EQ(u"", GetTextContent(2)); |
| EXPECT_EQ(u"", GetTextContent(3)); |
| EXPECT_EQ(u"", GetTextContent(4)); |
| |
| // Send a new update which settings the text content of node 2. |
| ui::AXNodeData start_node = test::TextNode(/* id= */ 2, u"Hello world"); |
| SendUpdateWithNodes({start_node}); |
| |
| EXPECT_EQ(u"Hello world", GetTextContent(1)); |
| EXPECT_EQ(u"Hello world", GetTextContent(2)); |
| EXPECT_EQ(u"", GetTextContent(3)); |
| EXPECT_EQ(u"", GetTextContent(4)); |
| |
| // Send three updates while distilling. |
| set_distillation_in_progress(true); |
| SendBatchUpdates(); |
| |
| // The updates shouldn't be applied yet. |
| EXPECT_EQ(u"Hello world", GetTextContent(1)); |
| EXPECT_EQ(u"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. |
| set_distillation_in_progress(false); |
| ui::AXNodeData final_node = test::TextNode(/* id= */ 2, u"Final update"); |
| SendUpdateWithNodes({final_node}); |
| |
| EXPECT_EQ(u"Final updateNode 3Node 4", GetTextContent(1)); |
| EXPECT_EQ(u"Final update", GetTextContent(2)); |
| EXPECT_EQ(u"Node 3", GetTextContent(3)); |
| EXPECT_EQ(u"Node 4", GetTextContent(4)); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, AccessibilityEventReceivedWhileSpeaking) { |
| // Tree starts off with no text content. |
| EXPECT_EQ(u"", GetTextContent(1)); |
| EXPECT_EQ(u"", GetTextContent(2)); |
| EXPECT_EQ(u"", GetTextContent(3)); |
| EXPECT_EQ(u"", GetTextContent(4)); |
| |
| // Send a new update which settings the text content of node 2. |
| ui::AXNodeData start_node = test::TextNode(/* id= */ 2, u"Hello world"); |
| SendUpdateWithNodes({start_node}); |
| |
| EXPECT_EQ(u"Hello world", GetTextContent(1)); |
| EXPECT_EQ(u"Hello world", GetTextContent(2)); |
| EXPECT_EQ(u"", GetTextContent(3)); |
| EXPECT_EQ(u"", GetTextContent(4)); |
| |
| // Send three updates while playing. |
| OnSpeechPlayingStateChanged(/* is_speech_active= */ true); |
| SendBatchUpdates(); |
| |
| // The updates shouldn't be applied yet. |
| EXPECT_EQ(u"Hello world", GetTextContent(1)); |
| EXPECT_EQ(u"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. |
| OnSpeechPlayingStateChanged(/* is_speech_active= */ false); |
| ui::AXNodeData final_node = test::TextNode(/* id= */ 2, u"Final update"); |
| SendUpdateWithNodes({final_node}); |
| |
| EXPECT_EQ(u"Final updateNode 3Node 4", GetTextContent(1)); |
| EXPECT_EQ(u"Final update", GetTextContent(2)); |
| EXPECT_EQ(u"Node 3", GetTextContent(3)); |
| EXPECT_EQ(u"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; |
| test::SetUpdateTreeID(&update, tree_ids[i]); |
| ui::AXNodeData node = |
| test::TextNode(/* id= */ 1, u"Tree " + base::NumberToString16(i)); |
| update.root_id = node.id; |
| update.nodes = {node}; |
| 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(u"Tree " + base::NumberToString16(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, IsGoogleDocs) { |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| update.root_id = 1; |
| |
| ui::AXNodeData node = test::LinkNode(/*id = */ 1, "www.google.com"); |
| update.nodes = {node}; |
| AccessibilityEventReceived({update}); |
| EXPECT_TRUE(IsUrlInformationSet(id_1)); |
| OnAXTreeDistilled({1}); |
| |
| EXPECT_CALL(*distiller_, Distill).Times(1); |
| OnActiveAXTreeIDChanged(id_1); |
| EXPECT_FALSE(IsGoogleDocs()); |
| Mock::VerifyAndClearExpectations(distiller_); |
| |
| ui::AXTreeUpdate update_1; |
| test::SetUpdateTreeID(&update_1, tree_id_); |
| ui::AXNodeData root = test::LinkNode(/*id = */ 1, DOCS_URL); |
| update_1.root_id = root.id; |
| update_1.nodes = {root}; |
| AccessibilityEventReceived({update_1}); |
| EXPECT_TRUE(IsUrlInformationSet(tree_id_)); |
| OnAXTreeDistilled({1}); |
| |
| EXPECT_CALL(*distiller_, Distill).Times(1); |
| OnActiveAXTreeIDChanged(tree_id_); |
| EXPECT_TRUE(IsGoogleDocs()); |
| 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; |
| test::SetUpdateTreeID(&update, tree_ids[i]); |
| ui::AXNodeData node; |
| node.id = 1; |
| update.root_id = node.id; |
| update.nodes = {node}; |
| 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) { |
| std::vector<int> child_ids = SendSimpleUpdateAndGetChildIds(); |
| std::vector<ui::AXTreeUpdate> updates = CreateSimpleUpdateList(child_ids); |
| |
| // Send update 0. |
| EXPECT_CALL(*distiller_, Distill).Times(0); |
| AccessibilityEventReceived({updates[0]}); |
| EXPECT_EQ(u"2345", GetTextContent(1)); |
| Mock::VerifyAndClearExpectations(distiller_); |
| |
| // Send update 1. |
| EXPECT_CALL(*distiller_, Distill).Times(0); |
| AccessibilityEventReceived({updates[1]}); |
| EXPECT_EQ(u"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) { |
| std::vector<int> child_ids = SendSimpleUpdateAndGetChildIds(); |
| std::vector<ui::AXTreeUpdate> updates = CreateSimpleUpdateList(child_ids); |
| |
| // Send update 0. Data gets unserialized. |
| EXPECT_CALL(*distiller_, Distill).Times(0); |
| AccessibilityEventReceived({updates[0]}); |
| EXPECT_EQ(u"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(u"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(u"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(u"234567", GetTextContent(1)); |
| Mock::VerifyAndClearExpectations(distiller_); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| SpeechPlaying_TreeUpdateReceivedOnActiveTree) { |
| std::vector<int> child_ids = SendSimpleUpdateAndGetChildIds(); |
| std::vector<ui::AXTreeUpdate> updates = CreateSimpleUpdateList(child_ids); |
| |
| // Send update 0. Data gets unserialized. |
| EXPECT_CALL(*distiller_, Distill).Times(0); |
| AccessibilityEventReceived({updates[0]}); |
| EXPECT_EQ(u"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(u"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. Speech starts playing |
| EXPECT_CALL(*distiller_, Distill).Times(0); |
| ui::AXEvent load_complete_2(2, ax::mojom::Event::kLoadComplete); |
| OnSpeechPlayingStateChanged(/*is_speech_active=*/true); |
| AccessibilityEventReceived({updates[2]}, {load_complete_2}); |
| EXPECT_EQ(u"23456", GetTextContent(1)); |
| Mock::VerifyAndClearExpectations(distiller_); |
| |
| // Complete distillation with speech still playing. This does not result in |
| // distillation (yet). The data is not unserialized |
| EXPECT_CALL(*distiller_, Distill).Times(0); |
| OnAXTreeDistilled({1}); |
| EXPECT_EQ(u"23456", GetTextContent(1)); |
| Mock::VerifyAndClearExpectations(distiller_); |
| |
| // Speech stops. We request distillation (deferred from above) |
| EXPECT_CALL(*distiller_, Distill).Times(1); |
| OnSpeechPlayingStateChanged(/*is_speech_active=*/false); |
| EXPECT_EQ(u"23456", GetTextContent(1)); |
| Mock::VerifyAndClearExpectations(distiller_); |
| |
| // Complete distillation. The queued up tree update gets unserialized. |
| EXPECT_CALL(*distiller_, Distill).Times(0); |
| OnAXTreeDistilled({1}); |
| EXPECT_EQ(u"234567", GetTextContent(1)); |
| Mock::VerifyAndClearExpectations(distiller_); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| AccessibilityReceivedAfterDistillingOnSameTree_DoesNotCrash) { |
| std::vector<int> child_ids = SendSimpleUpdateAndGetChildIds(); |
| std::vector<ui::AXTreeUpdate> updates = CreateSimpleUpdateList(child_ids); |
| |
| // 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}); |
| set_distillation_in_progress(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<int> child_ids = {2, 3, 4}; |
| std::vector<ui::AXTreeUpdate> updates = CreateSimpleUpdateList(child_ids); |
| |
| 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(u"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_, active_tree_id()); |
| OnActiveAXTreeIDChanged(tree_id_2); |
| ASSERT_EQ(tree_id_2, active_tree_id()); |
| 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<int> child_ids = {2, 3, 4}; |
| std::vector<ui::AXTreeUpdate> updates = CreateSimpleUpdateList(child_ids); |
| |
| // Create an update which has no tree id. |
| ui::AXTreeUpdate update; |
| ui::AXNodeData generic_container_node = test::GenericContainerNode(/*id =*/1); |
| update.nodes = {generic_container_node}; |
| 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, RequestImageDataUrl) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitWithFeatures( |
| {features::kReadAnythingImagesViaAlgorithm, |
| features::kReadAnythingReadAloud}, |
| {}); |
| ui::AXNodeID ax_node_id = 2; |
| EXPECT_CALL(page_handler_, OnImageDataRequested(tree_id_, ax_node_id)) |
| .Times(1); |
| |
| auto line_spacing = read_anything::mojom::LineSpacing::kDefaultValue; |
| auto letter_spacing = read_anything::mojom::LetterSpacing::kDefaultValue; |
| std::string font_name = "Roboto"; |
| double font_size = 18.0; |
| bool links_enabled = false; |
| bool images_enabled = true; |
| auto color = read_anything::mojom::Colors::kDefaultValue; |
| double speech_rate = 1.5; |
| std::string voice_value = "Italian voice 3"; |
| std::string language_value = "it-IT"; |
| base::Value::Dict voices = base::Value::Dict(); |
| voices.Set(language_value, voice_value); |
| base::Value::List languages_enabled_in_pref = base::Value::List(); |
| languages_enabled_in_pref.Append(language_value); |
| auto highlight_granularity = |
| read_anything::mojom::HighlightGranularity::kDefaultValue; |
| |
| OnSettingsRestoredFromPrefs( |
| line_spacing, letter_spacing, font_name, font_size, links_enabled, |
| images_enabled, color, speech_rate, std::move(voices), |
| std::move(languages_enabled_in_pref), highlight_granularity); |
| RequestImageDataUrl(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; |
| test::SetUpdateTreeID(&update, new_tree_id); |
| ui::AXNodeData node; |
| node.id = 1; |
| update.root_id = node.id; |
| update.nodes = {node}; |
| 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, ScrollToTargetNode_ScrollsIfGoogleDocs) { |
| ui::AXNodeData root; |
| ui::AXNodeData node; |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| |
| root.id = 1; |
| root.AddStringAttribute( |
| ax::mojom::StringAttribute::kUrl, |
| "https://docs.google.com/document/d/" |
| "1t6x1PQaQWjE8wb9iyYmFaoK1XAEgsl8G1Hx3rzfpoKA/" |
| "edit?ouid=103677288878638916900&usp=docs_home&ths=true"); |
| node.id = 2; |
| root.child_ids = {node.id}; |
| update.nodes = {root, node}; |
| update.root_id = root.id; |
| |
| AccessibilityEventReceived({update}); |
| EXPECT_TRUE(IsUrlInformationSet(id_1)); |
| OnAXTreeDistilled({1}); |
| EXPECT_CALL(*distiller_, Distill).Times(1); |
| OnActiveAXTreeIDChanged(id_1); |
| EXPECT_TRUE(IsGoogleDocs()); |
| |
| ui::AXNodeID ax_node_id = 4; |
| EXPECT_CALL(page_handler_, ScrollToTargetNode(id_1, ax_node_id)).Times(1); |
| OnScrolledToBottom(); |
| page_handler_.FlushForTesting(); |
| Mock::VerifyAndClearExpectations(distiller_); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| ScrollToTargetNode_DoesNotScrollIfNotGoogleDocs) { |
| ui::AXNodeData root; |
| ui::AXNodeData node; |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| |
| root.id = 1; |
| root.AddStringAttribute(ax::mojom::StringAttribute::kUrl, |
| "https://www.google.com/"); |
| node.id = 2; |
| root.child_ids = {node.id}; |
| update.nodes = {root, node}; |
| update.root_id = root.id; |
| |
| AccessibilityEventReceived({update}); |
| EXPECT_TRUE(IsUrlInformationSet(id_1)); |
| OnAXTreeDistilled({1}); |
| EXPECT_CALL(*distiller_, Distill).Times(1); |
| OnActiveAXTreeIDChanged(id_1); |
| EXPECT_FALSE(IsGoogleDocs()); |
| |
| ui::AXNodeID ax_node_id = 4; |
| EXPECT_CALL(page_handler_, ScrollToTargetNode(id_1, ax_node_id)).Times(0); |
| OnScrolledToBottom(); |
| page_handler_.FlushForTesting(); |
| Mock::VerifyAndClearExpectations(distiller_); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, OnSelectionChange) { |
| ui::AXNodeData node1 = test::TextNode(/* id= */ 2); |
| ui::AXNodeData node2 = test::TextNode(/* id= */ 3); |
| ui::AXNodeData node3 = test::TextNode(/* id= */ 4); |
| |
| SendUpdateWithNodes({node1, node2, node3}); |
| 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, OnCollapseSelection) { |
| ui::AXNodeData node1 = test::TextNode(/* id= */ 2); |
| ui::AXNodeData node2 = test::TextNode(/* id= */ 3); |
| ui::AXNodeData node3 = test::TextNode(/* id= */ 4); |
| |
| SendUpdateWithNodes({node1, node2, node3}); |
| EXPECT_CALL(page_handler_, OnCollapseSelection()).Times(1); |
| OnCollapseSelection(); |
| Mock::VerifyAndClearExpectations(distiller_); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| OnSelectionChange_ClickAfterClickDoesNotUpdateSelection) { |
| ui::AXNodeData node1 = test::TextNode(/* id= */ 2); |
| ui::AXNodeData node2 = test::TextNode(/* id= */ 3); |
| SendUpdateWithNodes({node1, node2}); |
| |
| 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::AXNodeData node1 = test::TextNode(/* id= */ 2); |
| ui::AXNodeData node2 = test::TextNode(/* id= */ 3); |
| SendUpdateWithNodes({node1, node2}); |
| |
| 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_, OnCollapseSelection()).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; |
| test::SetUpdateTreeID(&update, new_tree_id); |
| ui::AXNodeData root = test::TextNode(/* id= */ 1); |
| update.root_id = root.id; |
| update.nodes = {root}; |
| 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::AXNodeData text_field_node1; |
| text_field_node1.id = 2; |
| text_field_node1.role = ax::mojom::Role::kTextField; |
| |
| ui::AXNodeData container_node = test::GenericContainerNode(/*id= */ 3); |
| |
| ui::AXNodeData text_field_node2; |
| text_field_node2.id = 4; |
| text_field_node2.role = ax::mojom::Role::kTextField; |
| SendUpdateWithNodes({text_field_node1, container_node, text_field_node2}); |
| |
| 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; |
| ui::AXNodeData text_node = test::TextNode(/* id= */ 3, u"Hello"); |
| |
| ui::AXNodeData ignored_node; |
| ignored_node.id = 4; |
| ignored_node.role = ax::mojom::Role::kNone; // This node is ignored. |
| update.nodes = {text_node, ignored_node}; |
| 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({}); |
| |
| EXPECT_EQ(0, StartNodeId()); |
| EXPECT_EQ(0, EndNodeId()); |
| EXPECT_EQ(-1, StartOffset()); |
| EXPECT_EQ(-1, EndOffset()); |
| EXPECT_EQ(false, HasSelection()); |
| } |
| |
| 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(2, StartNodeId()); |
| EXPECT_EQ(2, EndNodeId()); |
| EXPECT_EQ(3, StartOffset()); |
| EXPECT_EQ(3, EndOffset()); |
| EXPECT_EQ(false, HasSelection()); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, OnFontSizeReset_SetsFontSizeToDefault) { |
| EXPECT_CALL(page_handler_, OnFontSizeChange(kReadAnythingDefaultFontScale)) |
| .Times(1); |
| OnFontSizeReset(); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| OnLinksEnabledChanged_SetsEnabledToFalse) { |
| EXPECT_CALL(page_handler_, |
| OnLinksEnabledChanged(!kReadAnythingDefaultLinksEnabled)) |
| .Times(1); |
| OnLinksEnabledToggled(); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, TurnedHighlightOn_SavesHighlightState) { |
| EXPECT_CALL(page_handler_, |
| OnHighlightGranularityChanged( |
| read_anything::mojom::HighlightGranularity::kOn)) |
| .Times(1); |
| EXPECT_CALL(page_handler_, |
| OnHighlightGranularityChanged( |
| read_anything::mojom::HighlightGranularity::kOff)) |
| .Times(0); |
| |
| TurnedHighlightOn(); |
| |
| EXPECT_TRUE(IsHighlightOn()); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, TurnedHighlightOff_SavesHighlightState) { |
| EXPECT_CALL(page_handler_, |
| OnHighlightGranularityChanged( |
| read_anything::mojom::HighlightGranularity::kOn)) |
| .Times(0); |
| EXPECT_CALL(page_handler_, |
| OnHighlightGranularityChanged( |
| read_anything::mojom::HighlightGranularity::kOff)) |
| .Times(1); |
| |
| TurnedHighlightOff(); |
| |
| EXPECT_FALSE(IsHighlightOn()); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, SetLanguageCode_UpdatesModelLanguage) { |
| SetLanguageCode("es"); |
| ASSERT_EQ(LanguageCodeForSpeech(), "es"); |
| |
| SetLanguageCode("en-UK"); |
| ASSERT_EQ(LanguageCodeForSpeech(), "en"); |
| |
| SetLanguageCode("zh-CN"); |
| ASSERT_EQ(LanguageCodeForSpeech(), "zh"); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| SetLanguageCode_EmptyCode_DoesNotUpdateModelLanguage) { |
| SetLanguageCode("es"); |
| ASSERT_EQ(LanguageCodeForSpeech(), "es"); |
| ASSERT_FALSE(RequiresTreeLang()); |
| |
| SetLanguageCode(""); |
| ASSERT_EQ(LanguageCodeForSpeech(), "es"); |
| ASSERT_TRUE(RequiresTreeLang()); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| SetLanguageCode_EmptyCode_SetsRootLanguageOnceAvailable) { |
| ASSERT_EQ(LanguageCodeForSpeech(), "en"); |
| |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| update.root_id = 1; |
| |
| ui::AXNodeData node; |
| node.id = 1; |
| node.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "yue"); |
| update.nodes = {node}; |
| AccessibilityEventReceived({update}); |
| OnAXTreeDistilled({1}); |
| |
| EXPECT_CALL(*distiller_, Distill).Times(1); |
| SetLanguageCode(""); |
| OnActiveAXTreeIDChanged(id_1); |
| ASSERT_EQ(LanguageCodeForSpeech(), "yue"); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_WhenCalledManyTimes_ReturnsSameNode) { |
| std::u16string sentence1 = u"This is a sentence. "; |
| std::u16string sentence2 = u"This is another sentence. "; |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2}); |
| |
| EXPECT_EQ((int)GetCurrentText().size(), 1); |
| // The returned id should be the first node id, 2 |
| EXPECT_EQ(GetCurrentText()[0], static_text1.id); |
| EXPECT_EQ(GetCurrentText()[0], static_text1.id); |
| EXPECT_EQ(GetCurrentText()[0], static_text1.id); |
| EXPECT_EQ(GetCurrentText()[0], static_text1.id); |
| // Confirm size is still 1. |
| EXPECT_EQ((int)GetCurrentText().size(), 1); |
| |
| // The returned id should be the second node id, 3 |
| MovePositionToNextGranularity(); |
| EXPECT_EQ((int)GetCurrentText().size(), 1); |
| EXPECT_EQ(GetCurrentText()[0], static_text2.id); |
| EXPECT_EQ(GetCurrentText()[0], static_text2.id); |
| EXPECT_EQ(GetCurrentText()[0], static_text2.id); |
| EXPECT_EQ(GetCurrentText()[0], static_text2.id); |
| // Confirm size is still 1. |
| EXPECT_EQ((int)GetCurrentText().size(), 1); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetCurrentText_ReturnsExpectedNodes) { |
| // TODO(crbug.com/40927698): Investigate if we can improve in scenarios when |
| // there's not a space between sentences. |
| 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::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| // The returned id should be the next node id, 2 |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| // The returned int should be the beginning of the node's text. |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| // The returned int should be equivalent to the text in the node. |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // Move to the next node |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence2.length()); |
| |
| // Move to the last node |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence3.length()); |
| |
| // Attempt to move to another node. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| PreprocessNodes_DoesNotImpactCurrentNodes) { |
| std::u16string sentence1 = u"Life was a chore. "; |
| std::u16string sentence2 = u"So she set sail. "; |
| std::u16string sentence3 = u"Fifteen twenty-two, came straight to the UK. "; |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| PreprocessTextForSpeech(); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| // The returned id should be the next node id, 2 |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| // The returned int should be the beginning of the node's text. |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| // The returned int should be equivalent to the text in the node. |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // Move to the next node |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence2.length()); |
| |
| // Move to the last node |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence3.length()); |
| |
| // Move backwards |
| next_node_ids = MoveToPreviousGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence2.length()); |
| |
| // Move to the last node again. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence3.length()); |
| |
| // Attempt to move to another node. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| PreprocessNodes_CalledMultipleTimes_DoesNotImpactCurrentNodes) { |
| std::u16string sentence1 = u"Keep a grip and take a deep breath. "; |
| std::u16string sentence2 = u"And soon we'll know what's what. "; |
| std::u16string sentence3 = |
| u"Put on a show, rewards will flow, and we'll go from there. "; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| PreprocessTextForSpeech(); |
| PreprocessTextForSpeech(); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| // The returned id should be the next node id, 2 |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| // The returned int should be the beginning of the node's text. |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| // The returned int should be equivalent to the text in the node. |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // Preprocess is called again. |
| PreprocessTextForSpeech(); |
| PreprocessTextForSpeech(); |
| |
| // But nothing changes with what's returned by GetCurrentText |
| next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| // The returned id should be the next node id, 2 |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| // The returned int should be the beginning of the node's text. |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| // The returned int should be equivalent to the text in the node. |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // Move to the next node |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence2.length()); |
| |
| // Move to the last node |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence3.length()); |
| |
| // Preprocess is called again. |
| PreprocessTextForSpeech(); |
| PreprocessTextForSpeech(); |
| |
| // And nothing has changed with the current text. |
| next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence3.length()); |
| |
| // Move backwards |
| next_node_ids = MoveToPreviousGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence2.length()); |
| |
| // Preprocess is called again. |
| PreprocessTextForSpeech(); |
| PreprocessTextForSpeech(); |
| |
| // And nothing has changed with the current text. |
| next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence2.length()); |
| |
| // Move to the last node again. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence3.length()); |
| |
| // Attempt to move to another node. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_AfterRestartReadAloud_StartsOver) { |
| std::u16string sentence1 = u"I've got the wind in my hair. "; |
| std::u16string sentence2 = u"And a gleam in my eyes. "; |
| std::u16string sentence3 = u"And an endless horizon. "; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/*id = */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/*id = */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/*id = */ 4, sentence3); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| |
| // Move to the next sentence. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text2.id); |
| |
| // If we init without restarting we should just go to the next sentence. |
| InitAXPosition(static_text1.id); |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| |
| // After reset and before an init, the current text should be empty. |
| ResetReadAloudState(); |
| std::vector<ui::AXNodeID> after_reset_ids = GetCurrentText(); |
| EXPECT_EQ((int)after_reset_ids.size(), 0); |
| |
| // After an init, we should get the first sentence again. |
| InitAXPosition(static_text1.id); |
| after_reset_ids = GetCurrentText(); |
| EXPECT_EQ((int)after_reset_ids.size(), 1); |
| EXPECT_EQ(after_reset_ids[0], static_text1.id); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_AfterResetGranularityIndex_StartsOver) { |
| std::u16string sentence1 = u"I've got the wind in my hair. "; |
| std::u16string sentence2 = u"And a gleam in my eyes. "; |
| std::u16string sentence3 = u"And an endless horizon. "; |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| |
| // Move to the next sentence. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text2.id); |
| |
| // If we init without restarting we should just go to the next sentence. |
| InitAXPosition(static_text1.id); |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| |
| // After reset, we should get the first sentence again. |
| ResetGranularityIndex(); |
| std::vector<ui::AXNodeID> after_reset_ids = GetCurrentText(); |
| EXPECT_EQ((int)after_reset_ids.size(), 1); |
| EXPECT_EQ(after_reset_ids[0], static_text1.id); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetCurrentText_AfterAXTreeRefresh) { |
| 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::AXNodeData static_text1 = test::TextNode(/* id = */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id = */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id = */ 4, sentence3); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // Simulate updating the page text. |
| std::u16string new_sentence_1 = |
| u"And so I read a book or maybe two or three. "; |
| std::u16string new_sentence_2 = |
| u"I will add a few new paitings to my gallery. "; |
| std::u16string new_sentence_3 = |
| u"I will play guitar and knit and cook and basically wonder when will my " |
| u"life begin."; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| ui::AXTreeUpdate update2; |
| test::SetUpdateTreeID(&update2, id_1); |
| ui::AXNodeData root; |
| root.id = 1; |
| |
| ui::AXNodeData new_static_text1 = |
| test::TextNode(/* id= */ 10, new_sentence_1); |
| ui::AXNodeData new_static_text2 = |
| test::TextNode(/* id= */ 12, new_sentence_2); |
| ui::AXNodeData new_static_text3 = |
| test::TextNode(/* id= */ 16, new_sentence_3); |
| |
| root.child_ids = {new_static_text1.id, new_static_text2.id, |
| new_static_text3.id}; |
| update2.root_id = root.id; |
| update2.nodes = {root, new_static_text1, new_static_text2, new_static_text3}; |
| OnActiveAXTreeIDChanged(id_1); |
| OnAXTreeDistilled({}); |
| AccessibilityEventReceived({update2}); |
| OnAXTreeDistilled( |
| id_1, {new_static_text1.id, new_static_text2.id, new_static_text3.id}); |
| InitAXPosition(update2.nodes[1].id); |
| |
| // The nodes from the new tree are used. |
| next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], update2.nodes[1].id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)new_sentence_1.length()); |
| |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], update2.nodes[2].id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)new_sentence_2.length()); |
| |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], update2.nodes[3].id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)new_sentence_3.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_SentenceSplitAcrossMultipleNodes) { |
| std::u16string sentence1 = u"The wind is howling like this "; |
| std::u16string sentence2 = u"swirling storm "; |
| std::u16string sentence3 = u"inside."; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/*id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/*id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/*id= */ 4, sentence3); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| |
| // The first segment was returned correctly. |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // The second segment was returned correctly. |
| EXPECT_EQ(next_node_ids[1], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[1]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[1]), (int)sentence2.length()); |
| |
| // The third segment was returned correctly. |
| EXPECT_EQ(next_node_ids[2], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[2]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[2]), (int)sentence3.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_SentenceSplitAcrossTwoNodes) { |
| std::u16string sentence1 = u"And I am almost "; |
| std::u16string sentence2 = u"there. "; |
| std::u16string sentence3 = u"I am almost there."; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 2); |
| |
| // The first segment was returned correctly. |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // The second segment was returned correctly. |
| EXPECT_EQ(next_node_ids[1], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[1]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[1]), (int)sentence2.length()); |
| |
| // The third segment was returned correctly after getting the next text. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence3.length()); |
| |
| // Nodes are empty at the end of the tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_OpeningPunctuationIgnored) { |
| std::u16string sentence1 = u"And I am almost there."; |
| std::u16string sentence2 = u"[2]"; |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2}); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| |
| // The first segment was returned correctly. |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // The parenthetical expression is returned as a single separate segment. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| |
| EXPECT_EQ(next_node_ids[0], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence2.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_OpeningPunctuationIncludedWhenEntireNode) { |
| // Simulate breaking up the brackets across a link. |
| std::u16string sentence1 = u"And I am almost there."; |
| std::u16string sentence2 = u"["; |
| std::u16string sentence3 = u"2"; |
| std::u16string sentence4 = u"]"; |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| ui::AXNodeData static_text4 = test::TextNode(/* id= */ 12, sentence4); |
| |
| ui::AXNodeData superscript = test::GenericContainerNode(/* id= */ 13); |
| superscript.child_ids = {static_text2.id, static_text3.id, static_text4.id}; |
| |
| ui::AXNodeData root; |
| root.id = 10; |
| root.child_ids = {static_text1.id, superscript.id}; |
| update.root_id = root.id; |
| |
| update.nodes = {root, static_text1, superscript, |
| static_text2, static_text3, static_text4}; |
| OnActiveAXTreeIDChanged(id_1); |
| AccessibilityEventReceived({update}); |
| OnAXTreeDistilled(id_1, {root.id, static_text1.id, superscript.id, |
| static_text2.id, static_text3.id, static_text4.id}); |
| InitAXPosition(static_text1.id); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| |
| // The first segment was returned correctly. |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // The next segment contains the entire bracketed statement '[2]' with both |
| // opening and closing brackets so neither bracket is read out-of-context. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 3); |
| |
| EXPECT_EQ(next_node_ids[0], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence2.length()); |
| |
| EXPECT_EQ(next_node_ids[1], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[1]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[1]), (int)sentence3.length()); |
| |
| EXPECT_EQ(next_node_ids[2], static_text4.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[2]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[2]), (int)sentence4.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_SuperscriptCombinedWithCurrentSegment) { |
| std::u16string sentence1 = u"And I am almost there."; |
| std::u16string sentence2 = u"2"; |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::SuperscriptNode(/* id= */ 3, sentence2); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2}); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 2); |
| |
| // The first segment was returned correctly. |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // The superscript is attached to the first sentence. |
| EXPECT_EQ(next_node_ids[1], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[1]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[1]), (int)sentence2.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_SuperscriptWithBracketsCombinedWithCurrentSegment) { |
| std::u16string sentence1 = u"And I am almost there."; |
| std::u16string sentence2 = u"[2]"; |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::SuperscriptNode(/* id= */ 3, sentence2); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2}); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 2); |
| |
| // The first segment was returned correctly. |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // The superscript is attached to the first sentence. |
| EXPECT_EQ(next_node_ids[1], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[1]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[1]), (int)sentence2.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_SuperscriptIncludedWhenEntireNode) { |
| // Simulate breaking up the brackets across a link. |
| std::u16string sentence1 = u"And I am almost there."; |
| std::u16string sentence2 = u"["; |
| std::u16string sentence3 = u"2"; |
| std::u16string sentence4 = u"]"; |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::SuperscriptNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::SuperscriptNode(/* id= */ 4, sentence3); |
| ui::AXNodeData static_text4 = test::SuperscriptNode(/* id= */ 12, sentence4); |
| |
| ui::AXNodeData superscript; |
| superscript.id = 13; |
| superscript.role = ax::mojom::Role::kSuperscript; |
| superscript.child_ids = {static_text2.id, static_text3.id, static_text4.id}; |
| |
| ui::AXNodeData root; |
| root.id = 10; |
| root.child_ids = {static_text1.id, superscript.id}; |
| update.root_id = root.id; |
| |
| update.nodes = {root, static_text1, superscript, |
| static_text2, static_text3, static_text4}; |
| OnActiveAXTreeIDChanged(id_1); |
| AccessibilityEventReceived({update}); |
| OnAXTreeDistilled(id_1, {root.id, static_text1.id, superscript.id, |
| static_text2.id, static_text3.id, static_text4.id}); |
| InitAXPosition(static_text1.id); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 4); |
| |
| // The first sentence and its superscript are returned as one segment. |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| EXPECT_EQ(next_node_ids[1], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[1]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[1]), (int)sentence2.length()); |
| |
| EXPECT_EQ(next_node_ids[2], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[2]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[2]), (int)sentence3.length()); |
| |
| EXPECT_EQ(next_node_ids[3], static_text4.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[3]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[3]), (int)sentence4.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_SuperscriptIncludedWhenEntireNodeAndMoreTextAfterScript) { |
| // Simulate breaking up the brackets across a link. |
| std::u16string sentence1 = u"And I am almost there."; |
| std::u16string sentence2 = u"["; |
| std::u16string sentence3 = u"2"; |
| std::u16string sentence4 = u"]"; |
| std::u16string sentence5 = u"People gon' come here from everywhere."; |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::SuperscriptNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::SuperscriptNode(/* id= */ 4, sentence3); |
| ui::AXNodeData static_text4 = test::SuperscriptNode(/* id= */ 12, sentence4); |
| |
| ui::AXNodeData superscript; |
| superscript.id = 13; |
| superscript.role = ax::mojom::Role::kSuperscript; |
| superscript.child_ids = {static_text2.id, static_text3.id, static_text4.id}; |
| |
| ui::AXNodeData static_text5 = test::TextNode(/* id= */ 100, sentence5); |
| |
| ui::AXNodeData root; |
| root.id = 10; |
| root.child_ids = {static_text1.id, superscript.id, static_text5.id}; |
| update.root_id = root.id; |
| |
| update.nodes = {root, static_text1, superscript, static_text2, |
| static_text3, static_text4, static_text5}; |
| OnActiveAXTreeIDChanged(id_1); |
| AccessibilityEventReceived({update}); |
| OnAXTreeDistilled(id_1, |
| {root.id, static_text1.id, superscript.id, static_text2.id, |
| static_text3.id, static_text4.id, static_text5.id}); |
| InitAXPosition(static_text1.id); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 4); |
| |
| // The first segment was returned correctly. |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // The superscript is returned as a segment. |
| EXPECT_EQ(next_node_ids[1], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[1]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[1]), (int)sentence2.length()); |
| |
| EXPECT_EQ(next_node_ids[2], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[2]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[2]), (int)sentence3.length()); |
| |
| EXPECT_EQ(next_node_ids[3], static_text4.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[3]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[3]), (int)sentence4.length()); |
| |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ(next_node_ids[0], static_text5.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence5.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetCurrentText_IncludesListMarkers) { |
| // Simulate breaking up the brackets across a link. |
| std::string marker_html_tag = "::marker"; |
| std::u16string bullet1 = u"1."; |
| std::u16string sentence1 = u"Realize numbers are ignored in Read Aloud. "; |
| std::u16string bullet2 = u"2."; |
| std::u16string sentence2 = u"Fix it."; |
| ui::AXTreeUpdate update; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| test::SetUpdateTreeID(&update, id_1); |
| |
| ui::AXNodeData list_marker1; |
| list_marker1.id = 2; |
| list_marker1.role = ax::mojom::Role::kListMarker; |
| list_marker1.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, |
| marker_html_tag); |
| list_marker1.SetName(bullet1); |
| list_marker1.SetNameFrom(ax::mojom::NameFrom::kContents); |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 3, sentence1); |
| |
| ui::AXNodeData list_marker2; |
| list_marker2.id = 4; |
| list_marker2.role = ax::mojom::Role::kListMarker; |
| list_marker2.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, |
| marker_html_tag); |
| list_marker2.SetName(bullet2); |
| list_marker2.SetNameFrom(ax::mojom::NameFrom::kContents); |
| |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 12, sentence2); |
| |
| ui::AXNodeData root; |
| root.id = 10; |
| root.child_ids = {list_marker1.id, static_text1.id, list_marker2.id, |
| static_text2.id}; |
| update.root_id = root.id; |
| |
| update.nodes = {root, list_marker1, static_text1, list_marker2, static_text2}; |
| OnActiveAXTreeIDChanged(id_1); |
| AccessibilityEventReceived({update}); |
| OnAXTreeDistilled(id_1, {root.id, list_marker1.id, static_text1.id, |
| list_marker2.id, static_text2.id}); |
| InitAXPosition(list_marker1.id); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| |
| // The first segment was returned correctly. |
| EXPECT_EQ(next_node_ids[0], list_marker1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)bullet1.length()); |
| |
| // Move to the next segment. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // Move to the next segment. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| |
| EXPECT_EQ(next_node_ids[0], list_marker2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)bullet2.length()); |
| |
| // Move to the next segment. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| |
| EXPECT_EQ(next_node_ids[0], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence2.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_SentenceSplitAcrossParagraphs) { |
| std::u16string header_text = u"Header Text"; |
| std::u16string paragraph_text1 = u"Paragraph one."; |
| std::u16string paragraph_text2 = u"Paragraph two."; |
| ui::AXTreeUpdate update; |
| SetUpdateTreeID(&update); |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, header_text); |
| ui::AXNodeData static_text2 = test::TextNode(/*id= */ 3, paragraph_text1); |
| ui::AXNodeData static_text3 = test::TextNode(/*id= */ 4, paragraph_text2); |
| |
| ui::AXNodeData header_node; |
| header_node.id = 5; |
| header_node.role = ax::mojom::Role::kHeader; |
| header_node.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject, |
| true); |
| header_node.child_ids = {static_text1.id}; |
| |
| ui::AXNodeData paragraph_node1; |
| paragraph_node1.id = 6; |
| paragraph_node1.role = ax::mojom::Role::kParagraph; |
| paragraph_node1.AddBoolAttribute( |
| ax::mojom::BoolAttribute::kIsLineBreakingObject, true); |
| paragraph_node1.child_ids = {static_text2.id}; |
| |
| ui::AXNodeData paragraph_node2; |
| paragraph_node2.id = 7; |
| paragraph_node2.role = ax::mojom::Role::kParagraph; |
| paragraph_node2.AddBoolAttribute( |
| ax::mojom::BoolAttribute::kIsLineBreakingObject, true); |
| paragraph_node2.child_ids = {static_text3.id}; |
| |
| ui::AXNodeData root; |
| root.id = 10; |
| root.role = ax::mojom::Role::kParagraph; |
| root.child_ids = {header_node.id, paragraph_node1.id, paragraph_node2.id}; |
| update.root_id = root.id; |
| |
| update.nodes = {root, header_node, static_text1, paragraph_node1, |
| static_text2, paragraph_node2, static_text3}; |
| AccessibilityEventReceived({update}); |
| OnAXTreeDistilled({root.id, header_node.id, static_text1.id, |
| paragraph_node1.id, static_text2.id, paragraph_node2.id, |
| static_text3.id}); |
| InitAXPosition(static_text1.id); |
| |
| // The header is returned alone. |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)header_text.length()); |
| |
| // Paragraph 1 is returned alone. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)paragraph_text1.length()); |
| |
| // Paragraph 2 is returned alone. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)paragraph_text2.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_SentenceSplitAcrossParagraphsWithoutParagraphRoles) { |
| std::u16string header_text = u"Header Text\n"; |
| std::u16string paragraph_text1 = u"Paragraph one.\n"; |
| std::u16string paragraph_text2 = u"Paragraph two."; |
| |
| ui::AXNodeData header_node = test::TextNode(/* id= */ 2, header_text); |
| ui::AXNodeData paragraph_node1 = test::TextNode(/* id= */ 3, paragraph_text1); |
| ui::AXNodeData paragraph_node2 = test::TextNode(/* id= */ 4, paragraph_text2); |
| |
| SendUpdateAndDistillNodes({header_node, paragraph_node1, paragraph_node2}); |
| |
| // The header is returned alone. |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], header_node.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)header_text.length()); |
| |
| // Paragraph 1 is returned alone. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], paragraph_node1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)paragraph_text1.length()); |
| |
| // Paragraph 2 is returned alone. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], paragraph_node2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)paragraph_text2.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_MultipleSentencesInSameNode) { |
| 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::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id = */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| |
| // The first segment was returned correctly. |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)sentence1.find(u"The")); |
| |
| // The second segment was returned correctly, across 3 nodes. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 3); |
| |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), |
| (int)sentence1.find(u"The")); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| EXPECT_EQ(next_node_ids[1], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[1]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[1]), (int)sentence2.length()); |
| |
| EXPECT_EQ(next_node_ids[2], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[2]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[2]), |
| (int)sentence3.find(u"And")); |
| |
| // The next sentence "And suddenly life seems so clear" was returned correctly |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), |
| (int)sentence3.find(u"And")); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)sentence3.find(u"And from")); |
| |
| // The next sentence "And from up here" was returned correctly |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), |
| (int)sentence3.find(u"And from")); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)sentence3.find(u"You")); |
| |
| // The next sentence "You coast past it all" was returned correctly |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), |
| (int)sentence3.find(u"You")); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)sentence3.find(u"The")); |
| |
| // The next sentence "The obstacles just disappear" was returned correctly |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), |
| (int)sentence3.find(u"The")); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence3.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetCurrentText_EmptyTree) { |
| // If InitAXPosition hasn't been called, GetCurrentText should return nothing. |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| |
| // GetCurrentTextStartIndex and GetCurrentTextEndIndex should return -1 on an |
| // invalid id. |
| EXPECT_EQ(GetCurrentTextStartIndex(0), -1); |
| EXPECT_EQ(GetCurrentTextEndIndex(0), -1); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetPreviousText_AfterAXTreeRefresh) { |
| 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::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| |
| std::vector<ui::AXNodeID> next_node_ids = GetCurrentText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence1.length()); |
| |
| // Simulate updating the page text. |
| std::u16string new_sentence1 = u"Welcome to the show to the histo-remix. "; |
| std::u16string new_sentence2 = |
| u"Switching up the flow, as we add the prefix. "; |
| std::u16string new_sentence3 = |
| u"Everybody knows that we used to be six wives. "; |
| ui::AXTreeID id_1 = ui::AXTreeID::CreateNewAXTreeID(); |
| ui::AXTreeUpdate update2; |
| test::SetUpdateTreeID(&update2, id_1); |
| ui::AXNodeData root; |
| root.id = 1; |
| |
| ui::AXNodeData new_static_text1 = test::TextNode(/* id= */ 10, new_sentence1); |
| ui::AXNodeData new_static_text2 = test::TextNode(/* id= */ 12, new_sentence2); |
| ui::AXNodeData new_static_text3 = test::TextNode(/* id= */ 16, new_sentence3); |
| |
| root.child_ids = {new_static_text1.id, new_static_text2.id, |
| new_static_text3.id}; |
| update2.root_id = root.id; |
| update2.nodes = {root, new_static_text1, new_static_text2, new_static_text3}; |
| OnActiveAXTreeIDChanged(id_1); |
| OnAXTreeDistilled({}); |
| AccessibilityEventReceived({update2}); |
| OnAXTreeDistilled( |
| id_1, {new_static_text1.id, new_static_text2.id, new_static_text3.id}); |
| InitAXPosition(update2.nodes[1].id); |
| |
| // The nodes from the new tree are used. |
| // Move to the last node of the content. |
| MovePositionToNextGranularity(); |
| MovePositionToNextGranularity(); |
| |
| std::vector<ui::AXNodeID> previous_node_ids = |
| MoveToPreviousGranularityAndGetText(); |
| EXPECT_EQ((int)previous_node_ids.size(), 1); |
| EXPECT_EQ(previous_node_ids[0], new_static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(previous_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(previous_node_ids[0]), |
| (int)new_sentence2.length()); |
| |
| previous_node_ids = MoveToPreviousGranularityAndGetText(); |
| EXPECT_EQ((int)previous_node_ids.size(), 1); |
| EXPECT_EQ(previous_node_ids[0], new_static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(previous_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(previous_node_ids[0]), |
| (int)new_sentence1.length()); |
| |
| // We're at the beginning of the content again, so the first sentence |
| // should be retrieved next. |
| previous_node_ids = MoveToPreviousGranularityAndGetText(); |
| EXPECT_EQ((int)previous_node_ids.size(), 1); |
| EXPECT_EQ(previous_node_ids[0], new_static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(previous_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(previous_node_ids[0]), |
| (int)new_sentence1.length()); |
| |
| // After navigating previous text, navigating forwards should continue |
| // to work as expected. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], new_static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)new_sentence2.length()); |
| |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], new_static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), |
| (int)new_sentence3.length()); |
| |
| // Attempt to move to another node. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetPreviousText_ReturnsExpectedNodes) { |
| std::u16string sentence1 = u"See the line where the sky meets the sea? "; |
| std::u16string sentence2 = u"It calls me. "; |
| std::u16string sentence3 = u"And no one knows how far it goes."; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| |
| // Move to the last granularity of the content. |
| MovePositionToNextGranularity(); |
| std::vector<ui::AXNodeID> next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| |
| std::vector<ui::AXNodeID> previous_node_ids = |
| MoveToPreviousGranularityAndGetText(); |
| EXPECT_EQ((int)previous_node_ids.size(), 1); |
| EXPECT_EQ(previous_node_ids[0], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(previous_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(previous_node_ids[0]), |
| (int)sentence2.length()); |
| |
| previous_node_ids = MoveToPreviousGranularityAndGetText(); |
| EXPECT_EQ((int)previous_node_ids.size(), 1); |
| EXPECT_EQ(previous_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(previous_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(previous_node_ids[0]), |
| (int)sentence1.length()); |
| |
| // We're at the beginning of the content again, so the first sentence |
| // should be retrieved next. |
| previous_node_ids = MoveToPreviousGranularityAndGetText(); |
| EXPECT_EQ((int)previous_node_ids.size(), 1); |
| EXPECT_EQ(previous_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(previous_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(previous_node_ids[0]), |
| (int)sentence1.length()); |
| |
| // After navigating previous text, navigating forwards should continue |
| // to work as expected. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence2.length()); |
| |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence3.length()); |
| |
| // Attempt to move to another node. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, GetPreviousText_EmptyTree) { |
| // If InitAXPosition hasn't been called, GetPreviousText should return |
| // nothing. |
| std::vector<ui::AXNodeID> previous_node_ids = |
| MoveToPreviousGranularityAndGetText(); |
| EXPECT_EQ((int)previous_node_ids.size(), 0); |
| |
| // GetCurrentTextStartIndex and GetCurrentTextEndIndex should return -1 on an |
| // invalid id. |
| EXPECT_EQ(GetCurrentTextStartIndex(0), -1); |
| EXPECT_EQ(GetCurrentTextEndIndex(0), -1); |
| } |
| |
| TEST_F( |
| ReadAnythingAppControllerTest, |
| MoveToPreviousGranularityAndGetText_WhenFirstInitialized_StillReturnsFirstGranularity) { |
| std::u16string sentence1 = u"This is a sentence. "; |
| std::u16string sentence2 = u"This is another sentence. "; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2}); |
| |
| // If we haven't called moveToNextGranularity, getCurrentText() should still |
| // return the first granularity. |
| std::vector<ui::AXNodeID> previous_node_ids = |
| MoveToPreviousGranularityAndGetText(); |
| EXPECT_EQ((int)previous_node_ids.size(), 1); |
| EXPECT_EQ((int)previous_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(previous_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(previous_node_ids[0]), |
| (int)sentence1.length()); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetCurrentText_WhenGranularityWasInitiallySkipped_ReturnsText) { |
| std::u16string sentence1 = u"See the line where the sky meets the sea? "; |
| std::u16string sentence2 = u"It calls me. "; |
| std::u16string sentence3 = u"And no one knows how far it goes."; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| |
| // Move to third node |
| MovePositionToNextGranularity(); |
| MovePositionToNextGranularity(); |
| EXPECT_EQ((int)GetCurrentText()[0], static_text3.id); |
| EXPECT_EQ((int)GetCurrentText().size(), 1); |
| |
| // Move to second node which was initially skipped |
| std::vector<ui::AXNodeID> previous_node_ids = |
| MoveToPreviousGranularityAndGetText(); |
| EXPECT_EQ(previous_node_ids[0], static_text2.id); |
| EXPECT_EQ((int)previous_node_ids.size(), 1); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetPreviousText_SentenceSplitAcrossMultipleNodes) { |
| std::u16string sentence1 = u"The wind is howling like this "; |
| std::u16string sentence2 = u"swirling storm "; |
| std::u16string sentence3 = u"inside."; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| |
| GetCurrentText(); |
| std::vector<ui::AXNodeID> previous_node_ids = |
| MoveToPreviousGranularityAndGetText(); |
| |
| // The first segment was returned correctly. |
| EXPECT_EQ(previous_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(previous_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(previous_node_ids[0]), |
| (int)sentence1.length()); |
| |
| // The second segment was returned correctly. |
| EXPECT_EQ(previous_node_ids[1], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(previous_node_ids[1]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(previous_node_ids[1]), |
| (int)sentence2.length()); |
| |
| // The third segment was returned correctly. |
| EXPECT_EQ(previous_node_ids[2], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(previous_node_ids[2]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(previous_node_ids[2]), |
| (int)sentence3.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| std::vector<ui::AXNodeID> next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetPreviousText_SentenceSplitAcrossTwoNodes) { |
| std::u16string sentence1 = u"And I am almost "; |
| std::u16string sentence2 = u"there. "; |
| std::u16string sentence3 = u"I am almost there."; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/*id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/*id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/*id= */ 4, sentence3); |
| |
| SendUpdateAndDistillNodes({static_text1, static_text2, static_text3}); |
| |
| // Move to last granularity. |
| MovePositionToNextGranularity(); |
| std::vector<ui::AXNodeID> previous_node_ids = |
| MoveToPreviousGranularityAndGetText(); |
| EXPECT_EQ((int)previous_node_ids.size(), 2); |
| |
| // Returns the 2nd segment correctly. |
| EXPECT_EQ(previous_node_ids[1], static_text2.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(previous_node_ids[1]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(previous_node_ids[1]), |
| (int)sentence2.length()); |
| |
| // Returns the 1st segment correctly. |
| EXPECT_EQ(previous_node_ids[0], static_text1.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(previous_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(previous_node_ids[0]), |
| (int)sentence1.length()); |
| |
| // After moving forward again, the third segment was returned correctly. |
| // The third segment was returned correctly after getting the next text. |
| std::vector<ui::AXNodeID> next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 1); |
| EXPECT_EQ(next_node_ids[0], static_text3.id); |
| EXPECT_EQ(GetCurrentTextStartIndex(next_node_ids[0]), 0); |
| EXPECT_EQ(GetCurrentTextEndIndex(next_node_ids[0]), (int)sentence3.length()); |
| |
| // Nodes are empty at the end of the new tree. |
| next_node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)next_node_ids.size(), 0); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetAccessibleBoundary_MaxLengthCutsOffSentence_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 = GetAccessibleBoundary(sentence, first_sentence.length() - 3); |
| EXPECT_TRUE(index < first_sentence.length()); |
| EXPECT_EQ(sentence.substr(0, index), u"This is a normal "); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetAccessibleBoundary_TextLongerThanMaxLength_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 = GetAccessibleBoundary( |
| sentence, first_sentence.length() + second_sentence.length() - 5); |
| EXPECT_EQ(index, first_sentence.length()); |
| EXPECT_EQ(sentence.substr(0, index), first_sentence); |
| } |
| |
| TEST_F( |
| ReadAnythingAppControllerTest, |
| GetAccessibleBoundary_MaxLengthCutsOffSentence_OnlyOneSentence_ReturnsCorrectIndex) { |
| const std::u16string sentence = u"Hello, this is a normal sentence."; |
| |
| size_t index = GetAccessibleBoundary(sentence, 12); |
| EXPECT_TRUE(index < sentence.length()); |
| EXPECT_EQ(sentence.substr(0, index), u"Hello, "); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, 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::AXNodeData static_text1 = test::TextNode(/*id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/*id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/*id= */ 4, sentence3); |
| |
| InitializeWithAndProcessNodes({static_text1, static_text2, static_text3}); |
| |
| 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(ReadAnythingAppControllerTest, GetNextValidPosition_SkipsNonTextNode) { |
| std::u16string sentence1 = u"This is a sentence."; |
| std::u16string sentence2 = u"This is another sentence."; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/*id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/*id= */ 4, sentence2); |
| |
| ui::AXNodeData empty_node; |
| empty_node.id = 3; |
| |
| InitializeWithAndProcessNodes({static_text1, empty_node, static_text2}); |
| |
| ui::AXNodePosition::AXPositionInstance new_position = GetNextNodePosition(); |
| EXPECT_EQ(new_position->anchor_id(), static_text2.id); |
| EXPECT_EQ(new_position->GetText(), sentence2); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| 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::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| SendUpdateWithNodes({static_text1, static_text2, static_text3}); |
| // Don't distill the node with id 3. |
| ProcessDisplayNodes({static_text1.id, static_text3.id}); |
| InitAXPosition(static_text1.id); |
| ui::AXNodePosition::AXPositionInstance new_position = GetNextNodePosition(); |
| EXPECT_EQ(new_position->anchor_id(), static_text3.id); |
| EXPECT_EQ(new_position->GetText(), sentence3); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| 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::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| static_text2.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "h1"); |
| |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| InitializeWithAndProcessNodes({static_text1, static_text2, static_text3}); |
| |
| ui::AXNodePosition::AXPositionInstance new_position = GetNextNodePosition(); |
| EXPECT_EQ(new_position->anchor_id(), static_text3.id); |
| EXPECT_EQ(new_position->GetText(), sentence3); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetNextValidPosition_ReturnsNullPositionAtEndOfTree) { |
| std::u16string sentence1 = u"This is a sentence."; |
| ui::AXNodeData static_text = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData empty_node1; |
| empty_node1.id = 3; |
| ui::AXNodeData empty_node2; |
| empty_node2.id = 4; |
| InitializeWithAndProcessNodes({static_text, empty_node1, empty_node2}); |
| |
| ui::AXNodePosition::AXPositionInstance new_position = GetNextNodePosition(); |
| EXPECT_TRUE(new_position->IsNullPosition()); |
| } |
| |
| TEST_F( |
| ReadAnythingAppControllerTest, |
| 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::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| InitializeWithAndProcessNodes({static_text1, static_text2, static_text3}); |
| |
| a11y::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); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetNextNodes_AfterResetReadAloudState_StartsOver) { |
| std::u16string sentence1 = u"Where the north wind meets the sea. "; |
| std::u16string sentence2 = u"There's a river full of memory. "; |
| std::u16string sentence3 = u"Sleep my darling safe and sound. "; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| InitializeWithAndProcessNodes({static_text1, static_text2, static_text3}); |
| |
| // Get first and second granularity. |
| a11y::ReadAloudCurrentGranularity first_granularity = GetNextNodes(); |
| EXPECT_EQ((int)first_granularity.node_ids.size(), 1); |
| EXPECT_TRUE(base::Contains(first_granularity.node_ids, static_text1.id)); |
| EXPECT_EQ(first_granularity.text, sentence1); |
| a11y::ReadAloudCurrentGranularity next_granularity = GetNextNodes(); |
| EXPECT_EQ((int)next_granularity.node_ids.size(), 1); |
| EXPECT_TRUE(base::Contains(next_granularity.node_ids, static_text2.id)); |
| EXPECT_EQ(next_granularity.text, sentence2); |
| |
| // If we init without resetting we should just go to the next sentence |
| InitAXPosition(static_text1.id); |
| a11y::ReadAloudCurrentGranularity last_granularity = GetNextNodes(); |
| EXPECT_EQ((int)last_granularity.node_ids.size(), 1); |
| EXPECT_TRUE(base::Contains(last_granularity.node_ids, static_text3.id)); |
| EXPECT_EQ(last_granularity.text, sentence3); |
| |
| // After reset and then init, we should get the first sentence again. |
| ResetReadAloudState(); |
| InitAXPosition(static_text1.id); |
| a11y::ReadAloudCurrentGranularity after_reset = GetNextNodes(); |
| EXPECT_EQ((int)after_reset.node_ids.size(), 1); |
| EXPECT_TRUE(base::Contains(after_reset.node_ids, static_text1.id)); |
| EXPECT_EQ(first_granularity.text, sentence1); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetHighlightStartIndex_ReturnsCorrectIndex) { |
| std::u16string sentence = u"I\'m crossing the line!"; |
| ui::AXNodeData static_text = test::TextNode(/*id= */ 2, sentence); |
| |
| InitializeWithAndProcessNodes({static_text}); |
| |
| // Before there are any processed granularities, GetHighlightStartIndex |
| // should return an invalid id. |
| EXPECT_EQ(GetHighlightStartIndex(static_text.id, 1), -1); |
| |
| std::vector<ui::AXNodeID> node_ids = GetCurrentText(); |
| EXPECT_EQ((int)node_ids.size(), 1); |
| |
| // Storing as a separate variable so we don't need to cast every time. |
| int sentence_length = (int)sentence.length(); |
| |
| // Since we just have one node with one text segment, the returned index |
| // should equal the passed parameter. |
| EXPECT_EQ(GetHighlightStartIndex(static_text.id, 0), 0); |
| EXPECT_EQ(GetHighlightStartIndex(static_text.id, 3), 3); |
| EXPECT_EQ(GetHighlightStartIndex(static_text.id, 7), 7); |
| EXPECT_EQ(GetHighlightStartIndex(static_text.id, sentence_length - 1), |
| sentence_length - 1); |
| EXPECT_EQ(GetHighlightStartIndex(static_text.id, sentence_length), -1); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetHighlightStartIndex_SegmentSpansMultipleNodes_ReturnsCorrectIndex) { |
| std::u16string sentence1 = u"And I\'m done holding back,"; |
| std::u16string sentence2 = u"so lookout, clear the track- it\'s my"; |
| std::u16string sentence3 = u"turn."; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| InitializeWithAndProcessNodes({static_text1, static_text2, static_text3}); |
| |
| // Before there are any processed granularities, GetHighlightStartIndex |
| // should return an invalid id. |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 1), -1); |
| EXPECT_EQ(GetHighlightStartIndex(static_text2.id, 1), -1); |
| EXPECT_EQ(GetHighlightStartIndex(static_text3.id, 1), -1); |
| |
| std::vector<ui::AXNodeID> node_ids = GetCurrentText(); |
| EXPECT_EQ((int)node_ids.size(), 3); |
| |
| // Storing as a separate variable so we don't need to cast every time. |
| int sentence1_length = (int)sentence1.length(); |
| int sentence2_length = (int)sentence2.length(); |
| int sentence3_length = (int)sentence3.length(); |
| |
| // For the first node in the first segment, the returned index should equal |
| // the passed parameter. |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 0), 0); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 3), 3); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 7), 7); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, sentence1_length - 1), |
| sentence1_length - 1); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, sentence1_length), -1); |
| |
| // For the second node, the correct index is the index that the boundary |
| // index corresponds to within the second node. |
| EXPECT_EQ(GetHighlightStartIndex(static_text2.id, sentence1_length), 0); |
| EXPECT_EQ(GetHighlightStartIndex(static_text2.id, sentence1_length + 3), 3); |
| EXPECT_EQ(GetHighlightStartIndex(static_text2.id, sentence1_length + 7), 7); |
| EXPECT_EQ(GetHighlightStartIndex(static_text2.id, |
| sentence1_length + sentence2_length - 1), |
| sentence2_length - 1); |
| EXPECT_EQ(GetHighlightStartIndex(static_text2.id, |
| sentence1_length + sentence2_length), |
| -1); |
| |
| // For the third node, the correct index is the index that the boundary |
| // index corresponds to within the third node. |
| EXPECT_EQ(GetHighlightStartIndex(static_text3.id, |
| sentence1_length + sentence2_length), |
| 0); |
| EXPECT_EQ(GetHighlightStartIndex(static_text3.id, |
| sentence1_length + sentence2_length + 1), |
| 1); |
| EXPECT_EQ(GetHighlightStartIndex( |
| static_text3.id, |
| sentence1_length + sentence2_length + sentence3_length - 1), |
| sentence3_length - 1); |
| EXPECT_EQ(GetHighlightStartIndex( |
| static_text3.id, |
| sentence1_length + sentence2_length + sentence3_length), |
| -1); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetHighlightStartIndex_NodeSpansMultipleSegments_ReturnsCorrectIndex) { |
| std::u16string segment1 = u"I\'m taking what\'s mine! "; |
| std::u16string segment2 = u"Every drop, every smidge. "; |
| std::u16string segment3 = u"If I\'m burning a bridge, let it burn. "; |
| std::u16string segment4 = u"But I\'m crossing the "; |
| |
| std::u16string node1_text = segment1 + segment2 + segment3 + segment4; |
| std::u16string node2_text = u"line."; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, node1_text); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, node2_text); |
| |
| InitializeWithAndProcessNodes({static_text1, static_text2}); |
| |
| // Before there are any processed granularities, GetHighlightStartIndex |
| // should return an invalid id. |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 1), -1); |
| EXPECT_EQ(GetHighlightStartIndex(static_text2.id, 1), -1); |
| |
| std::vector<ui::AXNodeID> node_ids = GetCurrentText(); |
| EXPECT_EQ((int)node_ids.size(), 1); |
| |
| // Storing as a separate variable so we don't need to cast every time. |
| int segment1_length = (int)segment1.length(); |
| int segment2_length = (int)segment2.length(); |
| int segment3_length = (int)segment3.length(); |
| int segment4_partial_length = (int)segment4.length(); |
| int segment4_full_length = (int)segment4.length() + (int)node2_text.length(); |
| |
| // For the first node in the first segment, the returned index should equal |
| // the passed parameter. |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 0), 0); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 6), 6); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 15), 15); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, segment1_length - 1), |
| segment1_length - 1); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, segment1_length), -1); |
| |
| // Move to segment 2. |
| node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)node_ids.size(), 1); |
| |
| // For the second segment, the boundary index will have reset for the new |
| // speech segment. The correct highlight start index is the index that the |
| // boundary index within the segment corresponds to within the node. |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 0), segment1_length); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 10), segment1_length + 10); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 13), segment1_length + 13); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, segment2_length - 1), |
| segment1_length + segment2_length - 1); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, |
| segment1_length + segment2_length), |
| -1); |
| |
| // Move to segment 3. |
| node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)node_ids.size(), 1); |
| |
| // For the third segment, the boundary index will have reset for the new |
| // speech segment. The correct highlight start index is the index that the |
| // boundary index within the segment corresponds to within the node. |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 0), |
| segment1_length + segment2_length); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 9), |
| segment1_length + segment2_length + 9); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 13), |
| segment1_length + +segment2_length + 13); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, segment3_length - 1), |
| segment1_length + segment2_length + segment3_length - 1); |
| EXPECT_EQ( |
| GetHighlightStartIndex( |
| static_text1.id, segment1_length + segment2_length + segment3_length), |
| -1); |
| |
| // Move to segment 4. |
| node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)node_ids.size(), 2); |
| EXPECT_EQ((int)node_ids[0], static_text1.id); |
| EXPECT_EQ((int)node_ids[1], static_text2.id); |
| |
| // For the fourth segment, there are two nodes. For the first node, |
| // the correct highlight start corresponds to the index within the first node. |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 0), |
| segment1_length + segment2_length + segment3_length); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 2), |
| segment1_length + segment2_length + segment3_length + 2); |
| EXPECT_EQ(GetHighlightStartIndex(static_text1.id, 8), |
| segment1_length + segment2_length + segment3_length + 8); |
| EXPECT_EQ( |
| GetHighlightStartIndex(static_text1.id, segment4_partial_length - 1), |
| segment1_length + segment2_length + segment3_length + |
| segment4_partial_length - 1); |
| EXPECT_EQ(GetHighlightStartIndex( |
| static_text1.id, segment1_length + segment2_length + |
| segment3_length + segment4_partial_length), |
| -1); |
| |
| // For the second node, the highlight index corresponds to the position within |
| // the second node. |
| EXPECT_EQ(GetHighlightStartIndex(static_text2.id, segment4_partial_length), |
| 0); |
| EXPECT_EQ( |
| GetHighlightStartIndex(static_text2.id, segment4_partial_length + 2), 2); |
| EXPECT_EQ(GetHighlightStartIndex(static_text2.id, segment4_full_length - 1), |
| (int)node2_text.length() - 1); |
| EXPECT_EQ(GetHighlightStartIndex(static_text2.id, segment4_full_length), -1); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetNodeIdForCurrentSegmentIndex_ReturnsCorrectNodes) { |
| std::u16string sentence1 = u"Never feel heavy "; |
| std::u16string sentence2 = u"or earthbound, "; |
| std::u16string sentence3 = u"no worries or doubts interfere."; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| InitializeWithAndProcessNodes({static_text1, static_text2, static_text3}); |
| |
| // Before there are any processed granularities, |
| // GetNodeIdForCurrentSegmentIndex should return an invalid id. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(1), ui::kInvalidAXNodeID); |
| |
| std::vector<ui::AXNodeID> node_ids = GetCurrentText(); |
| EXPECT_EQ((int)node_ids.size(), 3); |
| |
| // Spot check that indices 0->sentence1.length() map to the first node id. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(0), static_text1.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(7), static_text1.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence1.length() - 1), |
| static_text1.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence1.length()), |
| static_text2.id); |
| |
| // Spot check that indices in sentence 2 map to the second node id. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence1.length() + 1), |
| static_text2.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(26), static_text2.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence1.length() + |
| sentence2.length() - 1), |
| static_text2.id); |
| EXPECT_EQ( |
| GetNodeIdForCurrentSegmentIndex(sentence1.length() + sentence2.length()), |
| static_text3.id); |
| |
| // Spot check that indices in sentence 3 map to the third node id. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence1.length() + |
| sentence2.length() + 1), |
| static_text3.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(40), static_text3.id); |
| EXPECT_EQ( |
| GetNodeIdForCurrentSegmentIndex(sentence1.length() + sentence2.length() + |
| sentence3.length() - 1), |
| static_text3.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex( |
| sentence1.length() + sentence2.length() + sentence3.length()), |
| ui::kInvalidAXNodeID); |
| |
| // Out-of-bounds nodes return invalid. |
| EXPECT_EQ( |
| GetNodeIdForCurrentSegmentIndex(sentence1.length() + sentence2.length() + |
| sentence3.length() + 1), |
| ui::kInvalidAXNodeID); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(535), ui::kInvalidAXNodeID); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(-10), ui::kInvalidAXNodeID); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetNodeIdForCurrentSegmentIndex_AfterNext_ReturnsCorrectNodes) { |
| std::u16string sentence1 = u"Never feel heavy or earthbound. "; |
| std::u16string sentence2 = u"No worries or doubts "; |
| std::u16string sentence3 = u"interfere."; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| InitializeWithAndProcessNodes({static_text1, static_text2, static_text3}); |
| |
| // Before there are any processed granularities, |
| // GetNodeIdForCurrentSegmentIndex should return an invalid id. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(1), ui::kInvalidAXNodeID); |
| |
| std::vector<ui::AXNodeID> node_ids = GetCurrentText(); |
| EXPECT_EQ((int)node_ids.size(), 1); |
| |
| // Spot check that indices 0->sentence1.length() map to the first node id. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(0), static_text1.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(7), static_text1.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence1.length() - 1), |
| static_text1.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence1.length()), |
| ui::kInvalidAXNodeID); |
| |
| // Move to the next granularity. |
| node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)node_ids.size(), 2); |
| |
| // Spot check that indices in sentence 2 map to the second node id. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(0), static_text2.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(7), static_text2.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence2.length() - 1), |
| static_text2.id); |
| |
| // Spot check that indices in sentence 3 map to the third node id. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence2.length() + 1), |
| static_text3.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(27), static_text3.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence2.length() + |
| sentence3.length() - 1), |
| static_text3.id); |
| |
| // Out-of-bounds nodes return invalid. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence2.length() + |
| sentence3.length() + 1), |
| ui::kInvalidAXNodeID); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetNodeIdForCurrentSegmentIndex_AfterPrevious_ReturnsCorrectNodes) { |
| std::u16string sentence1 = u"There's nothing but you "; |
| std::u16string sentence2 = u"looking down on the view from up here. "; |
| std::u16string sentence3 = u"Stretch out with the wind behind you."; |
| |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence1); |
| ui::AXNodeData static_text2 = test::TextNode(/* id= */ 3, sentence2); |
| ui::AXNodeData static_text3 = test::TextNode(/* id= */ 4, sentence3); |
| |
| InitializeWithAndProcessNodes({static_text1, static_text2, static_text3}); |
| |
| // Before there are any processed granularities, |
| // GetNodeIdForCurrentSegmentIndex should return an invalid id. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(1), ui::kInvalidAXNodeID); |
| |
| std::vector<ui::AXNodeID> node_ids = GetCurrentText(); |
| EXPECT_EQ((int)node_ids.size(), 2); |
| |
| // Move forward. |
| node_ids = MoveToNextGranularityAndGetText(); |
| EXPECT_EQ((int)node_ids.size(), 1); |
| |
| // Spot check that indices 0->sentence3.length() map to the third node id. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(0), static_text3.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(7), static_text3.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence3.length() - 1), |
| static_text3.id); |
| |
| // Move backwards. |
| node_ids = MoveToPreviousGranularityAndGetText(); |
| EXPECT_EQ((int)node_ids.size(), 2); |
| |
| // Spot check that indices in sentence 1 map to the first node id. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(0), static_text1.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(6), static_text1.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence1.length() - 1), |
| static_text1.id); |
| |
| // Spot check that indices in sentence 2 map to the second node id. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence1.length() + 1), |
| static_text2.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(27), static_text2.id); |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence1.length() + |
| sentence2.length() - 1), |
| static_text2.id); |
| |
| // Out-of-bounds nodes return invalid. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(sentence1.length() + |
| sentence2.length() + 1), |
| ui::kInvalidAXNodeID); |
| } |
| |
| TEST_F(ReadAnythingAppControllerTest, |
| GetNextWordHighlightLength_ReturnsCorrectLength) { |
| std::u16string word1 = u"Stretch "; |
| std::u16string word2 = u"out "; |
| std::u16string word3 = u"with "; |
| std::u16string word4 = u"the "; |
| std::u16string word5 = u"wind "; |
| std::u16string word6 = u"behind "; |
| std::u16string word7 = u"you."; |
| std::u16string sentence = |
| word1 + word2 + word3 + word4 + word5 + word6 + word7; |
| ui::AXNodeData static_text1 = test::TextNode(/* id= */ 2, sentence); |
| |
| InitializeWithAndProcessNodes({static_text1}); |
| |
| // Before there are any processed granularities, |
| // GetNodeIdForCurrentSegmentIndex should return an invalid id. |
| EXPECT_EQ(GetNodeIdForCurrentSegmentIndex(1), ui::kInvalidAXNodeID); |
| |
| std::vector<ui::AXNodeID> node_ids = GetCurrentText(); |
| EXPECT_EQ((int)node_ids.size(), 1); |
| |
| // Throughout first word. |
| EXPECT_EQ(GetWordLength(0), (int)word1.length()); |
| EXPECT_EQ(GetWordLength(2), (int)word1.length() - 2); |
| EXPECT_EQ(GetWordLength((int)word1.length()) - 2, 2); |
| |
| // Throughout third word. |
| int third_word_index = sentence.find(word3); |
| EXPECT_EQ(GetWordLength(third_word_index), (int)word3.length()); |
| EXPECT_EQ(GetWordLength(third_word_index + 2), (int)word3.length() - 2); |
| |
| int last_word_index = sentence.find(word7); |
| EXPECT_EQ(GetWordLength(last_word_index), (int)word7.length()); |
| EXPECT_EQ(GetWordLength(last_word_index + 2), (int)word7.length() - 2); |
| |
| // Boundary testing. |
| EXPECT_EQ(GetWordLength(-5), 0); |
| EXPECT_EQ(GetWordLength(sentence.length()), 0); |
| EXPECT_EQ(GetWordLength(sentence.length() + 1), 0); |
| } |
| |
| class ReadAnythingAppControllerScreen2xDataCollectionModeTest |
| : public ReadAnythingAppControllerTest { |
| public: |
| void SetUp() override { |
| base::test::ScopedFeatureList features; |
| scoped_feature_list_.InitWithFeatures( |
| {features::kDataCollectionModeForScreen2x}, {}); |
| ChromeRenderViewTest::SetUp(); |
| |
| content::RenderFrame* render_frame = |
| content::RenderFrame::FromWebFrame(GetMainFrame()); |
| controller_ = ReadAnythingAppController::Install(render_frame); |
| |
| // 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()); |
| |
| tree_id_ = ui::AXTreeID::CreateNewAXTreeID(); |
| ui::AXTreeUpdate snapshot; |
| ui::AXNodeData root; |
| root.id = 1; |
| snapshot.root_id = root.id; |
| snapshot.nodes = {root}; |
| SetUpdateTreeID(&snapshot); |
| AccessibilityEventReceived({snapshot}); |
| OnAXTreeDistilled({}); |
| } |
| |
| void SetScreenAIServiceReady() { controller_->ScreenAIServiceReady(); } |
| }; |
| |
| TEST_F(ReadAnythingAppControllerScreen2xDataCollectionModeTest, |
| DoesNotDistillImmediately) { |
| // When the AXTreeID changes, the controller usually will call |
| // distiller_->Distill(). However, with the data collection mode enabled, |
| // Distill() is not called immediately. |
| EXPECT_CALL(*distiller_, Distill).Times(0); |
| SetScreenAIServiceReady(); |
| OnActiveAXTreeIDChanged(tree_id_); |
| Mock::VerifyAndClearExpectations(distiller_); |
| } |
| |
| TEST_F(ReadAnythingAppControllerScreen2xDataCollectionModeTest, |
| DistillsAfterDelay) { |
| // When the AXTreeID changes, and 30s pass, the controller calls |
| // distiller_->Distill(). |
| EXPECT_CALL(*distiller_, Distill).Times(1); |
| SetScreenAIServiceReady(); |
| OnActiveAXTreeIDChanged(tree_id_); |
| task_environment_.FastForwardBy(base::Seconds(31)); |
| Mock::VerifyAndClearExpectations(distiller_); |
| } |
| |
| TEST_F(ReadAnythingAppControllerScreen2xDataCollectionModeTest, |
| DistillsAfterDelayScreenAIServiceReady) { |
| // When the AXTreeID changes, and 30s pass, the controller calls |
| // distiller_->Distill() once the screenAI service is ready. |
| OnActiveAXTreeIDChanged(tree_id_); |
| task_environment_.FastForwardBy(base::Seconds(31)); |
| |
| EXPECT_CALL(*distiller_, Distill).Times(1); |
| SetScreenAIServiceReady(); |
| Mock::VerifyAndClearExpectations(distiller_); |
| } |
| |
| TEST_F(ReadAnythingAppControllerScreen2xDataCollectionModeTest, |
| DoesNotDistillIfScreenAIServiceNotReady) { |
| // When the AXTreeID changes, and 30s pass, the controller does not call |
| // distiller_->Distill() as the screenAI service is not ready. |
| EXPECT_CALL(*distiller_, Distill).Times(0); |
| OnActiveAXTreeIDChanged(tree_id_); |
| task_environment_.FastForwardBy(base::Seconds(31)); |
| Mock::VerifyAndClearExpectations(distiller_); |
| } |
| |
| TEST_F(ReadAnythingAppControllerScreen2xDataCollectionModeTest, |
| DistillsAfterDelayWhenTreeIsStable) { |
| ui::AXTreeUpdate update; |
| SetUpdateTreeID(&update); |
| ui::AXNodeData root; |
| root.id = 1; |
| ui::AXNodeData node; |
| node.id = 2; |
| root.child_ids = {node.id}; |
| update.nodes = {root, node}; |
| update.root_id = root.id; |
| |
| // When the load complete event is received, and the tree is stable for 10s, |
| // the controller calls distiller_->Distill(). |
| EXPECT_CALL(*distiller_, Distill).Times(1); |
| SetScreenAIServiceReady(); |
| ui::AXEvent load_complete(0, ax::mojom::Event::kLoadComplete); |
| OnActiveAXTreeIDChanged(tree_id_); |
| AccessibilityEventReceived({update}, {load_complete}); |
| task_environment_.FastForwardBy(base::Seconds(11)); |
| Mock::VerifyAndClearExpectations(distiller_); |
| } |
| |
| TEST_F(ReadAnythingAppControllerScreen2xDataCollectionModeTest, |
| DoesNotDistillAfterDelayIfTreeIsUnstable) { |
| std::vector<ui::AXTreeUpdate> updates; |
| std::vector<int> child_ids = {}; |
| for (int i = 0; i < 2; i++) { |
| ui::AXTreeUpdate update; |
| SetUpdateTreeID(&update); |
| ui::AXNodeData root; |
| root.id = 1; |
| ui::AXNodeData node; |
| node.id = i + 2; |
| child_ids.push_back(node.id); |
| root.child_ids = child_ids; |
| update.nodes = {root, node}; |
| update.root_id = root.id; |
| updates.push_back(update); |
| } |
| |
| // When the load complete event is received, and the tree remains unstable, |
| // the controller does not call distiller_->Distill(). |
| EXPECT_CALL(*distiller_, Distill).Times(0); |
| SetScreenAIServiceReady(); |
| |
| ui::AXEvent load_complete(0, ax::mojom::Event::kLoadComplete); |
| AccessibilityEventReceived({updates[0]}, {load_complete}); |
| OnActiveAXTreeIDChanged(tree_id_); |
| task_environment_.FastForwardBy(base::Seconds(9)); |
| |
| AccessibilityEventReceived({updates[1]}); |
| task_environment_.FastForwardBy(base::Seconds(5)); |
| |
| Mock::VerifyAndClearExpectations(distiller_); |
| } |
| |
| TEST_F(ReadAnythingAppControllerScreen2xDataCollectionModeTest, |
| DistillsAfter30sDelayEvenIfTreeIsUnstable) { |
| std::vector<ui::AXTreeUpdate> updates; |
| std::vector<int> child_ids = {}; |
| for (int i = 0; i < 4; i++) { |
| ui::AXTreeUpdate update; |
| SetUpdateTreeID(&update); |
| ui::AXNodeData root; |
| root.id = 1; |
| ui::AXNodeData node; |
| node.id = i + 2; |
| child_ids.push_back(node.id); |
| root.child_ids = child_ids; |
| update.nodes = {root, node}; |
| update.root_id = root.id; |
| updates.push_back(update); |
| } |
| |
| // When the load complete event is received, even if the tree remains |
| // unstable, the controller does not calls distiller_->Distill() after 30s. |
| EXPECT_CALL(*distiller_, Distill).Times(1); |
| SetScreenAIServiceReady(); |
| |
| ui::AXEvent load_complete(0, ax::mojom::Event::kLoadComplete); |
| AccessibilityEventReceived({updates[0]}, {load_complete}); |
| OnActiveAXTreeIDChanged(tree_id_); |
| task_environment_.FastForwardBy(base::Seconds(9)); |
| |
| AccessibilityEventReceived({updates[1]}); |
| task_environment_.FastForwardBy(base::Seconds(9)); |
| |
| AccessibilityEventReceived({updates[2]}); |
| task_environment_.FastForwardBy(base::Seconds(9)); |
| |
| AccessibilityEventReceived({updates[3]}); |
| task_environment_.FastForwardBy(base::Seconds(4)); |
| |
| Mock::VerifyAndClearExpectations(distiller_); |
| } |