chromium/chrome/browser/speech/extension_api/tts_extension_api_ash_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 <memory>
#include <vector>

#include "ash/constants/ash_features.h"
#include "base/memory/raw_ptr.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/unguessable_token.h"
#include "chrome/browser/ash/crosapi/ash_requires_lacros_extension_apitest.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/speech/extension_api/tts_engine_extension_api.h"
#include "chrome/browser/speech/tts_crosapi_util.h"
#include "chromeos/crosapi/mojom/test_controller.mojom-test-utils.h"
#include "chromeos/crosapi/mojom/test_controller.mojom.h"
#include "chromeos/crosapi/mojom/tts.mojom-forward.h"
#include "content/public/browser/tts_controller.h"
#include "content/public/browser/tts_utterance.h"
#include "content/public/test/browser_test.h"
#include "mojo/public/cpp/bindings/receiver.h"

using crosapi::AshRequiresLacrosExtensionApiTest;

namespace extensions {

// Test tts and ttsEngine APIs with Lacros Tts support enabled, which
// requires Lacros running to exercise crosapi calls.
class AshTtsApiTest : public AshRequiresLacrosExtensionApiTest,
                      public content::VoicesChangedDelegate {
 public:
  void SetUpInProcessBrowserTestFixture() override {
    AshRequiresLacrosExtensionApiTest::SetUpInProcessBrowserTestFixture();

    // Enable Lacros tts support feature, and disable the 1st party Ash
    // extension keeplist feature so that it will allow loading test extension
    // in Ash in Lacros only mode.
    scoped_feature_list_ = std::make_unique<base::test::ScopedFeatureList>();
    scoped_feature_list_->InitWithFeatures(
        {}, {ash::features::kEnforceAshExtensionKeeplist,
             ash::features::kDisableLacrosTtsSupport});

    content::TtsController::SkipAddNetworkChangeObserverForTests(true);
    content::TtsController* tts_controller =
        content::TtsController::GetInstance();
    TtsExtensionEngine::GetInstance()->DisableBuiltInTTSEngineForTesting();
    tts_controller->SetTtsEngineDelegate(TtsExtensionEngine::GetInstance());
  }

  void TearDownInProcessBrowserTestFixture() override {
    scoped_feature_list_.reset(nullptr);
  }

 protected:
  bool HasVoiceWithName(const std::string& name) {
    std::vector<content::VoiceData> voices;

    content::TtsController::GetInstance()->GetVoices(profile(), GURL(),
                                                     &voices);

    for (const auto& voice : voices) {
      if (voice.name == name)
        return true;
    }

    return false;
  }

  bool FoundVoiceInMojoVoices(
      const std::string& voice_name,
      const std::vector<crosapi::mojom::TtsVoicePtr>& mojo_voices) {
    for (const auto& voice : mojo_voices) {
      if (voice_name == voice->voice_name)
        return true;
    }
    return false;
  }

  // content::VoicesChangedDelegate:
  void OnVoicesChanged() override {
    voices_changed_ = true;
    std::vector<content::VoiceData> voices;
    content::TtsController::GetInstance()->GetVoices(profile(), GURL(),
                                                     &voices);
    expected_voice_loaded_ = false;
    for (const auto& voice : voices) {
      if (voice.name == "Amy") {
        expected_voice_loaded_ = true;
        break;
      }
    }
  }

  void WaitUntilVoicesLoaded() {
    ASSERT_TRUE(base::test::RunUntil([&] { return expected_voice_loaded_; }));
  }

  void WaitUntilTtsEventReceivedByLacrosUtteranceEventDelegate() {
    ASSERT_TRUE(
        base::test::RunUntil([&] { return tts_event_notified_in_lacros_; }));
  }

  void NotifyTtsEventReceivedByLacros(content::TtsEventType tts_event) {
    tts_event_notified_in_lacros_ = true;
    tts_event_received_ = tts_event;
  }

  bool TtsEventNotifiedInLacros() { return tts_event_notified_in_lacros_; }
  bool TtsEventReceivedEq(content::TtsEventType expected_tts_event) {
    return tts_event_received_ == expected_tts_event;
  }

  bool VoicesChangedNotified() { return voices_changed_; }
  void ResetVoicesChanged() { voices_changed_ = false; }

  std::unique_ptr<base::test::ScopedFeatureList> scoped_feature_list_;
  test::AshBrowserTestStarter ash_starter_;

  // Used to verify that the TtsEvent is received by the lacros utterance's
  // UtteranceEventDelegate in Lacros.
  class EventDelegate : public crosapi::mojom::TtsUtteranceClient {
   public:
    explicit EventDelegate(extensions::AshTtsApiTest* owner) : owner_(owner) {}
    EventDelegate(const EventDelegate&) = delete;
    EventDelegate& operator=(const EventDelegate&) = delete;
    ~EventDelegate() override = default;

    // crosapi::mojom::TtsUtteranceClient:
    void OnTtsEvent(crosapi::mojom::TtsEventType mojo_tts_event,
                    uint32_t char_index,
                    uint32_t char_length,
                    const std::string& error_message) override {
      content::TtsEventType tts_event =
          tts_crosapi_util::FromMojo(mojo_tts_event);
      owner_->NotifyTtsEventReceivedByLacros(tts_event);
    }

    mojo::PendingRemote<crosapi::mojom::TtsUtteranceClient>
    BindTtsUtteranceClient() {
      return receiver_.BindNewPipeAndPassRemoteWithVersion();
    }

   private:
    raw_ptr<extensions::AshTtsApiTest> owner_;
    mojo::Receiver<crosapi::mojom::TtsUtteranceClient> receiver_{this};
  };

 private:
  bool voices_changed_ = false;
  bool expected_voice_loaded_ = false;
  bool tts_event_notified_in_lacros_ = false;
  // TtsEvent received by Lacros UtteranceEventDelegate.
  content::TtsEventType tts_event_received_;
};

//
// TTS Engine tests.
//

IN_PROC_BROWSER_TEST_F(AshTtsApiTest, RegisterAshEngine) {
  if (!ash_starter_.HasLacrosArgument())
    return;

  EXPECT_FALSE(VoicesChangedNotified());
  EXPECT_FALSE(HasVoiceWithName("Amy"));
  EXPECT_FALSE(HasVoiceWithName("Alex"));
  EXPECT_FALSE(HasVoiceWithName("Amanda"));

  ResetVoicesChanged();
  content::TtsController::GetInstance()->AddVoicesChangedDelegate(this);
  ASSERT_TRUE(
      RunExtensionTest("tts_engine/lacros_tts_support/register_ash_engine", {},
                       {.ignore_manifest_warnings = true}))
      << message_;

  WaitUntilVoicesLoaded();

  EXPECT_TRUE(VoicesChangedNotified());

  // Verify all the voices from tts engine extension are returned by
  // TtsController::GetVoices().
  std::vector<content::VoiceData> voices;
  content::TtsController::GetInstance()->GetVoices(profile(), GURL(), &voices);
  EXPECT_TRUE(HasVoiceWithName("Amy"));
  EXPECT_TRUE(HasVoiceWithName("Alex"));
  EXPECT_TRUE(HasVoiceWithName("Amanda"));

  // Verify all the voices are loaded at Lacros side.
  std::vector<crosapi::mojom::TtsVoicePtr> mojo_voices;
  ASSERT_TRUE(base::test::RunUntil([&] {
    base::test::TestFuture<std::vector<crosapi::mojom::TtsVoicePtr>>
        mojo_voices_future;
    GetStandaloneBrowserTestController()->GetTtsVoices(
        mojo_voices_future.GetCallback());
    mojo_voices = mojo_voices_future.Take();
    return !mojo_voices.empty();
  }));

  EXPECT_TRUE(FoundVoiceInMojoVoices("Amy", mojo_voices));
  EXPECT_TRUE(FoundVoiceInMojoVoices("Alex", mojo_voices));
  EXPECT_TRUE(FoundVoiceInMojoVoices("Amanda", mojo_voices));
}

IN_PROC_BROWSER_TEST_F(AshTtsApiTest, SpeakLacrosUtteranceWithAshSpeechEngine) {
  if (!ash_starter_.HasLacrosArgument())
    return;

  EXPECT_FALSE(VoicesChangedNotified());
  EXPECT_FALSE(HasVoiceWithName("Amy"));
  EXPECT_FALSE(HasVoiceWithName("Alex"));
  EXPECT_FALSE(HasVoiceWithName("Amanda"));

  ResetVoicesChanged();
  content::TtsController::GetInstance()->AddVoicesChangedDelegate(this);

  // Load speech engine extension in Ash.
  ASSERT_TRUE(
      RunExtensionTest("tts_engine/lacros_tts_support/"
                       "tts_speak_lacros_utterance_with_ash_engine",
                       {}, {.ignore_manifest_warnings = true}))
      << message_;

  WaitUntilVoicesLoaded();

  EXPECT_TRUE(VoicesChangedNotified());

  // Verify all the voices from tts engine extension are laded in Ash.
  std::vector<content::VoiceData> voices;
  content::TtsController::GetInstance()->GetVoices(profile(), GURL(), &voices);
  EXPECT_TRUE(HasVoiceWithName("Amy"));
  EXPECT_TRUE(HasVoiceWithName("Alex"));
  EXPECT_TRUE(HasVoiceWithName("Amanda"));
  // Verify a random dummy voice is not loaded.
  EXPECT_FALSE(HasVoiceWithName("Tommy"));

  // Verify all the voices are loaded in Lacros.

  std::vector<crosapi::mojom::TtsVoicePtr> mojo_voices;
  ASSERT_TRUE(base::test::RunUntil([&] {
    base::test::TestFuture<std::vector<crosapi::mojom::TtsVoicePtr>>
        mojo_voices_future;
    GetStandaloneBrowserTestController()->GetTtsVoices(
        mojo_voices_future.GetCallback());
    mojo_voices = mojo_voices_future.Take();
    return !mojo_voices.empty();
  }));

  EXPECT_TRUE(FoundVoiceInMojoVoices("Amy", mojo_voices));
  EXPECT_TRUE(FoundVoiceInMojoVoices("Alex", mojo_voices));
  EXPECT_TRUE(FoundVoiceInMojoVoices("Amanda", mojo_voices));

  // Due to crbug/1368284, we can not write a Lacros browser test to load
  // a testing extension in Lacros calling tts.speak with lacros tts support
  // enabled. Instead, we make a workaround to have an ash browser test request
  // Lacros to speak a Lacros utterance with
  // StandaloneBrowserTestController::TtsSpeak.
  std::unique_ptr<content::TtsUtterance> utterance =
      content::TtsUtterance::Create();
  utterance->SetText("Hello from Lacros");
  utterance->SetVoiceName("Amy");
  crosapi::mojom::TtsUtterancePtr mojo_utterance =
      tts_crosapi_util::ToMojo(utterance.get());
  // Note: mojo_utterance requires a value for browser_context_id, but it will
  // not used in Lacros for the testing workaround case.
  mojo_utterance->browser_context_id = base::UnguessableToken::Create();
  auto pending_client = std::make_unique<EventDelegate>(this);
  GetStandaloneBrowserTestController()->TtsSpeak(
      std::move(mojo_utterance), pending_client->BindTtsUtteranceClient());

  // Verify that the tts event has been received by the UtteranceEventDelegate
  // in Lacros.
  WaitUntilTtsEventReceivedByLacrosUtteranceEventDelegate();
  EXPECT_TRUE(TtsEventNotifiedInLacros());
  EXPECT_TRUE(TtsEventReceivedEq(content::TTS_EVENT_END));
}

IN_PROC_BROWSER_TEST_F(AshTtsApiTest, IsSpeaking) {
  if (!ash_starter_.HasLacrosArgument()) {
    return;
  }

  // Load Ash tts engine extension, register the tts engine events, and
  // call tts.isSpeaking before/during/after tts.speak.
  ASSERT_TRUE(RunExtensionTest("tts/is_speaking/", {},
                               {.ignore_manifest_warnings = true}))
      << message_;
}

}  // namespace extensions