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

// Copyright 2014 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/browser/ash/accessibility/spoken_feedback_browsertest.h"

#include <queue>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/magnifier/fullscreen_magnifier_controller.h"
#include "ash/accessibility/ui/accessibility_confirmation_dialog.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/ash_switches.h"
#include "ash/display/display_configuration_controller.h"
#include "ash/public/cpp/accelerators.h"
#include "ash/public/cpp/event_rewriter_controller.h"
#include "ash/public/cpp/screen_backlight.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/public/cpp/test/test_shelf_item_delegate.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_view.h"
#include "ash/shelf/shelf_widget.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/notification_center/notification_center_test_api.h"
#include "ash/system/power/backlights_forced_off_setter.h"
#include "ash/system/status_area_widget.h"
#include "ash/system/status_area_widget_test_helper.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/wm/desks/templates/saved_desk_util.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_tick_clock.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.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/crosapi/browser_manager.h"
#include "chrome/browser/ash/login/test/device_state_mixin.h"
#include "chrome/browser/ash/login/test/login_manager_mixin.h"
#include "chrome/browser/ash/login/test/oobe_base_test.h"
#include "chrome/browser/ash/login/ui/login_display_host.h"
#include "chrome/browser/ash/login/wizard_context.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/input_method/candidate_window_view.h"
#include "chrome/browser/ui/ash/shelf/app_shortcut_shelf_item_controller.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/browser/ui/aura/accessibility/automation_manager_aura.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_types.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/user_manager/user_names.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/background_script_executor.h"
#include "extensions/browser/browsertest_util.h"
#include "extensions/browser/extension_host_test_helper.h"
#include "extensions/common/constants.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/base/ime/candidate_window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_features.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/base_event_utils.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/test/event_generator.h"
#include "ui/events/types/event_type.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/widget/widget.h"

namespace ash {

namespace {

const char* kChromeVoxPerformCommandMetric =
    "Accessibility.ChromeVox.PerformCommand";
const double kExpectedPhoneticSpeechAndHintDelayMS = 1000;

}  // namespace

LoggedInSpokenFeedbackTest::LoggedInSpokenFeedbackTest()
    : animation_mode_(ui::ScopedAnimationDurationScaleMode::ZERO_DURATION) {}

LoggedInSpokenFeedbackTest::~LoggedInSpokenFeedbackTest() = default;

void LoggedInSpokenFeedbackTest::SetUpInProcessBrowserTestFixture() {
  AccessibilityManager::SetBrailleControllerForTest(&braille_controller_);
  AccessibilityFeatureBrowserTest::SetUpInProcessBrowserTestFixture();
}

void LoggedInSpokenFeedbackTest::SetUpOnMainThread() {
  InProcessBrowserTest::SetUpOnMainThread();
  event_generator_ = std::make_unique<ui::test::EventGenerator>(
      Shell::Get()->GetPrimaryRootWindow());
  AccessibilityFeatureBrowserTest::SetUpOnMainThread();
}

void LoggedInSpokenFeedbackTest::TearDownOnMainThread() {
  event_generator_.reset();
  AccessibilityManager::SetBrailleControllerForTest(nullptr);
  // Unload the ChromeVox extension so the browser doesn't try to respond to
  // in-flight requests during test shutdown. https://crbug.com/923090
  AccessibilityManager::Get()->EnableSpokenFeedback(false);
  AutomationManagerAura::GetInstance()->Disable();
}

void LoggedInSpokenFeedbackTest::SendKeyPress(ui::KeyboardCode key) {
  ui::test::EmulateFullKeyPressReleaseSequence(
      event_generator_.get(), key,
      /*control=*/false, /*shift=*/false, /*alt=*/false, /*command=*/false);
}

void LoggedInSpokenFeedbackTest::SendKeyPressWithControl(ui::KeyboardCode key) {
  ui::test::EmulateFullKeyPressReleaseSequence(
      event_generator_.get(), key, /*control=*/true, /*shift=*/false,
      /*alt=*/false, /*command=*/false);
}

void LoggedInSpokenFeedbackTest::SendKeyPressWithControlAndAlt(
    ui::KeyboardCode key) {
  ui::test::EmulateFullKeyPressReleaseSequence(event_generator_.get(), key,
                                               /*control=*/true,
                                               /*shift=*/false,
                                               /*alt=*/true, /*command=*/false);
}

void LoggedInSpokenFeedbackTest::SendKeyPressWithControlAndShift(
    ui::KeyboardCode key) {
  ui::test::EmulateFullKeyPressReleaseSequence(event_generator_.get(), key,
                                               /*control=*/true, /*shift=*/true,
                                               /*alt=*/false,
                                               /*command=*/false);
}

void LoggedInSpokenFeedbackTest::SendKeyPressWithShift(ui::KeyboardCode key) {
  ui::test::EmulateFullKeyPressReleaseSequence(
      event_generator_.get(), key, /*control=*/false, /*shift=*/true,
      /*alt=*/false, /*command=*/false);
}

void LoggedInSpokenFeedbackTest::SendKeyPressWithAltAndShift(
    ui::KeyboardCode key) {
  ui::test::EmulateFullKeyPressReleaseSequence(
      event_generator_.get(), key, /*control=*/false, /*shift=*/true,
      /*alt=*/true, /*command=*/false);
}

void LoggedInSpokenFeedbackTest::SendKeyPressWithSearchAndShift(
    ui::KeyboardCode key) {
  ui::test::EmulateFullKeyPressReleaseSequence(event_generator_.get(), key,
                                               /*control=*/false,
                                               /*shift=*/true, /*alt=*/false,
                                               /*command=*/true);
}

void LoggedInSpokenFeedbackTest::SendKeyPressWithSearch(ui::KeyboardCode key) {
  ui::test::EmulateFullKeyPressReleaseSequence(
      event_generator_.get(), key, /*control=*/false, /*shift=*/false,
      /*alt=*/false, /*command=*/true);
}

void LoggedInSpokenFeedbackTest::SendKeyPressWithSearchAndControl(
    ui::KeyboardCode key) {
  ui::test::EmulateFullKeyPressReleaseSequence(event_generator_.get(), key,
                                               /*control=*/true,
                                               /*shift=*/false, /*alt=*/false,
                                               /*command=*/true);
}

void LoggedInSpokenFeedbackTest::SendKeyPressWithSearchAndControlAndShift(
    ui::KeyboardCode key) {
  ui::test::EmulateFullKeyPressReleaseSequence(event_generator_.get(), key,
                                               /*control=*/true, /*shift=*/true,
                                               /*alt=*/false, /*command=*/true);
}

void LoggedInSpokenFeedbackTest::SendStickyKeyCommand() {
  // To avoid flakes in sending keys, execute the command directly in js.
  ExecuteCommandHandlerCommand("toggleStickyMode");
}

void LoggedInSpokenFeedbackTest::SendMouseMoveTo(const gfx::Point& location) {
  event_generator_->MoveMouseTo(location.x(), location.y());
}

void LoggedInSpokenFeedbackTest::SetMouseSourceDeviceId(int id) {
  event_generator_->set_mouse_source_device_id(id);
}

bool LoggedInSpokenFeedbackTest::PerformAcceleratorAction(
    AcceleratorAction action) {
  return AcceleratorController::Get()->PerformActionIfEnabled(action, {});
}

void LoggedInSpokenFeedbackTest::RunJSForChromeVox(const std::string& script) {
  extensions::BackgroundScriptExecutor::ExecuteScriptAsync(
      GetProfile(), extension_misc::kChromeVoxExtensionId, script,
      extensions::browsertest_util::ScriptUserActivation::kDontActivate);
}

void LoggedInSpokenFeedbackTest::DisableEarcons() {
  // Playing earcons from within a test is not only annoying if you're
  // running the test locally, but seems to cause crashes
  // (http://crbug.com/396507). Work around this by just telling
  // ChromeVox to not ever play earcons (prerecorded sound effects).
  extensions::browsertest_util::ExecuteScriptInBackgroundPageNoWait(
      GetProfile(), extension_misc::kChromeVoxExtensionId,
      "ChromeVox.earcons.playEarcon = function() {};");
}

void LoggedInSpokenFeedbackTest::ImportJSModuleForChromeVox(std::string name,
                                                            std::string path) {
  extensions::browsertest_util::ExecuteScriptInBackgroundPageDeprecated(
      GetProfile(), extension_misc::kChromeVoxExtensionId,
      "import('" + path +
          "').then(mod => {"
          "globalThis." +
          name + " = mod." + name +
          ";"
          "window.domAutomationController.send('done')"
          "})");
}

void LoggedInSpokenFeedbackTest::EnableChromeVox(bool check_for_intro) {
  // Test setup.
  // Enable ChromeVox, disable earcons and wait for key mappings to be fetched.
  ASSERT_FALSE(AccessibilityManager::Get()->IsSpokenFeedbackEnabled());
  // TODO(accessibility): fix console error/warnings and insantiate
  // |console_observer_| here.

  // Load ChromeVox and block until it's fully loaded.
  AccessibilityManager::Get()->EnableSpokenFeedback(true);
  sm_.ExpectSpeechPattern(check_for_intro ? "ChromeVox spoken feedback is ready"
                                          : "*");
  sm_.Call([this]() {
    ImportJSModuleForChromeVox("ChromeVox",
                               "/chromevox/background/chromevox.js");
  });
  sm_.Call([this]() { DisableEarcons(); });
}

void LoggedInSpokenFeedbackTest::StablizeChromeVoxState() {
  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
        <button autofocus>Click me</button>)"));
  });
  sm_.ExpectSpeech("Click me");
}

void LoggedInSpokenFeedbackTest::ExecuteCommandHandlerCommand(
    std::string command) {
  ImportJSModuleForChromeVox(
      "CommandHandlerInterface",
      "/chromevox/background/input/command_handler_interface.js");
  RunJSForChromeVox("CommandHandlerInterface.instance.onCommand('" + command +
                    "');");
}

// Flaky test, crbug.com/1081563
IN_PROC_BROWSER_TEST_F(LoggedInSpokenFeedbackTest, DISABLED_AddBookmark) {
  EnableChromeVox();

  // Open the bookmarks bar.
  sm_.Call([this]() { SendKeyPressWithControlAndShift(ui::VKEY_B); });

  // Create a bookmark with title "foo".
  sm_.Call([this]() { SendKeyPressWithControl(ui::VKEY_D); });

  sm_.ExpectSpeech("Bookmark name");
  sm_.ExpectSpeech("about:blank");
  sm_.ExpectSpeech("selected");
  sm_.ExpectSpeech("Edit text");
  sm_.ExpectSpeech("Bookmark added");
  sm_.ExpectSpeech("Dialog");
  sm_.ExpectSpeech("Bookmark added, window");

  sm_.Call([this]() {
    SendKeyPress(ui::VKEY_F);
    SendKeyPress(ui::VKEY_O);
    SendKeyPress(ui::VKEY_O);
  });
  sm_.ExpectSpeech("F");
  sm_.ExpectSpeech("O");
  sm_.ExpectSpeech("O");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_TAB); });
  sm_.ExpectSpeech("Bookmark folder");
  sm_.ExpectSpeech("Bookmarks bar");
  sm_.ExpectSpeech("Button");
  sm_.ExpectSpeech("has pop up");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_TAB); });
  sm_.ExpectSpeech("More…");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_TAB); });
  sm_.ExpectSpeech("Remove");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_TAB); });
  sm_.ExpectSpeech("Done");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_RETURN); });
  // Focus goes back to window.
  sm_.ExpectSpeechPattern("about:blank*");

  // Focus bookmarks bar and listen for "foo".
  sm_.Call([this]() {
    // Focus bookmarks bar.
    SendKeyPressWithAltAndShift(ui::VKEY_B);
  });
  sm_.ExpectSpeech("foo");
  sm_.ExpectSpeech("Button");
  sm_.ExpectSpeech("Bookmarks");
  sm_.ExpectSpeech("Tool bar");
  sm_.Replay();
}

IN_PROC_BROWSER_TEST_F(LoggedInSpokenFeedbackTest, ChromeVoxSpeaksIntro) {
  EnableChromeVox(/*check_for_intro=*/false);
  sm_.ExpectSpeech("ChromeVox spoken feedback is ready");
  sm_.Replay();
  HistogramWaiter("Accessibility.ChromeVox.StartUpSpeechDelay").Wait();
}

// Test Learn Mode by pressing a few keys in Learn Mode. Only available while
// logged in.
IN_PROC_BROWSER_TEST_F(LoggedInSpokenFeedbackTest, LearnModeHardwareKeys) {
  EnableChromeVox();
  sm_.Call([this]() { ExecuteCommandHandlerCommand("showLearnModePage"); });
  sm_.ExpectSpeechPattern(
      "Press a qwerty key, refreshable braille key, or touch gesture to learn "
      "*");

  // These are the default top row keys and their descriptions which live in
  // ChromeVox.
  sm_.Call([this]() { SendKeyPress(ui::VKEY_F1); });
  sm_.ExpectSpeech("back");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_F2); });
  sm_.ExpectSpeech("forward");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_F3); });
  sm_.ExpectSpeech("refresh");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_F4); });
  sm_.ExpectSpeech("toggle full screen");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_F5); });
  sm_.ExpectSpeech("show windows");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_F6); });
  sm_.ExpectSpeech("Brightness down");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_F7); });
  sm_.ExpectSpeech("Brightness up");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_F8); });
  sm_.ExpectSpeech("volume mute");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_F9); });
  sm_.ExpectSpeech("volume down");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_F10); });
  sm_.ExpectSpeech("volume up");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_F(LoggedInSpokenFeedbackTest, LearnModeEscapeWithGesture) {
  EnableChromeVox();
  sm_.Call([this]() { ExecuteCommandHandlerCommand("showLearnModePage"); });
  sm_.ExpectSpeechPattern(
      "Press a qwerty key, refreshable braille key, or touch gesture to learn "
      "*");

  sm_.Call([]() {
    AccessibilityManager::Get()->HandleAccessibilityGesture(
        ax::mojom::Gesture::kSwipeLeft2, gfx::PointF());
  });
  sm_.ExpectSpeech("Swipe two fingers left");
  sm_.ExpectSpeech("Escape");
  sm_.ExpectSpeech("Stopping Learn Mode");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_F(LoggedInSpokenFeedbackTest,
                       LearnModePressEscapeTwiceToExit) {
  EnableChromeVox();
  sm_.Call([this]() { ExecuteCommandHandlerCommand("showLearnModePage"); });
  sm_.ExpectSpeechPattern(
      "Press a qwerty key, refreshable braille key, or touch gesture to learn "
      "*");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_ESCAPE); });
  sm_.ExpectSpeech("Escape");
  sm_.ExpectSpeech("Press escape again to exit Learn Mode");

  // Pressing a different key means the next escape key will not exit learn
  // mode, it has to be pressed twice in a row.
  sm_.Call([this]() { SendKeyPress(ui::VKEY_K); });
  sm_.ExpectSpeech("K");

  // Press escape again, it warns about exiting again.
  sm_.Call([this]() { SendKeyPress(ui::VKEY_ESCAPE); });
  sm_.ExpectSpeech("Escape");
  sm_.ExpectSpeech("Press escape again to exit Learn Mode");

  // Press it a second time in a row, should actually exit.
  sm_.Call([this]() { SendKeyPress(ui::VKEY_ESCAPE); });
  sm_.ExpectSpeech("Escape");
  sm_.ExpectSpeech("Stopping Learn Mode");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_F(LoggedInSpokenFeedbackTest, OpenLogPage) {
  // Enabling earcon logging should not crash ChromeVox at startup
  // (see b/318531241).
  AccessibilityManager::Get()->profile()->GetPrefs()->SetBoolean(
      prefs::kAccessibilityChromeVoxEnableEarconLogging, true);
  EnableChromeVox();
  StablizeChromeVoxState();

  // Open the log page.
  sm_.Call([this]() {
    SendKeyPressWithSearch(ui::VKEY_O);
    SendKeyPress(ui::VKEY_W);
  });
  sm_.ExpectSpeech("chromevox-log");
  sm_.Replay();
}

IN_PROC_BROWSER_TEST_F(LoggedInSpokenFeedbackTest,
                       CheckChromeVoxPerformCommandMetric) {
  EnableChromeVox(/*check_for_intro=*/false);
  base::HistogramTester histogram_tester;

  // Command.ANNOUNCE_BATTERY_DESCRIPTION
  sm_.Call(
      [this]() { ExecuteCommandHandlerCommand("announceBatteryDescription"); });
  sm_.ExpectSpeechPattern("*");
  // Command.NEXT_OBJECT
  sm_.Call([this]() { ExecuteCommandHandlerCommand("nextObject"); });
  sm_.ExpectSpeechPattern("*");
  // Command.DECREASE_TTS_RATE
  sm_.Call([this]() { ExecuteCommandHandlerCommand("decreaseTtsRate"); });
  sm_.ExpectSpeechPattern("*");
  // Command.NEXT_BUTTON
  sm_.Call([this]() { ExecuteCommandHandlerCommand("nextButton"); });
  sm_.ExpectSpeechPattern("*");
  // Command.HELP
  sm_.Call([this]() { ExecuteCommandHandlerCommand("help"); });
  sm_.ExpectSpeechPattern("*");
  sm_.Replay();

  histogram_tester.ExpectBucketCount(
      kChromeVoxPerformCommandMetric,
      0 /*ChromeVoxCommand.ANNOUNCE_BATTERY_DESCRIPTION*/, 1);
  histogram_tester.ExpectBucketCount(kChromeVoxPerformCommandMetric,
                                     81 /*ChromeVoxCommand.NEXT_OBJECT*/, 1);
  histogram_tester.ExpectBucketCount(kChromeVoxPerformCommandMetric,
                                     12 /*ChromeVoxCommand.DECREASE_TTS_RATE*/,
                                     1);
  histogram_tester.ExpectBucketCount(kChromeVoxPerformCommandMetric,
                                     55 /*ChromeVoxCommand.NEXT_BUTTON*/, 1);
  histogram_tester.ExpectBucketCount(kChromeVoxPerformCommandMetric,
                                     36 /*ChromeVoxCommand.HELP*/, 1);
  histogram_tester.ExpectTotalCount(kChromeVoxPerformCommandMetric, 5);
}

class NotificationCenterSpokenFeedbackTest : public LoggedInSpokenFeedbackTest {
 protected:
  NotificationCenterSpokenFeedbackTest() = default;
  ~NotificationCenterSpokenFeedbackTest() override = default;

  NotificationCenterTestApi* test_api() {
    if (!test_api_) {
      test_api_ = std::make_unique<NotificationCenterTestApi>();
    }
    return test_api_.get();
  }

 private:
  std::unique_ptr<NotificationCenterTestApi> test_api_;
};

// Tests the spoken feedback text when using the notification center accelerator
// to navigate to the notification center.
IN_PROC_BROWSER_TEST_F(NotificationCenterSpokenFeedbackTest,
                       NavigateNotificationCenter) {
  EnableChromeVox();

  // Add a notification so that the notification center tray is visible.
  test_api()->AddNotification();
  ASSERT_TRUE(test_api()->IsTrayShown());

  // Press the accelerator that toggles the notification center.
  sm_.Call([this]() {
    EXPECT_TRUE(PerformAcceleratorAction(
        AcceleratorAction::kToggleMessageCenterBubble));
  });

  // Verify the spoken feedback text.
  sm_.ExpectSpeech("Notification Center");
  sm_.Replay();
}

// Tests that clicking the notification center tray does not crash when spoken
// feedback is enabled.
IN_PROC_BROWSER_TEST_F(NotificationCenterSpokenFeedbackTest, OpenBubble) {
  // Enable spoken feedback and add a notification to ensure the tray is
  // visible.
  EnableChromeVox();
  test_api()->AddNotification();
  ASSERT_TRUE(test_api()->IsTrayShown());

  // Click on the tray and verify the bubble shows up.
  sm_.Call([this]() {
    test_api()->ToggleBubble();
    EXPECT_TRUE(test_api()->GetWidget()->IsActive());
    EXPECT_TRUE(test_api()->IsBubbleShown());
  });
  sm_.ExpectSpeech("Notification Center");

  sm_.Replay();
}

// Tests that an incoming silent notification (i.e. a notification that goes
// straight to the notification center without generating a popup) generates an
// accessibility announcement.
IN_PROC_BROWSER_TEST_F(NotificationCenterSpokenFeedbackTest,
                       SilentNotification) {
  // Enable spoken feedback.
  EnableChromeVox();

  // Add a silent notification while the notification center is not showing.
  sm_.Call([this]() {
    ASSERT_FALSE(
        message_center::MessageCenter::Get()->IsMessageCenterVisible());
    test_api()->AddLowPriorityNotification();
  });

  // Verify that the silent notification was announced.
  auto expected_announcement = l10n_util::GetStringFUTF16Int(
      IDS_ASH_MESSAGE_CENTER_SILENT_NOTIFICATION_ANNOUNCEMENT, 1);
  sm_.ExpectSpeech(base::UTF16ToUTF8(expected_announcement));

  // Add another silent notification, this time while the notification center is
  // showing.
  sm_.Call([this]() {
    test_api()->ToggleBubble();
    ASSERT_TRUE(message_center::MessageCenter::Get()->IsMessageCenterVisible());
    test_api()->AddLowPriorityNotification();
  });

  // Verify that the silent notification was announced.
  expected_announcement = l10n_util::GetStringFUTF16Int(
      IDS_ASH_MESSAGE_CENTER_SILENT_NOTIFICATION_ANNOUNCEMENT, 2);
  sm_.ExpectSpeech(base::UTF16ToUTF8(expected_announcement));

  sm_.Replay();
}

//
// Spoken feedback tests in both a logged in browser window and guest mode.
//

enum SpokenFeedbackTestVariant { kTestAsNormalUser, kTestAsGuestUser };

class SpokenFeedbackTest
    : public LoggedInSpokenFeedbackTest,
      public ::testing::WithParamInterface<SpokenFeedbackTestVariant> {
 protected:
  SpokenFeedbackTest() {}
  virtual ~SpokenFeedbackTest() {}

  void SetUpCommandLine(base::CommandLine* command_line) override {
    if (GetParam() == kTestAsGuestUser) {
      command_line->AppendSwitch(switches::kGuestSession);
      command_line->AppendSwitch(::switches::kIncognito);
      command_line->AppendSwitchASCII(switches::kLoginProfile, "user");
      command_line->AppendSwitchASCII(
          switches::kLoginUser, user_manager::GuestAccountId().GetUserEmail());
    }
  }
};

INSTANTIATE_TEST_SUITE_P(TestAsNormalAndGuestUser,
                         SpokenFeedbackTest,
                         ::testing::Values(kTestAsNormalUser,
                                           kTestAsGuestUser));

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, EnableSpokenFeedback) {
  EnableChromeVox();
  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, FocusToolbar) {
  EnableChromeVox();
  // Wait for the browser to show up.
  // In Ash, this results in a navigation, while in Lacros, this creates
  // a new window. Thus the former has a "back" button while the latter
  // does not, and focus will jump directly to the "reload" button.
  StablizeChromeVoxState();
  sm_.Call([this]() { SendKeyPressWithAltAndShift(ui::VKEY_T); });
  if (IsLacrosRunning()) {
    // The reload button should become focused.
    sm_.ExpectSpeech("Reload");
  } else {
    // The back button should become focused.
    sm_.ExpectSpeech("Back");
  }
  sm_.Replay();
}

// TODO(crbug.com/1065235): flaky.
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, DISABLED_TypeInOmnibox) {
  EnableChromeVox();

  sm_.Call([this]() {
    NavigateToUrl(GURL("data:text/html;charset=utf-8,<p>unused</p>"));
  });

  sm_.Call([this]() { SendKeyPressWithControl(ui::VKEY_L); });
  sm_.ExpectSpeech("Address and search bar");

  sm_.Call([this]() {
    // Select all the text.
    SendKeyPressWithControl(ui::VKEY_A);

    // Type x, y, and z.
    SendKeyPress(ui::VKEY_X);
    SendKeyPress(ui::VKEY_Y);
    SendKeyPress(ui::VKEY_Z);
  });
  sm_.ExpectSpeech("X");
  sm_.ExpectSpeech("Y");
  sm_.ExpectSpeech("Z");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_BACK); });
  sm_.ExpectSpeech("Z");

  // Auto completions.
  sm_.ExpectSpeech("xy search");
  sm_.ExpectSpeech("List item");
  sm_.ExpectSpeech("1 of 1");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, FocusShelf) {
  EnableChromeVox();

  sm_.Call([this]() {
    EXPECT_TRUE(PerformAcceleratorAction(AcceleratorAction::kFocusShelf));
  });
  sm_.ExpectSpeechPattern("Launcher");
  sm_.ExpectSpeech("Button");
  sm_.ExpectSpeech("Shelf");
  sm_.ExpectSpeech("Tool bar");
  sm_.ExpectSpeech(", window");
  sm_.ExpectSpeech("Press Search plus Space to activate");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_TAB); });
  sm_.ExpectSpeechPattern("Button");
  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, SelectChromeVoxMenuItem) {
  EnableChromeVox();

  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_OEM_PERIOD); });
  sm_.ExpectSpeech("ChromeVox Panel");
  sm_.ExpectSpeech("Search");
  sm_.Call([this]() {
    SendKeyPress(ui::VKEY_RIGHT);
    SendKeyPress(ui::VKEY_RIGHT);
  });
  sm_.ExpectSpeech("Speech");
  sm_.ExpectSpeech("Announce Current Battery Status");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_RETURN); });
  sm_.ExpectSpeechPattern("Battery at* percent*");

  sm_.Replay();
}

// Verifies that pressing right arrow button with search button should move
// focus to the next ShelfItem instead of the last one
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, ShelfIconFocusForward) {
  const std::string title("MockApp");
  ChromeShelfController* controller = ChromeShelfController::instance();

  // Add the ShelfItem to the ShelfModel after enabling the ChromeVox. Because
  // when an extension is enabled, the ShelfItems which are not recorded as
  // pinned apps in user preference will be removed.
  EnableChromeVox();
  sm_.Call([controller, title]() {
    controller->CreateAppItem(
        std::make_unique<AppShortcutShelfItemController>(ShelfID("FakeApp")),
        STATUS_CLOSED, /*pinned=*/true, base::ASCIIToUTF16(title));
  });

  // Focus on the shelf.
  sm_.Call(
      [this]() { PerformAcceleratorAction(AcceleratorAction::kFocusShelf); });
  sm_.ExpectSpeech("Launcher");
  sm_.ExpectSpeech("Button");
  sm_.ExpectSpeech("Shelf");
  sm_.ExpectSpeech("Tool bar");

  // Verifies that pressing right key with search key should move the focus of
  // ShelfItem correctly.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  // Chromium or Google Chrome button here (not being tested).
  sm_.ExpectSpeech("Button");
  sm_.ExpectSpeech("Shelf");
  sm_.ExpectSpeech("Tool bar");

  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.ExpectSpeech("MockApp");
  sm_.ExpectSpeech("Button");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, NavigateSpeechMenu) {
  EnableChromeVox();
  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
        <a href="https://google.com" autofocus>Link to Google</a>
        <p>Text after link</p>)"));
  });
  sm_.ExpectSpeech("Link to Google");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_OEM_PERIOD); });
  sm_.ExpectSpeech("Search the menus");
  sm_.Call([this]() {
    SendKeyPress(ui::VKEY_RIGHT);
    SendKeyPress(ui::VKEY_RIGHT);
  });
  sm_.ExpectSpeech("Speech Menu");
  sm_.ExpectSpeech("Announce Current Battery Status");
  sm_.ExpectSpeechPattern("Menu item 1 of *");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeechPattern("Menu item 2 of *");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeechPattern("Menu item 3 of *");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeechPattern("Menu item 4 of *");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeechPattern("Menu item 5 of *");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeechPattern("Menu item 6 of *");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_UP); });
  sm_.ExpectSpeech("Announce The URL Behind A Link");
  sm_.ExpectSpeechPattern("Menu item 5 of *");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_SPACE); });
  sm_.ExpectSpeech("Link URL: https colon slash slash google.com slash");

  // Verify that the menu has closed and we are back in the web contents.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.ExpectSpeech("Text after link");

  sm_.Replay();
}

// TODO(https://crbug.com/1486666): Fix flakiness
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, DISABLED_OpenContextMenu) {
  EnableChromeVox();
  StablizeChromeVoxState();
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_M); });
  sm_.ExpectSpeech("menu opened");
  // Close the menu
  sm_.Call([this]() { SendKeyPress(ui::VKEY_ESCAPE); });
  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
            <button autofocus>I'm a button</button>)"));
  });
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_M); });
  sm_.ExpectSpeech("menu opened");

  sm_.Replay();
}

// Verifies that speaking text under mouse works for Shelf button and voice
// announcements should not be stacked when mouse goes over many Shelf buttons
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, SpeakingTextUnderMouseForShelfItem) {
  SetMouseSourceDeviceId(1);
  AccessibilityManager::Get()->profile()->GetPrefs()->SetBoolean(
      prefs::kAccessibilityChromeVoxSpeakTextUnderMouse, true);
  // Add the ShelfItem to the ShelfModel after enabling the ChromeVox. Because
  // when an extension is enabled, the ShelfItems which are not recorded as
  // pinned apps in user preference will be removed.
  EnableChromeVox();

  sm_.Call([this]() {
    // Add three Shelf buttons. Wait for the change on ShelfModel to reach ash.
    ChromeShelfController* controller = ChromeShelfController::instance();
    const std::string title("MockApp");
    const std::string id("FakeApp");
    const int insert_app_num = 3;
    for (int i = 0; i < insert_app_num; i++) {
      std::string app_title = title + base::NumberToString(i);
      std::string app_id = id + base::NumberToString(i);
      controller->CreateAppItem(
          std::make_unique<AppShortcutShelfItemController>(ShelfID(app_id)),
          STATUS_CLOSED, /*pinned=*/true, base::ASCIIToUTF16(app_title));
    }

    // Focus on the Shelf because voice text for focusing on Shelf is fixed.
    // Wait until voice announcements are finished.
    EXPECT_TRUE(PerformAcceleratorAction(AcceleratorAction::kFocusShelf));
  });
  sm_.ExpectSpeechPattern("Launcher");

  // Hover mouse on the Shelf button. Verifies that text under mouse is spoken.
  sm_.Call([this]() {
    ShelfView* shelf_view =
        Shelf::ForWindow(Shell::Get()->GetPrimaryRootWindow())
            ->shelf_widget()
            ->shelf_view_for_testing();
    const int first_app_index = shelf_view->model()->GetItemIndexForType(
        ShelfItemType::TYPE_PINNED_APP);
    SendMouseMoveTo(shelf_view->view_model()
                        ->view_at(first_app_index)
                        ->GetBoundsInScreen()
                        .CenterPoint());
  });

  sm_.ExpectSpeechPattern("MockApp*");
  sm_.ExpectSpeech("Button");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, SpeakingTextUnderSynthesizedMouse) {
  AccessibilityManager::Get()->profile()->GetPrefs()->SetBoolean(
      prefs::kAccessibilityChromeVoxSpeakTextUnderMouse, true);

  EnableChromeVox();

  AutomationTestUtils test_utils(extension_misc::kChromeVoxExtensionId);
  sm_.Call([&test_utils]() {
    test_utils.SetUpTestSupport();
    // Enable the function of speaking text under mouse.
    EventRewriterController::Get()->SetSendMouseEvents(true);
  });

  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
            <button id="b1" autofocus>First</button>
            <button id="b2">Second</button>
            <button id="b3">Third</button>
        )"));
  });
  sm_.ExpectSpeech("First");
  sm_.Call([this, &test_utils]() {
    gfx::Rect b2_bounds = test_utils.GetNodeBoundsInRoot("Second", "button");
    SendMouseMoveTo(b2_bounds.CenterPoint());
  });
  sm_.ExpectSpeech("Second");
  sm_.Call([this, &test_utils]() {
    gfx::Rect b3_bounds = test_utils.GetNodeBoundsInRoot("Third", "button");
    SendMouseMoveTo(b3_bounds.CenterPoint());
  });
  sm_.ExpectSpeech("Third");

  sm_.Replay();
}

// Verifies that an announcement is triggered when focusing a ShelfItem with a
// notification badge shown.
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, ShelfNotificationBadgeAnnouncement) {
  EnableChromeVox();

  // Create and add a test app to the shelf model.
  ShelfItem item;
  item.id = ShelfID("TestApp");
  item.title = u"TestAppTitle";
  item.type = ShelfItemType::TYPE_APP;
  ShelfModel::Get()->Add(item,
                         std::make_unique<TestShelfItemDelegate>(item.id));

  // Set the notification badge to be shown for the test app.
  ShelfModel::Get()->UpdateItemNotification("TestApp", /*has_badge=*/true);

  // Focus on the shelf.
  sm_.Call(
      [this]() { PerformAcceleratorAction(AcceleratorAction::kFocusShelf); });
  sm_.ExpectSpeech("Launcher");
  sm_.ExpectSpeech("Button");
  sm_.ExpectSpeech("Shelf");
  sm_.ExpectSpeech("Tool bar");

  // Press right key twice to focus the test app.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.ExpectSpeech("TestAppTitle");
  sm_.ExpectSpeech("Button");

  // Check that when a shelf app button with a notification badge is focused,
  // the correct announcement occurs.
  sm_.ExpectSpeech("TestAppTitle requests your attention.");

  sm_.Replay();
}

// Verifies that an announcement is triggered when focusing a paused app
// ShelfItem.
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest,
                       ShelfPausedAppIconBadgeAnnouncement) {
  EnableChromeVox();

  std::string app_id = "TestApp";

  // Set the app status as paused;
  apps::AppPtr app =
      std::make_unique<apps::App>(apps::AppType::kBuiltIn, app_id);
  app->readiness = apps::Readiness::kReady;
  app->paused = true;

  std::vector<apps::AppPtr> apps;
  apps.push_back(std::move(app));
  apps::AppServiceProxyFactory::GetForProfile(GetProfile())
      ->OnApps(std::move(apps), apps::AppType::kBuiltIn,
               false /* should_notify_initialized */);

  // Create and add a test app to the shelf model.
  ShelfItem item;
  item.id = ShelfID(app_id);
  item.title = u"TestAppTitle";
  item.type = ShelfItemType::TYPE_APP;
  item.app_status = AppStatus::kPaused;
  ShelfModel::Get()->Add(item,
                         std::make_unique<TestShelfItemDelegate>(item.id));

  // Focus on the shelf.
  sm_.Call(
      [this]() { PerformAcceleratorAction(AcceleratorAction::kFocusShelf); });
  sm_.ExpectSpeech("Launcher");
  sm_.ExpectSpeech("Button");
  sm_.ExpectSpeech("Shelf");
  sm_.ExpectSpeech("Tool bar");

  // Press right key twice to focus the test app.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.ExpectSpeech("TestAppTitle");
  sm_.ExpectSpeech("Button");

  // Check that when a paused app shelf item is focused, the correct
  // announcement occurs.
  sm_.ExpectSpeech("Paused");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, ShowHeadingList) {
  EnableChromeVox();

  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
        <h1>Page Title</h1>
        <h2>First Section</h2>
        <h3>Sub-category</h3>
        <p>Text</p>
        <h3>Second sub-category<h3>
        <button autofocus>Next page</button>)"));
  });
  sm_.ExpectSpeech("Next page");
  sm_.Call([this]() { SendKeyPressWithSearchAndControl(ui::VKEY_H); });
  sm_.ExpectSpeech("Heading Menu");
  sm_.ExpectSpeechPattern("Page Title Heading 1 Menu item 1 of *");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeechPattern("First Section Heading 2 Menu item 2 of *");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeechPattern("Sub-category Heading 3 Menu item 3 of *");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeechPattern("Second sub-category Heading 3 Menu item 4 of *");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_UP); });
  sm_.ExpectSpeechPattern("Sub-category Heading 3 Menu item 3 of *");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_SPACE); });
  sm_.ExpectSpeech("Sub-category");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Sub-category");
  if (IsLacrosRunning()) {
    // With Lacros, it takes one more search+down to get out of the sub-category
    // after having been in the heading menu.
    sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_DOWN); });
    sm_.ExpectSpeech("Sub-category");
  }
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Text");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Second sub-category");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Next page Button");

  sm_.Replay();
}

// Verifies that an announcement is triggered when focusing a blocked app
// ShelfItem.
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest,
                       ShelfBlockedAppIconBadgeAnnouncement) {
  EnableChromeVox();

  std::string app_id = "TestApp";

  // Set the app status as paused;
  apps::AppPtr app =
      std::make_unique<apps::App>(apps::AppType::kBuiltIn, app_id);
  app->readiness = apps::Readiness::kDisabledByPolicy;
  std::vector<apps::AppPtr> apps;
  apps.push_back(std::move(app));
  apps::AppServiceProxyFactory::GetForProfile(GetProfile())
      ->OnApps(std::move(apps), apps::AppType::kBuiltIn,
               false /* should_notify_initialized */);

  // Create and add a test app to the shelf model.
  ShelfItem item;
  item.id = ShelfID(app_id);
  item.title = u"TestAppTitle";
  item.type = ShelfItemType::TYPE_APP;
  item.app_status = AppStatus::kBlocked;
  ShelfModel::Get()->Add(item,
                         std::make_unique<TestShelfItemDelegate>(item.id));

  // Focus on the shelf.
  sm_.Call(
      [this]() { PerformAcceleratorAction(AcceleratorAction::kFocusShelf); });
  sm_.ExpectSpeech("Launcher");
  sm_.ExpectSpeech("Button");
  sm_.ExpectSpeech("Shelf");
  sm_.ExpectSpeech("Tool bar");

  // Press right key twice to focus the test app.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.ExpectSpeech("TestAppTitle");
  sm_.ExpectSpeech("Button");

  // Check that when a blocked shelf app shelf item is focused, the correct
  // announcement occurs.
  sm_.ExpectSpeech("Blocked");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, NavigateChromeVoxMenu) {
  EnableChromeVox();
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_OEM_PERIOD); });
  sm_.ExpectSpeech("Search the menus");
  sm_.Call([this]() {
    SendKeyPress(ui::VKEY_RIGHT);
    SendKeyPress(ui::VKEY_RIGHT);
    SendKeyPress(ui::VKEY_RIGHT);
  });
  sm_.ExpectSpeech("ChromeVox Menu");
  sm_.Call([this]() {
    SendKeyPress(ui::VKEY_DOWN);
    SendKeyPress(ui::VKEY_DOWN);
  });
  sm_.ExpectSpeech("Open ChromeVox Tutorial");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_SPACE); });
  sm_.ExpectSpeech("ChromeVox tutorial");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, OpenStatusTray) {
  EnableChromeVox();

  sm_.Call([this]() {
    EXPECT_TRUE(
        PerformAcceleratorAction(AcceleratorAction::kToggleSystemTrayBubble));
  });
  sm_.ExpectSpeech("Quick Settings");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, OpenSettingsFromPanel) {
  EnableChromeVox();
  base::RunLoop waiter;
  AccessibilityManager::Get()->SetOpenSettingsSubpageObserverForTest(
      base::BindLambdaForTesting([&waiter]() { waiter.Quit(); }));

  // Find the settings button in the panel.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_OEM_PERIOD); });
  sm_.ExpectSpeech("Search the menus");
  sm_.Call([this]() {
    SendKeyPress(ui::VKEY_TAB);
    SendKeyPress(ui::VKEY_TAB);
  });
  sm_.ExpectSpeech("ChromeVox Menus collapse");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_TAB); });
  sm_.ExpectSpeech("ChromeVox Options");
  // Activate the settings button.
  sm_.Call([this]() { SendKeyPress(ui::VKEY_SPACE); });
  sm_.Replay();
  // We should have tried to open the settings subpage.
  waiter.Run();
}

// Fails on ASAN. See http://crbug.com/776308 . (Note MAYBE_ doesn't work well
// with parameterized tests).
#if !defined(ADDRESS_SANITIZER)
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, NavigateSystemTray) {
  EnableChromeVox();

  sm_.Call([this]() {
    (PerformAcceleratorAction(AcceleratorAction::kToggleSystemTrayBubble));
  });

    sm_.ExpectSpeech("Quick Settings");
    // Settings button.
    sm_.Call([this]() { SendKeyPressWithShift(ui::VKEY_TAB); });
    sm_.ExpectSpeech("Settings");
    sm_.ExpectSpeech("Button");

    // Battery indicator.
    sm_.Call([this]() { SendKeyPressWithShift(ui::VKEY_TAB); });
    sm_.ExpectSpeech("Battery");

    // Guest mode sign out button.
    if (GetParam() == kTestAsGuestUser) {
      sm_.Call([this]() { SendKeyPressWithShift(ui::VKEY_TAB); });
      sm_.ExpectSpeech("Exit guest");
    }

    // Shutdown button.
    sm_.Call([this]() { SendKeyPressWithShift(ui::VKEY_TAB); });
    sm_.ExpectSpeech("Power menu");
    sm_.ExpectSpeech("Button");

    sm_.Replay();
}
#endif  // !defined(ADDRESS_SANITIZER)

// TODO: these brightness announcements are actually not made.
// https://crbug.com/1064788
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, DISABLED_ScreenBrightness) {
  EnableChromeVox();

  sm_.Call([this]() {
    (PerformAcceleratorAction(AcceleratorAction::kBrightnessUp));
  });
  sm_.ExpectSpeechPattern("Brightness * percent");

  sm_.Call([this]() {
    (PerformAcceleratorAction(AcceleratorAction::kBrightnessDown));
  });
  sm_.ExpectSpeechPattern("Brightness * percent");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, VolumeSlider) {
  EnableChromeVox();

  sm_.Call([this]() {
    // Volume slider does not fire valueChanged event on first key press because
    // it has no widget.
    PerformAcceleratorAction(AcceleratorAction::kVolumeUp);
    PerformAcceleratorAction(AcceleratorAction::kVolumeUp);
  });
  sm_.ExpectSpeechPattern("* percent*");
  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, LandmarkNavigation) {
  ui::KeyboardCode semicolon = ui::VKEY_OEM_1;

  EnableChromeVox();
  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
        <button autofocus>Start here</button>
        <p>before first landmark</p>
        <div role="application">application</div>
        <p>after application</p>
        <div role="banner">banner</div>
        <p>after banner</p>
        <div role="complementary">complementary</div>
        <p>after complementary</p>
        <form aria-label="form"></form>
        <button>after form</button>
        <div role="main">main</div>
        <h2>after main</h2>
        <nav>navigation</nav>
        <img alt="after navigation"></img>
        <input type="text" role="search" id="search"></input>
        <label for="search">search</label>
        <p>after search</p>)"));
  });
  sm_.ExpectSpeech("Start here");
  sm_.Call([this, semicolon]() { SendKeyPressWithSearch(semicolon); });
  sm_.ExpectSpeech("application");
  sm_.Call([this, semicolon]() { SendKeyPressWithSearch(semicolon); });
  sm_.ExpectSpeech("banner");
  sm_.Call([this, semicolon]() { SendKeyPressWithSearch(semicolon); });
  sm_.ExpectSpeech("complementary");
  sm_.Call([this, semicolon]() { SendKeyPressWithSearch(semicolon); });
  sm_.ExpectSpeech("form");
  sm_.Call([this, semicolon]() { SendKeyPressWithSearch(semicolon); });
  sm_.ExpectSpeech("main");
  sm_.Call([this, semicolon]() { SendKeyPressWithSearch(semicolon); });
  sm_.ExpectSpeech("navigation");
  sm_.Call([this, semicolon]() { SendKeyPressWithSearch(semicolon); });
  sm_.ExpectSpeech("search");
  sm_.Call([this, semicolon]() { SendKeyPressWithSearchAndShift(semicolon); });
  sm_.ExpectSpeech("navigation");
  sm_.Call([this, semicolon]() { SendKeyPressWithSearchAndShift(semicolon); });
  sm_.ExpectSpeech("main");

  // Navigate the landmark list.
  sm_.Call(
      [this, semicolon]() { SendKeyPressWithSearchAndControl(semicolon); });
  sm_.ExpectSpeech("Landmark Menu");
  sm_.ExpectSpeech("Application Menu item 1 of 7");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Banner Menu item 2 of 7");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Complementary Menu item 3 of 7");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Form Menu item 4 of 7");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Main Menu item 5 of 7");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Navigation Menu item 6 of 7");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Search Menu item 7 of 7");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_UP); });
  sm_.ExpectSpeech("Navigation Menu item 6 of 7");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_SPACE); });
  sm_.ExpectSpeech("Navigation");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_UP); });
  sm_.ExpectSpeech("after main");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, OverviewMode) {
  EnableChromeVox();
  StablizeChromeVoxState();

  sm_.Call([this]() {
    (PerformAcceleratorAction(AcceleratorAction::kToggleOverview));
  });

  sm_.ExpectSpeech(
      "Entered window overview mode. Swipe to navigate, or press tab if using "
      "a keyboard.");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_TAB); });
  sm_.ExpectSpeechPattern(
      "*data:text slash html;charset equal utf-8, percent 0A less than "
      "button autofocus greater than Click me less than slash button greater "
      "than");
  sm_.ExpectSpeechPattern("Press Ctrl plus W to close.");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, NextGraphic) {
  EnableChromeVox();
  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
             <button autofocus>Start here</button>
             <p>before the image</p>
             <img src="cat.png" alt="A cat curled up on the couch">
             <p>between the images</p>
             <img src="dog.png" alt="A happy dog holding a stick in its mouth">
             <p>after the images</p>)"));
  });
  sm_.ExpectSpeech("Start here");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_G); });
  sm_.ExpectSpeech("A cat curled up on the couch");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_G); });
  sm_.ExpectSpeech("A happy dog holding a stick in its mouth");
  sm_.Call([this]() { SendKeyPressWithSearchAndShift(ui::VKEY_G); });
  sm_.ExpectSpeech("A cat curled up on the couch");
  sm_.Replay();
}

// TODO(crbug.com/40831399): Re-enable this test
// Verify that enable chromeVox won't end overview.
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest,
                       DISABLED_EnableChromeVoxOnOverviewMode) {
  StablizeChromeVoxState();

  sm_.Call([this]() {
    (PerformAcceleratorAction(AcceleratorAction::kToggleOverview));
  });

  EnableChromeVox();
  // Wait for Chromevox to start while in Overview before `sm_.Call`, which
  // pushes a callback when the last expected speech was seen.
  sm_.ExpectSpeechPattern(", window");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_TAB); });
  sm_.ExpectSpeechPattern(
      "Chrom* - data:text slash html;charset equal utf-8, less than button "
      "autofocus greater than Click me less than slash button greater than");

  sm_.Replay();
}

// TODO(crbug.com/40845611): Flaky on Linux ChromiumOS MSan.
#if defined(MEMORY_SANITIZER)
#define MAYBE_ChromeVoxFindInPage DISABLED_ChromeVoxFindInPage
#else
#define MAYBE_ChromeVoxFindInPage ChromeVoxFindInPage
#endif

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, MAYBE_ChromeVoxFindInPage) {
  EnableChromeVox();
  StablizeChromeVoxState();

  // Press Search+/ to enter ChromeVox's "find in page".
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_OEM_2); });
  sm_.ExpectSpeech("Find in page");
  sm_.ExpectSpeech("Search");
  sm_.ExpectSpeech(
      "Type to search the page. Press enter to jump to the result, up or down "
      "arrows to browse results, keep typing to change your search, or escape "
      "to cancel.");

  sm_.Replay();
}

// TODO(crbug.com/40748296) Re-enable test
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest,
                       DISABLED_ChromeVoxNavigateAndSelect) {
  EnableChromeVox();

  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
            <h1>Title</h1>
            <button autofocus>Click me</button>)"));
  });
  sm_.ExpectSpeech("Click me");

  // Press Search+Left to navigate to the previous item.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_LEFT); });
  sm_.ExpectSpeech("Title");
  sm_.ExpectSpeech("Heading 1");

  // Press Search+S to select the text.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_S); });
  sm_.ExpectSpeech("Title");
  sm_.ExpectSpeech("selected");

  // Press again to end the selection.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_S); });
  sm_.ExpectSpeech("End selection");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, ChromeVoxStickyMode) {
  EnableChromeVox();

  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
            <button autofocus>Click me</button>)"));
  });
  sm_.ExpectSpeech("Click me");

  // Press the sticky-key sequence: Search Search.
  sm_.Call([this]() { SendStickyKeyCommand(); });

  sm_.ExpectSpeech("Sticky mode enabled");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_H); });
  sm_.ExpectSpeech("No next heading");

  sm_.Call([this]() { SendStickyKeyCommand(); });
  sm_.ExpectSpeech("Sticky mode disabled");

  sm_.Replay();
}

// This tests ChromeVox sticky mode using raw key events as opposed to directly
// sending js commands above. This variant may be subject to flakes as it
// depends on more of the UI events stack and sticky mode invocation has a
// timing element to it.
// Consistently failing on ChromiumOS MSan and ASan. http://crbug.com/1182542
#if defined(MEMORY_SANITIZER) || defined(ADDRESS_SANITIZER)
#define MAYBE_ChromeVoxStickyModeRawKeys DISABLED_ChromeVoxStickyModeRawKeys
#else
#define MAYBE_ChromeVoxStickyModeRawKeys ChromeVoxStickyModeRawKeys
#endif
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, MAYBE_ChromeVoxStickyModeRawKeys) {
  EnableChromeVox();
  StablizeChromeVoxState();

  sm_.Call([this]() {
    SendKeyPress(ui::VKEY_LWIN);
    SendKeyPress(ui::VKEY_LWIN);
  });
  sm_.ExpectSpeech("Sticky mode enabled");

  sm_.Call([this]() {
    SendKeyPress(ui::VKEY_LWIN);
    SendKeyPress(ui::VKEY_LWIN);
  });
  sm_.ExpectSpeech("Sticky mode disabled");

  sm_.Replay();
}

// TODO(crbug.com/41337748): Test is flaky.
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, DISABLED_TouchExploreStatusTray) {
  EnableChromeVox();

  base::SimpleTestTickClock clock;
  auto* clock_ptr = &clock;
  ui::SetEventTickClockForTesting(clock_ptr);

  auto* root_window = Shell::Get()->GetPrimaryRootWindow();
  ui::test::EventGenerator generator(root_window);
  auto* generator_ptr = &generator;

  // Touch the status tray.
  sm_.Call([clock_ptr, generator_ptr]() {
    const gfx::Point& tray_center = Shell::Get()
                                        ->GetPrimaryRootWindowController()
                                        ->GetStatusAreaWidget()
                                        ->unified_system_tray()
                                        ->GetBoundsInScreen()
                                        .CenterPoint();

    ui::TouchEvent touch_press(
        ui::EventType::kTouchPressed, tray_center, base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_press);

    clock_ptr->Advance(base::Seconds(1));

    ui::TouchEvent touch_move(
        ui::EventType::kTouchMoved, tray_center, base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_move);
  });

  sm_.ExpectSpeechPattern("Status tray, time* Battery at* percent*");
  sm_.ExpectSpeech("Button");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, ShowLinksList) {
  EnableChromeVox();
  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
        <button autofocus>Start here</button>
        <a href="https://google.com/">Google Search Engine</a>
        <a href="https://docs.google.com/">Google Docs</a>
        <a href="https://mail.google.com/">Gmail</a>)"));
  });
  sm_.ExpectSpeech("Start here");
  sm_.Call([this]() { SendKeyPressWithSearchAndControl(ui::VKEY_L); });
  sm_.ExpectSpeech("Link Menu");
  sm_.ExpectSpeech("Google Search Engine");
  sm_.ExpectSpeech("1 of 3");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Google Docs");
  sm_.ExpectSpeech("2 of 3");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Gmail");
  sm_.ExpectSpeech("3 of 3");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_UP); });
  sm_.ExpectSpeech("Google Docs");
  sm_.ExpectSpeech("2 of 3");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest,
                       TouchExploreRightEdgeVolumeSliderOn) {
  EnableChromeVox();

  base::SimpleTestTickClock clock;
  auto* clock_ptr = &clock;
  ui::SetEventTickClockForTesting(clock_ptr);

  auto* root_window = Shell::Get()->GetPrimaryRootWindow();
  ui::test::EventGenerator generator(root_window);
  auto* generator_ptr = &generator;

  // Force right edge volume slider gesture on.
  sm_.Call([] {
    AccessibilityController::Get()->EnableChromeVoxVolumeSlideGesture();
  });

  // Touch and slide on the right edge of the screen.
  sm_.Call([clock_ptr, generator_ptr]() {
    ui::TouchEvent touch_press(
        ui::EventType::kTouchPressed, gfx::Point(1280, 200),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_press);

    clock_ptr->Advance(base::Seconds(1));

    ui::TouchEvent touch_move(
        ui::EventType::kTouchMoved, gfx::Point(1280, 300),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_move);

    clock_ptr->Advance(base::Seconds(1));

    ui::TouchEvent touch_move2(
        ui::EventType::kTouchMoved, gfx::Point(1280, 400),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_move2);
  });

  sm_.ExpectSpeech("Volume");
  sm_.ExpectSpeech("Slider");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest,
                       TouchExploreRightEdgeVolumeSliderOff) {
  EnableChromeVox();

  base::SimpleTestTickClock clock;
  auto* clock_ptr = &clock;
  ui::SetEventTickClockForTesting(clock_ptr);

  auto* root_window = Shell::Get()->GetPrimaryRootWindow();
  ui::test::EventGenerator generator(root_window);
  auto* generator_ptr = &generator;

  // Build a simple window with a button and position it at the right edge of
  // the screen.
  views::Widget* widget = new views::Widget;
  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW);

  // Assert the right edge fits the below window.
  ASSERT_GE(root_window->bounds().width(), 1280);
  ASSERT_GE(root_window->bounds().height(), 800);

  // This is the right edge of the screen.
  params.bounds = {1050, 0, 50, 700};
  widget->Init(std::move(params));

  views::View* view = new views::View();
  view->GetViewAccessibility().SetRole(ax::mojom::Role::kButton);
  view->GetViewAccessibility().SetName(u"hello");
  view->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
  widget->GetRootView()->AddChildView(view);

  // Show the widget, then touch and slide on the right edge of the screen.
  sm_.Call([widget, clock_ptr, generator_ptr]() {
    widget->Show();
    ui::TouchEvent touch_press(
        ui::EventType::kTouchPressed, gfx::Point(1080, 200),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_press);

    clock_ptr->Advance(base::Seconds(1));

    ui::TouchEvent touch_move(
        ui::EventType::kTouchMoved, gfx::Point(1080, 300),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_move);

    clock_ptr->Advance(base::Seconds(1));

    ui::TouchEvent touch_move2(
        ui::EventType::kTouchMoved, gfx::Point(1080, 400),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_move2);
  });

  // This should trigger reading of the button.
  sm_.ExpectSpeech("hello");
  sm_.ExpectSpeech("Button");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, TouchExploreSecondaryDisplay) {
  std::vector<RootWindowController*> root_controllers =
      Shell::GetAllRootWindowControllers();
  EXPECT_EQ(1U, root_controllers.size());

  // Make two displays, each 800 by 700, side by side.
  ShellTestApi shell_test_api;
  display::test::DisplayManagerTestApi(shell_test_api.display_manager())
      .UpdateDisplay("800x700,801+0-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);

  root_controllers = Shell::GetAllRootWindowControllers();
  EXPECT_EQ(2U, root_controllers.size());

  EnableChromeVox();

  base::SimpleTestTickClock clock;
  auto* clock_ptr = &clock;
  ui::SetEventTickClockForTesting(clock_ptr);

  // Generate events to the secondary window which is at (800, 0).
  auto* root_window = root_controllers[1]->GetRootWindow();
  ui::test::EventGenerator generator(root_window);
  auto* generator_ptr = &generator;

  // Build a simple window with a button and position it at the right edge of
  // the screen.
  views::Widget* widget = new views::Widget;
  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW);
  params.parent = root_window;

  // This is the right edge of the screen.
  params.bounds = {1550, 0, 50, 600};
  widget->Init(std::move(params));

  views::View* view = new views::View();
  view->GetViewAccessibility().SetRole(ax::mojom::Role::kButton);
  view->GetViewAccessibility().SetName(u"hello");
  view->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
  widget->GetRootView()->AddChildView(view);

  // Show the widget, then touch and slide on the right edge of the screen.
  sm_.Call([widget, clock_ptr, generator_ptr]() {
    widget->Show();

    ui::TouchEvent touch_press(
        ui::EventType::kTouchPressed, gfx::Point(1580, 200),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_press);

    clock_ptr->Advance(base::Seconds(1));

    ui::TouchEvent touch_move(
        ui::EventType::kTouchMoved, gfx::Point(1580, 300),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_move);

    clock_ptr->Advance(base::Seconds(1));

    ui::TouchEvent touch_move2(
        ui::EventType::kTouchMoved, gfx::Point(1580, 400),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_move2);
  });

  // This should trigger reading of the button.
  sm_.ExpectSpeech("hello");
  sm_.ExpectSpeech("Button");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, TouchExploreWebContents) {
  EnableChromeVox();

  AutomationTestUtils test_utils(extension_misc::kChromeVoxExtensionId);
  sm_.Call([&test_utils]() { test_utils.SetUpTestSupport(); });

  base::SimpleTestTickClock clock;
  auto* clock_ptr = &clock;
  ui::SetEventTickClockForTesting(clock_ptr);

  auto* root_window = Shell::Get()->GetPrimaryRootWindow();
  ui::test::EventGenerator generator(root_window);
  auto* generator_ptr = &generator;

  gfx::Rect b2_bounds;
  gfx::Rect b3_bounds;

  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
            <button id="b1" autofocus>First</button>
            <button id="b2">Second</button>
            <button id="b3">Third</button>
        )"));
  });
  sm_.ExpectSpeech("First");
  sm_.Call([clock_ptr, generator_ptr, &b2_bounds, &b3_bounds, &test_utils]() {
    b2_bounds = test_utils.GetNodeBoundsInRoot("Second", "button");
    b3_bounds = test_utils.GetNodeBoundsInRoot("Third", "button");

    ui::TouchEvent touch_press(
        ui::EventType::kTouchPressed, b2_bounds.top_center(),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_press);

    clock_ptr->Advance(base::Seconds(1));

    ui::TouchEvent touch_move(
        ui::EventType::kTouchMoved, b2_bounds.CenterPoint(),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_move);

    clock_ptr->Advance(base::Seconds(1));

    ui::TouchEvent touch_move2(
        ui::EventType::kTouchMoved, b2_bounds.left_center(),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_move2);
  });
  sm_.ExpectSpeech("Second");
  sm_.Call([clock_ptr, generator_ptr, &b3_bounds]() {
    clock_ptr->Advance(base::Seconds(1));

    ui::TouchEvent touch_move(
        ui::EventType::kTouchMoved, b3_bounds.right_center(),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_move);

    clock_ptr->Advance(base::Seconds(1));

    ui::TouchEvent touch_move2(
        ui::EventType::kTouchMoved, b3_bounds.CenterPoint(),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_move2);
  });
  sm_.ExpectSpeech("Third");
  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, TouchExploreWebContentsHighDPI) {
  ShellTestApi shell_test_api;
  // Use DPI of Strongbad, to reproduce b/295325508.
  display::test::DisplayManagerTestApi(shell_test_api.display_manager())
      .UpdateDisplay("800x700*1.77778");

  EnableChromeVox();
  AutomationTestUtils test_utils(extension_misc::kChromeVoxExtensionId);
  sm_.Call([&test_utils]() { test_utils.SetUpTestSupport(); });

  base::SimpleTestTickClock clock;
  auto* clock_ptr = &clock;
  ui::SetEventTickClockForTesting(clock_ptr);

  auto* root_window = Shell::Get()->GetPrimaryRootWindow();
  ui::test::EventGenerator generator(root_window);
  auto* generator_ptr = &generator;

  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
            <button id="b1" autofocus>First</button>
            <button id="b2">Second</button>
        )"));
  });
  sm_.ExpectSpeech("First");
  sm_.Call([clock_ptr, generator_ptr, &test_utils]() {
    float scale_factor = 1.77778;
    gfx::Rect b2_bounds = test_utils.GetNodeBoundsInRoot("Second", "button");
    // GetNodeBoundsInRoot returns in DIPs. Multiply by resolution to get px,
    // which is where we need to touch on a high density screen.
    b2_bounds.set_x(b2_bounds.x() * scale_factor);
    b2_bounds.set_y(b2_bounds.y() * scale_factor);
    b2_bounds.set_width(b2_bounds.width() * scale_factor);
    b2_bounds.set_height(b2_bounds.height() * scale_factor);

    ui::TouchEvent touch_press(
        ui::EventType::kTouchPressed, b2_bounds.bottom_center(),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_press);

    clock_ptr->Advance(base::Seconds(1));

    ui::TouchEvent touch_move(
        ui::EventType::kTouchMoved, b2_bounds.CenterPoint(),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_move);

    clock_ptr->Advance(base::Seconds(1));

    ui::TouchEvent touch_move2(
        ui::EventType::kTouchMoved, b2_bounds.right_center(),
        base::TimeTicks::Now(),
        ui::PointerDetails(ui::EventPointerType::kTouch, 0));
    generator_ptr->Dispatch(&touch_move2);
  });
  sm_.ExpectSpeech("Second");
  sm_.Replay();
}

// TODO(b/287488905): Add test for touch explore with screen magnifier and high
// DPI.

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, ChromeVoxNextTabRecovery) {
  EnableChromeVox();

  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
            <button id='b1' autofocus>11</button>
            <button>22</button>
            <button>33</button>
            <h1>Middle</h1>
            <button>44</button>
            <button>55</button>
            <div id=console aria-live=polite></div>
            <script>
              var b1 = document.getElementById('b1');
              b1.addEventListener('blur', function() {
                document.getElementById('console').innerText =
                    'button lost focus';
              });
            </script>)"));
  });
  sm_.ExpectSpeech("Button");

  // Press Search+H to go to the next heading
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_H); });

  sm_.ExpectSpeech("Middle");

  // To ensure that the setSequentialFocusNavigationStartingPoint has
  // executed before pressing Tab, the page has an event handler waiting
  // for the 'blur' event on the button, and when it loses focus it
  // triggers a live region announcement that we wait for, here.
  sm_.ExpectSpeech("button lost focus");

  // Now we know that focus has left the button, so the sequential focus
  // navigation starting point must be on the heading. Press Tab and
  // ensure that we land on the first link past the heading.
  sm_.Call([this]() { SendKeyPress(ui::VKEY_TAB); });
  sm_.ExpectSpeech("44");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest,
                       MoveByCharacterPhoneticSpeechAndHints) {
  EnableChromeVox();
  StablizeChromeVoxState();
  sm_.ExpectSpeech("Button");
  sm_.ExpectSpeech("Press Search plus Space to activate");

  // Move by character through the button.
  // Assert that phonetic speech and hints are delayed.
  sm_.Call([this]() { SendKeyPressWithSearchAndShift(ui::VKEY_RIGHT); });
  sm_.ExpectSpeech("L");
  sm_.ExpectSpeech("lima");
  sm_.Call([this]() {
    EXPECT_TRUE(sm_.GetDelayForLastUtteranceMS() >=
                kExpectedPhoneticSpeechAndHintDelayMS);
  });
  sm_.ExpectSpeech("Press Search plus Space to activate");
  sm_.Call([this]() {
    EXPECT_TRUE(sm_.GetDelayForLastUtteranceMS() >=
                kExpectedPhoneticSpeechAndHintDelayMS);
  });
  sm_.Call([this]() { SendKeyPressWithSearchAndShift(ui::VKEY_RIGHT); });
  sm_.ExpectSpeech("I");
  sm_.ExpectSpeech("india");
  sm_.Call([this]() {
    EXPECT_TRUE(sm_.GetDelayForLastUtteranceMS() >=
                kExpectedPhoneticSpeechAndHintDelayMS);
  });
  sm_.ExpectSpeech("Press Search plus Space to activate");
  sm_.Call([this]() {
    EXPECT_TRUE(sm_.GetDelayForLastUtteranceMS() >=
                kExpectedPhoneticSpeechAndHintDelayMS);
  });
  sm_.Call([this]() { SendKeyPressWithSearchAndShift(ui::VKEY_RIGHT); });
  sm_.ExpectSpeech("C");
  sm_.ExpectSpeech("charlie");
  sm_.Call([this]() {
    EXPECT_TRUE(sm_.GetDelayForLastUtteranceMS() >=
                kExpectedPhoneticSpeechAndHintDelayMS);
  });
  sm_.ExpectSpeech("Press Search plus Space to activate");
  sm_.Call([this]() {
    EXPECT_TRUE(sm_.GetDelayForLastUtteranceMS() >=
                kExpectedPhoneticSpeechAndHintDelayMS);
  });
  sm_.Call([this]() { SendKeyPressWithSearchAndShift(ui::VKEY_RIGHT); });
  sm_.ExpectSpeech("K");
  sm_.ExpectSpeech("kilo");
  sm_.Call([this]() {
    EXPECT_TRUE(sm_.GetDelayForLastUtteranceMS() >=
                kExpectedPhoneticSpeechAndHintDelayMS);
  });
  sm_.ExpectSpeech("Press Search plus Space to activate");
  sm_.Call([this]() {
    EXPECT_TRUE(sm_.GetDelayForLastUtteranceMS() >=
                kExpectedPhoneticSpeechAndHintDelayMS);
  });
  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, ResetTtsSettings) {
  EnableChromeVox();
  StablizeChromeVoxState();

  // Reset Tts settings using hotkey and assert speech output.
  sm_.Call(
      [this]() { SendKeyPressWithSearchAndControlAndShift(ui::VKEY_OEM_5); });
  sm_.ExpectSpeech("Reset text to speech settings to default values");
  // Increase speech rate.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_OEM_4); });
  sm_.ExpectSpeech("Rate 19 percent");
  // Increase speech pitch.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_OEM_6); });
  sm_.ExpectSpeech("Pitch 50 percent");
  // Reset Tts settings again.
  sm_.Call(
      [this]() { SendKeyPressWithSearchAndControlAndShift(ui::VKEY_OEM_5); });
  sm_.ExpectSpeech("Reset text to speech settings to default values");
  // Ensure that increasing speech rate and pitch jump to the same values as
  // before.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_OEM_4); });
  sm_.ExpectSpeech("Rate 19 percent");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_OEM_6); });
  sm_.ExpectSpeech("Pitch 50 percent");
  sm_.Replay();
}

// Tests the keyboard shortcut to cycle the punctuation echo setting,
// Search+A then P.
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, TogglePunctuationEcho) {
  EnableChromeVox();
  StablizeChromeVoxState();
  sm_.Call([this]() {
    SendKeyPressWithSearch(ui::VKEY_A);
    SendKeyPress(ui::VKEY_P);
  });
  sm_.ExpectSpeech("All punctuation");
  sm_.Call([this]() {
    SendKeyPressWithSearch(ui::VKEY_A);
    SendKeyPress(ui::VKEY_P);
  });
  sm_.ExpectSpeech("No punctuation");
  sm_.Call([this]() {
    SendKeyPressWithSearch(ui::VKEY_A);
    SendKeyPress(ui::VKEY_P);
  });
  sm_.ExpectSpeech("Some punctuation");
  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, ShowFormControlsList) {
  EnableChromeVox();
  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
            <button autofocus>Start here</button>
            <input type="text" id="text"></input>
            <label for="text">Name</label>
            <p>Other text</p>
            <button>Make it shiny</button>
            <input type="checkbox" id="checkbox"></input>
            <label for="checkbox">Express delivery</label>
            <input type="range" id="slider"></input>
            <label for="slider">Percent cotton</label>)"));
  });
  sm_.ExpectSpeech("Start here");
  sm_.Call([this]() { SendKeyPressWithSearchAndControl(ui::VKEY_F); });
  sm_.ExpectSpeech("Form Controls Menu");
  sm_.ExpectSpeech("Start here Button");
  sm_.ExpectSpeech("Menu item 1 of ");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Name Edit text");
  sm_.ExpectSpeech("Menu item 2 of ");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Make it shiny Button");
  sm_.ExpectSpeech("Menu item 3 of ");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Express delivery Check box");
  sm_.ExpectSpeech("Menu item 4 of ");
  sm_.Call([this]() { SendKeyPress(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Percent cotton Slider");
  sm_.ExpectSpeech("Menu item 5 of ");

  sm_.Replay();
}

// TODO(crbug.com/1310316): Test is flaky.
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, DISABLED_SmartStickyMode) {
  EnableChromeVox();
  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
        <p>start</p>
        <input autofocus type='text'>
        <p>end</p>)"));
  });

  // The input is autofocused.
  sm_.ExpectSpeech("Edit text");

  // First, navigate with sticky mode on.
  sm_.Call([this]() { SendStickyKeyCommand(); });
  sm_.ExpectSpeech("Sticky mode enabled");

  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.ExpectNextSpeechIsNotPattern("Sticky mode *abled");
  sm_.ExpectSpeech("end");

  // Jump to beginning.
  sm_.Call([this]() { SendKeyPressWithSearchAndControl(ui::VKEY_LEFT); });
  sm_.ExpectNextSpeechIsNotPattern("Sticky mode *abled");
  sm_.ExpectSpeech("start");

  // The nextEditText command is explicitly excluded from toggling.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_E); });
  sm_.ExpectNextSpeechIsNotPattern("Sticky mode *abled");
  sm_.ExpectSpeech("Edit text");

  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.ExpectNextSpeechIsNotPattern("Sticky mode *abled");
  sm_.ExpectSpeech("end");

  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_LEFT); });
  sm_.ExpectSpeech("Sticky mode disabled");
  sm_.ExpectSpeech("Edit text");

  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_LEFT); });
  sm_.ExpectSpeech("Sticky mode enabled");
  sm_.ExpectSpeech("start");

  // Try a few jump commands and linear nav with no Search modifier. We never
  // leave sticky mode.
  sm_.Call([this]() { SendKeyPress(ui::VKEY_E); });
  sm_.ExpectSpeech("Edit text");

  sm_.Call([this]() { SendKeyPressWithShift(ui::VKEY_F); });
  sm_.ExpectSpeech("Edit text");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_RIGHT); });
  sm_.ExpectSpeech("end");

  sm_.Call([this]() { SendKeyPress(ui::VKEY_F); });
  sm_.ExpectSpeech("Edit text");

  sm_.Call([this]() { SendKeyPressWithShift(ui::VKEY_E); });
  sm_.ExpectSpeech("Edit text");

  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_LEFT); });
  sm_.ExpectSpeech("start");

  // Now, navigate with sticky mode off.
  sm_.Call([this]() { SendStickyKeyCommand(); });
  sm_.ExpectSpeech("Sticky mode disabled");

  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.ExpectNextSpeechIsNotPattern("Sticky mode *abled");
  sm_.ExpectSpeech("Edit text");

  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.ExpectNextSpeechIsNotPattern("Sticky mode *abled");
  sm_.ExpectSpeech("end");

  sm_.Replay();
}

class TestBacklightsObserver : public ScreenBacklightObserver {
 public:
  explicit TestBacklightsObserver(
      BacklightsForcedOffSetter* backlights_setter) {
    backlights_forced_off_ = backlights_setter->backlights_forced_off();
    scoped_observation_.Observe(backlights_setter);
  }
  ~TestBacklightsObserver() override = default;
  TestBacklightsObserver(const TestBacklightsObserver&) = delete;
  TestBacklightsObserver& operator=(const TestBacklightsObserver&) = delete;

  // ScreenBacklightObserver:
  void OnBacklightsForcedOffChanged(bool backlights_forced_off) override {
    if (backlights_forced_off_ == backlights_forced_off) {
      return;
    }

    backlights_forced_off_ = backlights_forced_off;
    if (run_loop_) {
      run_loop_->Quit();
    }
  }

  void WaitForBacklightStateChange() {
    run_loop_ = std::make_unique<base::RunLoop>();
    run_loop_->Run();
  }

  bool backlights_forced_off() const { return backlights_forced_off_; }

 private:
  bool backlights_forced_off_;
  std::unique_ptr<base::RunLoop> run_loop_;

  base::ScopedObservation<BacklightsForcedOffSetter, ScreenBacklightObserver>
      scoped_observation_{this};
};

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, DarkenScreenConfirmation) {
  EnableChromeVox();
  StablizeChromeVoxState();
  EXPECT_FALSE(
      Shell::Get()->backlights_forced_off_setter()->backlights_forced_off());
  BacklightsForcedOffSetter* backlights_setter =
      Shell::Get()->backlights_forced_off_setter();
  TestBacklightsObserver observer(backlights_setter);

  // Try to darken screen and check the dialog is shown.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_F7); });
  sm_.ExpectSpeech("Turn off screen?");
  sm_.ExpectSpeech("Dialog");
  // TODO(crbug.com/40777708) - Improve the generation of summaries across
  // ChromeOS. Expect the content to be spoken once it has been improved.
  /*sm_.ExpectSpeech(
      "Turn off screen? This improves privacy by turning off your screen so it "
      "isn’t visible to others. You can always turn the screen back on by "
      "pressing Search plus Brightness up. Cancel Continue");*/
  sm_.ExpectSpeech("Continue");
  sm_.ExpectSpeech("default");
  sm_.ExpectSpeech("Button");

  sm_.Call([]() {
    // Accept the dialog and see that the screen is darkened.
    AccessibilityConfirmationDialog* dialog_ =
        Shell::Get()
            ->accessibility_controller()
            ->GetConfirmationDialogForTest();
    ASSERT_TRUE(dialog_ != nullptr);
    dialog_->Accept();
  });
  sm_.ExpectSpeech("Screen off");
  // Make sure Ash gets the backlight change request.
  sm_.Call([&observer = observer, backlights_setter = backlights_setter]() {
    if (observer.backlights_forced_off()) {
      return;
    }
    observer.WaitForBacklightStateChange();
    EXPECT_TRUE(backlights_setter->backlights_forced_off());
  });

  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_F7); });
  sm_.ExpectNextSpeechIsNot("Continue");
  sm_.ExpectSpeech("Screen on");
  sm_.Call([&observer = observer, backlights_setter = backlights_setter]() {
    if (!observer.backlights_forced_off()) {
      return;
    }
    observer.WaitForBacklightStateChange();
    EXPECT_FALSE(backlights_setter->backlights_forced_off());
  });

  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_F7); });
  sm_.ExpectNextSpeechIsNot("Continue");
  sm_.ExpectSpeech("Screen off");
  sm_.Call([&observer = observer, backlights_setter = backlights_setter]() {
    if (observer.backlights_forced_off()) {
      return;
    }
    observer.WaitForBacklightStateChange();
    EXPECT_TRUE(backlights_setter->backlights_forced_off());
  });

  sm_.Replay();
}

// Tests basic behavior of the tutorial when signed in.
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, Tutorial) {
  EnableChromeVox();
  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
        <button autofocus>Testing</button>)"));
  });
  sm_.ExpectSpeech("Testing");
  sm_.Call([this]() {
    SendKeyPressWithSearch(ui::VKEY_O);
    SendKeyPress(ui::VKEY_T);
  });
  sm_.ExpectSpeech("ChromeVox tutorial");
  sm_.ExpectSpeech(
      "Press Search plus Right Arrow, or Search plus Left Arrow to browse "
      "topics");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.ExpectSpeech("Quick orientation");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.ExpectSpeech("Essential keys");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_B); });
  sm_.ExpectSpeech("Exit tutorial");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_SPACE); });
  sm_.ExpectSpeech("Testing");
  sm_.Replay();
}

// TODO(crbug.com/40930988): Re-enable this test
IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, DISABLED_ClipboardCopySpeech) {
  EnableChromeVox();
  sm_.Call([this]() {
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
        <input autofocus type='text' value='Foo'></input>)"));
  });

  // The input is autofocused.
  sm_.ExpectSpeech("Edit text");

  // Select and copy the first character.
  sm_.Call([this]() {
    SendKeyPressWithShift(ui::VKEY_RIGHT);
    SendKeyPressWithControl(ui::VKEY_C);
  });
  sm_.ExpectSpeech("copy F.");

  // Select and copy the first two characters.
  sm_.Call([this]() {
    SendKeyPressWithShift(ui::VKEY_RIGHT);
    SendKeyPressWithControl(ui::VKEY_C);
  });
  sm_.ExpectSpeech("copy Fo.");

  // Select and copy all characters.
  sm_.Call([this]() {
    SendKeyPressWithShift(ui::VKEY_RIGHT);
    SendKeyPressWithControl(ui::VKEY_C);
  });
  sm_.ExpectSpeech("copy Foo.");

  // Do it again with the command Search+Ctrl+C, which should do the same thing
  // but triggered through ChromeVox via synthesized keys.
  sm_.Call([this]() { SendKeyPressWithSearchAndControl(ui::VKEY_C); });
  sm_.ExpectSpeech("copy Foo.");

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, OrientationChanged) {
  EnableChromeVox();

  sm_.Call([]() {
    Shell::Get()->display_configuration_controller()->SetDisplayRotation(
        ash::Shell::Get()->display_manager()->GetDisplayAt(0).id(),
        display::Display::ROTATE_90, display::Display::RotationSource::USER);
  });

  sm_.ExpectSpeech("portrait");

  sm_.Call([]() {
    Shell::Get()->display_configuration_controller()->SetDisplayRotation(
        ash::Shell::Get()->display_manager()->GetDisplayAt(0).id(),
        display::Display::ROTATE_180, display::Display::RotationSource::USER);
  });

  sm_.ExpectSpeech("landscape");

  sm_.Call([]() {
    Shell::Get()->display_configuration_controller()->SetDisplayRotation(
        ash::Shell::Get()->display_manager()->GetDisplayAt(0).id(),
        display::Display::ROTATE_270, display::Display::RotationSource::USER);
  });

  sm_.ExpectSpeech("portrait");

  sm_.ExpectHadNoRepeatedSpeech();

  sm_.Replay();
}

// Spoken feedback tests of the out-of-box experience.
class OobeSpokenFeedbackTest : public OobeBaseTest {
 public:
  OobeSpokenFeedbackTest() = default;
  OobeSpokenFeedbackTest(const OobeSpokenFeedbackTest&) = delete;
  OobeSpokenFeedbackTest& operator=(const OobeSpokenFeedbackTest&) = delete;
  ~OobeSpokenFeedbackTest() override = default;

 protected:
  // OobeBaseTest:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    OobeBaseTest::SetUpCommandLine(command_line);
    // Many bots don't have keyboard/mice which triggers the HID detection
    // dialog in the OOBE.  Avoid confusing the tests with that.
    command_line->AppendSwitch(switches::kDisableHIDDetectionOnOOBEForTesting);
    // We only start the tutorial in OOBE if the device is a Chromebook, so set
    // the device type so tutorial-related behavior can be tested.
    command_line->AppendSwitchASCII(switches::kFormFactor, "CHROMEBOOK");
  }
  void SetUpOnMainThread() override {
    OobeBaseTest::SetUpOnMainThread();
    event_generator_ = std::make_unique<ui::test::EventGenerator>(
        Shell::Get()->GetPrimaryRootWindow());
  }
  void TearDownOnMainThread() override {
    event_generator_.reset();
    OobeBaseTest::TearDownOnMainThread();
  }

  std::unique_ptr<ui::test::EventGenerator> event_generator_;
  test::SpeechMonitor sm_;
};

// TODO(crbug.com/1310682) - Re-enable this test.
IN_PROC_BROWSER_TEST_F(OobeSpokenFeedbackTest, DISABLED_SpokenFeedbackInOobe) {
  ASSERT_FALSE(AccessibilityManager::Get()->IsSpokenFeedbackEnabled());
  AccessibilityManager::Get()->EnableSpokenFeedbackWithTutorial();

  // If ChromeVox is started in OOBE, the tutorial is automatically opened.
  sm_.ExpectSpeech("Welcome to ChromeVox!");

  // The tutorial can be exited by pressing Escape.
  sm_.Call([&]() { event_generator_->PressAndReleaseKey(ui::VKEY_ESCAPE, 0); });

  sm_.ExpectSpeech("Get started");

  sm_.Call([&]() { event_generator_->PressAndReleaseKey(ui::VKEY_TAB, 0); });
  sm_.ExpectSpeech("Pause animation");

  sm_.Call([&]() { event_generator_->PressAndReleaseKey(ui::VKEY_TAB, 0); });
  sm_.ExpectSpeechPattern("*Status tray*");
  sm_.ExpectSpeech("Button");

  sm_.Replay();
}

// TODO(akihiroota): fix flakiness: http://crbug.com/1172390
IN_PROC_BROWSER_TEST_F(OobeSpokenFeedbackTest,
                       DISABLED_SpokenFeedbackTutorialInOobe) {
  ASSERT_FALSE(AccessibilityManager::Get()->IsSpokenFeedbackEnabled());
  AccessibilityManager::Get()->EnableSpokenFeedback(true);
  sm_.ExpectSpeech("Welcome to ChromeVox!");
  sm_.ExpectSpeechPattern(
      "Welcome to the ChromeVox tutorial*When you're ready, use the spacebar "
      "to move to the next lesson.");
  // Press space to move to the next lesson.
  sm_.Call([&]() { event_generator_->PressAndReleaseKey(ui::VKEY_SPACE, 0); });
  sm_.ExpectSpeech("Essential Keys: Control");
  sm_.ExpectSpeechPattern("*To continue, press the Control key.*");
  // Press control to move to the next lesson.
  sm_.Call(
      [&]() { event_generator_->PressAndReleaseKey(ui::VKEY_CONTROL, 0); });
  sm_.ExpectSpeechPattern("*To continue, press the left Shift key.");
  sm_.Replay();
}

class SigninToUserProfileSwitchTest : public OobeSpokenFeedbackTest {
 public:
  // OobeSpokenFeedbackTest:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    OobeSpokenFeedbackTest::SetUpCommandLine(command_line);
    // Force the help app to launch in the background.
    command_line->AppendSwitch(switches::kForceFirstRunUI);
  }

 protected:
  LoginManagerMixin login_manager_{&mixin_host_};
  DeviceStateMixin device_state_{
      &mixin_host_, DeviceStateMixin::State::OOBE_COMPLETED_UNOWNED};
};

// Verifies that spoken feedback correctly handles profile switch (signin ->
// user) and announces the sync consent screen correctly.
// TODO(crbug.com/1184714): Fix flakiness.
IN_PROC_BROWSER_TEST_F(SigninToUserProfileSwitchTest, DISABLED_LoginAsNewUser) {
  // Force sync screen.
  LoginDisplayHost::default_host()->GetWizardContext()->is_branded_build = true;
  AccessibilityManager::Get()->EnableSpokenFeedback(true);
  sm_.ExpectSpeechPattern("*");

  sm_.Call([this]() {
    ASSERT_TRUE(IsSigninBrowserContext(AccessibilityManager::Get()->profile()));
    login_manager_.LoginAsNewRegularUser();
  });

  sm_.ExpectSpeechPattern("Welcome to the ChromeVox tutorial*");

  // The tutorial can be exited by pressing Escape.
  sm_.Call([&]() { event_generator_->PressAndReleaseKey(ui::VKEY_ESCAPE, 0); });

  sm_.ExpectSpeech("Accept and continue");

  // Check that profile switched to the active user.
  sm_.Call([&]() {
    ASSERT_EQ(AccessibilityManager::Get()->profile(),
              ProfileManager::GetActiveUserProfile());
  });
  sm_.Replay();
}

class DeskTemplatesSpokenFeedbackTest : public LoggedInSpokenFeedbackTest {
 public:
  DeskTemplatesSpokenFeedbackTest() = default;
  DeskTemplatesSpokenFeedbackTest(const DeskTemplatesSpokenFeedbackTest&) =
      delete;
  DeskTemplatesSpokenFeedbackTest& operator=(
      const DeskTemplatesSpokenFeedbackTest&) = delete;
  ~DeskTemplatesSpokenFeedbackTest() override = default;

 private:
  base::test::ScopedFeatureList scoped_feature_list_{features::kDesksTemplates};
};

IN_PROC_BROWSER_TEST_F(DeskTemplatesSpokenFeedbackTest, DeskTemplatesBasic) {
  // TODO(http://b/350771229): This test tests clicking the "Save desk as
  // template" button that will not be shown if the Forest feature is enabled.
  // This test will be fixed before the button change is no longer hidden behind
  // Forest.
  if (ash::features::IsForestFeatureEnabled()) {
    GTEST_SKIP() << "Skipping test body for Forest Feature.";
  }

  EnableChromeVox();

  // Enter overview first. This is how we reach the desk templates UI.
  sm_.Call([this]() {
    (PerformAcceleratorAction(AcceleratorAction::kToggleOverview));
  });

  sm_.ExpectSpeech(
      "Entered window overview mode. Swipe to navigate, or press tab if using "
      "a keyboard.");

  // TODO(crbug.com/1360638): Remove the conditional here when the Save & Recall
  // flag flip has landed since it will always be true.
  if (saved_desk_util::ShouldShowSavedDesksOptions()) {
    sm_.Call([this]() { SendKeyPressWithShift(ui::VKEY_TAB); });
    sm_.ExpectSpeechPattern("Save desk for later");
    sm_.ExpectSpeech("Button");
  }

  // Reverse tab to focus the save desk as template button.
  sm_.Call([this]() { SendKeyPressWithShift(ui::VKEY_TAB); });
  sm_.ExpectSpeechPattern("Save desk as a template");
  sm_.ExpectSpeech("Button");

  // Hit enter on the save desk as template button. It should take us to the
  // templates grid, which triggers an accessibility alert. This should nudge
  // the template name view but not say anything extra.
  sm_.Call([this]() { SendKeyPress(ui::VKEY_RETURN); });
  sm_.ExpectSpeech("Viewing saved desks and templates. Press tab to navigate.");

  // The first item in the tab order is the template card, which is a button. It
  // has the same name as the desk it was created from, in this case the default
  // desk name is "Desk 1". The name view will be focused first, then we can go
  // backwards to the template card, which is a button.
  sm_.Call([this]() { SendKeyPressWithShift(ui::VKEY_TAB); });
  sm_.ExpectSpeechPattern("Template, Desk 1");
  sm_.ExpectSpeech("Button");
  sm_.ExpectSpeech("Press Ctrl plus W to delete");
  sm_.ExpectSpeech("Press Search plus Space to activate");

  // The next item is the textfield inside the template card, which also has the
  // same name as the desk it was created from.
  sm_.Call([this]() { SendKeyPress(ui::VKEY_TAB); });
  sm_.ExpectSpeechPattern("Desk 1");
  sm_.ExpectSpeech("Edit text");

  // Reverse tab to focus back on the template card.
  sm_.Call([this]() { SendKeyPressWithShift(ui::VKEY_TAB); });

  // Trigger a delete template dialog by pressing Ctrl+W.
  sm_.Call([this]() { SendKeyPressWithControl(ui::VKEY_W); });
  sm_.ExpectSpeech("Delete template?");
  sm_.ExpectSpeech("Dialog");
  sm_.ExpectSpeech("Delete");
  sm_.ExpectSpeech("default");
  sm_.ExpectSpeech("Button");
  sm_.ExpectSpeech("Press Search plus Space to activate");

  sm_.Replay();
}

class ShortcutsAppSpokenFeedbackTest : public LoggedInSpokenFeedbackTest {
 public:
  ShortcutsAppSpokenFeedbackTest() = default;
  ShortcutsAppSpokenFeedbackTest(const ShortcutsAppSpokenFeedbackTest&) =
      delete;
  ShortcutsAppSpokenFeedbackTest& operator=(
      const ShortcutsAppSpokenFeedbackTest&) = delete;
  ~ShortcutsAppSpokenFeedbackTest() override = default;
};

// TODO(b/288602247): The test is flaky.
IN_PROC_BROWSER_TEST_F(ShortcutsAppSpokenFeedbackTest,
                       DISABLED_ShortcutCustomization) {
  EnableChromeVox();
  sm_.Call(
      [this]() { NavigateToUrl(GURL("chrome://shortcut-customization")); });
  sm_.ExpectSpeech("Search shortcuts");

  // Move through all tabs; make a few expectations along the way.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("General");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_DOWN); });
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_DOWN); });
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_DOWN); });
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_DOWN); });
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Accessibility");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Keyboard settings");

  // Moving forward again should dive into the list of shortcuts for the
  // category.
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_RIGHT); });
  sm_.ExpectSpeech("General controls");
  sm_.Call([this]() { SendKeyPressWithSearch(ui::VKEY_DOWN); });
  sm_.ExpectSpeech("Open slash close Launcher");
  sm_.ExpectSpeech("row 1 column 1");
  sm_.Replay();
}

class SpokenFeedbackWithCandidateWindowTest
    : public LoggedInSpokenFeedbackTest {
 public:
  SpokenFeedbackWithCandidateWindowTest() = default;
  SpokenFeedbackWithCandidateWindowTest(
      const SpokenFeedbackWithCandidateWindowTest&) = delete;
  SpokenFeedbackWithCandidateWindowTest& operator=(
      const SpokenFeedbackWithCandidateWindowTest&) = delete;
  ~SpokenFeedbackWithCandidateWindowTest() override = default;

  void SetUpOnMainThread() override {
    LoggedInSpokenFeedbackTest::SetUpOnMainThread();

    aura::Window* parent =
        ash::Shell::GetContainer(Shell::Get()->GetPrimaryRootWindow(),
                                 ash::kShellWindowId_MenuContainer);

    candidate_window_view_ = new ui::ime::CandidateWindowView(parent);
    candidate_window_view_->InitWidget();
  }
  void TearDownOnMainThread() override {
    candidate_window_view_.ExtractAsDangling()->GetWidget()->CloseNow();
    LoggedInSpokenFeedbackTest::TearDownOnMainThread();
  }

  raw_ptr<ui::ime::CandidateWindowView> candidate_window_view_;
};

IN_PROC_BROWSER_TEST_F(SpokenFeedbackWithCandidateWindowTest,
                       SpeakSelectedItem) {
  EnableChromeVox();

  ui::CandidateWindow candidate_window;
  candidate_window.set_cursor_position(0);
  candidate_window.set_page_size(2);
  candidate_window.mutable_candidates()->clear();
  candidate_window.set_orientation(ui::CandidateWindow::VERTICAL);
  candidate_window.set_is_user_selecting(true);
  for (size_t i = 0; i < 2; ++i) {
    ui::CandidateWindow::Entry entry;
    entry.value = u"value " + base::NumberToString16(i);
    entry.label = u"label " + base::NumberToString16(i);
    candidate_window.mutable_candidates()->push_back(entry);
  }

  sm_.Call([this, &candidate_window]() {
    candidate_window_view_->GetWidget()->Show();
    candidate_window_view_->UpdateCandidates(candidate_window);
    candidate_window_view_->ShowLookupTable();
  });
  sm_.ExpectSpeech("value 0");

  // Move selection to another item.
  sm_.Call([this, &candidate_window]() {
    candidate_window.set_cursor_position(1);
    candidate_window_view_->UpdateCandidates(candidate_window);
  });
  sm_.ExpectSpeech("value 1");

  // Simulate pagination.
  sm_.Call([this, &candidate_window]() {
    candidate_window.set_cursor_position(0);
    candidate_window.mutable_candidates()->at(0).value = u"value 2";
    candidate_window.mutable_candidates()->at(0).label = u"label 2";
    candidate_window.mutable_candidates()->at(1).value = u"value 3";
    candidate_window.mutable_candidates()->at(1).label = u"label 3";
    candidate_window_view_->UpdateCandidates(candidate_window);
  });
  sm_.ExpectSpeech(test::SpeechMonitor::Expectation("value 2").WithoutText(
      {"value 0", "value 1", "value 3"}));

  sm_.Replay();
}

class SpokenFeedbackWithMagnifierTest : public SpokenFeedbackTest {
 protected:
  SpokenFeedbackWithMagnifierTest() = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    scoped_feature_list_.InitAndEnableFeature(
        ::features::kAccessibilityMagnifierFollowsChromeVox);
    SpokenFeedbackTest::SetUpCommandLine(command_line);
  }

  void SetUpOnMainThread() override {
    SpokenFeedbackTest::SetUpOnMainThread();

    EnableMagnifier();
    EnableChromeVox();

    test_utils_ = std::make_unique<AutomationTestUtils>(
        extension_misc::kChromeVoxExtensionId);
  }

  void EnableMagnifier() {
    Profile* profile = AccessibilityManager::Get()->profile();
    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();

    ASSERT_TRUE(MagnificationManager::Get()->IsMagnifierEnabled());
    host_helper.WaitForHostCompletedFirstLoad();
    FullscreenMagnifierTestHelper::WaitForMagnifierJSReady(profile);

    // Set Magnifier.IGNORE_AT_UPDATES_AFTER_OTHER_MOVE_MS to a small duration
    // to allow for testing with automation interactions, which move faster than
    // the ignore duration used in production.
    base::ScopedAllowBlockingForTesting allow_blocking;
    std::string script = base::StringPrintf(R"JS(
        (async function() {
          globalThis.accessibilityCommon.setFeatureLoadCallbackForTest(
              'magnifier', () => {
                globalThis.accessibilityCommon.magnifier_.setIgnoreAssistiveTechnologyUpdatesAfterOtherMoveDurationForTest(
                    200);
                chrome.test.sendScriptResult('ready');
              });
        })();
      )JS");
    base::Value result =
        extensions::browsertest_util::ExecuteScriptInBackgroundPage(
            profile, extension_misc::kAccessibilityCommonExtensionId, script);
    ASSERT_EQ("ready", result);
  }

  void WaitForMagnifierViewportOnBounds(gfx::Rect focus_bounds) {
    FullscreenMagnifierController* fullscreen_magnifier_controller =
        Shell::Get()->fullscreen_magnifier_controller();
    MagnifierAnimationWaiter waiter(fullscreen_magnifier_controller);

    // Magnifier should now move to the focused area.
    while (!fullscreen_magnifier_controller->GetViewportRect().Intersects(
        focus_bounds)) {
      waiter.Wait();
    }

    // Check that magnifier viewport is on node.
    gfx::Rect final_viewport =
        fullscreen_magnifier_controller->GetViewportRect();
    EXPECT_TRUE(final_viewport.Intersects(focus_bounds));
  }

  void EnsureMagnifierViewportNotOnBounds(gfx::Rect focus_bounds) {
    gfx::Rect current_viewport =
        Shell::Get()->fullscreen_magnifier_controller()->GetViewportRect();
    EXPECT_FALSE(current_viewport.IsEmpty());
    EXPECT_FALSE(focus_bounds.size().IsEmpty());

    // Ensure magnifier viewport is not currently already intersecting node.
    EXPECT_FALSE(current_viewport.Intersects(focus_bounds));
  }

  AutomationTestUtils* test_utils() { return test_utils_.get(); }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<AutomationTestUtils> test_utils_;
};

INSTANTIATE_TEST_SUITE_P(TestAsNormalAndGuestUser,
                         SpokenFeedbackWithMagnifierTest,
                         ::testing::Values(kTestAsNormalUser,
                                           kTestAsGuestUser));

IN_PROC_BROWSER_TEST_P(SpokenFeedbackWithMagnifierTest,
                       FullscreenMagnifierButton) {
  gfx::Rect focus_bounds;

  sm_.Call([this, &focus_bounds]() {
    test_utils()->SetUpTestSupport();

    // Load a page with interactive text node that would get keyboard
    // focus and should get magnifier focus.
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
        <button>Hello world</button>)"));

    // Set magnifier scale to something quite big so that the initial bounds
    // of the button are not within the magnifier bounds.
    AccessibilityManager::Get()->profile()->GetPrefs()->SetDouble(
        prefs::kAccessibilityScreenMagnifierScale, 4.0);

    focus_bounds = test_utils()->GetNodeBoundsInRoot("Hello world", "button");

    EnsureMagnifierViewportNotOnBounds(focus_bounds);

    // Press right key to focus the button.
    SendKeyPressWithSearch(ui::VKEY_RIGHT);
  });

  sm_.ExpectSpeech("Hello world");

  sm_.Call([this, &focus_bounds]() {
    WaitForMagnifierViewportOnBounds(focus_bounds);
  });

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackWithMagnifierTest,
                       FullscreenMagnifierStaticTextSingleLine) {
  gfx::Rect focus_bounds;

  sm_.Call([this, &focus_bounds]() {
    test_utils()->SetUpTestSupport();

    // Load a page with non-interactive text node that would not get keyboard
    // focus so would not already get magnifier focus.
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
        <p>Hello world</p>)"));

    // Set magnifier scale to something quite big so that the initial bounds
    // of the text are not within the magnifier bounds.
    AccessibilityManager::Get()->profile()->GetPrefs()->SetDouble(
        prefs::kAccessibilityScreenMagnifierScale, 4.0);

    focus_bounds =
        test_utils()->GetNodeBoundsInRoot("Hello world", "staticText");
    EnsureMagnifierViewportNotOnBounds(focus_bounds);

    // Press right key to focus the text node.
    SendKeyPressWithSearch(ui::VKEY_RIGHT);
  });

  sm_.ExpectSpeech("Hello world");

  sm_.Call([this, &focus_bounds]() {
    WaitForMagnifierViewportOnBounds(focus_bounds);
  });

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackWithMagnifierTest,
                       FullscreenMagnifierStaticTextMultipleLines) {
  gfx::Rect focus_bounds;

  sm_.Call([this, &focus_bounds]() {
    test_utils()->SetUpTestSupport();

    // Load a page with non-interactive text node that would not get
    // keyboard focus so would not already get magnifier focus.
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
        <p>Line 1</p>
        <p>Line 2</p>
        <p>Line 3</p>)"));

    // Set magnifier scale to something quite big so that the initial bounds
    // of the text are not within the magnifier bounds.
    AccessibilityManager::Get()->profile()->GetPrefs()->SetDouble(
        prefs::kAccessibilityScreenMagnifierScale, 8.0);

    // Verify first line.
    focus_bounds = test_utils()->GetNodeBoundsInRoot("Line 1", "staticText");
    EnsureMagnifierViewportNotOnBounds(focus_bounds);

    // Press right key to focus the text node.
    SendKeyPressWithSearch(ui::VKEY_RIGHT);
  });

  sm_.ExpectSpeech("Line 1");

  sm_.Call([this, &focus_bounds]() {
    WaitForMagnifierViewportOnBounds(focus_bounds);

    // Verify last line, which should not be currently intersecting the
    // viewport.
    focus_bounds = test_utils()->GetNodeBoundsInRoot("Line 3", "staticText");
    EnsureMagnifierViewportNotOnBounds(focus_bounds);

    // Press right key to focus the text node.
    SendKeyPressWithSearch(ui::VKEY_RIGHT);
    SendKeyPressWithSearch(ui::VKEY_RIGHT);
  });

  sm_.ExpectSpeech("Line 2");
  sm_.ExpectSpeech("Line 3");

  sm_.Call([this, &focus_bounds]() {
    WaitForMagnifierViewportOnBounds(focus_bounds);
  });

  sm_.Replay();
}

IN_PROC_BROWSER_TEST_P(SpokenFeedbackWithMagnifierTest,
                       FullscreenMagnifierTable) {
  gfx::Rect focus_bounds;

  sm_.Call([this, &focus_bounds]() {
    test_utils()->SetUpTestSupport();

    // Load a page with non-interactive table that would not get keyboard
    // focus.
    NavigateToUrl(GURL(R"(data:text/html;charset=utf-8,
        <table>
          <tr>
            <th>Heading 1</th>
            <th>Heading 2</th>
            <th>Heading 3</th>
            <th>Heading 4</th>
          </tr>
          <tr>
            <td>Cell 1</td>
            <td>Cell 2</td>
            <td>Cell 3</td>
            <td>Cell 4</td>
          </tr>
        </table>)"));

    // Set magnifier scale to something quite big so that the initial bounds
    // of the button are not within the magnifier bounds.
    AccessibilityManager::Get()->profile()->GetPrefs()->SetDouble(
        prefs::kAccessibilityScreenMagnifierScale, 8.0);

    focus_bounds = test_utils()->GetNodeBoundsInRoot("Heading 1", "staticText");
    EnsureMagnifierViewportNotOnBounds(focus_bounds);

    // Press T key to focus the table.
    SendKeyPressWithSearch(ui::VKEY_T);
  });

  sm_.ExpectSpeech("Heading 1");

  sm_.Call([this, &focus_bounds]() {
    WaitForMagnifierViewportOnBounds(focus_bounds);

    focus_bounds = test_utils()->GetNodeBoundsInRoot("Heading 4", "staticText");
    EnsureMagnifierViewportNotOnBounds(focus_bounds);

    // Press right key to focus the last heading which should not be currently
    // in the viewport.
    SendKeyPressWithSearch(ui::VKEY_RIGHT);
    SendKeyPressWithSearch(ui::VKEY_RIGHT);
    SendKeyPressWithSearch(ui::VKEY_RIGHT);
  });

  sm_.ExpectSpeech("Heading 2");
  sm_.ExpectSpeech("Heading 3");
  sm_.ExpectSpeech("Heading 4");

  sm_.Call([this, &focus_bounds]() {
    WaitForMagnifierViewportOnBounds(focus_bounds);
  });

  sm_.Replay();
}

}  // namespace ash