chromium/chrome/browser/ash/accessibility/select_to_speak_browsertest.cc

// 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/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 {
namespace {
constexpr char kSpeechDurationMetric[] =
    "Accessibility.CrosSelectToSpeak.SpeechDuration";
}  // namespace

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();
    }
  }

  void ExpectTotalSpeechDurationSamples(int expected_count) {
    histogram_tester_.ExpectTotalCount(kSpeechDurationMetric, expected_count);
  }

 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_;
  base::HistogramTester histogram_tester_;
  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,
                       FullscreenMagnifierFollowsTextBoundsWhenPrefOn) {
  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);

  // Set magnifier following STS Pref on
  profile->GetPrefs()->SetBoolean(prefs::kAccessibilityMagnifierFollowsSts,
                                  true);

  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 big 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());
}

// TODO(anastasi): Test that metrics record duration here.
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_.Call([this]() {
    generator_->PressKey(ui::VKEY_CONTROL, /*flags=*/0);
    generator_->ReleaseKey(ui::VKEY_CONTROL, /*flags=*/0);
  });
  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