blob: 593814ad203efbce0fbd8f521d1a6f05e85bcbb5 [file] [log] [blame]
// Copyright (c) 2018 The Chromium Authors. All rights reserved.
// 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/values.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 defined(OS_CHROMEOS)
#include "content/public/browser/tts_controller_delegate.h"
#endif
namespace content {
// Platform Tts implementation that does nothing.
class MockTtsPlatformImpl : public TtsPlatform {
public:
MockTtsPlatformImpl() = default;
virtual ~MockTtsPlatformImpl() = default;
void set_voices(const std::vector<VoiceData>& voices) { voices_ = voices; }
void set_run_speak_callback(bool value) { run_speak_callback_ = value; }
void set_is_speaking(bool value) { is_speaking_ = value; }
// TtsPlatform:
bool PlatformImplAvailable() override { return true; }
void Speak(int utterance_id,
const std::string& utterance,
const std::string& lang,
const VoiceData& voice,
const UtteranceContinuousParameters& params,
base::OnceCallback<void(bool)> on_speak_finished) override {
if (run_speak_callback_)
std::move(on_speak_finished).Run(true);
}
bool IsSpeaking() override { return is_speaking_; }
bool StopSpeaking() override { return true; }
void Pause() override {}
void Resume() override {}
void GetVoices(std::vector<VoiceData>* out_voices) override {
*out_voices = voices_;
}
bool LoadBuiltInTtsEngine(BrowserContext* browser_context) override {
return false;
}
void WillSpeakUtteranceWithVoice(TtsUtterance* utterance,
const VoiceData& voice_data) override {}
void SetError(const std::string& error) override {}
std::string GetError() override { return std::string(); }
void ClearError() override {}
private:
std::vector<VoiceData> voices_;
bool run_speak_callback_ = true;
bool is_speaking_ = false;
};
#if defined(OS_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:
BrowserContext* last_browser_context_ = nullptr;
PreferredVoiceIds ids_;
};
#endif
// Subclass of TtsController with a public ctor and dtor.
class TtsControllerForTesting : public TtsControllerImpl {
public:
TtsControllerForTesting() {}
~TtsControllerForTesting() override {}
};
TEST(TtsControllerTest, TestTtsControllerShutdown) {
MockTtsPlatformImpl platform_impl;
std::unique_ptr<TtsControllerForTesting> controller =
std::make_unique<TtsControllerForTesting>();
#if defined(OS_CHROMEOS)
MockTtsControllerDelegate delegate;
controller->delegate_ = &delegate;
#endif
controller->SetTtsPlatform(&platform_impl);
std::unique_ptr<TtsUtterance> utterance1 = TtsUtterance::Create(nullptr);
utterance1->SetCanEnqueue(true);
utterance1->SetSrcId(1);
controller->SpeakOrEnqueue(std::move(utterance1));
std::unique_ptr<TtsUtterance> utterance2 = TtsUtterance::Create(nullptr);
utterance2->SetCanEnqueue(true);
utterance2->SetSrcId(2);
controller->SpeakOrEnqueue(std::move(utterance2));
// Make sure that deleting the controller when there are pending
// utterances doesn't cause a crash.
controller.reset();
}
#if defined(OS_CHROMEOS)
TEST(TtsControllerTest, TestBrowserContextRemoved) {
// Create a controller, mock other stuff, and create a test
// browser context.
TtsControllerImpl* controller = TtsControllerImpl::GetInstance();
MockTtsPlatformImpl platform_impl;
MockTtsControllerDelegate delegate;
controller->delegate_ = &delegate;
controller->SetTtsPlatform(&platform_impl);
content::BrowserTaskEnvironment task_environment;
auto browser_context = std::make_unique<TestBrowserContext>();
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.get());
utterance1->SetEngineId("x");
utterance1->SetCanEnqueue(true);
utterance1->SetSrcId(1);
controller->SpeakOrEnqueue(std::move(utterance1));
// Assert that the delegate was called and it got our browser context.
ASSERT_EQ(browser_context.get(), 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.get());
utterance2->SetEngineId("x");
utterance2->SetCanEnqueue(true);
utterance2->SetSrcId(2);
controller->SpeakOrEnqueue(std::move(utterance2));
// Destroy the browser context before the utterance is spoken.
browser_context.reset();
// 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(TtsControllerTest, TestTtsControllerUtteranceDefaults) {
std::unique_ptr<TtsControllerForTesting> controller =
std::make_unique<TtsControllerForTesting>();
std::unique_ptr<TtsUtterance> utterance1 =
content::TtsUtterance::Create(nullptr);
// 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(TtsControllerTest, TestGetMatchingVoice) {
std::unique_ptr<TtsControllerForTesting> controller =
std::make_unique<TtsControllerForTesting>();
#if defined(OS_CHROMEOS)
MockTtsControllerDelegate delegate;
controller->delegate_ = &delegate;
#endif
TestContentBrowserClient::GetInstance()->set_application_locale("en");
{
// Calling GetMatchingVoice with no voices returns -1.
std::unique_ptr<TtsUtterance> utterance(TtsUtterance::Create(nullptr));
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(nullptr));
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(nullptr));
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(nullptr));
EXPECT_EQ(0, controller->GetMatchingVoice(utterance.get(), voices));
std::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 defined(OS_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
}
{
// 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(nullptr));
// 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 defined(OS_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
}
}
class TtsControllerTestHelper {
public:
TtsControllerTestHelper() {
controller_.SetTtsPlatform(&platform_impl_);
// This ensures utterances don't immediately complete.
platform_impl_.set_run_speak_callback(false);
platform_impl_.set_is_speaking(true);
}
std::unique_ptr<TestWebContents> CreateWebContents() {
return std::unique_ptr<TestWebContents>(
TestWebContents::Create(&browser_context_, nullptr));
}
std::unique_ptr<TtsUtteranceImpl> CreateUtterance(WebContents* web_contents) {
return std::make_unique<TtsUtteranceImpl>(&browser_context_, web_contents);
}
MockTtsPlatformImpl* platform_impl() { return &platform_impl_; }
TtsControllerForTesting* controller() { return &controller_; }
TtsUtterance* TtsControllerCurrentUtterance() {
return controller_.current_utterance_.get();
}
bool IsUtteranceListEmpty() { return controller_.utterance_list_.empty(); }
private:
content::BrowserTaskEnvironment task_environment_;
RenderViewHostTestEnabler rvh_enabler_;
TestBrowserContext browser_context_;
MockTtsPlatformImpl platform_impl_;
TtsControllerForTesting controller_;
};
TEST(TtsControllerTest, StopsWhenWebContentsDestroyed) {
TtsControllerTestHelper helper;
std::unique_ptr<WebContents> web_contents = helper.CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance =
helper.CreateUtterance(web_contents.get());
helper.controller()->SpeakOrEnqueue(std::move(utterance));
EXPECT_TRUE(helper.controller()->IsSpeaking());
EXPECT_TRUE(helper.TtsControllerCurrentUtterance());
web_contents.reset();
// Destroying the WebContents should reset
// |TtsController::current_utterance_|.
EXPECT_FALSE(helper.TtsControllerCurrentUtterance());
}
TEST(TtsControllerTest, StartsQueuedUtteranceWhenWebContentsDestroyed) {
TtsControllerTestHelper helper;
std::unique_ptr<WebContents> web_contents1 = helper.CreateWebContents();
std::unique_ptr<WebContents> web_contents2 = helper.CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance1 =
helper.CreateUtterance(web_contents1.get());
void* raw_utterance1 = utterance1.get();
std::unique_ptr<TtsUtteranceImpl> utterance2 =
helper.CreateUtterance(web_contents2.get());
utterance2->SetCanEnqueue(true);
void* raw_utterance2 = utterance2.get();
helper.controller()->SpeakOrEnqueue(std::move(utterance1));
EXPECT_TRUE(helper.controller()->IsSpeaking());
EXPECT_TRUE(helper.TtsControllerCurrentUtterance());
helper.controller()->SpeakOrEnqueue(std::move(utterance2));
EXPECT_EQ(raw_utterance1, helper.TtsControllerCurrentUtterance());
web_contents1.reset();
// Destroying |web_contents1| should delete |utterance1| and start
// |utterance2|.
EXPECT_TRUE(helper.TtsControllerCurrentUtterance());
EXPECT_EQ(raw_utterance2, helper.TtsControllerCurrentUtterance());
}
TEST(TtsControllerTest, StartsQueuedUtteranceWhenWebContentsDestroyed2) {
TtsControllerTestHelper helper;
std::unique_ptr<WebContents> web_contents1 = helper.CreateWebContents();
std::unique_ptr<WebContents> web_contents2 = helper.CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance1 =
helper.CreateUtterance(web_contents1.get());
void* raw_utterance1 = utterance1.get();
std::unique_ptr<TtsUtteranceImpl> utterance2 =
helper.CreateUtterance(web_contents1.get());
std::unique_ptr<TtsUtteranceImpl> utterance3 =
helper.CreateUtterance(web_contents2.get());
void* raw_utterance3 = utterance3.get();
utterance2->SetCanEnqueue(true);
utterance3->SetCanEnqueue(true);
helper.controller()->SpeakOrEnqueue(std::move(utterance1));
helper.controller()->SpeakOrEnqueue(std::move(utterance2));
helper.controller()->SpeakOrEnqueue(std::move(utterance3));
EXPECT_TRUE(helper.controller()->IsSpeaking());
EXPECT_EQ(raw_utterance1, helper.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, helper.TtsControllerCurrentUtterance());
EXPECT_TRUE(helper.IsUtteranceListEmpty());
web_contents2.reset();
// Deleting |web_contents2| should delete |utterance3| as it's from a
// different WebContents.
EXPECT_EQ(nullptr, helper.TtsControllerCurrentUtterance());
}
TEST(TtsControllerTest, StartsUtteranceWhenWebContentsHidden) {
TtsControllerTestHelper helper;
std::unique_ptr<TestWebContents> web_contents = helper.CreateWebContents();
web_contents->SetVisibilityAndNotifyObservers(Visibility::HIDDEN);
std::unique_ptr<TtsUtteranceImpl> utterance =
helper.CreateUtterance(web_contents.get());
helper.controller()->SpeakOrEnqueue(std::move(utterance));
EXPECT_TRUE(helper.controller()->IsSpeaking());
}
TEST(TtsControllerTest,
DoesNotStartUtteranceWhenWebContentsHiddenAndStopSpeakingWhenHiddenSet) {
TtsControllerTestHelper helper;
std::unique_ptr<TestWebContents> web_contents = helper.CreateWebContents();
web_contents->SetVisibilityAndNotifyObservers(Visibility::HIDDEN);
std::unique_ptr<TtsUtteranceImpl> utterance =
helper.CreateUtterance(web_contents.get());
helper.controller()->SetStopSpeakingWhenHidden(true);
helper.controller()->SpeakOrEnqueue(std::move(utterance));
EXPECT_EQ(nullptr, helper.TtsControllerCurrentUtterance());
EXPECT_TRUE(helper.IsUtteranceListEmpty());
}
TEST(TtsControllerTest, SkipsQueuedUtteranceFromHiddenWebContents) {
TtsControllerTestHelper helper;
helper.controller()->SetStopSpeakingWhenHidden(true);
std::unique_ptr<WebContents> web_contents1 = helper.CreateWebContents();
std::unique_ptr<TestWebContents> web_contents2 = helper.CreateWebContents();
std::unique_ptr<TtsUtteranceImpl> utterance1 =
helper.CreateUtterance(web_contents1.get());
const int utterance1_id = utterance1->GetId();
std::unique_ptr<TtsUtteranceImpl> utterance2 =
helper.CreateUtterance(web_contents2.get());
utterance2->SetCanEnqueue(true);
helper.controller()->SpeakOrEnqueue(std::move(utterance1));
EXPECT_TRUE(helper.TtsControllerCurrentUtterance());
EXPECT_TRUE(helper.IsUtteranceListEmpty());
// Speak |utterance2|, which should get queued.
helper.controller()->SpeakOrEnqueue(std::move(utterance2));
EXPECT_FALSE(helper.IsUtteranceListEmpty());
// Make the second WebContents hidden, this shouldn't change anything in
// TtsController.
web_contents2->SetVisibilityAndNotifyObservers(Visibility::HIDDEN);
EXPECT_FALSE(helper.IsUtteranceListEmpty());
// Finish |utterance1|, which should skip |utterance2| because |web_contents2|
// is hidden.
helper.controller()->OnTtsEvent(utterance1_id, TTS_EVENT_END, 0, 0, {});
EXPECT_EQ(nullptr, helper.TtsControllerCurrentUtterance());
EXPECT_TRUE(helper.IsUtteranceListEmpty());
}
} // namespace content