| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| #include <vector> |
| |
| #include "ash/accessibility/magnifier/fullscreen_magnifier_controller.h" |
| #include "ash/accessibility/ui/accessibility_focus_ring_controller_impl.h" |
| #include "ash/accessibility/ui/accessibility_focus_ring_layer.h" |
| #include "ash/accessibility/ui/accessibility_highlight_layer.h" |
| #include "ash/constants/ash_pref_names.h" |
| #include "ash/public/cpp/ash_view_ids.h" |
| #include "ash/public/cpp/system_tray_test_api.h" |
| #include "ash/public/cpp/test/shell_test_api.h" |
| #include "ash/root_window_controller.h" |
| #include "ash/shell.h" |
| #include "ash/system/accessibility/select_to_speak/select_to_speak_tray.h" |
| #include "ash/system/status_area_widget.h" |
| #include "ash/system/unified/unified_system_tray.h" |
| #include "ash/test/ash_test_util.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "chrome/browser/ash/accessibility/accessibility_feature_browsertest.h" |
| #include "chrome/browser/ash/accessibility/accessibility_manager.h" |
| #include "chrome/browser/ash/accessibility/accessibility_test_utils.h" |
| #include "chrome/browser/ash/accessibility/automation_test_utils.h" |
| #include "chrome/browser/ash/accessibility/fullscreen_magnifier_test_helper.h" |
| #include "chrome/browser/ash/accessibility/magnification_manager.h" |
| #include "chrome/browser/ash/accessibility/magnifier_animation_waiter.h" |
| #include "chrome/browser/ash/accessibility/select_to_speak_test_utils.h" |
| #include "chrome/browser/ash/accessibility/speech_monitor.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/common/extensions/extension_constants.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "content/public/test/accessibility_notification_waiter.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "extensions/browser/browsertest_util.h" |
| #include "extensions/browser/extension_host_test_helper.h" |
| #include "extensions/browser/process_manager.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "ui/accessibility/accessibility_features.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/display/manager/display_manager.h" |
| #include "ui/display/screen.h" |
| #include "ui/display/test/display_manager_test_api.h" |
| #include "ui/events/keycodes/keyboard_codes_posix.h" |
| #include "ui/events/test/event_generator.h" |
| #include "url/url_constants.h" |
| |
| namespace ash { |
| |
| class SelectToSpeakTest : public AccessibilityFeatureBrowserTest { |
| public: |
| SelectToSpeakTest(const SelectToSpeakTest&) = delete; |
| SelectToSpeakTest& operator=(const SelectToSpeakTest&) = delete; |
| |
| void OnFocusRingChanged() { |
| if (loop_runner_ && loop_runner_->running()) { |
| loop_runner_->Quit(); |
| } |
| } |
| |
| void OnHighlightsAdded() { |
| if (highlights_runner_ && highlights_runner_->running()) { |
| highlights_runner_->Quit(); |
| } |
| } |
| |
| void SetSelectToSpeakState() { |
| if (tray_loop_runner_ && tray_loop_runner_->running()) { |
| tray_loop_runner_->Quit(); |
| } |
| } |
| |
| protected: |
| SelectToSpeakTest() {} |
| ~SelectToSpeakTest() override {} |
| |
| // Note that we do not enable Select to Speak in the SetUp method because |
| // tests are less flaky if we load the page URL before loading up the |
| // Select to Speak extension. |
| void SetUpOnMainThread() override { |
| AccessibilityFeatureBrowserTest::SetUpOnMainThread(); |
| |
| ASSERT_FALSE(AccessibilityManager::Get()->IsSelectToSpeakEnabled()); |
| console_observer_ = std::make_unique<ExtensionConsoleErrorObserver>( |
| AccessibilityManager::Get()->profile(), |
| extension_misc::kSelectToSpeakExtensionId); |
| |
| tray_test_api_ = SystemTrayTestApi::Create(); |
| aura::Window* root_window = Shell::Get()->GetPrimaryRootWindow(); |
| generator_ = std::make_unique<ui::test::EventGenerator>(root_window); |
| |
| automation_test_utils_ = std::make_unique<AutomationTestUtils>( |
| extension_misc::kSelectToSpeakExtensionId); |
| |
| NavigateToUrl(GURL(url::kAboutBlankURL)); |
| |
| // Pretend that enhanced network voices dialog has been accepted so that the |
| // dialog does not block. |
| AccessibilityManager::Get()->profile()->GetPrefs()->SetBoolean( |
| prefs::kAccessibilitySelectToSpeakEnhancedVoicesDialogShown, true); |
| } |
| |
| test::SpeechMonitor sm_; |
| std::unique_ptr<ui::test::EventGenerator> generator_; |
| std::unique_ptr<SystemTrayTestApi> tray_test_api_; |
| std::unique_ptr<ExtensionConsoleErrorObserver> console_observer_; |
| std::unique_ptr<AutomationTestUtils> automation_test_utils_; |
| |
| gfx::Rect GetWebContentsBounds(const std::string& url) const { |
| // TODO(katie): Find a way to get the exact bounds programmatically. |
| gfx::Rect bounds = automation_test_utils_->GetBoundsOfRootWebArea(url); |
| return bounds; |
| } |
| |
| void LoadURLAndSelectToSpeak(const std::string& url) { |
| if (!AccessibilityManager::Get()->IsSelectToSpeakEnabled()) { |
| sts_test_utils::TurnOnSelectToSpeakForTest( |
| AccessibilityManager::Get()->profile()); |
| } |
| automation_test_utils_->SetUpTestSupport(); |
| NavigateToUrl(GURL(url)); |
| automation_test_utils_->WaitForPageLoad(url); |
| } |
| |
| virtual void ActivateSelectToSpeakInWindowBounds(const std::string& url) { |
| // Load the URL before Select to Speak to avoid flakes. |
| LoadURLAndSelectToSpeak(url); |
| |
| sts_test_utils::StartSelectToSpeakInBrowserWithUrl( |
| url, automation_test_utils_.get(), generator_.get()); |
| } |
| |
| // Set document selection to be the node with text `text` using Automation |
| // API. |
| void SelectNodeWithText(const std::string& text) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| std::string script = |
| base::StringPrintf(R"JS( |
| (async function() { |
| chrome.automation.getDesktop(desktop => { |
| const textNode = desktop.find( |
| {role: 'staticText', attributes: {name: '%s'}}); |
| chrome.automation.setDocumentSelection({ |
| anchorObject: textNode, |
| anchorOffset: %d, |
| focusObject: textNode, |
| focusOffset: %d, |
| }); |
| const callback = (() => { |
| desktop.removeEventListener('documentSelectionChanged', |
| callback, /*capture=*/false); |
| chrome.test.sendScriptResult('ready'); |
| }); |
| desktop.addEventListener('documentSelectionChanged', |
| callback, /*capture=*/false); |
| }); |
| })(); |
| )JS", |
| text.c_str(), 0, static_cast<int>(text.size())); |
| base::Value result = |
| extensions::browsertest_util::ExecuteScriptInBackgroundPage( |
| AccessibilityManager::Get()->profile(), |
| extension_misc::kSelectToSpeakExtensionId, script); |
| ASSERT_EQ("ready", result); |
| } |
| |
| void PrepareToWaitForHighlightAdded() { |
| highlights_runner_ = std::make_unique<base::RunLoop>(); |
| base::RepeatingCallback<void()> callback = base::BindRepeating( |
| &SelectToSpeakTest::OnHighlightsAdded, GetWeakPtr()); |
| AccessibilityManager::Get()->SetHighlightsObserverForTest(callback); |
| } |
| |
| void WaitForHighlightAdded() { |
| DCHECK(highlights_runner_); |
| highlights_runner_->Run(); |
| highlights_runner_ = nullptr; |
| } |
| |
| void PrepareToWaitForSelectToSpeakStatusChanged() { |
| tray_loop_runner_ = std::make_unique<base::RunLoop>(); |
| } |
| |
| // Blocks until the select-to-speak tray status is changed. |
| void WaitForSelectToSpeakStatusChanged() { |
| tray_loop_runner_->Run(); |
| tray_loop_runner_ = nullptr; |
| } |
| |
| void TapSelectToSpeakTray() { |
| PrepareToWaitForSelectToSpeakStatusChanged(); |
| tray_test_api_->TapSelectToSpeakTray(); |
| WaitForSelectToSpeakStatusChanged(); |
| } |
| |
| void PrepareToWaitForFocusRingChanged() { |
| loop_runner_ = std::make_unique<base::RunLoop>(); |
| } |
| |
| // Blocks until the focus ring is changed. |
| void WaitForFocusRingChanged() { |
| loop_runner_->Run(); |
| loop_runner_ = nullptr; |
| } |
| |
| base::WeakPtr<SelectToSpeakTest> GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| void ExecuteJavaScriptAsync(const std::string& script) { |
| content::ExecuteScriptAsync( |
| browser()->tab_strip_model()->GetActiveWebContents(), script); |
| } |
| |
| private: |
| std::unique_ptr<base::RunLoop> loop_runner_; |
| std::unique_ptr<base::RunLoop> highlights_runner_; |
| std::unique_ptr<base::RunLoop> tray_loop_runner_; |
| base::WeakPtrFactory<SelectToSpeakTest> weak_ptr_factory_{this}; |
| }; |
| |
| class SelectToSpeakTestWithVoiceSwitching : public SelectToSpeakTest { |
| protected: |
| void SetUpOnMainThread() override { |
| PrefService* prefs = AccessibilityManager::Get()->profile()->GetPrefs(); |
| prefs->SetBoolean(prefs::kAccessibilitySelectToSpeakVoiceSwitching, true); |
| SelectToSpeakTest::SetUpOnMainThread(); |
| } |
| }; |
| |
| class SelectToSpeakTestWithMagnifierFollowing : public SelectToSpeakTest { |
| public: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| scoped_feature_list_.InitAndEnableFeature( |
| ::features::kAccessibilityMagnifierFollowsSts); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, SpeakStatusTray) { |
| sts_test_utils::TurnOnSelectToSpeakForTest( |
| AccessibilityManager::Get()->profile()); |
| gfx::Rect tray_bounds = Shell::Get() |
| ->GetPrimaryRootWindowController() |
| ->GetStatusAreaWidget() |
| ->unified_system_tray() |
| ->GetBoundsInScreen(); |
| |
| // Hold down Search and click a few pixels into the status tray bounds. |
| generator_->PressKey(ui::VKEY_LWIN, 0 /* flags */); |
| generator_->MoveMouseTo(tray_bounds.x() + 8, tray_bounds.y() + 8); |
| generator_->PressLeftButton(); |
| generator_->ReleaseLeftButton(); |
| generator_->ReleaseKey(ui::VKEY_LWIN, 0 /* flags */); |
| |
| sm_.ExpectSpeechPattern("*Status tray*"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, ActivatesWithTapOnSelectToSpeakTray) { |
| LoadURLAndSelectToSpeak( |
| "data:text/html;charset=utf-8,<p>This is some text</p>"); |
| |
| base::RepeatingCallback<void()> callback = base::BindRepeating( |
| &SelectToSpeakTest::SetSelectToSpeakState, GetWeakPtr()); |
| AccessibilityManager::Get()->SetSelectToSpeakStateObserverForTest(callback); |
| // Click in the tray bounds to start 'selection' mode. |
| TapSelectToSpeakTray(); |
| |
| // We should be in "selection" mode, so clicking with the mouse should |
| // start speech. |
| gfx::Rect bounds = automation_test_utils_->GetNodeBoundsInRoot( |
| "This is some text", "staticText"); |
| generator_->MoveMouseTo(bounds.x(), bounds.y()); |
| generator_->PressLeftButton(); |
| generator_->MoveMouseTo(bounds.x() + bounds.width(), |
| bounds.y() + bounds.height()); |
| generator_->ReleaseLeftButton(); |
| |
| sm_.ExpectSpeechPattern("This is some text*"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, WorksWithTouchSelection) { |
| LoadURLAndSelectToSpeak( |
| "data:text/html;charset=utf-8,<p>This is some text</p>"); |
| |
| base::RepeatingCallback<void()> callback = base::BindRepeating( |
| &SelectToSpeakTest::SetSelectToSpeakState, GetWeakPtr()); |
| AccessibilityManager::Get()->SetSelectToSpeakStateObserverForTest(callback); |
| |
| // Click in the tray bounds to start 'selection' mode. |
| TapSelectToSpeakTray(); |
| |
| // We should be in "selection" mode, so tapping and dragging should |
| // start speech. |
| gfx::Rect bounds = automation_test_utils_->GetNodeBoundsInRoot( |
| "This is some text", "staticText"); |
| generator_->PressTouch(gfx::Point(bounds.x(), bounds.y())); |
| generator_->PressMoveAndReleaseTouchTo(bounds.x() + bounds.width(), |
| bounds.y() + bounds.height()); |
| generator_->ReleaseLeftButton(); |
| |
| sm_.ExpectSpeechPattern("This is some text*"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, |
| WorksWithTouchSelectionOnNonPrimaryMonitor) { |
| // Don't observe error messages. |
| // An error message is observed consistently on MSAN, see crbug.com/1201212, |
| // and flakily on other builds, see crbug.com/1213451. |
| // Run the rest of this test on but don't try to catch console errors. |
| // TODO: Figure out why the "unable to load tab" error is occurring |
| // and bring back the console observer. |
| console_observer_.reset(); |
| |
| ShellTestApi shell_test_api; |
| display::test::DisplayManagerTestApi(shell_test_api.display_manager()) |
| .UpdateDisplay("1+0-800x700,801+1-800x700"); |
| ASSERT_EQ(2u, shell_test_api.display_manager()->GetNumDisplays()); |
| display::test::DisplayManagerTestApi display_manager_test_api( |
| shell_test_api.display_manager()); |
| |
| display::Screen* screen = display::Screen::GetScreen(); |
| int64_t display2 = display_manager_test_api.GetSecondaryDisplay().id(); |
| screen->SetDisplayForNewWindows(display2); |
| |
| // Ctrl+N to open a new browser window. This will load on the new display. |
| generator_->PressAndReleaseKey(ui::VKEY_N, ui::EF_CONTROL_DOWN); |
| |
| std::string url = "data:text/html;charset=utf-8,<p>This is some text</p>"; |
| NavigateToUrl(GURL(url)); |
| |
| sts_test_utils::TurnOnSelectToSpeakForTest( |
| AccessibilityManager::Get()->profile()); |
| automation_test_utils_->SetUpTestSupport(); |
| base::RepeatingCallback<void()> callback = base::BindRepeating( |
| &SelectToSpeakTest::SetSelectToSpeakState, GetWeakPtr()); |
| AccessibilityManager::Get()->SetSelectToSpeakStateObserverForTest(callback); |
| |
| // Also waits for load complete. |
| gfx::Rect bounds = GetWebContentsBounds(url); |
| |
| // Click in the tray bounds to start 'selection' mode. |
| TapSelectToSpeakTray(); |
| // We should be in "selection" mode, so tapping and dragging should |
| // start speech. |
| generator_->PressTouch(gfx::Point(bounds.x(), bounds.y())); |
| generator_->PressMoveAndReleaseTouchTo(bounds.x() + bounds.width(), |
| bounds.y() + bounds.height()); |
| generator_->ReleaseLeftButton(); |
| |
| sm_.ExpectSpeechPattern("This is some text*"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, SelectToSpeakTrayNotSpoken) { |
| sts_test_utils::TurnOnSelectToSpeakForTest( |
| AccessibilityManager::Get()->profile()); |
| |
| base::RepeatingCallback<void()> callback = base::BindRepeating( |
| &SelectToSpeakTest::SetSelectToSpeakState, GetWeakPtr()); |
| AccessibilityManager::Get()->SetSelectToSpeakStateObserverForTest(callback); |
| |
| // Tap it once to enter selection mode. |
| TapSelectToSpeakTray(); |
| |
| // Tap again to turn off selection mode. |
| TapSelectToSpeakTray(); |
| |
| // The next should be the first thing spoken -- the tray was not spoken. |
| ActivateSelectToSpeakInWindowBounds( |
| "data:text/html;charset=utf-8,<p>This is some text</p>"); |
| |
| sm_.ExpectSpeechPattern("This is some text*"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, SmoothlyReadsAcrossInlineUrl) { |
| // Make sure an inline URL is read smoothly. |
| ActivateSelectToSpeakInWindowBounds( |
| "data:text/html;charset=utf-8,<p>This is some text <a href=\"\">with a" |
| " node</a> in the middle"); |
| // Should combine nodes in a paragraph into one utterance. |
| // Includes some wildcards between words because there may be extra |
| // spaces. Spaces are not pronounced, so extra spaces do not impact output. |
| sm_.ExpectSpeechPattern("This is some text*with a node*in the middle*"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, SetsWordHighlights) { |
| AccessibilityFocusRingControllerImpl* controller = |
| Shell::Get()->accessibility_focus_ring_controller(); |
| EXPECT_FALSE(controller->highlight_layer_for_testing()); |
| PrepareToWaitForHighlightAdded(); |
| ActivateSelectToSpeakInWindowBounds( |
| "data:text/html;charset=utf-8,<p>Highlight me"); |
| sm_.ExpectSpeechPattern("*Highlight me*"); |
| sm_.Replay(); |
| |
| // Some highlighting should have occurred. OK to do this after speech as |
| // Select to Speak refreshes the UI intermittently. |
| WaitForHighlightAdded(); |
| |
| // Check the highlight exists and the color is as expected. |
| AccessibilityHighlightLayer* highlight_layer = |
| controller->highlight_layer_for_testing(); |
| EXPECT_TRUE(highlight_layer); |
| EXPECT_EQ(1u, highlight_layer->rects_for_test().size()); |
| EXPECT_EQ(SkColorSetRGB(94, 155, 255), highlight_layer->color_for_test()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, SmoothlyReadsAcrossMultipleLines) { |
| // Sentences spanning multiple lines. |
| ActivateSelectToSpeakInWindowBounds( |
| "data:text/html;charset=utf-8,<div style=\"width:100px\">This" |
| " is some text with a node in the middle"); |
| // Should combine nodes in a paragraph into one utterance. |
| // Includes some wildcards between words because there may be extra |
| // spaces, for example at line wraps. Extra wildcards included to |
| // reduce flakyness in case wrapping is not consistent. |
| // Spaces are not pronounced, so extra spaces do not impact output. |
| sm_.ExpectSpeechPattern("This is some*text*with*a*node*in*the*middle*"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, SmoothlyReadsAcrossFormattedText) { |
| // Bold or formatted text |
| ActivateSelectToSpeakInWindowBounds( |
| "data:text/html;charset=utf-8,<p>This is some text <b>with a node" |
| "</b> in the middle"); |
| |
| // Should combine nodes in a paragraph into one utterance. |
| // Includes some wildcards between words because there may be extra |
| // spaces. Spaces are not pronounced, so extra spaces do not impact output. |
| sm_.ExpectSpeechPattern("This is some text*with a node*in the middle*"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, |
| ReadsStaticTextWithoutInlineTextChildren) { |
| // Bold or formatted text |
| ActivateSelectToSpeakInWindowBounds( |
| "data:text/html;charset=utf-8,<canvas>This is some text</canvas>"); |
| |
| sm_.ExpectSpeechPattern("This is some text*"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, BreaksAtParagraphBounds) { |
| ActivateSelectToSpeakInWindowBounds( |
| "data:text/html;charset=utf-8,<div><p>First paragraph</p>" |
| "<p>Second paragraph</p></div>"); |
| |
| // Should keep each paragraph as its own utterance. |
| sm_.ExpectSpeechPattern("First paragraph*"); |
| sm_.ExpectSpeechPattern("Second paragraph*"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, LanguageBoundsIgnoredByDefault) { |
| // Splitting at language bounds is behind a feature flag, test the default |
| // behaviour doesn't introduce a regression. |
| ActivateSelectToSpeakInWindowBounds( |
| "data:text/html;charset=utf-8,<div>" |
| "<span lang='en-US'>The first paragraph</span>" |
| "<span lang='fr-FR'>le deuxieme paragraphe</span></div>"); |
| |
| sm_.ExpectSpeechPattern("The first paragraph* le deuxieme paragraphe*"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTestWithVoiceSwitching, |
| BreaksAtLanguageBounds) { |
| ActivateSelectToSpeakInWindowBounds( |
| "data:text/html;charset=utf-8,<div>" |
| "<span lang='en-US'>The first paragraph</span>" |
| "<span lang='fr-FR'>le deuxieme paragraphe</span></div>"); |
| |
| sm_.ExpectSpeech(test::SpeechMonitor::Expectation("The first paragraph*") |
| .AsPattern() |
| .WithLocale("en-US")); |
| sm_.ExpectSpeech(test::SpeechMonitor::Expectation("le deuxieme paragraphe*") |
| .AsPattern() |
| .WithLocale("fr-FR")); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, DoesNotCrashWithMousewheelEvent) { |
| std::string url = "data:text/html;charset=utf-8,<p>This is some text</p>"; |
| LoadURLAndSelectToSpeak(url); |
| gfx::Rect bounds = GetWebContentsBounds(url); |
| |
| // Hold down Search and drag over the web contents to select everything. |
| generator_->PressKey(ui::VKEY_LWIN, 0 /* flags */); |
| generator_->MoveMouseTo(bounds.x(), bounds.y()); |
| generator_->PressLeftButton(); |
| // Ensure this does not crash. It should have no effect. |
| generator_->MoveMouseWheel(10, 10); |
| generator_->MoveMouseTo(bounds.x() + bounds.width(), |
| bounds.y() + bounds.height()); |
| generator_->MoveMouseWheel(100, 5); |
| generator_->ReleaseLeftButton(); |
| generator_->ReleaseKey(ui::VKEY_LWIN, 0 /* flags */); |
| |
| sm_.ExpectSpeechPattern("This is some text*"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, FocusRingMovesWithMouse) { |
| std::string url = |
| "data:text/html;charset=utf-8," |
| "<p>This is some text</p>"; |
| LoadURLAndSelectToSpeak(url); |
| |
| // Create a callback for the focus ring observer. |
| base::RepeatingCallback<void()> callback = |
| base::BindRepeating(&SelectToSpeakTest::OnFocusRingChanged, GetWeakPtr()); |
| AccessibilityManager::Get()->SetFocusRingObserverForTest(callback); |
| |
| std::string focus_ring_id = AccessibilityManager::Get()->GetFocusRingId( |
| ax::mojom::AssistiveTechnologyType::kSelectToSpeak, ""); |
| |
| AccessibilityFocusRingControllerImpl* controller = |
| Shell::Get()->accessibility_focus_ring_controller(); |
| controller->SetNoFadeForTesting(); |
| const AccessibilityFocusRingGroup* focus_ring_group = |
| controller->GetFocusRingGroupForTesting(focus_ring_id); |
| // No focus rings to start. |
| EXPECT_EQ(nullptr, focus_ring_group); |
| |
| gfx::Rect bounds = GetWebContentsBounds(url); |
| PrepareToWaitForFocusRingChanged(); |
| generator_->PressKey(ui::VKEY_LWIN, 0 /* flags */); |
| generator_->MoveMouseTo(bounds.x(), bounds.y()); |
| generator_->PressLeftButton(); |
| |
| // Expect a focus ring to have been drawn. |
| WaitForFocusRingChanged(); |
| focus_ring_group = controller->GetFocusRingGroupForTesting(focus_ring_id); |
| ASSERT_NE(nullptr, focus_ring_group); |
| std::vector<std::unique_ptr<AccessibilityFocusRingLayer>> const& focus_rings = |
| focus_ring_group->focus_layers_for_testing(); |
| EXPECT_EQ(focus_rings.size(), 1u); |
| |
| gfx::Rect target_bounds = focus_rings.at(0)->layer()->GetTargetBounds(); |
| |
| // Make sure it's in a reasonable position. |
| EXPECT_LT(abs(target_bounds.x() - bounds.x()), 50); |
| EXPECT_LT(abs(target_bounds.y() - bounds.y()), 50); |
| EXPECT_LT(target_bounds.width(), 50); |
| EXPECT_LT(target_bounds.height(), 50); |
| |
| // Move the mouse. |
| PrepareToWaitForFocusRingChanged(); |
| generator_->MoveMouseTo(bounds.x() + 100, bounds.y() + 100); |
| |
| // Expect focus ring to have moved with the mouse. |
| // The size should have grown to be over 100 (the rect is now size 100, |
| // and the focus ring has some buffer). Position should be unchanged. |
| WaitForFocusRingChanged(); |
| target_bounds = focus_rings.at(0)->layer()->GetTargetBounds(); |
| EXPECT_LT(abs(target_bounds.x() - bounds.x()), 50); |
| EXPECT_LT(abs(target_bounds.y() - bounds.y()), 50); |
| EXPECT_GT(target_bounds.width(), 100); |
| EXPECT_GT(target_bounds.height(), 100); |
| |
| // Move the mouse smaller again, it should shrink. |
| PrepareToWaitForFocusRingChanged(); |
| generator_->MoveMouseTo(bounds.x() + 10, bounds.y() + 18); |
| WaitForFocusRingChanged(); |
| target_bounds = focus_rings.at(0)->layer()->GetTargetBounds(); |
| EXPECT_LT(target_bounds.width(), 50); |
| EXPECT_LT(target_bounds.height(), 50); |
| |
| // Cancel this by releasing the key before the mouse. |
| PrepareToWaitForFocusRingChanged(); |
| generator_->ReleaseKey(ui::VKEY_LWIN, 0 /* flags */); |
| generator_->ReleaseLeftButton(); |
| |
| // Expect focus ring to have been cleared, this was canceled in STS |
| // by releasing the key before the button. |
| WaitForFocusRingChanged(); |
| EXPECT_EQ(focus_rings.size(), 0u); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, |
| SelectToSpeakSelectionPansFullscreenMagnifier) { |
| FullscreenMagnifierController* fullscreen_magnifier_controller = |
| Shell::Get()->fullscreen_magnifier_controller(); |
| fullscreen_magnifier_controller->SetEnabled(true); |
| |
| // Wait for Fullscreen magnifier to initialize. |
| MagnifierAnimationWaiter waiter(fullscreen_magnifier_controller); |
| waiter.Wait(); |
| gfx::Point const initial_window_position = |
| fullscreen_magnifier_controller->GetWindowPosition(); |
| std::string url = |
| "data:text/html;charset=utf-8," |
| "<p>This is some text</p>"; |
| LoadURLAndSelectToSpeak(url); |
| gfx::Rect bounds = GetWebContentsBounds(url); |
| PrepareToWaitForFocusRingChanged(); |
| |
| // Hold down Search, and move mouse to start of text. |
| generator_->PressKey(ui::VKEY_LWIN, 0 /* flags */); |
| generator_->MoveMouseTo(bounds.x(), bounds.y()); |
| |
| // FullscreenMagnifierController moves the magnifier window with animation |
| // when the magnifier is set to be enabled. Wait until the animation |
| // completes, so that the mouse movement controls the position of magnifier |
| // window later. |
| waiter.Wait(); |
| |
| // Press and drag mouse past bounds of magnified screen, to move the viewport. |
| generator_->PressLeftButton(); |
| |
| // Move mouse to bottom right area of screen. Multiply by scale as fullscreen |
| // magnifier is enabled, so input needs to be transformed. |
| const float scale = fullscreen_magnifier_controller->GetScale(); |
| generator_->MoveMouseTo( |
| (bounds.right() - initial_window_position.x()) * scale, |
| (bounds.bottom() - initial_window_position.y()) * scale); |
| |
| gfx::Point const final_window_position = |
| fullscreen_magnifier_controller->GetWindowPosition(); |
| |
| // Expect Magnifier window to move with mouse drag during STS selection. |
| EXPECT_GT(final_window_position.x(), initial_window_position.x()); |
| EXPECT_GT(final_window_position.y(), initial_window_position.y()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTestWithMagnifierFollowing, |
| FullscreenMagnifierFollowsTextBounds) { |
| sm_.send_word_events_and_wait_to_finish(true); |
| Profile* profile = AccessibilityManager::Get()->profile(); |
| // Turn off navigation controls as focus on these buttons changes magnifier |
| // bounds, and then there's a delay before focus can move to the highlighted |
| // area. In real life, focus would then update to the spoken text a short time |
| // after navigation controls, but speech monitor speaks everything instantly |
| // so we cannot test that. |
| profile->GetPrefs()->SetBoolean( |
| prefs::kAccessibilitySelectToSpeakNavigationControls, false); |
| |
| std::string text = "Read me first!"; |
| std::string second_text = "Read me last!"; |
| LoadURLAndSelectToSpeak( |
| base::StringPrintf("data:text/html;charset=utf-8,<p>Not me!</p>" |
| "<p>Skip me!</p><p>%s</p><p>Nor me!</p><p>%s</p>", |
| text.c_str(), second_text.c_str())); |
| SelectNodeWithText(text); |
| |
| // Set magnifier scale to something quite so that the initial bounds of the |
| // text are not within the magnifier bounds. |
| profile->GetPrefs()->SetDouble(prefs::kAccessibilityScreenMagnifierScale, |
| 8.0); |
| |
| // Wait for Fullscreen magnifier to initialize. |
| extensions::ExtensionHostTestHelper host_helper( |
| profile, extension_misc::kAccessibilityCommonExtensionId); |
| profile->GetPrefs()->SetBoolean(prefs::kAccessibilityScreenMagnifierEnabled, |
| true); |
| |
| FullscreenMagnifierController* fullscreen_magnifier_controller = |
| Shell::Get()->fullscreen_magnifier_controller(); |
| MagnifierAnimationWaiter waiter(fullscreen_magnifier_controller); |
| waiter.Wait(); |
| |
| host_helper.WaitForHostCompletedFirstLoad(); |
| FullscreenMagnifierTestHelper::WaitForMagnifierJSReady(profile); |
| |
| gfx::Rect initial_viewport = |
| fullscreen_magnifier_controller->GetViewportRect(); |
| |
| AccessibilityFocusRingControllerImpl* controller = |
| Shell::Get()->accessibility_focus_ring_controller(); |
| EXPECT_FALSE(controller->highlight_layer_for_testing()); |
| PrepareToWaitForHighlightAdded(); |
| // Activate select to speak on the selection (which is outside the magnifier |
| // bounds) using search+s. |
| generator_->PressKey(ui::VKEY_LWIN, /*flags=*/0); |
| generator_->PressKey(ui::VKEY_S, /*flags=*/0); |
| generator_->ReleaseKey(ui::VKEY_LWIN, /*flags=*/0); |
| generator_->ReleaseKey(ui::VKEY_S, /*flags=*/0); |
| |
| // Some highlighting should have occurred. OK to do this after speech as |
| // Select to Speak refreshes the UI intermittently. |
| WaitForHighlightAdded(); |
| |
| // Check the highlight exists. |
| AccessibilityHighlightLayer* highlight_layer = |
| controller->highlight_layer_for_testing(); |
| ASSERT_TRUE(highlight_layer); |
| EXPECT_EQ(1u, highlight_layer->rects_for_test().size()); |
| gfx::Rect highlight_bounds = highlight_layer->rects_for_test()[0]; |
| EXPECT_FALSE(initial_viewport.Intersects(highlight_bounds)); |
| |
| // Magnifier should now move to the highlighted area. |
| while (!fullscreen_magnifier_controller->GetViewportRect().Intersects( |
| highlight_bounds)) { |
| waiter.Wait(); |
| } |
| gfx::Rect final_viewport = fullscreen_magnifier_controller->GetViewportRect(); |
| EXPECT_FALSE(initial_viewport.Intersects(final_viewport)); |
| EXPECT_TRUE(final_viewport.Intersects(highlight_bounds)); |
| |
| // Finish speech and make sure the right thing was actually read. |
| sm_.FinishSpeech(); |
| sm_.ExpectSpeechPattern("*Read me first!*"); |
| sm_.Replay(); |
| |
| SelectNodeWithText(second_text); |
| PrepareToWaitForHighlightAdded(); |
| generator_->PressKey(ui::VKEY_LWIN, /*flags=*/0); |
| generator_->PressKey(ui::VKEY_S, /*flags=*/0); |
| generator_->ReleaseKey(ui::VKEY_LWIN, /*flags=*/0); |
| generator_->ReleaseKey(ui::VKEY_S, /*flags=*/0); |
| |
| WaitForHighlightAdded(); |
| |
| // Highlight should have updated. |
| // First it will be cleared when the previous utterance is completed. |
| // Then make sure it's no longer in the magnifier's viewport. |
| while (!highlight_layer->rects_for_test().size() || |
| final_viewport.Intersects(highlight_layer->rects_for_test()[0])) { |
| PrepareToWaitForHighlightAdded(); |
| WaitForHighlightAdded(); |
| } |
| highlight_bounds = highlight_layer->rects_for_test()[0]; |
| EXPECT_FALSE(final_viewport.Intersects(highlight_bounds)); |
| |
| // Magnifier should update enough to cover the new highlighted text. |
| while (!fullscreen_magnifier_controller->GetViewportRect().Intersects( |
| highlight_bounds)) { |
| waiter.Wait(); |
| } |
| gfx::Rect second_final_viewport = |
| fullscreen_magnifier_controller->GetViewportRect(); |
| EXPECT_TRUE(second_final_viewport.Intersects(highlight_bounds)); |
| |
| // Finish speech and make sure the right thing was actually read. |
| sm_.FinishSpeech(); |
| sm_.ExpectSpeechPattern("*Read me last!*"); |
| sm_.Replay(); |
| |
| // Reset state. |
| sm_.send_word_events_and_wait_to_finish(false); |
| profile->GetPrefs()->SetBoolean(prefs::kAccessibilityScreenMagnifierEnabled, |
| false); |
| } |
| |
| // TODO(b/259363112): Add a test that Select to Speak follows focus for nodes |
| // with no inline text boxes. |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, ContinuesReadingDuringResize) { |
| ActivateSelectToSpeakInWindowBounds( |
| "data:text/html;charset=utf-8,<p>First paragraph</p>" |
| "<div id='resize' style='width:300px; font-size: 1em'>" |
| "<p>Second paragraph is longer than 300 pixels and will wrap when " |
| "resized</p></div>"); |
| |
| sm_.ExpectSpeechPattern("First paragraph*"); |
| |
| // Resize before second is spoken. If resizing caused errors finding the |
| // inlineTextBoxes in the node, speech would be stopped early. |
| sm_.Call([this]() { |
| ExecuteJavaScriptAsync( |
| "document.getElementById('resize').style.width='100px'"); |
| }); |
| sm_.ExpectSpeechPattern("*when*resized*"); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, WorksWithStickyKeys) { |
| AccessibilityManager::Get()->EnableStickyKeys(true); |
| |
| std::string url = "data:text/html;charset=utf-8,<p>This is some text</p>"; |
| LoadURLAndSelectToSpeak(url); |
| |
| // Tap Search and click a few pixels into the window bounds. |
| generator_->PressKey(ui::VKEY_LWIN, 0 /* flags */); |
| generator_->ReleaseKey(ui::VKEY_LWIN, 0 /* flags */); |
| |
| // Sticky keys should remember the 'search' key was clicked, so STS is |
| // actually in a capturing mode now. |
| gfx::Rect bounds = GetWebContentsBounds(url); |
| generator_->MoveMouseTo(bounds.x(), bounds.y()); |
| generator_->PressLeftButton(); |
| generator_->MoveMouseTo(bounds.x() + bounds.width(), |
| bounds.y() + bounds.height()); |
| generator_->ReleaseLeftButton(); |
| |
| sm_.ExpectSpeechPattern("This is some text*"); |
| |
| // Reset state. |
| sm_.Call([]() { AccessibilityManager::Get()->EnableStickyKeys(false); }); |
| |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, |
| SelectToSpeakDoesNotDismissTrayBubble) { |
| sts_test_utils::TurnOnSelectToSpeakForTest( |
| AccessibilityManager::Get()->profile()); |
| |
| // Open tray bubble menu. |
| tray_test_api_->ShowBubble(); |
| |
| // Search key + click the settings button. |
| generator_->PressKey(ui::VKEY_LWIN, 0 /* flags */); |
| tray_test_api_->ClickBubbleView(ViewID::VIEW_ID_QS_SETTINGS_BUTTON); |
| generator_->ReleaseKey(ui::VKEY_LWIN, 0 /* flags */); |
| |
| // Should read out text. |
| sm_.ExpectSpeechPattern("*Settings*"); |
| sm_.Replay(); |
| |
| // Tray bubble menu should remain open. |
| ASSERT_TRUE(tray_test_api_->IsTrayBubbleOpen()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, ReadsSelectedTextWithSearchS) { |
| std::string text = "This is some selected text"; |
| LoadURLAndSelectToSpeak(base::StringPrintf( |
| "data:text/html;charset=utf-8,<p>Not me!</p><p>%s</p><p>Nor me!</p>", |
| text.c_str())); |
| SelectNodeWithText(text); |
| |
| generator_->PressKey(ui::VKEY_LWIN, /*flags=*/0); |
| generator_->PressKey(ui::VKEY_S, /*flags=*/0); |
| generator_->ReleaseKey(ui::VKEY_LWIN, /*flags=*/0); |
| generator_->ReleaseKey(ui::VKEY_S, /*flags=*/0); |
| |
| sm_.ExpectSpeechPattern(text); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, |
| ReadsSelectedTextFromContextMenuClick) { |
| std::string text = "This is some selected text"; |
| LoadURLAndSelectToSpeak(base::StringPrintf( |
| "data:text/html;charset=utf-8,<p>Not me!</p><p>%s</p><p>Nor me!</p>", |
| text.c_str())); |
| |
| SelectNodeWithText(text); |
| |
| gfx::Rect text_bounds = |
| automation_test_utils_->GetNodeBoundsInRoot(text, "staticText"); |
| text_bounds.Inset(2.0f); |
| generator_->MoveMouseTo(text_bounds.right_center()); |
| |
| const std::string name = "Listen to selected text"; |
| |
| // If the EmbeddedA11yHelper in Lacros hasn't completed loading the helper |
| // extension, the context menu option won't be present. Keep trying until it |
| // is present. For users, it gets installed so quickly in Lacros that they |
| // won't see this type of behavior. |
| while (true) { |
| // Right-click the selected region. |
| generator_->PressRightButton(); |
| generator_->ReleaseRightButton(); |
| |
| // Wait for the copy context menu item to be shown, |
| // this means the menu is displayed. |
| automation_test_utils_->GetNodeBoundsInRoot("Copy Ctrl+C", "menuItem"); |
| if (automation_test_utils_->NodeExistsNoWait(name, "menuItem")) { |
| break; |
| } |
| // Close the menu and wait for the close to propagate. |
| generator_->PressAndReleaseKey(ui::VKEY_ESCAPE, /*flags=*/0); |
| automation_test_utils_->WaitForChildrenChangedEvent(); |
| } |
| |
| // Click the Select to Speak menu item. |
| gfx::Rect menu_item_bounds = |
| automation_test_utils_->GetNodeBoundsInRoot(name, "menuItem"); |
| generator_->MoveMouseTo(menu_item_bounds.CenterPoint()); |
| generator_->PressLeftButton(); |
| generator_->ReleaseLeftButton(); |
| |
| sm_.ExpectSpeechPattern(text); |
| sm_.Replay(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, |
| ReadsSelectedTextWithContextMenuNotification) { |
| std::string text = "Pick me! Read me!"; |
| LoadURLAndSelectToSpeak(base::StringPrintf( |
| "data:text/html;charset=utf-8,<p>Not me!</p><p>%s</p><p>Nor me!</p>", |
| text.c_str())); |
| SelectNodeWithText(text); |
| |
| AccessibilityManager::Get()->OnSelectToSpeakContextMenuClick(); |
| |
| sm_.ExpectSpeechPattern(text); |
| sm_.Replay(); |
| } |
| |
| } // namespace ash |