blob: 2b7d51791bb32ca1202396b9e7f41af941e0e1de [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Unit tests for the TTS Controller.
#include "content/browser/speech/tts_controller_impl.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/values.h"
#include "build/build_config.h"
#include "content/browser/speech/tts_utterance_impl.h"
#include "content/public/browser/tts_platform.h"
#include "content/public/browser/visibility.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_renderer_host.h"
#include "content/test/test_content_browser_client.h"
#include "content/test/test_web_contents.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/speech/speech_synthesis.mojom.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "content/public/browser/tts_controller_delegate.h"
#endif
namespace content {
// Platform Tts implementation that does nothing.
class MockTtsPlatformImpl : public TtsPlatform {
public:
explicit MockTtsPlatformImpl(TtsController* controller)
: controller_(controller) {}
virtual ~MockTtsPlatformImpl() = default;
// Override the Mock API results.
void set_voices(const std::vector<VoiceData>& voices) { voices_ = voices; }
void set_is_speaking(bool value) { is_speaking_ = value; }
// TtsPlatform:
bool PlatformImplSupported() override { return platform_supported_; }
bool PlatformImplInitialized() override { return platform_initialized_; }
void Speak(
int utterance_id,
const std::string& utterance,
const std::string& lang,
const VoiceData& voice,
const UtteranceContinuousParameters& params,
base::OnceCallback<void(bool)> did_start_speaking_callback) override {
utterance_id_ = utterance_id;
did_start_speaking_callback_ = std::move(did_start_speaking_callback);
utterance_voice_ = voice;
}
bool IsSpeaking() override { return is_speaking_; }
bool StopSpeaking() override {
++stop_speaking_called_;
return true;
}
void Pause() override { ++pause_called_; }
void Resume() override { ++resume_called_; }
void GetVoices(std::vector<VoiceData>* out_voices) override {
for (const auto& voice : voices_)
out_voices->push_back(voice);
}
void LoadBuiltInTtsEngine(BrowserContext* browser_context) override {}
void WillSpeakUtteranceWithVoice(TtsUtterance* utterance,
const VoiceData& voice_data) override {}
void SetError(const std::string& error) override { error_ = error; }
std::string GetError() override { return error_; }
void ClearError() override { error_.clear(); }
void Shutdown() override {}
void FinalizeVoiceOrdering(std::vector<VoiceData>& voices) override {}
void RefreshVoices() override {}
void SetPlatformImplSupported(bool state) { platform_supported_ = state; }
void SetPlatformImplInitialized(bool state) { platform_initialized_ = state; }
// Returns the VoiceData passed in by mock Speak API.
VoiceData get_utterance_voice() const { return utterance_voice_; }
// Returns the amount of calls to Mock API.
int pause_called() const { return pause_called_; }
int resume_called() const { return resume_called_; }
int stop_speaking_called() const { return stop_speaking_called_; }
// Simulate the TTS platform calling back the closure
// |did_start_speaking_callback| passed to Speak(...). This closure can be
// called synchronously or asynchronously.
void StartSpeaking(bool result) {
is_speaking_ = true;
std::move(did_start_speaking_callback_).Run(result);
}
void FinishSpeaking() {
is_speaking_ = false;
controller_->OnTtsEvent(utterance_id_, TTS_EVENT_END, 0, 0, {});
utterance_id_ = -1;
}
void ClearController() { controller_ = nullptr; }
private:
raw_ptr<TtsController> controller_;
bool platform_supported_ = true;
bool platform_initialized_ = true;
std::vector<VoiceData> voices_;
int utterance_id_ = -1;
VoiceData utterance_voice_;
bool is_speaking_ = false;
int pause_called_ = 0;
int resume_called_ = 0;
int stop_speaking_called_ = 0;
std::string error_;
base::OnceCallback<void(bool)> did_start_speaking_callback_;
};
class MockTtsEngineDelegate : public TtsEngineDelegate {
public:
int utterance_id() { return utterance_id_; }
void set_is_built_in_tts_engine_initialized(bool value) {
is_built_in_tts_engine_initialized_ = value;
}
void set_voices(const std::vector<VoiceData>& voices) { voices_ = voices; }
// TtsEngineDelegate:
void Speak(TtsUtterance* utterance, const VoiceData& voice) override {
utterance_id_ = utterance->GetId();
}
void UninstallLanguageRequest(content::BrowserContext* browser_context,
const std::string& lang,
const std::string& client_id,
int source,
bool uninstall_immediately) override {}
void InstallLanguageRequest(BrowserContext* browser_context,
const std::string& lang,
const std::string& client_id,
int source) override {}
void LanguageStatusRequest(BrowserContext* browser_context,
const std::string& lang,
const std::string& client_id,
int source) override {}
void LoadBuiltInTtsEngine(BrowserContext* browser_context) override {}
bool IsBuiltInTtsEngineInitialized(BrowserContext* browser_context) override {
return is_built_in_tts_engine_initialized_;
}
void GetVoices(BrowserContext* browser_context,
const GURL& source_url,
std::vector<VoiceData>* out_voices) override {
for (const auto& voice : voices_)
out_voices->push_back(voice);
}
// Count API calls (TtsEngineDelegate:)
void Stop(TtsUtterance* utterance) override { ++stop_called_; }
void Pause(TtsUtterance* utterance) override { ++pause_called_; }
void Resume(TtsUtterance* utterance) override { ++resume_called_; }
// Returns the amount of calls to Mock API.
int pause_called() const { return pause_called_; }
int resume_called() const { return resume_called_; }
int stop_called() const { return stop_called_; }
private:
bool is_built_in_tts_engine_initialized_ = true;
int utterance_id_ = -1;
std::vector<VoiceData> voices_;
int pause_called_ = 0;
int resume_called_ = 0;
int stop_called_ = 0;
};
#if BUILDFLAG(IS_CHROMEOS)
class MockTtsControllerDelegate : public TtsControllerDelegate {
public:
MockTtsControllerDelegate() = default;
~MockTtsControllerDelegate() override = default;
void SetPreferredVoiceIds(const PreferredVoiceIds& ids) { ids_ = ids; }
BrowserContext* GetLastBrowserContext() {
BrowserContext* result = last_browser_context_;
last_browser_context_ = nullptr;
return result;
}
// TtsControllerDelegate:
std::unique_ptr<PreferredVoiceIds> GetPreferredVoiceIdsForUtterance(
TtsUtterance* utterance) override {
last_browser_context_ = utterance->GetBrowserContext();
auto ids = std::make_unique<PreferredVoiceIds>(ids_);
return ids;
}
void UpdateUtteranceDefaultsFromPrefs(content::TtsUtterance* utterance,
double* rate,
double* pitch,
double* volume) override {}
private:
raw_ptr<BrowserContext> last_browser_context_ = nullptr;
PreferredVoiceIds ids_;
};
#endif
class MockVoicesChangedDelegate : public VoicesChangedDelegate {
public:
void OnVoicesChanged() override {}
};
class TestTtsControllerImpl : public TtsControllerImpl {
public:
TestTtsControllerImpl() = default;
~TestTtsControllerImpl() override = default;
// Exposed API for testing.
using TtsControllerImpl::FinishCurrentUtterance;
using TtsControllerImpl::GetMatchingVoice;
using TtsControllerImpl::SpeakNextUtterance;
using TtsControllerImpl::UpdateUtteranceDefaults;
#if BUILDFLAG(IS_CHROMEOS)
using TtsControllerImpl::SetTtsControllerDelegateForTesting;
#endif
using TtsControllerImpl::IsPausedForTesting;
TtsUtterance* current_utterance() { return current_utterance_.get(); }
void set_allow_remote_voices(bool value) { allow_remote_voices_ = value; }
};
class TtsControllerTest : public testing::Test {
public:
TtsControllerTest() = default;
~TtsControllerTest() override = default;
void SetUp() override {
controller_ = std::make_unique<TestTtsControllerImpl>();
platform_impl_ = std::make_unique<MockTtsPlatformImpl>(controller_.get());
browser_context_ = std::make_unique<TestBrowserContext>();
controller()->SetTtsPlatform(platform_impl_.get());
#if !BUILDFLAG(IS_ANDROID)
// TtsEngineDelegate isn't set for Android in ChromeContentBrowserClient
// since it has no extensions.
controller()->SetTtsEngineDelegate(&engine_delegate_);
#endif // !BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(IS_CHROMEOS)
controller()->SetTtsControllerDelegateForTesting(&delegate_);
#endif
controller()->AddVoicesChangedDelegate(&voices_changed_);
}
void TearDown() override {
if (controller())
controller()->RemoveVoicesChangedDelegate(&voices_changed_);
}
MockTtsPlatformImpl* platform_impl() { return platform_impl_.get(); }
TestTtsControllerImpl* controller() { return controller_.get(); }
TestBrowserContext* browser_context() { return browser_context_.get(); }
MockTtsEngineDelegate* engine_delegate() { return &engine_delegate_; }
#if BUILDFLAG(IS_CHROMEOS)
MockTtsControllerDelegate* delegate() { return &delegate_; }
#endif
void ReleaseTtsController() {
// Need to clear the controller on MockTtsPlatformImpl to avoid a dangling
// pointer.
platform_impl_->ClearController();
controller_.reset();
}
void ReleaseBrowserContext() {
// BrowserContext::~BrowserContext(...) is calling OnBrowserContextDestroyed
// on the tts controller singleton. That call is simulated here to ensures
// it is called on our test controller instances.
controller()->OnBrowserContextDestroyed(browser_context_.get());
browser_context_.reset();
}
std::unique_ptr<TestWebContents> CreateWebContents() {
return std::unique_ptr<TestWebContents>(
TestWebContents::Create(browser_context_.get(), nullptr));
}
std::unique_ptr<TtsUtteranceImpl> CreateUtteranceImpl(
WebContents* web_contents = nullptr) {
return std::make_unique<TtsUtteranceImpl>(browser_context_.get(),
web_contents);
}
TtsUtterance* TtsControllerCurrentUtterance() {
return controller()->current_utterance();
}
bool IsUtteranceListEmpty() { return controller()->QueueSize() == 0; }
private:
content::BrowserTaskEnvironment task_environment_;
RenderViewHostTestEnabler rvh_enabler_;
std::unique_ptr<TestTtsControllerImpl> controller_;
std::unique_ptr<MockTtsPlatformImpl> platform_impl_;
std::unique_ptr<TestBrowserContext> browser_context_;
MockTtsEngineDelegate engine_delegate_;
#if BUILDFLAG(IS_CHROMEOS)
MockTtsControllerDelegate delegate_;
#endif
MockVoicesChangedDelegate voices_changed_;
};
#if BUILDFLAG(IS_CHROMEOS)
TEST_F(TtsControllerTest, TestBrowserContextRemoved) {
std::vector<VoiceData> voices;
VoiceData voice_data;
voice_data.engine_id = "x";
voice_data.events.insert(TTS_EVENT_END);
voices.push_back(voice_data);
platform_impl()->set_voices(voices);
// Speak an utterances associated with this test browser context.
std::unique_ptr<TtsUtterance> utterance1 =
TtsUtterance::Create(browser_context());
utterance1->SetEngineId("x");
utterance1->SetShouldClearQueue(false);
utterance1->SetSrcId(1);
controller()->SpeakOrEnqueue(std::move(utterance1));
// Assert that the delegate was called and it got our browser context.
ASSERT_EQ(browser_context(), delegate()->GetLastBrowserContext());
// Now queue up a second utterance to be spoken, also associated with
// this browser context.
std::unique_ptr<TtsUtterance> utterance2 =
TtsUtterance::Create(browser_context());
utterance2->SetEngineId("x");
utterance2->SetShouldClearQueue(false);
utterance2->SetSrcId(2);
controller()->SpeakOrEnqueue(std::move(utterance2));
// Destroy the browser context before the utterance is spoken.
ReleaseBrowserContext();
// Now speak the next utterance, and ensure that we don't get the
// destroyed browser context.
controller()->FinishCurrentUtterance();
controller()->SpeakNextUtterance();
ASSERT_EQ(nullptr, delegate()->GetLastBrowserContext());
}
#else
TEST_F(TtsControllerTest, TestTtsControllerUtteranceDefaults) {
std::unique_ptr<TtsUtterance> utterance1 = content::TtsUtterance::Create();
// Initialized to default (unset constant) values.
EXPECT_EQ(blink::mojom::kSpeechSynthesisDoublePrefNotSet,
utterance1->GetContinuousParameters().rate);
EXPECT_EQ(blink::mojom::kSpeechSynthesisDoublePrefNotSet,
utterance1->GetContinuousParameters().pitch);
EXPECT_EQ(blink::mojom::kSpeechSynthesisDoublePrefNotSet,
utterance1->GetContinuousParameters().volume);
controller()->UpdateUtteranceDefaults(utterance1.get());
// Updated to global defaults.
EXPECT_EQ(blink::mojom::kSpeechSynthesisDefaultRate,
utterance1->GetContinuousParameters().rate);
EXPECT_EQ(blink::mojom::kSpeechSynthesisDefaultPitch,
utterance1->GetContinuousParameters().pitch);
EXPECT_EQ(blink::mojom::kSpeechSynthesisDefaultVolume,
utterance1->GetContinuousParameters().volume);
}
#endif
TEST_F(TtsControllerTest, TestGetMatchingVoice) {
TestContentBrowserClient::GetInstance()->set_application_locale("en");
{
// Calling GetMatchingVoice with no voices returns -1.
std::unique_ptr<TtsUtterance> utterance(TtsUtterance::Create());
std::vector<VoiceData> voices;
EXPECT_EQ(-1, controller()->GetMatchingVoice(utterance.get(), voices));
}
{
// Calling GetMatchingVoice with any voices returns the first one
// even if there are no criteria that match.
std::unique_ptr<TtsUtterance> utterance(TtsUtterance::Create());
std::vector<VoiceData> voices(2);
EXPECT_EQ(0, controller()->GetMatchingVoice(utterance.get(), voices));
}
{
// If nothing else matches, the English voice is returned.
// (In tests the language will always be English.)
std::unique_ptr<TtsUtterance> utterance(TtsUtterance::Create());
std::vector<VoiceData> voices;
VoiceData fr_voice;
fr_voice.lang = "fr";
voices.push_back(fr_voice);
VoiceData en_voice;
en_voice.lang = "en";
voices.push_back(en_voice);
VoiceData de_voice;
de_voice.lang = "de";
voices.push_back(de_voice);
EXPECT_EQ(1, controller()->GetMatchingVoice(utterance.get(), voices));
}
{
// Check precedence of various matching criteria.
std::vector<VoiceData> voices;
VoiceData voice0;
voices.push_back(voice0);
VoiceData voice1;
voice1.events.insert(TTS_EVENT_WORD);
voices.push_back(voice1);
VoiceData voice2;
voice2.lang = "de-DE";
voices.push_back(voice2);
VoiceData voice3;
voice3.lang = "fr-CA";
voices.push_back(voice3);
VoiceData voice4;
voice4.name = "Voice4";
voices.push_back(voice4);
VoiceData voice5;
voice5.engine_id = "id5";
voices.push_back(voice5);
VoiceData voice6;
voice6.engine_id = "id7";
voice6.name = "Voice6";
voice6.lang = "es-es";
voices.push_back(voice6);
VoiceData voice7;
voice7.engine_id = "id7";
voice7.name = "Voice7";
voice7.lang = "es-mx";
voices.push_back(voice7);
VoiceData voice8;
voice8.engine_id = "";
voice8.name = "Android";
voice8.lang = "";
voice8.native = true;
voices.push_back(voice8);
std::unique_ptr<TtsUtterance> utterance(TtsUtterance::Create());
EXPECT_EQ(0, controller()->GetMatchingVoice(utterance.get(), voices));
base::flat_set<TtsEventType> types;
types.insert(TTS_EVENT_WORD);
utterance->SetRequiredEventTypes(types);
EXPECT_EQ(1, controller()->GetMatchingVoice(utterance.get(), voices));
utterance->SetLang("de-DE");
EXPECT_EQ(2, controller()->GetMatchingVoice(utterance.get(), voices));
utterance->SetLang("fr-FR");
EXPECT_EQ(3, controller()->GetMatchingVoice(utterance.get(), voices));
utterance->SetVoiceName("Voice4");
EXPECT_EQ(4, controller()->GetMatchingVoice(utterance.get(), voices));
utterance->SetVoiceName("");
utterance->SetEngineId("id5");
EXPECT_EQ(5, controller()->GetMatchingVoice(utterance.get(), voices));
#if BUILDFLAG(IS_CHROMEOS)
TtsControllerDelegate::PreferredVoiceIds preferred_voice_ids;
preferred_voice_ids.locale_voice_id.emplace("Voice7", "id7");
preferred_voice_ids.any_locale_voice_id.emplace("Android", "");
delegate()->SetPreferredVoiceIds(preferred_voice_ids);
// Voice6 is matched when the utterance locale exactly matches its locale.
utterance->SetEngineId("");
utterance->SetLang("es-es");
EXPECT_EQ(6, controller()->GetMatchingVoice(utterance.get(), voices));
// The 7th voice is the default for "es", even though the utterance is
// "es-ar". |voice6| is not matched because it is not the default.
utterance->SetEngineId("");
utterance->SetLang("es-ar");
EXPECT_EQ(7, controller()->GetMatchingVoice(utterance.get(), voices));
// The 8th voice is like the built-in "Android" voice, it has no lang
// and no extension ID. Make sure it can still be matched.
preferred_voice_ids.locale_voice_id.reset();
delegate()->SetPreferredVoiceIds(preferred_voice_ids);
utterance->SetVoiceName("Android");
utterance->SetEngineId("");
utterance->SetLang("");
EXPECT_EQ(8, controller()->GetMatchingVoice(utterance.get(), voices));
delegate()->SetPreferredVoiceIds({});
#endif
}
{
// When engine_id is set in utterance.
std::vector<VoiceData> voices;
VoiceData voice0;
voice0.engine_id = "id0";
voice0.name = "voice0";
voice0.lang = "en-GB";
voices.push_back(voice0);
std::unique_ptr<TtsUtterance> utterance(TtsUtterance::Create());
// No matching voice for engine_id is set. Returns -1.
TestContentBrowserClient::GetInstance()->set_application_locale("en-US");
utterance->SetLang("en-US");
utterance->SetEngineId("test_engine");
EXPECT_EQ(-1, controller()->GetMatchingVoice(utterance.get(), voices));
}
{
// Check voices against system language.
std::vector<VoiceData> voices;
VoiceData voice0;
voice0.engine_id = "id0";
voice0.name = "voice0";
voice0.lang = "en-GB";
voices.push_back(voice0);
VoiceData voice1;
voice1.engine_id = "id1";
voice1.name = "voice1";
voice1.lang = "en-US";
voices.push_back(voice1);
std::unique_ptr<TtsUtterance> utterance(TtsUtterance::Create());
// voice1 is matched against the exact default system language.
TestContentBrowserClient::GetInstance()->set_application_locale("en-US");
utterance->SetLang("");
EXPECT_EQ(1, controller()->GetMatchingVoice(utterance.get(), voices));
#if BUILDFLAG(IS_CHROMEOS)
// voice0 is matched against the system language which has no region piece.
TestContentBrowserClient::GetInstance()->set_application_locale("en");
EXPECT_EQ(0, controller()->GetMatchingVoice(utterance.get(), voices));
TtsControllerDelegate::PreferredVoiceIds preferred_voice_ids2;
preferred_voice_ids2.locale_voice_id.emplace("voice0", "id0");
delegate()->SetPreferredVoiceIds(preferred_voice_ids2);
// voice0 is matched against the pref over the system language.
TestContentBrowserClient::GetInstance()->set_application_locale("en-US");
EXPECT_EQ(0, controller()->GetMatchingVoice(utterance.get(), voices));
#endif
}
{
// This block ensures that voices can be matched, even if their locale's
// casing doesn't exactly match that of the utterance e.g. "en-us" will
// match with "en-US".
std::vector<VoiceData> voices;
VoiceData voice0;
voice0.engine_id = "id0";
voice0.name = "English voice";
voice0.lang = "en-US";
voices.push_back(voice0);
VoiceData voice1;
voice1.engine_id = "id0";
voice1.name = "French voice";
voice1.lang = "fr";
voices.push_back(voice1);
std::unique_ptr<TtsUtterance> utterance(TtsUtterance::Create());
utterance->SetLang("en-us");
EXPECT_EQ(0, controller()->GetMatchingVoice(utterance.get(), voices));
utterance->SetLang("en-US");
EXPECT_EQ(0, controller()->GetMatchingVoice(utterance.get(), voices));
utterance->SetLang("EN-US");
EXPECT_EQ(0, controller()->GetMatchingVoice(utterance.get(), voices));
#if BUILDFLAG(IS_CHROMEOS)
// Add another English voice.
VoiceData voice2;
voice2.engine_id = "id1";
voice2.name = "Another English voice";
voice2.lang = "en-us";
voices.push_back(voice2);
// Set voice2 as the preferred voice for English.
TtsControllerDelegate::PreferredVoiceIds preferred_voice_ids;
preferred_voice_ids.lang_voice_id.emplace(voice2.name, voice2.engine_id);
delegate()->SetPreferredVoiceIds(preferred_voice_ids);
// Ensure that voice2 is chosen over voice0, even though the locales don't
// match exactly. The utterance has a locale of "en-US", while voice2 has
// a locale of "en-us"; this shouldn't prevent voice2 from being used.
EXPECT_EQ(2, controller()->GetMatchingVoice(utterance.get(), voices));
#endif
}
}
TEST_F(TtsControllerTest, TestTtsControllerShutdown) {
std::unique_ptr<TtsUtterance> utterance1 = TtsUtterance::Create();
utterance1->SetShouldClearQueue(false);
utterance1->SetSrcId(1);
controller()->SpeakOrEnqueue(std::move(utterance1));
std::unique_ptr<TtsUtterance> utterance2 = TtsUtterance::Create();
utterance2->SetShouldClearQueue(false);
utterance2->SetSrcId(2);
controller()->SpeakOrEnqueue(std::move(utterance2));
// Make sure that deleting the controller when there are pending
// utterances doesn't cause a crash.
ReleaseTtsController();
}
TEST_F(TtsControllerTest, StopsWhenWebContentsDestroyed) {
std::unique_ptr<WebContents> web_contents = CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance =
CreateUtteranceImpl(web_contents.get());
controller()->SpeakOrEnqueue(std::move(utterance));
EXPECT_TRUE(controller()->IsSpeaking());
EXPECT_TRUE(TtsControllerCurrentUtterance());
web_contents.reset();
// Destroying the WebContents should reset
// |TtsController::current_utterance_|.
EXPECT_FALSE(TtsControllerCurrentUtterance());
}
TEST_F(TtsControllerTest, StartsQueuedUtteranceWhenWebContentsDestroyed) {
std::unique_ptr<WebContents> web_contents1 = CreateWebContents();
std::unique_ptr<WebContents> web_contents2 = CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance1 =
CreateUtteranceImpl(web_contents1.get());
void* raw_utterance1 = utterance1.get();
std::unique_ptr<TtsUtteranceImpl> utterance2 =
CreateUtteranceImpl(web_contents2.get());
utterance2->SetShouldClearQueue(false);
void* raw_utterance2 = utterance2.get();
controller()->SpeakOrEnqueue(std::move(utterance1));
EXPECT_TRUE(controller()->IsSpeaking());
EXPECT_TRUE(TtsControllerCurrentUtterance());
controller()->SpeakOrEnqueue(std::move(utterance2));
EXPECT_EQ(raw_utterance1, TtsControllerCurrentUtterance());
web_contents1.reset();
// Destroying |web_contents1| should delete |utterance1| and start
// |utterance2|.
EXPECT_TRUE(TtsControllerCurrentUtterance());
EXPECT_EQ(raw_utterance2, TtsControllerCurrentUtterance());
}
TEST_F(TtsControllerTest, StartsQueuedUtteranceWhenWebContentsDestroyed2) {
std::unique_ptr<WebContents> web_contents1 = CreateWebContents();
std::unique_ptr<WebContents> web_contents2 = CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance1 =
CreateUtteranceImpl(web_contents1.get());
void* raw_utterance1 = utterance1.get();
std::unique_ptr<TtsUtteranceImpl> utterance2 =
CreateUtteranceImpl(web_contents1.get());
std::unique_ptr<TtsUtteranceImpl> utterance3 =
CreateUtteranceImpl(web_contents2.get());
void* raw_utterance3 = utterance3.get();
utterance2->SetShouldClearQueue(false);
utterance3->SetShouldClearQueue(false);
controller()->SpeakOrEnqueue(std::move(utterance1));
controller()->SpeakOrEnqueue(std::move(utterance2));
controller()->SpeakOrEnqueue(std::move(utterance3));
EXPECT_TRUE(controller()->IsSpeaking());
EXPECT_EQ(raw_utterance1, TtsControllerCurrentUtterance());
web_contents1.reset();
// Deleting |web_contents1| should delete |utterance1| and |utterance2| as
// they are both from |web_contents1|. |raw_utterance3| should be made the
// current as it's from a different WebContents.
EXPECT_EQ(raw_utterance3, TtsControllerCurrentUtterance());
EXPECT_TRUE(IsUtteranceListEmpty());
web_contents2.reset();
// Deleting |web_contents2| should delete |utterance3| as it's from a
// different WebContents.
EXPECT_EQ(nullptr, TtsControllerCurrentUtterance());
}
TEST_F(TtsControllerTest, StartsUtteranceWhenWebContentsHidden) {
std::unique_ptr<TestWebContents> web_contents = CreateWebContents();
web_contents->SetVisibilityAndNotifyObservers(Visibility::HIDDEN);
std::unique_ptr<TtsUtteranceImpl> utterance =
CreateUtteranceImpl(web_contents.get());
controller()->SpeakOrEnqueue(std::move(utterance));
EXPECT_TRUE(controller()->IsSpeaking());
}
TEST_F(TtsControllerTest,
DoesNotStartUtteranceWhenWebContentsHiddenAndStopSpeakingWhenHiddenSet) {
std::unique_ptr<TestWebContents> web_contents = CreateWebContents();
web_contents->SetVisibilityAndNotifyObservers(Visibility::HIDDEN);
std::unique_ptr<TtsUtteranceImpl> utterance =
CreateUtteranceImpl(web_contents.get());
controller()->SetStopSpeakingWhenHidden(true);
controller()->SpeakOrEnqueue(std::move(utterance));
EXPECT_EQ(nullptr, TtsControllerCurrentUtterance());
EXPECT_TRUE(IsUtteranceListEmpty());
}
TEST_F(TtsControllerTest, SkipsQueuedUtteranceFromHiddenWebContents) {
controller()->SetStopSpeakingWhenHidden(true);
std::unique_ptr<WebContents> web_contents1 = CreateWebContents();
std::unique_ptr<TestWebContents> web_contents2 = CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance1 =
CreateUtteranceImpl(web_contents1.get());
const int utterance1_id = utterance1->GetId();
std::unique_ptr<TtsUtteranceImpl> utterance2 =
CreateUtteranceImpl(web_contents2.get());
utterance2->SetShouldClearQueue(false);
controller()->SpeakOrEnqueue(std::move(utterance1));
EXPECT_TRUE(TtsControllerCurrentUtterance());
EXPECT_TRUE(IsUtteranceListEmpty());
// Speak |utterance2|, which should get queued.
controller()->SpeakOrEnqueue(std::move(utterance2));
EXPECT_FALSE(IsUtteranceListEmpty());
// Make the second WebContents hidden, this shouldn't change anything in
// TtsController.
web_contents2->SetVisibilityAndNotifyObservers(Visibility::HIDDEN);
EXPECT_FALSE(IsUtteranceListEmpty());
// Finish |utterance1|, which should skip |utterance2| because |web_contents2|
// is hidden.
controller()->OnTtsEvent(utterance1_id, TTS_EVENT_END, 0, 0, {});
EXPECT_EQ(nullptr, TtsControllerCurrentUtterance());
EXPECT_TRUE(IsUtteranceListEmpty());
}
TEST_F(TtsControllerTest, SpeakPauseResume) {
std::unique_ptr<WebContents> web_contents = CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance =
CreateUtteranceImpl(web_contents.get());
utterance->SetShouldClearQueue(false);
// Start speaking an utterance.
controller()->SpeakOrEnqueue(std::move(utterance));
platform_impl()->StartSpeaking(true);
// Pause the currently playing utterance should call the platform API pause.
controller()->Pause();
EXPECT_TRUE(controller()->IsPausedForTesting());
EXPECT_EQ(1, platform_impl()->pause_called());
// Double pause should not call again the platform API pause.
controller()->Pause();
EXPECT_EQ(1, platform_impl()->pause_called());
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_TRUE(TtsControllerCurrentUtterance());
// Resuming the playing utterance should call the platform API resume.
controller()->Resume();
EXPECT_FALSE(controller()->IsPausedForTesting());
EXPECT_EQ(1, platform_impl()->resume_called());
// Double resume should not call again the platform API resume.
controller()->Resume();
EXPECT_EQ(1, platform_impl()->resume_called());
EXPECT_TRUE(controller()->IsSpeaking());
// Complete the playing utterance.
platform_impl()->FinishSpeaking();
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_FALSE(TtsControllerCurrentUtterance());
EXPECT_FALSE(controller()->IsSpeaking());
}
TEST_F(TtsControllerTest, SpeakWhenPaused) {
std::unique_ptr<WebContents> web_contents = CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance =
CreateUtteranceImpl(web_contents.get());
utterance->SetShouldClearQueue(false);
// Pause the controller.
controller()->Pause();
EXPECT_TRUE(controller()->IsPausedForTesting());
// Speak an utterance while controller is paused, the utterance should be
// queued.
controller()->SpeakOrEnqueue(std::move(utterance));
EXPECT_FALSE(IsUtteranceListEmpty());
EXPECT_FALSE(TtsControllerCurrentUtterance());
// Resume speaking, the utterance should start playing.
controller()->Resume();
EXPECT_FALSE(controller()->IsPausedForTesting());
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_TRUE(TtsControllerCurrentUtterance());
// Simulate platform starting to play the utterance.
platform_impl()->StartSpeaking(true);
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_TRUE(TtsControllerCurrentUtterance());
EXPECT_TRUE(controller()->IsSpeaking());
// Complete the playing utterance.
platform_impl()->FinishSpeaking();
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_FALSE(TtsControllerCurrentUtterance());
EXPECT_FALSE(controller()->IsSpeaking());
}
TEST_F(TtsControllerTest, SpeakWhenPausedAndCannotEnqueueUtterance) {
std::unique_ptr<WebContents> web_contents = CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance1 =
CreateUtteranceImpl(web_contents.get());
utterance1->SetShouldClearQueue(true);
// Pause the controller.
controller()->Pause();
EXPECT_TRUE(controller()->IsPausedForTesting());
// Speak an utterance while controller is paused. The utterance clears the
// queue and is enqueued itself.
controller()->SpeakOrEnqueue(std::move(utterance1));
EXPECT_FALSE(IsUtteranceListEmpty());
EXPECT_EQ(1, controller()->QueueSize());
EXPECT_FALSE(TtsControllerCurrentUtterance());
// Speak an utterance that can be queued. The controller should stay paused
// and the second utterance must be queued with the first also queued.
std::unique_ptr<TtsUtteranceImpl> utterance2 =
CreateUtteranceImpl(web_contents.get());
utterance2->SetShouldClearQueue(false);
controller()->SpeakOrEnqueue(std::move(utterance2));
EXPECT_TRUE(controller()->IsPausedForTesting());
EXPECT_EQ(2, controller()->QueueSize());
EXPECT_FALSE(IsUtteranceListEmpty());
EXPECT_FALSE(TtsControllerCurrentUtterance());
// Speak an utterance that cannot be queued should clear the queue, then
// enqueue the new utterance.
std::unique_ptr<TtsUtteranceImpl> utterance3 =
CreateUtteranceImpl(web_contents.get());
utterance3->SetShouldClearQueue(true);
controller()->SpeakOrEnqueue(std::move(utterance3));
EXPECT_TRUE(controller()->IsPausedForTesting());
EXPECT_FALSE(IsUtteranceListEmpty());
EXPECT_EQ(1, controller()->QueueSize());
EXPECT_FALSE(TtsControllerCurrentUtterance());
// Resume the controller.
controller()->Resume();
EXPECT_FALSE(controller()->IsPausedForTesting());
}
TEST_F(TtsControllerTest, StopMustResumeController) {
std::unique_ptr<WebContents> web_contents = CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance =
CreateUtteranceImpl(web_contents.get());
utterance->SetShouldClearQueue(false);
// Speak an utterance while controller is paused. The utterance is queued.
controller()->SpeakOrEnqueue(std::move(utterance));
platform_impl()->StartSpeaking(true);
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_TRUE(TtsControllerCurrentUtterance());
platform_impl()->SetError("dummy");
// Stop must resume the controller and clear the current utterance.
controller()->Stop();
platform_impl()->FinishSpeaking();
EXPECT_EQ(2, platform_impl()->stop_speaking_called());
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_FALSE(TtsControllerCurrentUtterance());
EXPECT_TRUE(platform_impl()->GetError().empty());
EXPECT_FALSE(controller()->IsSpeaking());
}
TEST_F(TtsControllerTest, PauseAndStopMustResumeController) {
std::unique_ptr<WebContents> web_contents = CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance =
CreateUtteranceImpl(web_contents.get());
utterance->SetShouldClearQueue(false);
// Pause the controller.
controller()->Pause();
EXPECT_TRUE(controller()->IsPausedForTesting());
// Speak an utterance while controller is paused. The utterance is queued.
controller()->SpeakOrEnqueue(std::move(utterance));
EXPECT_FALSE(IsUtteranceListEmpty());
EXPECT_FALSE(TtsControllerCurrentUtterance());
platform_impl()->SetError("dummy");
// Stop must resume the controller and clear the queue.
controller()->Stop();
EXPECT_FALSE(controller()->IsPausedForTesting());
EXPECT_EQ(1, platform_impl()->stop_speaking_called());
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_FALSE(TtsControllerCurrentUtterance());
EXPECT_TRUE(platform_impl()->GetError().empty());
}
TEST_F(TtsControllerTest, EngineIdSetNoDelegateSpeakPauseResumeStop) {
std::unique_ptr<WebContents> web_contents = CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance =
CreateUtteranceImpl(web_contents.get());
utterance->SetShouldClearQueue(false);
utterance->SetLang("es-US");
utterance->SetEngineId("test_engine_id");
utterance->SetVoiceName("test_name");
std::vector<VoiceData> voices;
VoiceData voice_data0;
voice_data0.name = "voice0";
voice_data0.lang = "es-US";
voices.push_back(std::move(voice_data0));
VoiceData voice_data1;
voice_data1.name = "voice1";
voice_data1.lang = "es-ES";
voices.push_back(std::move(voice_data1));
platform_impl()->set_voices(voices);
// Start speaking an utterance.
controller()->SpeakOrEnqueue(std::move(utterance));
platform_impl()->StartSpeaking(true);
// We get a native voice and passthrough parameters from Utterance when
// Utterance has an engine_id.
EXPECT_TRUE(platform_impl()->get_utterance_voice().native);
EXPECT_EQ("es-US", platform_impl()->get_utterance_voice().lang);
EXPECT_EQ("test_engine_id", platform_impl()->get_utterance_voice().engine_id);
EXPECT_EQ("test_name", platform_impl()->get_utterance_voice().name);
// Stop() is called once when we have a new utterance (and not in
// paused state).
EXPECT_EQ(1, platform_impl()->stop_speaking_called());
// Pause the currently playing utterance should call the platform API pause.
controller()->Pause();
EXPECT_TRUE(controller()->IsPausedForTesting());
if (controller()->GetTtsEngineDelegate() == nullptr) {
// We do not set Engine Delegate for Android
EXPECT_EQ(1, platform_impl()->pause_called());
} else {
EXPECT_EQ(1, engine_delegate()->pause_called());
}
// Double pause should not call again the platform API pause.
controller()->Pause();
if (controller()->GetTtsEngineDelegate() == nullptr) {
EXPECT_EQ(1, platform_impl()->pause_called());
} else {
EXPECT_EQ(1, engine_delegate()->pause_called());
}
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_TRUE(TtsControllerCurrentUtterance());
// Resuming the playing utterance should call the platform API resume.
controller()->Resume();
EXPECT_FALSE(controller()->IsPausedForTesting());
EXPECT_TRUE(controller()->IsSpeaking());
if (controller()->GetTtsEngineDelegate() == nullptr) {
EXPECT_EQ(1, platform_impl()->resume_called());
} else {
EXPECT_EQ(1, engine_delegate()->resume_called());
}
// Double resume should not call again the platform API resume.
controller()->Resume();
EXPECT_TRUE(controller()->IsSpeaking());
if (controller()->GetTtsEngineDelegate() == nullptr) {
EXPECT_EQ(1, platform_impl()->resume_called());
} else {
EXPECT_EQ(1, engine_delegate()->resume_called());
}
// Stop should call Platform stop
controller()->Stop();
if (controller()->GetTtsEngineDelegate() == nullptr) {
EXPECT_EQ(2, platform_impl()->stop_speaking_called());
} else {
EXPECT_EQ(1, engine_delegate()->stop_called());
}
platform_impl()->FinishSpeaking();
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_FALSE(TtsControllerCurrentUtterance());
EXPECT_FALSE(controller()->IsSpeaking());
}
TEST_F(TtsControllerTest, PauseResumeNoUtterance) {
// Pause should not call the platform API when there is no utterance.
controller()->Pause();
controller()->Resume();
EXPECT_EQ(0, platform_impl()->pause_called());
EXPECT_EQ(0, platform_impl()->resume_called());
}
TEST_F(TtsControllerTest, PlatformNotSupported) {
std::unique_ptr<WebContents> web_contents = CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance =
CreateUtteranceImpl(web_contents.get());
// The utterance is dropped when the platform is not available.
platform_impl()->SetPlatformImplSupported(false);
controller()->SpeakOrEnqueue(std::move(utterance));
EXPECT_FALSE(TtsControllerCurrentUtterance());
EXPECT_TRUE(IsUtteranceListEmpty());
// No methods are called on the platform when not available.
controller()->Pause();
controller()->Resume();
controller()->Stop();
EXPECT_EQ(0, platform_impl()->pause_called());
EXPECT_EQ(0, platform_impl()->resume_called());
EXPECT_EQ(0, platform_impl()->stop_speaking_called());
}
TEST_F(TtsControllerTest, SpeakWhenLoadingPlatformImpl) {
platform_impl()->SetPlatformImplInitialized(false);
std::unique_ptr<WebContents> web_contents = CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance =
CreateUtteranceImpl(web_contents.get());
utterance->SetShouldClearQueue(false);
// Speak an utterance while platform is loading, the utterance should be
// queued.
controller()->SpeakOrEnqueue(std::move(utterance));
EXPECT_FALSE(IsUtteranceListEmpty());
EXPECT_FALSE(TtsControllerCurrentUtterance());
// Simulate the completion of the initialisation.
platform_impl()->SetPlatformImplInitialized(true);
controller()->VoicesChanged();
platform_impl()->StartSpeaking(true);
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_TRUE(TtsControllerCurrentUtterance());
EXPECT_TRUE(controller()->IsSpeaking());
// Complete the playing utterance.
platform_impl()->FinishSpeaking();
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_FALSE(TtsControllerCurrentUtterance());
EXPECT_FALSE(controller()->IsSpeaking());
}
TEST_F(TtsControllerTest, GetVoicesOnlineOffline) {
std::vector<VoiceData> voices;
VoiceData voice_data0;
voice_data0.name = "offline";
voices.push_back(std::move(voice_data0));
VoiceData voice_data1;
voice_data1.name = "online";
voice_data1.remote = true;
voices.push_back(std::move(voice_data1));
platform_impl()->set_voices(voices);
controller()->set_allow_remote_voices(true);
std::vector<VoiceData> controller_voices;
controller()->GetVoices(browser_context(), GURL(), &controller_voices);
EXPECT_EQ(2U, controller_voices.size());
controller_voices.clear();
controller()->set_allow_remote_voices(false);
controller()->GetVoices(browser_context(), GURL(), &controller_voices);
EXPECT_EQ(1U, controller_voices.size());
EXPECT_EQ("offline", controller_voices[0].name);
}
#if !BUILDFLAG(IS_ANDROID)
TEST_F(TtsControllerTest, SpeakWhenLoadingBuiltInEngine) {
engine_delegate()->set_is_built_in_tts_engine_initialized(false);
std::vector<VoiceData> voices;
VoiceData voice_data;
voice_data.engine_id = "x";
voice_data.events.insert(TTS_EVENT_START);
voice_data.events.insert(TTS_EVENT_END);
voices.push_back(voice_data);
engine_delegate()->set_voices(voices);
std::unique_ptr<WebContents> web_contents = CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance =
CreateUtteranceImpl(web_contents.get());
utterance->SetShouldClearQueue(false);
// Speak an utterance while the built in engine is initializing, the utterance
// should be queued.
controller()->SpeakOrEnqueue(std::move(utterance));
EXPECT_FALSE(IsUtteranceListEmpty());
EXPECT_FALSE(TtsControllerCurrentUtterance());
// Simulate the completion of the initialisation.
engine_delegate()->set_is_built_in_tts_engine_initialized(true);
controller()->VoicesChanged();
int utterance_id = engine_delegate()->utterance_id();
controller()->OnTtsEvent(utterance_id, content::TTS_EVENT_START, 0, 0,
std::string());
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_TRUE(TtsControllerCurrentUtterance());
EXPECT_TRUE(controller()->IsSpeaking());
// Complete the playing utterance.
controller()->OnTtsEvent(utterance_id, content::TTS_EVENT_END, 0, 0,
std::string());
EXPECT_TRUE(IsUtteranceListEmpty());
EXPECT_FALSE(TtsControllerCurrentUtterance());
EXPECT_FALSE(controller()->IsSpeaking());
}
#endif // !BUILDFLAG(IS_ANDROID)
} // namespace content