| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/functional/bind.h" |
| #include "base/location.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/extensions/component_loader.h" |
| #include "chrome/browser/extensions/extension_apitest.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/speech/extension_api/tts_engine_extension_api.h" |
| #include "chrome/browser/speech/extension_api/tts_extension_api.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/tts_controller.h" |
| #include "content/public/browser/tts_platform.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/test_utils.h" |
| #include "extensions/browser/event_router.h" |
| #include "extensions/browser/extension_host.h" |
| #include "extensions/browser/extension_host_test_helper.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/common/mojom/view_type.mojom.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "net/base/network_change_notifier.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/accessibility/accessibility_features.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chrome/browser/speech/extension_api/tts_engine_extension_observer_chromeos.h" |
| #include "chrome/browser/speech/extension_api/tts_engine_extension_observer_chromeos_factory.h" |
| #include "chromeos/services/tts/tts_service.h" |
| #endif // IS_CHROMEOS |
| |
| using ::testing::_; |
| using ::testing::AnyNumber; |
| using ::testing::DoAll; |
| using ::testing::InSequence; |
| using ::testing::Invoke; |
| using ::testing::InvokeWithoutArgs; |
| using ::testing::Return; |
| using ::testing::SaveArg; |
| using ::testing::SetArgPointee; |
| using ::testing::StrictMock; |
| |
| namespace { |
| int g_saved_utterance_id; |
| } |
| |
| namespace extensions { |
| |
| class MockUpdateLanguageStatusDelegate |
| : public content::UpdateLanguageStatusDelegate { |
| public: |
| MockUpdateLanguageStatusDelegate() = default; |
| ~MockUpdateLanguageStatusDelegate() override = default; |
| |
| void OnUpdateLanguageStatus(content::BrowserContext* browser_context, |
| const std::string& lang, |
| content::LanguageInstallStatus install_status, |
| const std::string& error) override { |
| this->on_update_language_status_params.browser_context = browser_context; |
| this->on_update_language_status_params.lang = lang; |
| this->on_update_language_status_params.install_status = install_status; |
| this->on_update_language_status_params.error = error; |
| } |
| |
| struct OnUpdateLanguageStatusParams { |
| raw_ptr<content::BrowserContext> browser_context; |
| std::string lang; |
| content::LanguageInstallStatus install_status; |
| std::string error; |
| }; |
| |
| OnUpdateLanguageStatusParams GetLastUpdateLanguageStatusParams() { |
| return on_update_language_status_params; |
| } |
| |
| private: |
| OnUpdateLanguageStatusParams on_update_language_status_params; |
| }; |
| class MockTtsPlatformImpl : public content::TtsPlatform { |
| public: |
| MockTtsPlatformImpl() : should_fake_get_voices_(false) {} |
| |
| 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_; } |
| |
| // Work-around for functions that take move-only types as arguments. |
| // https://github.com/google/googlemock/blob/master/googlemock/docs/CookBook.md |
| // Delegate the Speak() method to DoSpeak(), which doesn't take any move |
| // parameters. |
| 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)> on_speak_finished) override { |
| DoSpeak(utterance_id, utterance, lang, voice, params); |
| // Logic for PlatformSpeakError test. Needs to fail the first time, but |
| // succeed the second time. |
| if (speak_error_test_) { |
| speak_error_count_ > 0 ? std::move(on_speak_finished).Run(true) |
| : std::move(on_speak_finished).Run(false); |
| ++speak_error_count_; |
| } else { |
| std::move(on_speak_finished).Run(true); |
| } |
| } |
| MOCK_METHOD5(DoSpeak, |
| void(int utterance_id, |
| const std::string& utterance, |
| const std::string& lang, |
| const content::VoiceData& voice, |
| const content::UtteranceContinuousParameters& params)); |
| |
| MOCK_METHOD0(StopSpeaking, bool(void)); |
| |
| MOCK_METHOD0(Pause, void(void)); |
| |
| MOCK_METHOD0(Resume, void(void)); |
| |
| MOCK_METHOD0(IsSpeaking, bool(void)); |
| |
| // Fake this method to add a native voice. |
| void GetVoices(std::vector<content::VoiceData>* voices) override { |
| if (!should_fake_get_voices_) |
| return; |
| |
| content::VoiceData voice; |
| voice.name = "TestNativeVoice"; |
| voice.native = true; |
| voice.lang = "en-GB"; |
| voice.events.insert(content::TTS_EVENT_START); |
| voice.events.insert(content::TTS_EVENT_END); |
| voices->push_back(voice); |
| } |
| |
| void Shutdown() override {} |
| |
| void FinalizeVoiceOrdering(std::vector<content::VoiceData>& voices) override { |
| // Prefer non-native voices. |
| std::stable_partition( |
| voices.begin(), voices.end(), |
| [](const content::VoiceData& voice) { return !voice.native; }); |
| } |
| |
| void RefreshVoices() override {} |
| |
| void set_should_fake_get_voices(bool val) { should_fake_get_voices_ = val; } |
| |
| void SetErrorToEpicFail() { SetError("epic fail"); } |
| |
| void SendEndEventOnSavedUtteranceId() { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&MockTtsPlatformImpl::SendEvent, |
| ptr_factory_.GetWeakPtr(), false, g_saved_utterance_id, |
| content::TTS_EVENT_END, 0, 0, std::string()), |
| base::TimeDelta()); |
| } |
| |
| void SendEndEvent(int utterance_id, |
| const std::string& utterance, |
| const std::string& lang, |
| const content::VoiceData& voice, |
| const content::UtteranceContinuousParameters& params) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&MockTtsPlatformImpl::SendEvent, |
| ptr_factory_.GetWeakPtr(), false, utterance_id, |
| content::TTS_EVENT_END, utterance.size(), 0, |
| std::string()), |
| base::TimeDelta()); |
| } |
| |
| void SendEndEventWhenQueueNotEmpty( |
| int utterance_id, |
| const std::string& utterance, |
| const std::string& lang, |
| const content::VoiceData& voice, |
| const content::UtteranceContinuousParameters& params) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&MockTtsPlatformImpl::SendEvent, |
| ptr_factory_.GetWeakPtr(), true, utterance_id, |
| content::TTS_EVENT_END, utterance.size(), 0, |
| std::string()), |
| base::TimeDelta()); |
| } |
| |
| void SendWordEvents(int utterance_id, |
| const std::string& utterance, |
| const std::string& lang, |
| const content::VoiceData& voice, |
| const content::UtteranceContinuousParameters& params) { |
| for (int i = 0; i < static_cast<int>(utterance.size()); i++) { |
| if (i == 0 || utterance[i - 1] == ' ') { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&MockTtsPlatformImpl::SendEvent, |
| ptr_factory_.GetWeakPtr(), false, utterance_id, |
| content::TTS_EVENT_WORD, i, 1, std::string()), |
| base::TimeDelta()); |
| } |
| } |
| } |
| |
| void SendEvent(bool wait_for_non_empty_queue, |
| int utterance_id, |
| content::TtsEventType event_type, |
| int char_index, |
| int length, |
| const std::string& message) { |
| content::TtsController* tts_controller = |
| content::TtsController::GetInstance(); |
| if (wait_for_non_empty_queue && tts_controller->QueueSize() == 0) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&MockTtsPlatformImpl::SendEvent, |
| ptr_factory_.GetWeakPtr(), true, utterance_id, |
| event_type, char_index, length, message), |
| base::Milliseconds(100)); |
| return; |
| } |
| |
| tts_controller->OnTtsEvent(utterance_id, event_type, char_index, length, |
| message); |
| } |
| |
| void SetSpeakErrorTest(bool value) { speak_error_test_ = value; } |
| |
| private: |
| bool speak_error_test_ = false; |
| int speak_error_count_ = 0; |
| bool should_fake_get_voices_; |
| std::string error_; |
| base::WeakPtrFactory<MockTtsPlatformImpl> ptr_factory_{this}; |
| }; |
| |
| class FakeNetworkOnlineStateForTest : public net::NetworkChangeNotifier { |
| public: |
| explicit FakeNetworkOnlineStateForTest(bool online) : online_(online) {} |
| |
| FakeNetworkOnlineStateForTest(const FakeNetworkOnlineStateForTest&) = delete; |
| FakeNetworkOnlineStateForTest& operator=( |
| const FakeNetworkOnlineStateForTest&) = delete; |
| |
| ~FakeNetworkOnlineStateForTest() override = default; |
| |
| ConnectionType GetCurrentConnectionType() const override { |
| return online_ ? net::NetworkChangeNotifier::CONNECTION_ETHERNET |
| : net::NetworkChangeNotifier::CONNECTION_NONE; |
| } |
| |
| private: |
| bool online_; |
| }; |
| |
| class EventRouterAddListenerWaiter : public EventRouter::Observer { |
| public: |
| EventRouterAddListenerWaiter(Profile* profile, const std::string& event_name) |
| : event_router_(EventRouter::EventRouter::Get(profile)) { |
| DCHECK(profile); |
| event_router_->RegisterObserver(this, event_name); |
| } |
| |
| EventRouterAddListenerWaiter(const EventRouterAddListenerWaiter&) = delete; |
| EventRouterAddListenerWaiter& operator=(const EventRouterAddListenerWaiter&) = |
| delete; |
| |
| ~EventRouterAddListenerWaiter() override { |
| event_router_->UnregisterObserver(this); |
| } |
| |
| void Wait() { loop_runner_.Run(); } |
| |
| // EventRouter::Observer overrides. |
| void OnListenerAdded(const EventListenerInfo& details) override { |
| loop_runner_.Quit(); |
| } |
| |
| private: |
| const raw_ptr<EventRouter> event_router_; |
| base::RunLoop loop_runner_; |
| }; |
| |
| using ContextType = extensions::browser_test_util::ContextType; |
| |
| struct FeaturesTestParam { |
| ContextType context_type; |
| std::vector<base::test::FeatureRef> enabled_features; |
| std::vector<base::test::FeatureRef> disabled_features; |
| }; |
| |
| class TtsApiTest : public ExtensionApiTest, |
| public testing::WithParamInterface<FeaturesTestParam> { |
| public: |
| TtsApiTest() : ExtensionApiTest(GetParam().context_type) { |
| const FeaturesTestParam& features_test_param = GetParam(); |
| scoped_feature_list_.InitWithFeatures( |
| features_test_param.enabled_features, |
| features_test_param.disabled_features); |
| } |
| |
| ~TtsApiTest() override = default; |
| TtsApiTest(const TtsApiTest&) = delete; |
| TtsApiTest& operator=(const TtsApiTest&) = delete; |
| |
| void SetUpInProcessBrowserTestFixture() override { |
| ExtensionApiTest::SetUpInProcessBrowserTestFixture(); |
| content::TtsController::SkipAddNetworkChangeObserverForTests(true); |
| content::TtsController* tts_controller = |
| content::TtsController::GetInstance(); |
| tts_controller->SetTtsPlatform(&mock_platform_impl_); |
| TtsExtensionEngine::GetInstance()->DisableBuiltInTTSEngineForTesting(); |
| tts_controller->SetTtsEngineDelegate(TtsExtensionEngine::GetInstance()); |
| } |
| |
| void AddNetworkSpeechSynthesisExtension() { |
| auto* component_loader = extensions::ComponentLoader::Get(profile()); |
| component_loader->AddNetworkSpeechSynthesisExtension(); |
| |
| // Wait for any tts engine event listener to be added by the network tts |
| // engine so that tests can be ready to validate state. |
| EventRouterAddListenerWaiter waiter(profile(), tts_engine_events::kOnStop); |
| waiter.Wait(); |
| } |
| |
| protected: |
| bool HasVoiceWithName(const std::string& name) { |
| std::vector<content::VoiceData> voices; |
| content::TtsController::GetInstance()->GetVoices(profile(), GURL(), |
| &voices); |
| for (auto& voice : voices) { |
| if (voice.name == name) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| StrictMock<MockTtsPlatformImpl> mock_platform_impl_; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| // Android only support MV3 / service worker. |
| INSTANTIATE_TEST_SUITE_P( |
| PersistentBackground, |
| TtsApiTest, |
| ::testing::Values(FeaturesTestParam{ |
| .context_type = ContextType::kPersistentBackground})); |
| #endif // !BUILDFLAG(IS_ANDROID) |
| |
| INSTANTIATE_TEST_SUITE_P( |
| ServiceWorker, |
| TtsApiTest, |
| ::testing::Values( |
| FeaturesTestParam{ |
| .context_type = ContextType::kServiceWorker, |
| .enabled_features = |
| {features::kExtensionManifestV3NetworkSpeechSynthesis}}, |
| FeaturesTestParam{ |
| .context_type = ContextType::kServiceWorker, |
| .disabled_features = { |
| features::kExtensionManifestV3NetworkSpeechSynthesis}})); |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, PlatformSpeakOptionalArgs) { |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()); |
| |
| InSequence s; |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "", _, _, _)).WillOnce(Return()); |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "Alpha", _, _, _)) |
| .WillOnce(Return()); |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "Bravo", _, _, _)) |
| .WillOnce(Return()); |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "Charlie", _, _, _)) |
| .WillOnce(Return()); |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "Echo", _, _, _)) |
| .WillOnce(Return()); |
| |
| // Called when the extension's background host shuts down on unload. Service |
| // worker-based extensions don't have hosts, so this won't happen. |
| // See crbug.com/339682312. |
| if (GetParam().context_type == ContextType::kPersistentBackground) { |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(false)); |
| } |
| |
| ASSERT_TRUE(RunExtensionTest("tts/optional_args")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, PlatformSpeakFinishesImmediately) { |
| InSequence s; |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()); |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, _, _, _, _)) |
| .WillOnce(DoAll( |
| Invoke(&mock_platform_impl_, &MockTtsPlatformImpl::SendEndEvent), |
| Return())); |
| ASSERT_TRUE(RunExtensionTest("tts/speak_once")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, PlatformSpeakInterrupt) { |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()); |
| |
| // One utterance starts speaking, and then a second interrupts. |
| InSequence s; |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "text 1", _, _, _)) |
| .WillOnce(Return()); |
| // Expect the second utterance and allow it to finish. |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "text 2", _, _, _)) |
| .WillOnce(DoAll( |
| Invoke(&mock_platform_impl_, &MockTtsPlatformImpl::SendEndEvent), |
| Return())); |
| ASSERT_TRUE(RunExtensionTest("tts/interrupt")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, PlatformSpeakQueueInterrupt) { |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()); |
| |
| // In this test, two utterances are queued, and then a third |
| // interrupts. Speak(, _) never gets called on the second utterance. |
| InSequence s; |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "text 1", _, _, _)) |
| .WillOnce(Return()); |
| // Don't expect the second utterance, because it's queued up and the |
| // first never finishes. |
| // Expect the third utterance and allow it to finish successfully. |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "text 3", _, _, _)) |
| .WillOnce(DoAll( |
| Invoke(&mock_platform_impl_, &MockTtsPlatformImpl::SendEndEvent), |
| Return())); |
| ASSERT_TRUE(RunExtensionTest("tts/queue_interrupt")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, PlatformSpeakEnqueue) { |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()); |
| |
| InSequence s; |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "text 1", _, _, _)) |
| .WillOnce( |
| DoAll(Invoke(&mock_platform_impl_, |
| &MockTtsPlatformImpl::SendEndEventWhenQueueNotEmpty), |
| Return())); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "text 2", _, _, _)) |
| .WillOnce(DoAll( |
| Invoke(&mock_platform_impl_, &MockTtsPlatformImpl::SendEndEvent), |
| Return())); |
| ASSERT_TRUE(RunExtensionTest("tts/enqueue")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, PlatformSpeakError) { |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()).Times(AnyNumber()); |
| |
| mock_platform_impl_.SetSpeakErrorTest(true); |
| |
| InSequence s; |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "first try", _, _, _)) |
| .WillOnce( |
| DoAll(InvokeWithoutArgs(&mock_platform_impl_, |
| &MockTtsPlatformImpl::SetErrorToEpicFail), |
| Return())); |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "second try", _, _, _)) |
| .WillOnce(DoAll( |
| Invoke(&mock_platform_impl_, &MockTtsPlatformImpl::SendEndEvent), |
| Return())); |
| ASSERT_TRUE(RunExtensionTest("tts/speak_error")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, PlatformWordCallbacks) { |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()); |
| |
| InSequence s; |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "one two three", _, _, _)) |
| .WillOnce(DoAll( |
| Invoke(&mock_platform_impl_, &MockTtsPlatformImpl::SendWordEvents), |
| Invoke(&mock_platform_impl_, &MockTtsPlatformImpl::SendEndEvent), |
| Return())); |
| ASSERT_TRUE(RunExtensionTest("tts/word_callbacks")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, PlatformPauseResume) { |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()).Times(AnyNumber()); |
| |
| InSequence s; |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "test 1", _, _, _)) |
| .WillOnce(DoAll( |
| Invoke(&mock_platform_impl_, &MockTtsPlatformImpl::SendEndEvent), |
| Return())); |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "test 2", _, _, _)) |
| .WillOnce(DoAll(SaveArg<0>(&g_saved_utterance_id), Return())); |
| EXPECT_CALL(mock_platform_impl_, Pause()); |
| EXPECT_CALL(mock_platform_impl_, Resume()) |
| .WillOnce(InvokeWithoutArgs( |
| &mock_platform_impl_, |
| &MockTtsPlatformImpl::SendEndEventOnSavedUtteranceId)); |
| ASSERT_TRUE(RunExtensionTest("tts/pause_resume")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, PlatformPauseSpeakNoEnqueue) { |
| // While paused, one utterance is enqueued, and then a second utterance that |
| // cannot be enqueued cancels only the first. |
| InSequence s; |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "text 2", _, _, _)); |
| // Called when the extension's background host shuts down on unload. Service |
| // worker-based extensions don't have hosts, so this only happens with a |
| // persistent background page. |
| // See crbug.com/339682312. |
| if (GetParam().context_type == ContextType::kPersistentBackground) { |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(false)); |
| } |
| ASSERT_TRUE(RunExtensionTest("tts/pause_speak_no_enqueue")) << message_; |
| } |
| |
| // The service worker-based tests can't be parameterized. |
| using TtsApiServiceWorkerTest = TtsApiTest; |
| |
| INSTANTIATE_TEST_SUITE_P(ServiceWorker, |
| TtsApiServiceWorkerTest, |
| ::testing::Values(ContextType::kNone)); |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiServiceWorkerTest, Enqueue) { |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()); |
| |
| InSequence s; |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillOnce(Return(true)); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "text 1", _, _, _)) |
| .WillOnce( |
| DoAll(Invoke(&mock_platform_impl_, |
| &MockTtsPlatformImpl::SendEndEventWhenQueueNotEmpty), |
| Return())); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "text 2", _, _, _)) |
| .WillOnce(DoAll( |
| Invoke(&mock_platform_impl_, &MockTtsPlatformImpl::SendEndEvent), |
| Return())); |
| ASSERT_TRUE(RunExtensionTest("tts/service_worker_enqueue")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiServiceWorkerTest, SpeakError) { |
| ASSERT_TRUE(RunExtensionTest("tts/service_worker_speak_error")) << message_; |
| } |
| |
| // |
| // TTS Engine tests. |
| // |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // TODO(crbug.com/432798510): Port to desktop Android. The speech does not seem |
| // to finish before test shutdown causes a DCHECK in ~TtsUtteranceImpl. Also, |
| // some tests use chrome.app.getDetails() which is not supported on Android. |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, RegisterEngine) { |
| mock_platform_impl_.set_should_fake_get_voices(true); |
| |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()).Times(AnyNumber()); |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillRepeatedly(Return(true)); |
| |
| { |
| InSequence s; |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "native speech", _, _, _)) |
| .WillOnce(DoAll( |
| Invoke(&mock_platform_impl_, &MockTtsPlatformImpl::SendEndEvent), |
| Return())); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "native speech 2", _, _, _)) |
| .WillOnce(DoAll( |
| Invoke(&mock_platform_impl_, &MockTtsPlatformImpl::SendEndEvent), |
| Return())); |
| EXPECT_CALL(mock_platform_impl_, DoSpeak(_, "native speech 3", _, _, _)) |
| .WillOnce(DoAll( |
| Invoke(&mock_platform_impl_, &MockTtsPlatformImpl::SendEndEvent), |
| Return())); |
| } |
| |
| // TODO(katie): Expect the deprecated gender warning rather than ignoring |
| // warnings. |
| ASSERT_TRUE(RunExtensionTest("tts_engine/register_engine", {}, |
| {.ignore_manifest_warnings = true})) |
| << message_; |
| } |
| |
| // https://crbug.com/709115 tracks test flakiness. |
| #if BUILDFLAG(IS_POSIX) |
| #define MAYBE_EngineError DISABLED_EngineError |
| #else |
| #define MAYBE_EngineError EngineError |
| #endif |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, MAYBE_EngineError) { |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()); |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillRepeatedly(Return(true)); |
| |
| ASSERT_TRUE(RunExtensionTest("tts_engine/engine_error")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, EngineWordCallbacks) { |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()); |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillRepeatedly(Return(true)); |
| |
| ASSERT_TRUE(RunExtensionTest("tts_engine/engine_word_callbacks")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, LangMatching) { |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()); |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillRepeatedly(Return(true)); |
| |
| ASSERT_TRUE(RunExtensionTest("tts_engine/lang_matching")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, NetworkSpeechEngine) { |
| // Simulate online network state. |
| net::NetworkChangeNotifier::DisableForTest disable_for_test; |
| FakeNetworkOnlineStateForTest fake_online_state(true); |
| |
| ASSERT_NO_FATAL_FAILURE(AddNetworkSpeechSynthesisExtension()); |
| ASSERT_TRUE(RunExtensionTest("tts_engine/network_speech_engine")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, NoNetworkSpeechEngineWhenOffline) { |
| // Simulate offline network state. |
| net::NetworkChangeNotifier::DisableForTest disable_for_test; |
| FakeNetworkOnlineStateForTest fake_online_state(false); |
| |
| ASSERT_NO_FATAL_FAILURE(AddNetworkSpeechSynthesisExtension()); |
| // Test should fail when offline. |
| ASSERT_FALSE(RunExtensionTest("tts_engine/network_speech_engine")); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| // http://crbug.com/122474 |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, EngineApi) { |
| ASSERT_TRUE(RunExtensionTest("tts_engine/engine_api")) << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, UpdateVoicesApi) { |
| ASSERT_TRUE(RunExtensionTest("tts_engine/update_voices_api")) << message_; |
| } |
| |
| #if !BUILDFLAG(IS_CHROMEOS) |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, UpdateLanguageCallsDelegate) { |
| MockUpdateLanguageStatusDelegate delegate_; |
| content::TtsController::GetInstance()->AddUpdateLanguageStatusDelegate( |
| &delegate_); |
| |
| ASSERT_TRUE(RunExtensionTest("tts_engine/call_update_language")) << message_; |
| ASSERT_EQ(delegate_.GetLastUpdateLanguageStatusParams().browser_context, |
| profile()); |
| ASSERT_EQ(delegate_.GetLastUpdateLanguageStatusParams().install_status, |
| content::LanguageInstallStatus::INSTALLING); |
| ASSERT_EQ(delegate_.GetLastUpdateLanguageStatusParams().lang, "fr"); |
| ASSERT_EQ(delegate_.GetLastUpdateLanguageStatusParams().error, ""); |
| content::TtsController::GetInstance()->RemoveUpdateLanguageStatusDelegate( |
| &delegate_); |
| } |
| #endif |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, UninstallLanguageRequestEmitsEvent) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| ExtensionTestMessageListener validate_lang_param_listener("lang:fr"); |
| ExtensionTestMessageListener validate_requestor_param_listener( |
| "requestor.id:client ID 3, requestor.source:chromefeature"); |
| ExtensionTestMessageListener validate_uninstall_immediately_param_listener( |
| "uninstallImmediately:true"); |
| |
| const Extension* extension = LoadExtension( |
| test_data_dir_.AppendASCII("tts_engine/language_uninstall_request")); |
| ASSERT_TRUE(extension); |
| |
| content::TtsController::GetInstance()->UninstallLanguageRequest( |
| profile(), /* lang= */ "fr", /* client_id= */ "client ID 3", |
| /* source= */ |
| static_cast<int>(tts_engine_events::TtsClientSource::CHROMEFEATURE), |
| /* uninstall_immediately= */ true); |
| |
| ASSERT_TRUE(validate_lang_param_listener.WaitUntilSatisfied()); |
| ASSERT_TRUE(validate_requestor_param_listener.WaitUntilSatisfied()); |
| ASSERT_TRUE( |
| validate_uninstall_immediately_param_listener.WaitUntilSatisfied()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, LanguageInstallRequestEmitsEvent) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| ExtensionTestMessageListener validate_lang_param_listener("lang:en"); |
| ExtensionTestMessageListener validate_requestor_param_listener( |
| "requestor.id:client ID, requestor.source:chromefeature"); |
| |
| const Extension* extension = LoadExtension( |
| test_data_dir_.AppendASCII("tts_engine/language_install_request")); |
| ASSERT_TRUE(extension); |
| |
| content::TtsController::GetInstance()->InstallLanguageRequest( |
| profile(), /* lang= */ "en", /* client_id= */ "client ID", |
| /* source= */ |
| static_cast<int>(tts_engine_events::TtsClientSource::CHROMEFEATURE)); |
| |
| ASSERT_TRUE(validate_lang_param_listener.WaitUntilSatisfied()); |
| ASSERT_TRUE(validate_requestor_param_listener.WaitUntilSatisfied()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, LanguageStatusRequestEmitsEvent) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| ExtensionTestMessageListener validate_lang_param_listener("lang:br"); |
| ExtensionTestMessageListener validate_requestor_param_listener( |
| "requestor.id:client ID 2, requestor.source:extension"); |
| |
| const Extension* extension = LoadExtension( |
| test_data_dir_.AppendASCII("tts_engine/language_status_request")); |
| ASSERT_TRUE(extension); |
| |
| content::TtsController::GetInstance()->LanguageStatusRequest( |
| profile(), /* lang= */ "br", /* client_id= */ "client ID 2", |
| /* source= */ |
| static_cast<int>(tts_engine_events::TtsClientSource::EXTENSION)); |
| |
| ASSERT_TRUE(validate_lang_param_listener.WaitUntilSatisfied()); |
| ASSERT_TRUE(validate_requestor_param_listener.WaitUntilSatisfied()); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // TODO(crbug.com/40200835): Port to desktop Android when PRE_ steps are |
| // supported. |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, PRE_VoicesAreCached) { |
| EXPECT_FALSE(HasVoiceWithName("Dynamic Voice 1")); |
| EXPECT_FALSE(HasVoiceWithName("Dynamic Voice 2")); |
| ASSERT_TRUE(RunExtensionTest("tts_engine/call_update_voices")) << message_; |
| EXPECT_TRUE(HasVoiceWithName("Dynamic Voice 1")); |
| EXPECT_TRUE(HasVoiceWithName("Dynamic Voice 2")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, VoicesAreCached) { |
| // Make sure the dynamically loaded voices are available even though |
| // the extension didn't "run". Note that the voices might not be available |
| // immediately when the test runs, but the test should pass shortly after |
| // the extension's event listeners are registered. |
| while (!HasVoiceWithName("Dynamic Voice 1") || |
| !HasVoiceWithName("Dynamic Voice 2")) { |
| // Wait for the extension's event listener to be registered before |
| // checking what voices are registered. |
| EventRouterAddListenerWaiter waiter(profile(), tts_engine_events::kOnStop); |
| waiter.Wait(); |
| } |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, OnSpeakWithAudioStream) { |
| TtsExtensionEngine::GetInstance()->DisableBuiltInTTSEngineForTesting(); |
| TtsEngineExtensionObserverChromeOS* engine_observer = |
| TtsEngineExtensionObserverChromeOSFactory::GetForProfile(profile()); |
| mojo::Remote<chromeos::tts::mojom::TtsService>* tts_service_remote = |
| engine_observer->tts_service_for_testing(); |
| chromeos::tts::TtsService tts_service( |
| tts_service_remote->BindNewPipeAndPassReceiver()); |
| |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()).Times(AnyNumber()); |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillRepeatedly(Return(true)); |
| |
| ASSERT_TRUE(RunExtensionTest("tts_engine/on_speak_with_audio_stream")) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_P(TtsApiTest, OnSpeakWithAudioStreamAudioOptions) { |
| TtsExtensionEngine::GetInstance()->DisableBuiltInTTSEngineForTesting(); |
| TtsEngineExtensionObserverChromeOS* engine_observer = |
| TtsEngineExtensionObserverChromeOSFactory::GetForProfile(profile()); |
| mojo::Remote<chromeos::tts::mojom::TtsService>* tts_service_remote = |
| engine_observer->tts_service_for_testing(); |
| chromeos::tts::TtsService tts_service( |
| tts_service_remote->BindNewPipeAndPassReceiver()); |
| |
| EXPECT_CALL(mock_platform_impl_, IsSpeaking()).Times(AnyNumber()); |
| EXPECT_CALL(mock_platform_impl_, StopSpeaking()).WillRepeatedly(Return(true)); |
| |
| ASSERT_TRUE(RunExtensionTest( |
| "tts_engine/on_speak_with_audio_stream_using_audio_options")) |
| << message_; |
| } |
| #endif // IS_CHROMEOS |
| |
| } // namespace extensions |