chromium/chrome/browser/ash/accessibility/service/accessibility_service_client_browsertest.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/accessibility/service/accessibility_service_client.h"

#include <optional>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/ui/accessibility_confirmation_dialog.h"
#include "ash/accessibility/ui/accessibility_focus_ring_controller_impl.h"
#include "ash/accessibility/ui/accessibility_highlight_layer.h"
#include "ash/keyboard/keyboard_controller_impl.h"
#include "ash/keyboard/ui/keyboard_util.h"
#include "ash/public/cpp/accessibility_focus_ring_info.h"
#include "ash/public/cpp/window_tree_host_lookup.h"
#include "ash/shell.h"
#include "base/command_line.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "chrome/browser/accessibility/service/accessibility_service_router_factory.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/ash/accessibility/service/automation_client_impl.h"
#include "chrome/browser/ash/accessibility/service/fake_accessibility_service.h"
#include "chrome/browser/ash/accessibility/service/speech_recognition_impl.h"
#include "chrome/browser/ash/accessibility/speech_monitor.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/speech/speech_recognition_test_helper.h"
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_controller_client.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/dbus/power/fake_power_manager_client.h"
#include "components/keyed_service/core/keyed_service.h"
#include "content/public/browser/tts_utterance.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/fake_speech_recognition_manager.h"
#include "services/accessibility/public/mojom/accessibility_service.mojom.h"
#include "services/accessibility/public/mojom/speech_recognition.mojom.h"
#include "services/accessibility/public/mojom/tts.mojom.h"
#include "services/accessibility/public/mojom/user_interface.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_tree_host.h"
#include "ui/compositor/layer.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/dom/dom_code.h"

using ax::mojom::AssistiveTechnologyType;

class KeyboardVisibleWaiter : public ChromeKeyboardControllerClient::Observer {
 public:
  explicit KeyboardVisibleWaiter(bool visible) : visible_(visible) {
    ChromeKeyboardControllerClient::Get()->AddObserver(this);
  }

  KeyboardVisibleWaiter(const KeyboardVisibleWaiter&) = delete;
  KeyboardVisibleWaiter& operator=(const KeyboardVisibleWaiter&) = delete;

  ~KeyboardVisibleWaiter() override {
    ChromeKeyboardControllerClient::Get()->RemoveObserver(this);
  }

  void Wait() { run_loop_.Run(); }

  // ChromeKeyboardControllerClient::Observer
  void OnKeyboardVisibilityChanged(bool visible) override {
    if (visible == visible_) {
      run_loop_.QuitWhenIdle();
    }
  }

 private:
  base::RunLoop run_loop_;
  const bool visible_;
};  // namespace

namespace ash {

class DialogShownWaiter {
 public:
  DialogShownWaiter() {
    ash::Shell::Get()
        ->accessibility_controller()
        ->AddShowConfirmationDialogCallbackForTesting(base::BindRepeating(
            &DialogShownWaiter::OnDialogShown, weak_factory_.GetWeakPtr()));
  }

  DialogShownWaiter(const DialogShownWaiter&) = delete;
  DialogShownWaiter& operator=(const DialogShownWaiter&) = delete;

  ~DialogShownWaiter() = default;

  void Wait() { run_loop_.Run(); }

  void OnDialogShown() { run_loop_.QuitWhenIdle(); }

 private:
  base::RunLoop run_loop_;
  base::WeakPtrFactory<DialogShownWaiter> weak_factory_{this};
};  // namespace

namespace {
// Matches max utterance from the TTS extension API.
const int kMaxUtteranceLength = 32768;

// TtsUtteranceClient that will pass along TtsEvents to a repeating callback.
class TtsUtteranceClientImpl : public ax::mojom::TtsUtteranceClient {
 public:
  TtsUtteranceClientImpl(
      mojo::PendingReceiver<ax::mojom::TtsUtteranceClient> pending_receiver,
      base::RepeatingCallback<void(ax::mojom::TtsEventPtr)> event_callback)
      : receiver_(this, std::move(pending_receiver)),
        callback_(std::move(event_callback)) {}

  TtsUtteranceClientImpl(const TtsUtteranceClientImpl&) = delete;
  TtsUtteranceClientImpl& operator=(const TtsUtteranceClientImpl&) = delete;
  ~TtsUtteranceClientImpl() override {}

  void OnEvent(ax::mojom::TtsEventPtr event) override {
    callback_.Run(std::move(event));
  }

 private:
  mojo::Receiver<ax::mojom::TtsUtteranceClient> receiver_;
  base::RepeatingCallback<void(ax::mojom::TtsEventPtr)> callback_;
};

// Mock TtsPlatform that can keep some state about an utterance and
// send events.
class MockTtsPlatformImpl : public content::TtsPlatform {
 public:
  MockTtsPlatformImpl() {
    content::TtsController::SkipAddNetworkChangeObserverForTests(true);
    content::TtsController::GetInstance()->SetTtsPlatform(this);
  }

  MockTtsPlatformImpl(const MockTtsPlatformImpl&) = delete;
  MockTtsPlatformImpl& operator=(const MockTtsPlatformImpl&) = delete;
  ~MockTtsPlatformImpl() {
    content::TtsController::GetInstance()->SetTtsPlatform(
        content::TtsPlatform::GetInstance());
  }

  // content::TtsPlatform:
  bool PlatformImplSupported() override { return true; }

  bool PlatformImplInitialized() override { return true; }

  void WillSpeakUtteranceWithVoice(
      content::TtsUtterance* utterance,
      const content::VoiceData& voice_data) override {}

  void LoadBuiltInTtsEngine(content::BrowserContext* browser_context) override {
  }

  void ClearError() override { error_ = ""; }

  void SetError(const std::string& error) override { error_ = error; }

  std::string GetError() override { return error_; }

  void Speak(int utterance_id,
             const std::string& utterance,
             const std::string& lang,
             const content::VoiceData& voice,
             const content::UtteranceContinuousParameters& params,
             base::OnceCallback<void(bool)> speech_started_callback) override {
    utterance_id_ = utterance_id;
    utterance_ = utterance;
    lang_ = lang;
    voice_ = voice;
    params_ = params;
    content::TtsController::GetInstance()->OnTtsEvent(
        utterance_id, /*event_type=*/content::TTS_EVENT_START, /*char_index=*/0,
        /*length=*/static_cast<int>(utterance.size()),
        /*error_message=*/std::string());
    if (next_utterance_error_.empty()) {
      std::move(speech_started_callback).Run(true);
      return;
    }
    SetError(next_utterance_error_);
    next_utterance_error_ = "";
    content::TtsController::GetInstance()->OnTtsEvent(
        utterance_id, /*event_type=*/content::TTS_EVENT_ERROR, /*char_index=*/0,
        /*length=*/-1, /*error_message=*/GetError());
    std::move(speech_started_callback).Run(false);
  }

  bool StopSpeaking() override {
    if (utterance_id_ != -1) {
      content::TtsController::GetInstance()->OnTtsEvent(
          utterance_id_, /*event_type*/ content::TTS_EVENT_INTERRUPTED,
          /*char_index=*/0, /*length=*/0, /*error_message=*/"");
      utterance_id_ = -1;
      utterance_ = "";
      return true;
    }
    return false;
  }

  void Pause() override {
    content::TtsController::GetInstance()->OnTtsEvent(
        utterance_id_, /*event_type=*/content::TTS_EVENT_PAUSE,
        /*char_index=*/3, /*length=*/4, /*error_message=*/"");
  }

  void Resume() override {
    content::TtsController::GetInstance()->OnTtsEvent(
        utterance_id_, /*event_type=*/content::TTS_EVENT_RESUME,
        /*char_index=*/3, /*length=*/4, /*error_message=*/"");
  }

  bool IsSpeaking() override { return utterance_id_ != -1; }

  void GetVoices(std::vector<content::VoiceData>* voices) override {
    for (int i = 0; i < 3; i++) {
      voices->emplace_back();
      content::VoiceData& voice = voices->back();
      voice.native = true;
      voice.name = "TestyMcTestFace" + base::NumberToString(i);
      voice.lang = "en-NZ";
      voice.engine_id = extension_misc::kGoogleSpeechSynthesisExtensionId;
      voice.events.insert(content::TTS_EVENT_END);
      voice.events.insert(content::TTS_EVENT_START);
      voice.events.insert(content::TTS_EVENT_PAUSE);
      voice.events.insert(content::TTS_EVENT_RESUME);
      voice.events.insert(content::TTS_EVENT_INTERRUPTED);
      voice.events.insert(content::TTS_EVENT_WORD);
      voice.events.insert(content::TTS_EVENT_SENTENCE);
      voice.events.insert(content::TTS_EVENT_MARKER);
      voice.events.insert(content::TTS_EVENT_CANCELLED);
      voice.events.insert(content::TTS_EVENT_ERROR);
    }
  }

  void Shutdown() override {}

  void FinalizeVoiceOrdering(std::vector<content::VoiceData>& voices) override {
  }

  void RefreshVoices() override {}

  content::ExternalPlatformDelegate* GetExternalPlatformDelegate() override {
    return nullptr;
  }

  // Methods for testing.
  void SendEvent(content::TtsEventType event_type,
                 int char_index,
                 int length,
                 const std::string& error_message) {
    ASSERT_NE(utterance_id_, -1);
    content::TtsController::GetInstance()->OnTtsEvent(
        utterance_id_, event_type, char_index, length, error_message);
  }
  void SetNextUtteranceError(const std::string& error) {
    next_utterance_error_ = error;
  }
  const std::string& lang() { return lang_; }
  const content::VoiceData& voice() { return voice_; }
  const content::UtteranceContinuousParameters& params() { return params_; }

 private:
  std::string utterance_ = "";
  int utterance_id_ = -1;
  std::string lang_ = "";
  content::VoiceData voice_;
  content::UtteranceContinuousParameters params_;
  std::string error_ = "";
  std::string next_utterance_error_ = "";
};

class TestEventHandler : public ui::EventHandler {
 public:
  explicit TestEventHandler(base::RepeatingClosure callback)
      : callback_(callback) {
    Shell::Get()->AddPreTargetHandler(this);
  }
  ~TestEventHandler() override { Shell::Get()->RemovePreTargetHandler(this); }

  // ui::EventHandler:
  void OnKeyEvent(ui::KeyEvent* event) override {
    // Make a copy of the event, so it's valid outside this function context.
    key_events.push_back(std::make_unique<ui::KeyEvent>(event));
    callback_.Run();
  }

  void OnMouseEvent(ui::MouseEvent* event) override {
    // Make a copy of the event, so it's valid outside this function context.
    mouse_events.push_back(std::make_unique<ui::MouseEvent>(event));
    callback_.Run();
  }

  std::vector<std::unique_ptr<ui::KeyEvent>> key_events;
  std::vector<std::unique_ptr<ui::MouseEvent>> mouse_events;

 private:
  base::RepeatingClosure callback_;
};

}  // namespace

// Tests for the AccessibilityServiceClientTest using a fake service
// implemented in FakeAccessibilityService.
class AccessibilityServiceClientTest : public InProcessBrowserTest {
 public:
  AccessibilityServiceClientTest() = default;
  AccessibilityServiceClientTest(const AccessibilityServiceClientTest&) =
      delete;
  AccessibilityServiceClientTest& operator=(
      const AccessibilityServiceClientTest&) = delete;
  ~AccessibilityServiceClientTest() override = default;

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

  void SetUp() override {
    content::TtsController::SkipAddNetworkChangeObserverForTests(true);
    InProcessBrowserTest::SetUp();
  }

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    // Replaces normal AccessibilityService with a fake one.
    ax::AccessibilityServiceRouterFactory::GetInstanceForTest()
        ->SetTestingFactoryAndUse(
            browser()->profile(),
            base::BindRepeating(
                &AccessibilityServiceClientTest::CreateTestAccessibilityService,
                base::Unretained(this)));
    sr_test_helper_ = std::make_unique<SpeechRecognitionTestHelper>(
        speech::SpeechRecognitionType::kNetwork,
        media::mojom::RecognizerClientType::kDictation);
    sr_test_helper_->SetUp(browser()->profile());
  }

  void TearDownOnMainThread() override {
    content::SpeechRecognitionManager::SetManagerForTesting(nullptr);
    InProcessBrowserTest::TearDownOnMainThread();
  }

 protected:
  AccessibilityServiceClient* Client() {
    AccessibilityManager* accessibility_manager = AccessibilityManager::Get();
    return accessibility_manager->accessibility_service_client_.get();
  }

  UserInputImpl* UserInputClient() {
    return Client()->user_input_client_.get();
  }

  bool ServiceHasATEnabled(AssistiveTechnologyType type) {
    std::set<AssistiveTechnologyType> enabled_ATs =
        fake_service_->GetEnabledATs();
    return enabled_ATs.find(type) != enabled_ATs.end();
  }

  bool ServiceIsBound() { return fake_service_->IsBound(); }

  void ToggleAutomationEnabled(AccessibilityServiceClient* client,
                               bool enabled) {
    if (enabled)
      client->automation_client_->Enable(base::DoNothing());
    else
      client->automation_client_->Disable();
  }

  // TODO(crbug.com/40936728): Toggle features on AccessibilityManager for
  // client test.
  void TurnOnAccessibilityService(AssistiveTechnologyType type) {
    switch (type) {
      case ax::mojom::AssistiveTechnologyType::kUnknown:
        NOTREACHED_IN_MIGRATION() << "Unknown AT type";
        break;
      case ax::mojom::AssistiveTechnologyType::kChromeVox:
        Client()->SetChromeVoxEnabled(true);
        break;
      case ax::mojom::AssistiveTechnologyType::kSelectToSpeak:
        Client()->SetSelectToSpeakEnabled(true);
        break;
      case ax::mojom::AssistiveTechnologyType::kSwitchAccess:
        Client()->SetSwitchAccessEnabled(true);
        break;
      case ax::mojom::AssistiveTechnologyType::kAutoClick:
        Client()->SetAutoclickEnabled(true);
        break;
      case ax::mojom::AssistiveTechnologyType::kMagnifier:
        Client()->SetMagnifierEnabled(true);
        break;
      case ax::mojom::AssistiveTechnologyType::kDictation:
        Client()->SetDictationEnabled(true);
        break;
    }
    EXPECT_TRUE(ServiceHasATEnabled(type));
  }

  // Unowned.
  raw_ptr<FakeAccessibilityService, DanglingUntriaged> fake_service_ = nullptr;

  std::unique_ptr<SpeechRecognitionTestHelper> sr_test_helper_;

 private:
  std::unique_ptr<KeyedService> CreateTestAccessibilityService(
      content::BrowserContext* context) {
    std::unique_ptr<FakeAccessibilityService> fake_service =
        std::make_unique<FakeAccessibilityService>();
    fake_service_ = fake_service.get();
    return std::move(fake_service);
  }

  base::test::ScopedFeatureList scoped_feature_list_;
};

// Test that nothing crashes if the profile isn't set yet.
// Note that this should never happen as enabling/disabling
// features from AccessibilityManager will only happen when
// there is a profile.
IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       DoesNotCrashWithNoProfile) {
  Client()->SetProfile(nullptr);
  Client()->SetChromeVoxEnabled(true);
  Client()->SetSelectToSpeakEnabled(true);

  EXPECT_FALSE(ServiceIsBound());
}

// AccessibilityServiceClient shouldn't try to use the service
// when features are all disabled.
IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       DoesNotCreateServiceForDisabledFeatures) {
  EXPECT_FALSE(ServiceIsBound());

  Client()->SetChromeVoxEnabled(false);
  EXPECT_FALSE(ServiceIsBound());

  Client()->SetDictationEnabled(false);
  EXPECT_FALSE(ServiceIsBound());
}

// Test that any previously enabled features are copied when
// the profile changes.
IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       CopiesFeaturesWhenProfileChanges) {
  Client()->SetProfile(nullptr);
  Client()->SetChromeVoxEnabled(true);
  Client()->SetSwitchAccessEnabled(true);
  Client()->SetAutoclickEnabled(true);
  Client()->SetAutoclickEnabled(false);

  // Service isn't constructed yet because there is no profile.
  EXPECT_FALSE(ServiceIsBound());

  Client()->SetProfile(browser()->profile());

  ASSERT_TRUE(ServiceIsBound());
  EXPECT_TRUE(ServiceHasATEnabled(AssistiveTechnologyType::kChromeVox));
  EXPECT_TRUE(ServiceHasATEnabled(AssistiveTechnologyType::kSwitchAccess));
  EXPECT_FALSE(ServiceHasATEnabled(AssistiveTechnologyType::kAutoClick));
}

// Test that the AccessibilityServiceClient can toggle features in the service
// using the mojom interface.
IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       TogglesAccessibilityFeatures) {
  EXPECT_FALSE(ServiceHasATEnabled(AssistiveTechnologyType::kChromeVox));
  EXPECT_FALSE(ServiceHasATEnabled(AssistiveTechnologyType::kSelectToSpeak));
  EXPECT_FALSE(ServiceHasATEnabled(AssistiveTechnologyType::kSwitchAccess));
  EXPECT_FALSE(ServiceHasATEnabled(AssistiveTechnologyType::kAutoClick));
  EXPECT_FALSE(ServiceHasATEnabled(AssistiveTechnologyType::kDictation));
  EXPECT_FALSE(ServiceHasATEnabled(AssistiveTechnologyType::kMagnifier));

  // The first time we enable/disable an AT, the AT controller should be bound
  // with the enabled AT type.
  Client()->SetChromeVoxEnabled(true);
  fake_service_->WaitForATChangeCount(1);
  EXPECT_TRUE(ServiceHasATEnabled(AssistiveTechnologyType::kChromeVox));
  Client()->SetSelectToSpeakEnabled(true);
  fake_service_->WaitForATChangeCount(2);
  EXPECT_TRUE(ServiceHasATEnabled(AssistiveTechnologyType::kSelectToSpeak));
  Client()->SetSwitchAccessEnabled(true);
  fake_service_->WaitForATChangeCount(3);
  EXPECT_TRUE(ServiceHasATEnabled(AssistiveTechnologyType::kSwitchAccess));
  Client()->SetAutoclickEnabled(true);
  fake_service_->WaitForATChangeCount(4);
  EXPECT_TRUE(ServiceHasATEnabled(AssistiveTechnologyType::kAutoClick));
  Client()->SetDictationEnabled(true);
  fake_service_->WaitForATChangeCount(5);
  EXPECT_TRUE(ServiceHasATEnabled(AssistiveTechnologyType::kDictation));
  Client()->SetMagnifierEnabled(true);
  fake_service_->WaitForATChangeCount(6);
  EXPECT_TRUE(ServiceHasATEnabled(AssistiveTechnologyType::kMagnifier));
  Client()->SetChromeVoxEnabled(false);
  fake_service_->WaitForATChangeCount(7);
  EXPECT_FALSE(ServiceHasATEnabled(AssistiveTechnologyType::kChromeVox));
  Client()->SetSelectToSpeakEnabled(false);
  fake_service_->WaitForATChangeCount(8);
  EXPECT_FALSE(ServiceHasATEnabled(AssistiveTechnologyType::kSelectToSpeak));
  Client()->SetSwitchAccessEnabled(false);
  fake_service_->WaitForATChangeCount(9);
  EXPECT_FALSE(ServiceHasATEnabled(AssistiveTechnologyType::kSwitchAccess));
  Client()->SetAutoclickEnabled(false);
  fake_service_->WaitForATChangeCount(10);
  EXPECT_FALSE(ServiceHasATEnabled(AssistiveTechnologyType::kAutoClick));
  Client()->SetDictationEnabled(false);
  fake_service_->WaitForATChangeCount(11);
  EXPECT_FALSE(ServiceHasATEnabled(AssistiveTechnologyType::kDictation));
  Client()->SetMagnifierEnabled(false);
  fake_service_->WaitForATChangeCount(12);
  EXPECT_FALSE(ServiceHasATEnabled(AssistiveTechnologyType::kMagnifier));
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       SendsAutomationToTheService) {
  // Enable an assistive technology. The service will not be started until
  // some AT needs it.
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);

  // The service may bind multiple Automations to the AutomationClient.
  for (int i = 0; i < 3; i++) {
    fake_service_->BindAnotherAutomation();
  }

  // TODO(crbug.com/1355633): Replace once mojom to Enable lands.
  ToggleAutomationEnabled(Client(), true);
  // Enable can be called multiple times (once for each bound Automation)
  // with no bad effects.
  // fake_service_->AutomationClientEnable(true);

  // Real accessibility events should have come through.
  fake_service_->WaitForAutomationEvents();

  // TODO(crbug.com/1355633): Replace once mojom to Disable lands.
  ToggleAutomationEnabled(Client(), false);
  // Disabling multiple times has no bad effect.
  // fake_service_->AutomationClientEnable(false);
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       DevToolsAgentHostCreated) {
  // Enable an assistive technology. The service will not be started until
  // some AT needs it.
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);

  // A single agent host should have been created for chromevox.
  auto count = fake_service_->GetDevtoolsConnectionCount(
      AssistiveTechnologyType::kChromeVox);
  EXPECT_EQ(count, 1);
  // Disable and re-enable
  Client()->SetChromeVoxEnabled(false);
  Client()->SetChromeVoxEnabled(true);
  count = fake_service_->GetDevtoolsConnectionCount(
      AssistiveTechnologyType::kChromeVox);
  EXPECT_EQ(count, 2);
  // Different AT
  Client()->SetSelectToSpeakEnabled(true);
  count = fake_service_->GetDevtoolsConnectionCount(
      AssistiveTechnologyType::kSelectToSpeak);
  EXPECT_EQ(count, 1);
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsGetVoices) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kSelectToSpeak);
  MockTtsPlatformImpl tts_platform;

  fake_service_->BindAnotherTts();

  base::RunLoop waiter;
  fake_service_->RequestTtsVoices(base::BindLambdaForTesting(
      [&waiter](std::vector<ax::mojom::TtsVoicePtr> voices) {
        waiter.Quit();
        ASSERT_EQ(voices.size(), 3u);
        auto& voice = voices[0];
        EXPECT_EQ(voice->voice_name, "TestyMcTestFace0");
        EXPECT_EQ(voice->engine_id,
                  extension_misc::kGoogleSpeechSynthesisExtensionId);
        ASSERT_TRUE(voice->event_types);
        ASSERT_EQ(voice->event_types.value().size(), 10u);
        // Spot check.
        EXPECT_EQ(voice->event_types.value()[0],
                  ax::mojom::TtsEventType::kStart);
        EXPECT_EQ(voice->event_types.value()[1], ax::mojom::TtsEventType::kEnd);
      }));
  waiter.Run();

  // The service may bind multiple TTS without crashing.
  for (int i = 0; i < 2; i++) {
    fake_service_->BindAnotherTts();
  }
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsSpeakSimple) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kSelectToSpeak);
  test::SpeechMonitor sm;

  fake_service_->BindAnotherTts();
  fake_service_->RequestSpeak("Hello, world", base::DoNothing());
  sm.ExpectSpeech("Hello, world");
  sm.Replay();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsSendsStartEndEvents) {
  test::SpeechMonitor sm;
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherTts();

  base::RunLoop waiter;
  int start_count = 0;
  int end_count = 0;
  std::string text = "Hello, world";

  // This callback is called on tts events.
  // See SpeechMonitor for when tts events are sent.
  base::RepeatingCallback<void(ax::mojom::TtsEventPtr event)> callback =
      base::BindLambdaForTesting([&waiter, &start_count, &end_count,
                                  &text](ax::mojom::TtsEventPtr event) {
        if (event->type == ax::mojom::TtsEventType::kStart) {
          start_count++;
          EXPECT_EQ(end_count, 0);
          EXPECT_EQ(0, event->char_index);
          EXPECT_FALSE(event->is_final);
        } else if (event->type == ax::mojom::TtsEventType::kEnd) {
          end_count++;
          EXPECT_EQ(start_count, 1);
          EXPECT_EQ(static_cast<int>(text.size()), event->char_index);
          EXPECT_TRUE(event->is_final);
          waiter.Quit();
        }
      });

  std::unique_ptr<TtsUtteranceClientImpl> utterance_client;
  fake_service_->RequestSpeak(
      text,
      base::BindLambdaForTesting(
          [&utterance_client, &callback](ax::mojom::TtsSpeakResultPtr result) {
            EXPECT_EQ(result->error, ax::mojom::TtsError::kNoError);
            utterance_client = std::make_unique<TtsUtteranceClientImpl>(
                std::move(result->utterance_client), std::move(callback));
          }));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsPauseResume) {
  MockTtsPlatformImpl tts_platform;
  TurnOnAccessibilityService(AssistiveTechnologyType::kSelectToSpeak);
  fake_service_->BindAnotherTts();

  base::RunLoop waiter;
  int start_count = 0;
  int pause_count = 0;
  int resume_count = 0;
  int interrupted_count = 0;
  std::string text = "Hello, world";

  // This callback is called on tts events.
  base::RepeatingCallback<void(ax::mojom::TtsEventPtr event)> callback =
      base::BindLambdaForTesting(
          [&waiter, &start_count, &pause_count, &resume_count,
           &interrupted_count](ax::mojom::TtsEventPtr event) {
            if (event->type == ax::mojom::TtsEventType::kStart) {
              start_count++;
              EXPECT_EQ(pause_count, 0);
              EXPECT_EQ(resume_count, 0);
              EXPECT_EQ(interrupted_count, 0);
              EXPECT_EQ(0, event->char_index);
              EXPECT_FALSE(event->is_final);
            } else if (event->type == ax::mojom::TtsEventType::kPause) {
              pause_count++;
              EXPECT_EQ(resume_count, 0);
              EXPECT_EQ(interrupted_count, 0);
              EXPECT_FALSE(event->is_final);
            } else if (event->type == ax::mojom::TtsEventType::kResume) {
              resume_count++;
              EXPECT_EQ(interrupted_count, 0);
              EXPECT_FALSE(event->is_final);
            } else if (event->type == ax::mojom::TtsEventType::kInterrupted) {
              interrupted_count++;
              EXPECT_TRUE(event->is_final);
              waiter.Quit();
            }
          });

  std::unique_ptr<TtsUtteranceClientImpl> utterance_client;
  fake_service_->RequestSpeak(
      text,
      base::BindLambdaForTesting([&utterance_client, &callback,
                                  this](ax::mojom::TtsSpeakResultPtr result) {
        ASSERT_EQ(result->error, ax::mojom::TtsError::kNoError);
        utterance_client = std::make_unique<TtsUtteranceClientImpl>(
            std::move(result->utterance_client), std::move(callback));
        fake_service_->RequestPause();
        fake_service_->RequestResume();
        fake_service_->RequestStop();
      }));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsIsSpeaking) {
  MockTtsPlatformImpl tts_platform;
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherTts();

  base::RunLoop waiter;
  std::string text = "Hello, world";

  fake_service_->RequestSpeak(
      text, base::BindLambdaForTesting(
                [&waiter, this](ax::mojom::TtsSpeakResultPtr result) {
                  ASSERT_EQ(result->error, ax::mojom::TtsError::kNoError);
                  fake_service_->IsTtsSpeaking(
                      base::BindLambdaForTesting([&waiter](bool is_speaking) {
                        EXPECT_TRUE(is_speaking);
                        waiter.Quit();
                      }));
                }));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsIsNotSpeaking) {
  MockTtsPlatformImpl tts_platform;
  TurnOnAccessibilityService(AssistiveTechnologyType::kSelectToSpeak);
  fake_service_->BindAnotherTts();

  base::RunLoop waiter;

  fake_service_->IsTtsSpeaking(
      base::BindLambdaForTesting([&waiter](bool is_speaking) {
        EXPECT_FALSE(is_speaking);
        waiter.Quit();
      }));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsMaxUtteranceError) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kSelectToSpeak);
  fake_service_->BindAnotherTts();
  base::RunLoop waiter;

  fake_service_->RequestSpeak(
      std::string(kMaxUtteranceLength + 1, 'a'),
      base::BindLambdaForTesting([&waiter](
                                     ax::mojom::TtsSpeakResultPtr result) {
        EXPECT_EQ(result->error, ax::mojom::TtsError::kErrorUtteranceTooLong);
        waiter.Quit();
      }));

  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsUtteranceError) {
  MockTtsPlatformImpl tts_platform;
  tts_platform.SetNextUtteranceError("One does not simply walk into Mordor");
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherTts();

  base::RunLoop waiter;

  // This callback is called on tts events.
  base::RepeatingCallback<void(ax::mojom::TtsEventPtr event)> callback =
      base::BindLambdaForTesting([&waiter](ax::mojom::TtsEventPtr event) {
        if (event->type == ax::mojom::TtsEventType::kStart) {
          return;
        }
        EXPECT_EQ(event->type, ax::mojom::TtsEventType::kError);
        EXPECT_EQ(event->error_message, "One does not simply walk into Mordor");
        waiter.Quit();
      });

  std::unique_ptr<TtsUtteranceClientImpl> utterance_client;
  fake_service_->RequestSpeak(
      "All we have to decide is what to do with the time that is given to us.",
      base::BindLambdaForTesting(
          [&utterance_client, &callback](ax::mojom::TtsSpeakResultPtr result) {
            utterance_client = std::make_unique<TtsUtteranceClientImpl>(
                std::move(result->utterance_client), std::move(callback));
          }));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsOptions) {
  MockTtsPlatformImpl tts_platform;
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherTts();
  base::RunLoop waiter;

  auto options = ax::mojom::TtsOptions::New();
  options->rate = .5;
  options->pitch = 1.5;
  options->volume = .8;
  options->enqueue = true;
  options->voice_name = "TestyMcTestFace2";
  options->engine_id = extension_misc::kGoogleSpeechSynthesisExtensionId;
  options->lang = "en-NZ";
  options->on_event = false;

  fake_service_->RequestSpeak(
      "I can't recall the taste of strawberries", std::move(options),
      base::BindLambdaForTesting([&waiter, &tts_platform](
                                     ax::mojom::TtsSpeakResultPtr result) {
        waiter.Quit();
        content::UtteranceContinuousParameters params = tts_platform.params();
        EXPECT_EQ(params.rate, .5);
        EXPECT_EQ(params.pitch, 1.5);
        EXPECT_EQ(params.volume, .8);
        content::VoiceData voice = tts_platform.voice();
        EXPECT_EQ(voice.name, "TestyMcTestFace2");
        EXPECT_EQ(tts_platform.lang(), "en-NZ");
      }));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsOptionsPitchError) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherTts();
  base::RunLoop waiter;
  auto options = ax::mojom::TtsOptions::New();
  options->pitch = 3.0;

  fake_service_->RequestSpeak(
      "You shall not pass", std::move(options),
      base::BindLambdaForTesting(
          [&waiter](ax::mojom::TtsSpeakResultPtr result) {
            waiter.Quit();
            EXPECT_EQ(result->error, ax::mojom::TtsError::kErrorInvalidPitch);
          }));

  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsOptionsRateError) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherTts();
  base::RunLoop waiter;
  auto options = ax::mojom::TtsOptions::New();
  options->rate = 0.01;
  fake_service_->RequestSpeak(
      "For frodo", std::move(options),
      base::BindLambdaForTesting(
          [&waiter](ax::mojom::TtsSpeakResultPtr result) {
            waiter.Quit();
            EXPECT_EQ(result->error, ax::mojom::TtsError::kErrorInvalidRate);
          }));

  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsOptionsVolumeError) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherTts();
  base::RunLoop waiter;
  auto options = ax::mojom::TtsOptions::New();
  options->volume = 1.5;
  fake_service_->RequestSpeak(
      "The board is set. The pieces are moving.", std::move(options),
      base::BindLambdaForTesting(
          [&waiter](ax::mojom::TtsSpeakResultPtr result) {
            waiter.Quit();
            EXPECT_EQ(result->error, ax::mojom::TtsError::kErrorInvalidVolume);
          }));

  waiter.Run();
}

// Starts two requests for speech, the second starting just after the first
// is in progress. With the option to enqueue, they should not interrupt.
IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsEnqueue) {
  MockTtsPlatformImpl tts_platform;
  TurnOnAccessibilityService(AssistiveTechnologyType::kSelectToSpeak);
  fake_service_->BindAnotherTts();
  base::RunLoop waiter;

  base::RepeatingCallback<void(ax::mojom::TtsEventPtr event)> first_callback =
      base::BindLambdaForTesting([](ax::mojom::TtsEventPtr event) {
        EXPECT_EQ(event->type, ax::mojom::TtsEventType::kStart);
      });
  auto first_options = ax::mojom::TtsOptions::New();
  first_options->enqueue = true;
  first_options->on_event = true;
  std::unique_ptr<TtsUtteranceClientImpl> first_utterance_client;
  fake_service_->RequestSpeak(
      "Shadowfax, show us the meaning of haste.", std::move(first_options),
      base::BindLambdaForTesting([&first_callback, &first_utterance_client](
                                     ax::mojom::TtsSpeakResultPtr result) {
        first_utterance_client = std::make_unique<TtsUtteranceClientImpl>(
            std::move(result->utterance_client), std::move(first_callback));
      }));
  EXPECT_EQ(content::TtsController::GetInstance()->QueueSize(), 0);

  auto second_options = ax::mojom::TtsOptions::New();
  second_options->enqueue = true;
  second_options->on_event = true;
  std::unique_ptr<TtsUtteranceClientImpl> second_utterance_client;
  fake_service_->RequestSpeak(
      "Keep it secret. Keep it safe.", std::move(second_options),
      base::BindLambdaForTesting(
          [&waiter](ax::mojom::TtsSpeakResultPtr result) {
            EXPECT_EQ(content::TtsController::GetInstance()->QueueSize(), 1);
            waiter.Quit();
          }));
  waiter.Run();
}

// Starts two requests for speech, the second starting just after the first
// is in progress. With the the option to enqueue false, the second interrupts
// the first.
IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, TtsInterrupt) {
  MockTtsPlatformImpl tts_platform;
  TurnOnAccessibilityService(AssistiveTechnologyType::kSelectToSpeak);
  fake_service_->BindAnotherTts();
  base::RunLoop waiter;
  int start_count = 0;
  base::RepeatingCallback<void(ax::mojom::TtsEventPtr event)> first_callback =
      base::BindLambdaForTesting([&start_count](ax::mojom::TtsEventPtr event) {
        if (event->type == ax::mojom::TtsEventType::kStart) {
          // The first event should be started.
          EXPECT_EQ(start_count, 0);
          start_count++;
          return;
        }
        // And then interrupted.
        EXPECT_EQ(event->type, ax::mojom::TtsEventType::kInterrupted);
      });
  auto first_options = ax::mojom::TtsOptions::New();
  first_options->enqueue = true;
  first_options->on_event = true;
  std::unique_ptr<TtsUtteranceClientImpl> first_utterance_client;
  fake_service_->RequestSpeak(
      "Shadowfax, show us the meaning of haste.", std::move(first_options),
      base::BindLambdaForTesting([&first_callback, &first_utterance_client](
                                     ax::mojom::TtsSpeakResultPtr result) {
        first_utterance_client = std::make_unique<TtsUtteranceClientImpl>(
            std::move(result->utterance_client), std::move(first_callback));
      }));

  base::RepeatingCallback<void(ax::mojom::TtsEventPtr event)> second_callback =
      base::BindLambdaForTesting(
          [&start_count, &waiter](ax::mojom::TtsEventPtr event) {
            EXPECT_EQ(event->type, ax::mojom::TtsEventType::kStart);
            // The second utterance should start after the first started.
            EXPECT_EQ(start_count, 1);
            waiter.Quit();
          });

  auto second_options = ax::mojom::TtsOptions::New();
  second_options->enqueue = false;
  second_options->on_event = true;
  std::unique_ptr<TtsUtteranceClientImpl> second_utterance_client;
  fake_service_->RequestSpeak(
      "Keep it secret. Keep it safe.", std::move(second_options),
      base::BindLambdaForTesting([&second_utterance_client, &second_callback](
                                     ax::mojom::TtsSpeakResultPtr result) {
        EXPECT_EQ(content::TtsController::GetInstance()->QueueSize(), 0);
        second_utterance_client = std::make_unique<TtsUtteranceClientImpl>(
            std::move(result->utterance_client), std::move(second_callback));
      }));

  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, DarkenScreen) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherUserInterface();

  base::RunLoop waiter;
  AccessibilityManager::Get()->SetScreenDarkenObserverForTest(
      base::BindLambdaForTesting([&waiter] {
        waiter.Quit();

        EXPECT_TRUE(
            chromeos::FakePowerManagerClient::Get()->backlights_forced_off());
      }));

  fake_service_->RequestDarkenScreen(true);

  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, OpenSettingsSubpage) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherUserInterface();

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

  fake_service_->RequestOpenSettingsSubpage("manageAccessibility/tts");

  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       AcceptConfirmationDialog) {
  // Initialize ATP.
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherUserInterface();

  // Request confirmation dialog.
  base::RunLoop waiter;
  fake_service_->RequestShowConfirmationDialog(
      "Order Confirmation", "Ok to purchase 10,000 lbs of canned corn?",
      "No, thank you", base::BindLambdaForTesting([&waiter](bool confirmed) {
        waiter.Quit();
        EXPECT_TRUE(confirmed);
      }));

  // Wait for dialog shown.
  DialogShownWaiter().Wait();

  // Verify dialog was created.
  AccessibilityConfirmationDialog* dialog =
      Shell::Get()->accessibility_controller()->GetConfirmationDialogForTest();
  ASSERT_NE(dialog, nullptr);

  // Verify title.
  EXPECT_EQ(dialog->GetWindowTitle(), u"Order Confirmation");

  // Accept dialog.
  dialog->AcceptDialog();

  // Wait for dialog callback and closing.
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       CancelConfirmationDialog) {
  // Initialize ATP.
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherUserInterface();

  // Request confirmation dialog.
  base::RunLoop waiter;
  fake_service_->RequestShowConfirmationDialog(
      "Order Confirmation", "Ok to purchase 10,000 lbs of canned corn?",
      "No, thank you", base::BindLambdaForTesting([&waiter](bool confirmed) {
        waiter.Quit();
        EXPECT_FALSE(confirmed);
      }));

  // Wait for dialog shown.
  DialogShownWaiter().Wait();

  // Verify dialog was created.
  AccessibilityConfirmationDialog* dialog =
      Shell::Get()->accessibility_controller()->GetConfirmationDialogForTest();
  ASSERT_NE(dialog, nullptr);

  // Verify title.
  EXPECT_EQ(dialog->GetWindowTitle(), u"Order Confirmation");

  // Cancel dialog.
  dialog->CancelDialog();

  // Wait for dialog callback and closing.
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       CloseConfirmationDialog) {
  // Initialize ATP.
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherUserInterface();

  // Request confirmation dialog.
  base::RunLoop waiter;
  fake_service_->RequestShowConfirmationDialog(
      "Order Confirmation", "Ok to purchase 10,000 lbs of canned corn?",
      "No, thank you", base::BindLambdaForTesting([&waiter](bool confirmed) {
        waiter.Quit();
        EXPECT_FALSE(confirmed);
      }));

  // Wait for dialog shown.
  DialogShownWaiter().Wait();

  // Verify dialog was created.
  AccessibilityConfirmationDialog* dialog =
      Shell::Get()->accessibility_controller()->GetConfirmationDialogForTest();
  ASSERT_NE(dialog, nullptr);

  // Verify title.
  EXPECT_EQ(dialog->GetWindowTitle(), u"Order Confirmation");

  // Close dialog.
  dialog->GetWidget()->CloseWithReason(
      views::Widget::ClosedReason::kCloseButtonClicked);

  // Wait for callback and closing.
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       AutoCloseSecondConfirmationDialog) {
  // If a dialog is already being shown, we do not show a new one.
  // Instead, we return false through the callback on the new dialog
  // to indicate it was closed without the user taking any action.
  // (See the implementation in user_interface_impl.cc and
  // accessibility_controller_impl.cc)

  // Initialize ATP.
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherUserInterface();

  // Request first confirmation dialog.
  base::RunLoop waiter1;
  fake_service_->RequestShowConfirmationDialog(
      "Order Confirmation", "Ok to purchase 10,000 lbs of canned corn?",
      "No, thank you", base::BindLambdaForTesting([&waiter1](bool confirmed) {
        waiter1.Quit();
        EXPECT_TRUE(confirmed);
      }));

  // Wait for dialog shown.
  DialogShownWaiter().Wait();

  // Verify dialog was created.
  AccessibilityConfirmationDialog* dialog =
      Shell::Get()->accessibility_controller()->GetConfirmationDialogForTest();
  ASSERT_NE(dialog, nullptr);

  // Verify title.
  EXPECT_EQ(dialog->GetWindowTitle(), u"Order Confirmation");

  // Request second confirmation dialog.
  base::RunLoop waiter2;
  fake_service_->RequestShowConfirmationDialog(
      "Are we there yet?",
      "Do you confirm that the journey to the destination is completed?", "No",
      base::BindLambdaForTesting([&waiter2](bool confirmed) {
        waiter2.Quit();
        EXPECT_FALSE(confirmed);
      }));

  // Wait for second dialog to get automatically closed.
  waiter2.Run();

  // Verify current dialog is first dialog.
  dialog =
      Shell::Get()->accessibility_controller()->GetConfirmationDialogForTest();
  EXPECT_EQ(dialog->GetWindowTitle(), u"Order Confirmation");

  // Accept first dialog.
  dialog->AcceptDialog();

  // Wait for first dialog callback and closing.
  waiter1.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       OpenSecondConfirmationDialogAfterClosingFirst) {
  // Initialize ATP.
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherUserInterface();

  // Request first confirmation dialog.
  base::RunLoop waiter1;
  fake_service_->RequestShowConfirmationDialog(
      "Order Confirmation", "Ok to purchase 10,000 lbs of canned corn?",
      "No, thank you", base::BindLambdaForTesting([&waiter1](bool confirmed) {
        waiter1.Quit();
        EXPECT_TRUE(confirmed);
      }));

  // Wait for dialog shown.
  DialogShownWaiter().Wait();

  // Verify dialog was created.
  AccessibilityConfirmationDialog* dialog =
      Shell::Get()->accessibility_controller()->GetConfirmationDialogForTest();
  ASSERT_NE(dialog, nullptr);

  // Verify first dialog title.
  EXPECT_EQ(dialog->GetWindowTitle(), u"Order Confirmation");

  // Accept first dialog.
  dialog->AcceptDialog();

  // Wait for first dialog callback and closing.
  waiter1.Run();

  // Request second confirmation dialog.
  base::RunLoop waiter2;
  fake_service_->RequestShowConfirmationDialog(
      "Are we there yet?",
      "Do you confirm that the journey to the destination is completed?", "No",
      base::BindLambdaForTesting([&waiter2](bool confirmed) {
        waiter2.Quit();
        EXPECT_TRUE(confirmed);
      }));

  // Wait for second dialog shown.
  DialogShownWaiter().Wait();

  // Verify second dialog was created.
  dialog =
      Shell::Get()->accessibility_controller()->GetConfirmationDialogForTest();
  ASSERT_NE(dialog, nullptr);

  // Verify second dialog title.
  EXPECT_EQ(dialog->GetWindowTitle(), u"Are we there yet?");

  // Accept second dialog.
  dialog->AcceptDialog();

  // Wait for second dialog callback and closing.
  waiter2.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, SetFocusRings) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kSwitchAccess);
  fake_service_->BindAnotherUserInterface();

  AccessibilityFocusRingControllerImpl* controller =
      Shell::Get()->accessibility_focus_ring_controller();
  controller->SetNoFadeForTesting();

  std::string focus_ring_id1 = AccessibilityManager::Get()->GetFocusRingId(
      ax::mojom::AssistiveTechnologyType::kSwitchAccess, "");
  const AccessibilityFocusRingGroup* focus_ring_group1 =
      controller->GetFocusRingGroupForTesting(focus_ring_id1);
  std::string focus_ring_id2 = AccessibilityManager::Get()->GetFocusRingId(
      ax::mojom::AssistiveTechnologyType::kSwitchAccess, "mySpoonIsTooBig");
  const AccessibilityFocusRingGroup* focus_ring_group2 =
      controller->GetFocusRingGroupForTesting(focus_ring_id2);
  // No focus rings to start.
  EXPECT_EQ(nullptr, focus_ring_group1);
  EXPECT_EQ(nullptr, focus_ring_group2);

  // Number of times the focus ring observer is called.
  int count = 0;

  base::RunLoop waiter;
  AccessibilityManager::Get()->SetFocusRingObserverForTest(
      base::BindLambdaForTesting([&waiter, &controller, &focus_ring_id1,
                                  &focus_ring_group1, &focus_ring_id2,
                                  &focus_ring_group2, &count]() {
        if (count == 0) {
          // Wait for this to be called twice.
          count++;
          return;
        }
        waiter.Quit();
        // Check that the focus rings have been set appropriately.
        focus_ring_group1 =
            controller->GetFocusRingGroupForTesting(focus_ring_id1);
        ASSERT_NE(nullptr, focus_ring_group1);
        std::vector<std::unique_ptr<AccessibilityFocusRingLayer>> const&
            focus_rings = focus_ring_group1->focus_layers_for_testing();
        EXPECT_EQ(focus_rings.size(), 1u);
        gfx::Rect target_bounds = focus_rings.at(0)->layer()->GetTargetBounds();
        EXPECT_EQ(target_bounds.CenterPoint(),
                  gfx::Rect(50, 100, 42, 84).CenterPoint());
        AccessibilityFocusRingInfo* focus_ring_info =
            focus_ring_group1->focus_ring_info_for_testing();
        EXPECT_EQ(focus_ring_info->type, FocusRingType::GLOW);
        EXPECT_EQ(focus_ring_info->color, SK_ColorRED);
        EXPECT_EQ(focus_ring_info->behavior, FocusRingBehavior::PERSIST);

        focus_ring_group2 =
            controller->GetFocusRingGroupForTesting(focus_ring_id2);
        ASSERT_NE(nullptr, focus_ring_group2);
        std::vector<std::unique_ptr<AccessibilityFocusRingLayer>> const&
            focus_rings2 = focus_ring_group2->focus_layers_for_testing();
        EXPECT_EQ(focus_rings2.size(), 1u);
        target_bounds = focus_rings2.at(0)->layer()->GetTargetBounds();
        EXPECT_EQ(target_bounds.CenterPoint(),
                  gfx::Rect(500, 200, 84, 42).CenterPoint());
        focus_ring_info = focus_ring_group2->focus_ring_info_for_testing();
        EXPECT_EQ(focus_ring_info->type, FocusRingType::DASHED);
        EXPECT_EQ(focus_ring_info->color, SK_ColorBLUE);
        EXPECT_EQ(focus_ring_info->background_color, SK_ColorGREEN);
        EXPECT_EQ(focus_ring_info->secondary_color, SK_ColorBLACK);
        EXPECT_EQ(focus_ring_info->behavior, FocusRingBehavior::PERSIST);
      }));

  // Set two focus rings.
  std::vector<ax::mojom::FocusRingInfoPtr> focus_rings;
  auto focus_ring1 = ax::mojom::FocusRingInfo::New();
  focus_ring1->color = SK_ColorRED;
  focus_ring1->rects.emplace_back(50, 100, 42, 84);
  focus_ring1->type = ax::mojom::FocusType::kGlow;
  focus_rings.emplace_back(std::move(focus_ring1));

  auto focus_ring2 = ax::mojom::FocusRingInfo::New();
  focus_ring2->color = SK_ColorBLUE;
  focus_ring2->rects.emplace_back(500, 200, 84, 42);
  focus_ring2->type = ax::mojom::FocusType::kDashed;
  focus_ring2->background_color = SK_ColorGREEN;
  focus_ring2->secondary_color = SK_ColorBLACK;
  focus_ring2->stacking_order =
      ax::mojom::FocusRingStackingOrder::kBelowAccessibilityBubbles;
  focus_ring2->id = "mySpoonIsTooBig";
  focus_rings.emplace_back(std::move(focus_ring2));
  fake_service_->RequestSetFocusRings(
      std::move(focus_rings),
      ax::mojom::AssistiveTechnologyType::kSwitchAccess);

  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest, SetHighlights) {
      TurnOnAccessibilityService(AssistiveTechnologyType::kSwitchAccess);
  fake_service_->BindAnotherUserInterface();

  std::vector<gfx::Rect> rects;
  rects.emplace_back(gfx::Rect(0, 1, 22, 1973));

  base::RunLoop waiter;
  AccessibilityManager::Get()->SetHighlightsObserverForTest(
      base::BindLambdaForTesting([&waiter, &rects] {
        waiter.Quit();
        AccessibilityFocusRingControllerImpl* controller =
            Shell::Get()->accessibility_focus_ring_controller();
        AccessibilityHighlightLayer* highlight_layer =
            controller->highlight_layer_for_testing();
        EXPECT_TRUE(highlight_layer);
        ASSERT_EQ(1u, highlight_layer->rects_for_test().size());
        EXPECT_EQ(rects[0], highlight_layer->rects_for_test()[0]);
        EXPECT_EQ(SK_ColorMAGENTA, highlight_layer->color_for_test());
      }));

  fake_service_->RequestSetHighlights(rects, SK_ColorMAGENTA);

  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       RequestSetVirtualKeyboardVisible) {
  // Initialize ATP.
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  fake_service_->BindAnotherUserInterface();

  // Enable virtual keyboard.
  keyboard::SetAccessibilityKeyboardEnabled(true);

  // Verify keyboard is hidden.
  KeyboardControllerImpl* keyboard_controller_ =
      Shell::Get()->keyboard_controller();
  EXPECT_FALSE(keyboard_controller_->IsKeyboardVisible());

  // Show keyboard, and verify visible.
  fake_service_->RequestSetVirtualKeyboardVisible(true);
  KeyboardVisibleWaiter(true).Wait();
  EXPECT_TRUE(keyboard_controller_->IsKeyboardVisible());

  // Hide keyboard, and verify invisible.
  fake_service_->RequestSetVirtualKeyboardVisible(false);
  KeyboardVisibleWaiter(false).Wait();
  EXPECT_FALSE(keyboard_controller_->IsKeyboardVisible());
}

// Verifies that speech recognition can be started and stopped using the
// AccessibilityServiceClient.
IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       SpeechRecognitionStartAndStop) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kDictation);
  fake_service_->BindAnotherSpeechRecognition();

  auto start_options = ax::mojom::StartOptions::New();
  start_options->type = ax::mojom::AssistiveTechnologyType::kDictation;
  fake_service_->RequestSpeechRecognitionStart(std::move(start_options),
                                               base::DoNothing());
  sr_test_helper_->WaitForRecognitionStarted();

  auto stop_options = ax::mojom::StopOptions::New();
  stop_options->type = ax::mojom::AssistiveTechnologyType::kDictation;
  fake_service_->RequestSpeechRecognitionStop(std::move(stop_options),
                                              base::DoNothing());
  sr_test_helper_->WaitForRecognitionStopped();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       SpeechRecognitionStartAndStopCallbacks) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kDictation);
  fake_service_->BindAnotherSpeechRecognition();

  base::RunLoop start_waiter;
  auto start_options = ax::mojom::StartOptions::New();
  start_options->type = ax::mojom::AssistiveTechnologyType::kDictation;
  fake_service_->RequestSpeechRecognitionStart(
      std::move(start_options),
      base::BindLambdaForTesting(
          [&start_waiter](ax::mojom::SpeechRecognitionStartInfoPtr info) {
            EXPECT_EQ(ax::mojom::SpeechRecognitionType::kNetwork, info->type);
            ASSERT_FALSE(info->observer_or_error->is_error());
            start_waiter.Quit();
          }));
  start_waiter.Run();

  base::RunLoop stop_waiter;
  auto stop_options = ax::mojom::StopOptions::New();
  stop_options->type = ax::mojom::AssistiveTechnologyType::kDictation;
  fake_service_->RequestSpeechRecognitionStop(
      std::move(stop_options),
      base::BindLambdaForTesting(
          [&stop_waiter](const std::optional<std::string>& error) {
            ASSERT_FALSE(error.has_value());
            stop_waiter.Quit();
          }));
  stop_waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       AccessibilityServiceAsksClientToLoadAFile) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kChromeVox);
  base::RunLoop loop;
  fake_service_->RequestLoadFile(
      base::FilePath("chromevox/common/closure_loader.js"),
      base::BindLambdaForTesting([&loop](base::File file) mutable {
        // Note: we post a task to the thread pool here because dealing with the
        // file causes blocking operations. Since this is a single process
        // browser test setup, this would run in the main UI thread, which can't
        // block. The destructor of the base::File here must be invoked in a
        // sequence that allows blocking (here, in the task itself). So we
        // return its value of is_valid() to be checked in the main thread, and
        // finally stop the loop.
        base::ThreadPool::PostTaskAndReplyWithResult(
            FROM_HERE, {base::MayBlock()},
            /*task=*/base::BindLambdaForTesting([file = std::move(file)]() {
              return file.IsValid();
            }),
            /*reply=*/
            base::BindLambdaForTesting(
                [quit_closure = loop.QuitClosure()](bool result) {
                  ASSERT_TRUE(result);
                  std::move(quit_closure).Run();
                }));
      }));
  loop.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       SendSyntheticKeyEventForShortcutOrNavigation) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kSwitchAccess);
  fake_service_->BindAnotherUserInput();

  auto key_press_event = ax::mojom::SyntheticKeyEvent::New();
  key_press_event->type = ui::mojom::EventType::KEY_PRESSED;
  key_press_event->key_data = ui::mojom::KeyData::New();
  key_press_event->key_data->key_code = ui::VKEY_P;
  // TODO(b/307553499): Populate dom_code and dom_key for synthetic key events.
  key_press_event->key_data->dom_code = 0;
  key_press_event->key_data->dom_key = 0;
  key_press_event->key_data->is_char = false;

  auto key_release_event = ax::mojom::SyntheticKeyEvent::New();
  key_release_event->type = ui::mojom::EventType::KEY_RELEASED;
  key_release_event->key_data = ui::mojom::KeyData::New();
  key_release_event->key_data->key_code = ui::VKEY_P;
  // TODO(b/307553499): Populate dom_code and dom_key for synthetic key
  // events.
  key_release_event->key_data->dom_code = 0;
  key_release_event->key_data->dom_key = 0;
  key_release_event->key_data->is_char = false;

  base::RunLoop waiter;

  TestEventHandler test_event_handler(
      base::BindLambdaForTesting([&test_event_handler, &waiter]() {
        if (test_event_handler.key_events.size() != 2) {
          return;
        }

        ui::KeyEvent* press_event = test_event_handler.key_events[0].get();
        EXPECT_EQ(press_event->type(), ui::EventType::kKeyPressed);
        EXPECT_EQ(press_event->code(), ui::DomCode::US_P);
        EXPECT_FALSE(press_event->IsAltDown());
        EXPECT_FALSE(press_event->IsCommandDown());
        EXPECT_FALSE(press_event->IsControlDown());
        EXPECT_FALSE(press_event->IsShiftDown());

        ui::KeyEvent* release_event = test_event_handler.key_events[1].get();
        EXPECT_EQ(release_event->type(), ui::EventType::kKeyReleased);
        EXPECT_EQ(release_event->code(), ui::DomCode::US_P);
        EXPECT_FALSE(release_event->IsAltDown());
        EXPECT_FALSE(release_event->IsCommandDown());
        EXPECT_FALSE(release_event->IsControlDown());
        EXPECT_FALSE(release_event->IsShiftDown());

        waiter.Quit();
      }));

  // Send a press.
  fake_service_->RequestSendSyntheticKeyEventForShortcutOrNavigation(
      std::move(key_press_event));
  // Send a release.
  fake_service_->RequestSendSyntheticKeyEventForShortcutOrNavigation(
      std::move(key_release_event));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(
    AccessibilityServiceClientTest,
    SendSyntheticKeyEventForShortcutOrNavigationWithModifiers) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kSwitchAccess);
  fake_service_->BindAnotherUserInput();

  auto key_press_event = ax::mojom::SyntheticKeyEvent::New();
  key_press_event->type = ui::mojom::EventType::KEY_PRESSED;
  key_press_event->key_data = ui::mojom::KeyData::New();
  key_press_event->key_data->key_code = ui::VKEY_S;
  // TODO(b/307553499): Populate dom_code and dom_key for synthetic key events.
  key_press_event->key_data->dom_code = 0;
  key_press_event->key_data->dom_key = 0;
  key_press_event->key_data->is_char = false;
  key_press_event->flags =
      ui::mojom::kEventFlagAltDown | ui::mojom::kEventFlagControlDown |
      ui::mojom::kEventFlagCommandDown | ui::mojom::kEventFlagShiftDown;

  auto key_release_event = ax::mojom::SyntheticKeyEvent::New();
  key_release_event->type = ui::mojom::EventType::KEY_RELEASED;
  key_release_event->key_data = ui::mojom::KeyData::New();
  key_release_event->key_data->key_code = ui::VKEY_S;
  // TODO(b/307553499): Populate dom_code and dom_key for synthetic key
  // events.
  key_release_event->key_data->dom_code = 0;
  key_release_event->key_data->dom_key = 0;
  key_release_event->key_data->is_char = false;
  key_release_event->flags =
      ui::mojom::kEventFlagAltDown | ui::mojom::kEventFlagControlDown |
      ui::mojom::kEventFlagCommandDown | ui::mojom::kEventFlagShiftDown;

  base::RunLoop waiter;

  TestEventHandler test_event_handler(
      base::BindLambdaForTesting([&test_event_handler, &waiter]() {
        if (test_event_handler.key_events.size() != 2) {
          return;
        }

        ui::KeyEvent* press_event = test_event_handler.key_events[0].get();
        EXPECT_EQ(press_event->type(), ui::EventType::kKeyPressed);
        EXPECT_EQ(press_event->code(), ui::DomCode::US_S);
        EXPECT_TRUE(press_event->IsAltDown());
        EXPECT_TRUE(press_event->IsCommandDown());
        EXPECT_TRUE(press_event->IsControlDown());
        EXPECT_TRUE(press_event->IsShiftDown());

        ui::KeyEvent* release_event = test_event_handler.key_events[1].get();
        EXPECT_EQ(release_event->type(), ui::EventType::kKeyReleased);
        EXPECT_EQ(release_event->code(), ui::DomCode::US_S);
        EXPECT_TRUE(release_event->IsAltDown());
        EXPECT_TRUE(release_event->IsCommandDown());
        EXPECT_TRUE(release_event->IsControlDown());
        EXPECT_TRUE(release_event->IsShiftDown());

        waiter.Quit();
      }));

  // Send a press.
  fake_service_->RequestSendSyntheticKeyEventForShortcutOrNavigation(
      std::move(key_press_event));
  // Send a release.
  fake_service_->RequestSendSyntheticKeyEventForShortcutOrNavigation(
      std::move(key_release_event));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       SendSyntheticMouseEventPress) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kSwitchAccess);
  fake_service_->BindAnotherUserInput();

  auto mouse_event = ax::mojom::SyntheticMouseEvent::New();
  mouse_event->type = ui::mojom::EventType::MOUSE_PRESSED_EVENT;
  mouse_event->point = gfx::Point(0, 0);

  base::RunLoop waiter;

  TestEventHandler test_event_handler(base::BindLambdaForTesting([&]() {
    ASSERT_NE(0u, test_event_handler.mouse_events.size());
    ui::MouseEvent* mouse_event = test_event_handler.mouse_events.back().get();
    EXPECT_EQ(mouse_event->type(), ui::EventType::kMousePressed);
    EXPECT_FALSE(mouse_event->flags() & ui::EF_TOUCH_ACCESSIBILITY);
    EXPECT_TRUE(mouse_event->IsOnlyLeftMouseButton());
    waiter.Quit();
  }));

  fake_service_->RequestSendSyntheticMouseEvent(std::move(mouse_event));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       SendSyntheticMouseEventRelease) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kSwitchAccess);
  fake_service_->BindAnotherUserInput();

  auto mouse_event = ax::mojom::SyntheticMouseEvent::New();
  mouse_event->type = ui::mojom::EventType::MOUSE_RELEASED_EVENT;
  mouse_event->point = gfx::Point(0, 0);
  mouse_event->mouse_button = ax::mojom::SyntheticMouseEventButton::kMiddle;
  mouse_event->touch_accessibility = false;

  base::RunLoop waiter;

  TestEventHandler test_event_handler(base::BindLambdaForTesting([&]() {
    ASSERT_NE(0u, test_event_handler.mouse_events.size());
    ui::MouseEvent* mouse_event = test_event_handler.mouse_events.back().get();
    EXPECT_EQ(mouse_event->type(), ui::EventType::kMouseReleased);
    EXPECT_FALSE(mouse_event->flags() & ui::EF_TOUCH_ACCESSIBILITY);
    EXPECT_EQ(mouse_event->button_flags(), ui::EF_MIDDLE_MOUSE_BUTTON);
    waiter.Quit();
  }));

  fake_service_->RequestSendSyntheticMouseEvent(std::move(mouse_event));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       SendSyntheticMouseEventDrag) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kSwitchAccess);
  fake_service_->BindAnotherUserInput();

  auto mouse_event = ax::mojom::SyntheticMouseEvent::New();
  mouse_event->type = ui::mojom::EventType::MOUSE_DRAGGED_EVENT;
  mouse_event->point = gfx::Point(0, 0);
  mouse_event->mouse_button = ax::mojom::SyntheticMouseEventButton::kRight;
  mouse_event->touch_accessibility = true;

  base::RunLoop waiter;

  TestEventHandler test_event_handler(base::BindLambdaForTesting([&]() {
    ASSERT_NE(0u, test_event_handler.mouse_events.size());
    ui::MouseEvent* mouse_event = test_event_handler.mouse_events.back().get();
    EXPECT_EQ(mouse_event->type(), ui::EventType::kMouseDragged);
    EXPECT_TRUE(mouse_event->flags() & ui::EF_TOUCH_ACCESSIBILITY);
    EXPECT_EQ(mouse_event->button_flags(), ui::EF_RIGHT_MOUSE_BUTTON);
    waiter.Quit();
  }));

  fake_service_->RequestSendSyntheticMouseEvent(std::move(mouse_event));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       SendSyntheticMouseEventMove) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kSwitchAccess);
  fake_service_->BindAnotherUserInput();

  auto mouse_event = ax::mojom::SyntheticMouseEvent::New();
  mouse_event->type = ui::mojom::EventType::MOUSE_MOVED_EVENT;
  mouse_event->point = gfx::Point(0, 0);

  base::RunLoop waiter;

  TestEventHandler test_event_handler(base::BindLambdaForTesting([&]() {
    ASSERT_NE(0u, test_event_handler.mouse_events.size());
    ui::MouseEvent* mouse_event = test_event_handler.mouse_events.back().get();
    // We may see an enter event fired before the actual move event.
    if (mouse_event->type() == ui::EventType::kMouseEntered) {
      return;
    }

    EXPECT_EQ(mouse_event->type(), ui::EventType::kMouseMoved);
    EXPECT_FALSE(mouse_event->flags() & ui::EF_TOUCH_ACCESSIBILITY);
    EXPECT_EQ(mouse_event->button_flags(), 0);
    waiter.Quit();
  }));

  fake_service_->RequestSendSyntheticMouseEvent(std::move(mouse_event));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       SendSyntheticMouseEventEnter) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kSwitchAccess);
  fake_service_->BindAnotherUserInput();

  auto mouse_event = ax::mojom::SyntheticMouseEvent::New();
  mouse_event->type = ui::mojom::EventType::MOUSE_ENTERED_EVENT;
  mouse_event->point = gfx::Point(0, 0);
  mouse_event->mouse_button = ax::mojom::SyntheticMouseEventButton::kBack;

  base::RunLoop waiter;

  TestEventHandler test_event_handler(base::BindLambdaForTesting([&]() {
    ASSERT_NE(0u, test_event_handler.mouse_events.size());
    ui::MouseEvent* mouse_event = test_event_handler.mouse_events.back().get();
    EXPECT_EQ(mouse_event->type(), ui::EventType::kMouseEntered);
    EXPECT_FALSE(mouse_event->flags() & ui::EF_TOUCH_ACCESSIBILITY);
    EXPECT_EQ(mouse_event->button_flags(), ui::EF_BACK_MOUSE_BUTTON);
    waiter.Quit();
  }));

  fake_service_->RequestSendSyntheticMouseEvent(std::move(mouse_event));
  waiter.Run();
}

IN_PROC_BROWSER_TEST_F(AccessibilityServiceClientTest,
                       SendSyntheticMouseEventExit) {
  TurnOnAccessibilityService(AssistiveTechnologyType::kSwitchAccess);
  fake_service_->BindAnotherUserInput();

  auto mouse_event = ax::mojom::SyntheticMouseEvent::New();
  mouse_event->type = ui::mojom::EventType::MOUSE_EXITED_EVENT;
  mouse_event->point = gfx::Point(0, 0);
  mouse_event->mouse_button = ax::mojom::SyntheticMouseEventButton::kForward;

  base::RunLoop waiter;

  TestEventHandler test_event_handler(base::BindLambdaForTesting([&]() {
    ASSERT_NE(0u, test_event_handler.mouse_events.size());
    ui::MouseEvent* mouse_event = test_event_handler.mouse_events.back().get();
    EXPECT_EQ(mouse_event->type(), ui::EventType::kMouseExited);
    EXPECT_FALSE(mouse_event->flags() & ui::EF_TOUCH_ACCESSIBILITY);
    EXPECT_EQ(mouse_event->button_flags(), ui::EF_FORWARD_MOUSE_BUTTON);
    waiter.Quit();
  }));

  fake_service_->RequestSendSyntheticMouseEvent(std::move(mouse_event));
  waiter.Run();
}

}  // namespace ash