blob: 83d884565d415e25be4edebfba50ed615f7f849e [file]
// 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/views/caption_bubble.h"
#include <memory>
#include <utility>
#include "base/cfi_buildflags.h"
#include "base/command_line.h"
#include "base/functional/callback.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/test/bind.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/test/test_timeouts.h"
#include "base/types/expected.h"
#include "build/build_config.h"
#include "caption_bubble_model.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/confirm_bubble.h"
#include "chrome/browser/ui/test/test_browser_ui.h"
#include "chrome/browser/ui/views/accessibility/caption_bubble_context_views.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/live_caption/caption_bubble_context.h"
#include "components/live_caption/caption_bubble_controller.h"
#include "components/live_caption/caption_bubble_settings.h"
#include "components/live_caption/live_caption_bubble_settings.h"
#include "components/live_caption/pref_names.h"
#include "components/live_caption/views/translation_view_wrapper.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/testing_pref_service.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/test/ui_controls.h"
namespace captions {
namespace {
constexpr char kEnglishLanguage[] = "en-US";
class CaptionBubbleBrowserTest : public UiBrowserTest {
protected:
CaptionBubbleBrowserTest() {
scoped_feature_list_.InitWithFeatures({captions::kLiveCaptionScrollable},
{});
}
void SetUpOnMainThread() override {
pref_service_.registry()->RegisterBooleanPref(
prefs::kLiveCaptionBubbleExpanded, false);
pref_service_.registry()->RegisterBooleanPref(prefs::kLiveTranslateEnabled,
false);
pref_service_.registry()->RegisterBooleanPref(prefs::kLiveCaptionEnabled,
false);
pref_service_.registry()->RegisterStringPref(
prefs::kLiveCaptionLanguageCode, kEnglishLanguage);
pref_service_.registry()->RegisterStringPref(
prefs::kLiveTranslateTargetLanguageCode, kEnglishLanguage);
UiBrowserTest::SetUpOnMainThread();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII("--force-device-scale-factor", "1.01");
UiBrowserTest::SetUpCommandLine(command_line);
}
void TearDownOnMainThread() override {
context_.reset();
UiBrowserTest::TearDownOnMainThread();
}
// UiBrowserTest:
void ShowUi(const std::string& name) override {
context_ = std::make_unique<CaptionBubbleContextViews>(
browser()->GetActiveTabInterface()->GetContents());
OnCaptionBubbleClosedCallback callback;
model_ = std::make_unique<CaptionBubbleModel>(context_.get(),
std::move(callback));
settings_ = std::make_unique<LiveCaptionBubbleSettings>(&pref_service_);
settings_->SetLiveCaptionBubbleExpanded(true);
const std::string application_locale;
base::OnceClosure destroyed_callback;
auto bubble = std::make_unique<CaptionBubble>(
settings_.get(),
std::make_unique<TranslationViewWrapper>(settings_.get()),
application_locale, std::move(destroyed_callback));
bubble_ = bubble.get();
views::BubbleDialogDelegateView::CreateBubble(std::move(bubble))->Show();
bubble_->SetModel(model_.get());
// Prepare RunLoop,
run_loop_ = std::make_unique<base::RunLoop>();
// Increase to 1024u for a manual run to observe scrolling on screen.
SingleStep(128u);
// The test will wait until all steps are completed.
run_loop_->Run();
}
// These next two are not necessary if subclassing DialogBrowserTest.
bool VerifyUi() override {
views::Widget* widget = GetWidgetForScreenshot();
auto* const test_info =
testing::UnitTest::GetInstance()->current_test_info();
const std::string screenshot_name =
base::StrCat({test_info->test_suite_name(), "_", test_info->name()});
return VerifyPixelUi(widget, "CaptureBubblePixelTest", screenshot_name) !=
ui::test::ActionResult::kFailed;
}
void DismissUi() override {
if (bubble_) {
bubble_->SetModel(nullptr);
bubble_ = nullptr;
}
IgnoreNetworkServiceCrashes();
}
void WaitForUserDismissal() override {
/* Block until the UI has been dismissed. */
ui_test_utils::BrowserDestroyedObserver(browser()).Wait();
if (bubble_) {
bubble_->SetModel(nullptr);
bubble_ = nullptr;
}
IgnoreNetworkServiceCrashes();
}
protected:
// This method adds one more piece of text to the bubble and then
// performs scroll to the start and scroll to the end (for testing only).
// It schedules asynchronous call to itself for the next piece of text,
// in order to yield the the UI thread and let scrolls repaint the view.
void SingleStep(uint64_t i) {
if (!bubble_) {
return;
}
model_->SetPartialText(base::StringPrintf("ABCDEF %ul ", i));
model_->CommitPartialText();
auto* const scroll_bar =
bubble_->GetScrollViewForTesting()->vertical_scroll_bar();
ASSERT_EQ(scroll_bar->GetOrientation(),
views::ScrollBar::Orientation::kVertical);
// Next iteration needs to be reschedued on UI thread, so that views can
// be repainted.
auto next_step =
(i > 1) ? base::BindPostTask(
base::SingleThreadTaskRunner::GetCurrentDefault(),
base::BindOnce(&CaptionBubbleBrowserTest::SingleStep,
base::Unretained(this), i - 1))
: base::BindOnce(&base::RunLoop::Quit,
base::Unretained(run_loop_.get()));
if (scroll_bar->GetVisible()) {
// If the scrollbar is already visible, reschedule simulated scroll to the
// beginning, then scroll to the end, and then do the next step.
// The scrolled view should be repainted after ~every action.
next_step =
base::BindPostTask(base::SingleThreadTaskRunner::GetCurrentDefault(),
base::BindOnce(
[](views::ScrollBar* scroll_bar) {
ASSERT_TRUE(scroll_bar->ScrollByAmount(
views::ScrollBar::ScrollAmount::kStart));
},
base::Unretained(scroll_bar)))
.Then(base::BindPostTask(
base::SingleThreadTaskRunner::GetCurrentDefault(),
base::BindOnce(
[](views::ScrollBar* scroll_bar) {
ASSERT_TRUE(scroll_bar->ScrollByAmount(
views::ScrollBar::ScrollAmount::kEnd));
},
base::Unretained(scroll_bar))))
.Then(std::move(next_step));
}
std::move(next_step).Run();
}
base::test::ScopedFeatureList scoped_feature_list_;
TestingPrefServiceSimple pref_service_;
std::unique_ptr<CaptionBubbleContextViews> context_;
std::unique_ptr<CaptionBubbleModel> model_;
std::unique_ptr<LiveCaptionBubbleSettings> settings_;
raw_ptr<CaptionBubble> bubble_;
std::unique_ptr<base::RunLoop> run_loop_;
private:
views::Widget* GetWidgetForScreenshot() const { return bubble_->GetWidget(); }
};
// Test that calls ShowUi("default").
// TODO(crbug.com/422524764): Flakily failing on Windows
#if BUILDFLAG(IS_WIN)
#define MAYBE_InvokeUi_default DISABLED_InvokeUi_default
#else
#define MAYBE_InvokeUi_default InvokeUi_default
#endif
IN_PROC_BROWSER_TEST_F(CaptionBubbleBrowserTest, MAYBE_InvokeUi_default) {
ShowAndVerifyUi();
}
IN_PROC_BROWSER_TEST_F(CaptionBubbleBrowserTest, InvokeUi_NoWiggleOnResize) {
ShowUi("NoWiggleOnResize");
views::Widget* widget = bubble_->GetWidget();
gfx::Point initial_origin = widget->GetWindowBoundsInScreen().origin();
model_->SetPartialText("A new line of text.");
model_->CommitPartialText();
base::RunLoop().RunUntilIdle();
gfx::Point new_origin = widget->GetWindowBoundsInScreen().origin();
EXPECT_EQ(initial_origin, new_origin);
DismissUi();
}
} // namespace
} // namespace captions