| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/live_caption/caption_controller_base.h" |
| |
| #include <memory> |
| #include <string> |
| |
| #include "components/live_caption/caption_bubble_context.h" |
| #include "components/live_caption/caption_bubble_controller.h" |
| #include "components/live_caption/pref_names.h" |
| #include "components/live_caption/views/translation_view_wrapper_base.h" |
| #include "components/prefs/pref_registry_simple.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/testing_pref_service.h" |
| #include "media/mojo/mojom/speech_recognition_result.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using testing::_; |
| using testing::Return; |
| |
| namespace captions { |
| namespace { |
| |
| // CaptionControllerBase that we'll instantiate for test. |
| class TestCaptionControllerBase : public CaptionControllerBase { |
| public: |
| TestCaptionControllerBase(PrefService* profile_prefs, |
| const std::string& application_locale, |
| std::unique_ptr<Delegate> delegate) |
| : CaptionControllerBase(profile_prefs, |
| application_locale, |
| std::move(delegate)) {} |
| |
| MOCK_METHOD(CaptionBubbleSettings*, caption_bubble_settings, (), (override)); |
| }; |
| |
| class MockListener : public CaptionControllerBase::Listener { |
| public: |
| explicit MockListener(bool* free_flag = nullptr) : free_flag_(free_flag) {} |
| ~MockListener() override { |
| if (free_flag_) { |
| *free_flag_ = true; |
| } |
| } |
| |
| MOCK_METHOD(bool, |
| OnTranscription, |
| (content::RenderFrameHost*, |
| CaptionBubbleContext*, |
| const media::SpeechRecognitionResult&), |
| (override)); |
| MOCK_METHOD(void, |
| OnAudioStreamEnd, |
| (content::RenderFrameHost*, CaptionBubbleContext*), |
| (override)); |
| MOCK_METHOD(void, |
| OnLanguageIdentificationEvent, |
| (content::RenderFrameHost*, |
| CaptionBubbleContext*, |
| const media::mojom::LanguageIdentificationEventPtr&), |
| (override)); |
| |
| private: |
| raw_ptr<bool> free_flag_ = nullptr; |
| }; |
| |
| class MockCaptionBubbleContext : public CaptionBubbleContext { |
| public: |
| ~MockCaptionBubbleContext() override = default; |
| |
| MOCK_METHOD(void, GetBounds, (GetBoundsCallback), (const override)); |
| // MOCK_METHOD hates "const std::string" as a return type. |
| const std::string GetSessionId() const override { return {}; } |
| MOCK_METHOD(void, Activate, (), (override)); |
| MOCK_METHOD(bool, IsActivatable, (), (const override)); |
| MOCK_METHOD(bool, ShouldAvoidOverlap, (), (const override)); |
| MOCK_METHOD(std::unique_ptr<CaptionBubbleSessionObserver>, |
| GetCaptionBubbleSessionObserver, |
| (), |
| (override)); |
| MOCK_METHOD(OpenCaptionSettingsCallback, |
| GetOpenCaptionSettingsCallback, |
| (), |
| (override)); |
| }; |
| |
| class MockCaptionBubbleController : public CaptionBubbleController { |
| public: |
| MockCaptionBubbleController() = default; |
| ~MockCaptionBubbleController() override = default; |
| |
| // CaptionControllerBase::Listener |
| MOCK_METHOD(bool, |
| OnTranscription, |
| (content::RenderFrameHost*, |
| CaptionBubbleContext*, |
| const media::SpeechRecognitionResult&), |
| (override)); |
| MOCK_METHOD(void, |
| OnAudioStreamEnd, |
| (content::RenderFrameHost*, CaptionBubbleContext*), |
| (override)); |
| MOCK_METHOD(void, |
| OnLanguageIdentificationEvent, |
| (content::RenderFrameHost*, |
| CaptionBubbleContext*, |
| const media::mojom::LanguageIdentificationEventPtr&), |
| (override)); |
| |
| // CaptionBubbleController |
| MOCK_METHOD(void, |
| OnError, |
| (CaptionBubbleContext*, |
| CaptionBubbleErrorType, |
| OnErrorClickedCallback, |
| OnDoNotShowAgainClickedCallback), |
| (override)); |
| MOCK_METHOD(void, |
| UpdateCaptionStyle, |
| (std::optional<ui::CaptionStyle>), |
| (override)); |
| MOCK_METHOD(bool, IsWidgetVisibleForTesting, (), (override)); |
| MOCK_METHOD(bool, IsGenericErrorMessageVisibleForTesting, (), (override)); |
| MOCK_METHOD(std::string, GetBubbleLabelTextForTesting, (), (override)); |
| MOCK_METHOD(void, CloseActiveModelForTesting, (), (override)); |
| }; |
| |
| class MockCaptionControllerDelegate : public CaptionControllerBase::Delegate { |
| public: |
| explicit MockCaptionControllerDelegate( |
| std::unique_ptr<CaptionBubbleController> bubble_controller) { |
| EXPECT_CALL(*this, CreateCaptionBubbleController(_, _, _)) |
| .WillOnce(Return(std::move(bubble_controller))); |
| } |
| ~MockCaptionControllerDelegate() override = default; |
| |
| MOCK_METHOD(std::unique_ptr<CaptionBubbleController>, |
| CreateCaptionBubbleController, |
| (CaptionBubbleSettings*, |
| const std::string&, |
| std::unique_ptr<TranslationViewWrapperBase>), |
| (override)); |
| |
| void AddCaptionStyleObserver(ui::NativeThemeObserver*) override {} |
| |
| void RemoveCaptionStyleObserver(ui::NativeThemeObserver*) override {} |
| }; |
| |
| void RegisterStylePrefs(TestingPrefServiceSimple* pref_service) { |
| const std::string kCaptionsTextSize = "20%"; |
| const std::string kCaptionsTextFont = "aerial"; |
| const std::string kCaptionsTextColor = "255,99,71"; |
| const std::string kCaptionsBackgroundColor = "90,255,50"; |
| const std::string kCaptionsTextShadow = "10px"; |
| constexpr int kCaptionsTextOpacity = 50; |
| constexpr int kCaptionsBackgroundOpacity = 30; |
| |
| pref_service->registry()->RegisterStringPref( |
| prefs::kAccessibilityCaptionsTextSize, kCaptionsTextSize); |
| pref_service->registry()->RegisterStringPref( |
| prefs::kAccessibilityCaptionsTextFont, kCaptionsTextFont); |
| pref_service->registry()->RegisterStringPref( |
| prefs::kAccessibilityCaptionsTextColor, kCaptionsTextColor); |
| pref_service->registry()->RegisterIntegerPref( |
| prefs::kAccessibilityCaptionsTextOpacity, kCaptionsTextOpacity); |
| pref_service->registry()->RegisterStringPref( |
| prefs::kAccessibilityCaptionsBackgroundColor, kCaptionsBackgroundColor); |
| pref_service->registry()->RegisterStringPref( |
| prefs::kAccessibilityCaptionsTextShadow, kCaptionsTextShadow); |
| pref_service->registry()->RegisterIntegerPref( |
| prefs::kAccessibilityCaptionsBackgroundOpacity, |
| kCaptionsBackgroundOpacity); |
| } |
| |
| class CaptionControllerBaseTest : public testing::Test { |
| public: |
| ~CaptionControllerBaseTest() override = default; |
| |
| void SetUp() override { |
| testing::Test::SetUp(); |
| |
| RegisterStylePrefs(&testing_pref_service_); |
| } |
| |
| std::unique_ptr<TestCaptionControllerBase> CreateController( |
| std::unique_ptr<CaptionControllerBase::Delegate> delegate = nullptr) { |
| return std::make_unique<TestCaptionControllerBase>( |
| &testing_pref_service_, speech::kUsEnglishLocale, std::move(delegate)); |
| } |
| |
| TestingPrefServiceSimple testing_pref_service_; |
| }; |
| |
| TEST_F(CaptionControllerBaseTest, ListenersAreFreedOnDestruction) { |
| bool was_freed = false; |
| auto listener = std::make_unique<MockListener>(&was_freed); |
| |
| auto controller_under_test = CreateController(); |
| controller_under_test->AddListener(std::move(listener)); |
| EXPECT_FALSE(was_freed); |
| controller_under_test.reset(); |
| EXPECT_TRUE(was_freed); |
| } |
| |
| TEST_F(CaptionControllerBaseTest, CaptionBubbleControllerReceivesCallbacks) { |
| auto mock_bubble_controller = std::make_unique<MockCaptionBubbleController>(); |
| auto* mock_bubble_controller_raw = mock_bubble_controller.get(); |
| auto controller_under_test = |
| CreateController(std::make_unique<MockCaptionControllerDelegate>( |
| std::move(mock_bubble_controller))); |
| controller_under_test->create_ui_for_testing(); |
| EXPECT_CALL(*mock_bubble_controller_raw, OnAudioStreamEnd(nullptr, nullptr)); |
| controller_under_test->OnAudioStreamEnd(nullptr, nullptr); |
| } |
| |
| TEST_F(CaptionControllerBaseTest, CaptionBubbleAliasIsAddedAndRemoved) { |
| auto mock_bubble_controller = std::make_unique<MockCaptionBubbleController>(); |
| auto* mock_bubble_controller_raw = mock_bubble_controller.get(); |
| auto controller_under_test = |
| CreateController(std::make_unique<MockCaptionControllerDelegate>( |
| std::move(mock_bubble_controller))); |
| |
| // Create the UI, and expect that its alias is now correct. |
| controller_under_test->create_ui_for_testing(); |
| EXPECT_EQ(mock_bubble_controller_raw, |
| controller_under_test->caption_bubble_controller_for_testing()); |
| |
| // Delete the UI, and expect that its alias is now cleared. |
| controller_under_test->destroy_ui_for_testing(); |
| EXPECT_EQ(nullptr, |
| controller_under_test->caption_bubble_controller_for_testing()); |
| } |
| |
| TEST_F(CaptionControllerBaseTest, ListenersReceiveTranscription) { |
| MockCaptionBubbleContext context; |
| |
| auto listener = std::make_unique<MockListener>(); |
| content::RenderFrameHost* rfh = |
| reinterpret_cast<content::RenderFrameHost*>(listener.get()); |
| EXPECT_CALL(*listener, OnAudioStreamEnd(rfh, &context)); |
| |
| auto controller_under_test = CreateController(); |
| controller_under_test->AddListener(std::move(listener)); |
| controller_under_test->OnAudioStreamEnd(rfh, &context); |
| } |
| |
| TEST_F(CaptionControllerBaseTest, TranscriptionStopsIfNoListeners) { |
| MockCaptionBubbleContext context; |
| media::SpeechRecognitionResult result; |
| |
| auto controller_under_test = CreateController(); |
| EXPECT_FALSE(controller_under_test->DispatchTranscription( |
| /*rfh=*/nullptr, &context, result)); |
| } |
| |
| TEST_F(CaptionControllerBaseTest, ListenersReceiveAudioEnd) { |
| MockCaptionBubbleContext context; |
| media::SpeechRecognitionResult result; |
| |
| auto listener = std::make_unique<MockListener>(); |
| content::RenderFrameHost* rfh = |
| reinterpret_cast<content::RenderFrameHost*>(listener.get()); |
| EXPECT_CALL(*listener, OnTranscription(rfh, &context, result)); |
| |
| auto controller_under_test = CreateController(); |
| controller_under_test->AddListener(std::move(listener)); |
| controller_under_test->DispatchTranscription(rfh, &context, result); |
| } |
| |
| class CaptionControllerBaseListenerTest : public CaptionControllerBaseTest { |
| public: |
| class TestCaptionController : public CaptionControllerBase { |
| public: |
| TestCaptionController(PrefService* profile_prefs, |
| const std::string& application_locale) |
| : CaptionControllerBase(profile_prefs, application_locale, nullptr) {} |
| ~TestCaptionController() override = default; |
| |
| MOCK_METHOD(void, OnFirstListenerAdded, (), (override)); |
| MOCK_METHOD(void, OnLastListenerRemoved, (), (override)); |
| MOCK_METHOD(CaptionBubbleSettings*, |
| caption_bubble_settings, |
| (), |
| (override)); |
| }; |
| }; |
| |
| TEST_F(CaptionControllerBaseListenerTest, OnFirstListenerAddedCalled) { |
| auto controller = std::make_unique<TestCaptionController>( |
| &testing_pref_service_, speech::kUsEnglishLocale); |
| EXPECT_CALL(*controller, OnFirstListenerAdded).Times(1); |
| EXPECT_CALL(*controller, OnLastListenerRemoved).Times(0); |
| |
| auto listener1 = std::make_unique<MockListener>(); |
| controller->AddListener(std::move(listener1)); |
| |
| // Adding a second listener should not trigger the callback. |
| auto listener2 = std::make_unique<MockListener>(); |
| controller->AddListener(std::move(listener2)); |
| } |
| |
| TEST_F(CaptionControllerBaseListenerTest, OnLastListenerRemovedCalled) { |
| auto controller = std::make_unique<TestCaptionController>( |
| &testing_pref_service_, speech::kUsEnglishLocale); |
| EXPECT_CALL(*controller, OnFirstListenerAdded).Times(1); |
| EXPECT_CALL(*controller, OnLastListenerRemoved).Times(1); |
| |
| auto listener1 = std::make_unique<MockListener>(); |
| auto* listener1_ptr = listener1.get(); |
| controller->AddListener(std::move(listener1)); |
| |
| auto listener2 = std::make_unique<MockListener>(); |
| auto* listener2_ptr = listener2.get(); |
| controller->AddListener(std::move(listener2)); |
| |
| // Removing the first listener should not trigger the callback. |
| controller->remove_listener_for_testing(listener1_ptr); |
| |
| // Removing the last listener should trigger the callback. |
| controller->remove_listener_for_testing(listener2_ptr); |
| } |
| |
| } // namespace |
| } // namespace captions |