blob: 0e346337a3f542a424cdd3aee911997aa2b3a734 [file] [log] [blame]
// 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