blob: 6352c6515d2fda0f79878d939517f97185d364aa [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/android/modal_dialog_wrapper.h"
#include "base/android/jni_android.h"
#include "base/android/scoped_java_ref.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/test/bind.h"
#include "base/test/gtest_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/android/fake_modal_dialog_manager_bridge.h"
#include "ui/android/window_android.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/models/dialog_model.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_key.h"
#include "ui/color/color_provider_manager.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/gfx/vector_icon_types.h"
namespace ui {
namespace {
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kCheckboxId);
// Icons used in tests below.
constexpr int kIconDim = 24;
const gfx::PathElement kTestIconPath[] = {
// A 16x16 canvas.
gfx::CANVAS_DIMENSIONS,
16,
// A square path.
gfx::MOVE_TO,
0,
0,
gfx::LINE_TO,
16,
0,
gfx::LINE_TO,
16,
16,
gfx::LINE_TO,
0,
16,
gfx::CLOSE,
};
const gfx::VectorIconRep kTestIconReps[] = {
{.path = kTestIconPath},
};
const gfx::VectorIcon kTestIcon(kTestIconReps,
std::size(kTestIconReps),
"test_icon");
ui::ImageModel CreateBitmapImage(SkColor color) {
SkBitmap bitmap;
bitmap.allocN32Pixels(kIconDim, kIconDim);
bitmap.eraseColor(color);
return ui::ImageModel::FromImage(gfx::Image::CreateFrom1xBitmap(bitmap));
}
} // namespace
class ModalDialogWrapperTest : public testing::Test {
protected:
// Test helper class to build a DialogModel with a fluent interface.
class DialogModelBuilder {
public:
explicit DialogModelBuilder(bool* dialog_destroyed_flag)
: dialog_destroyed_flag_(dialog_destroyed_flag) {}
DialogModelBuilder& WithOkButton(
base::OnceClosure callback,
ui::ButtonStyle style = ui::ButtonStyle::kDefault) {
ok_callback_ = std::move(callback);
ok_button_style_ = style;
return *this;
}
DialogModelBuilder& WithCancelButton(
base::OnceClosure callback,
ui::ButtonStyle style = ui::ButtonStyle::kDefault) {
has_cancel_button_ = true;
cancel_callback_ = std::move(callback);
cancel_button_style_ = style;
return *this;
}
DialogModelBuilder& WithCloseAction(base::OnceClosure callback) {
close_callback_ = std::move(callback);
return *this;
}
DialogModelBuilder& OverrideDefaultButton(
mojom::DialogButton override_button) {
override_button_ = override_button;
return *this;
}
DialogModelBuilder& WithParagraphs(
const std::vector<std::u16string>& paragraphs) {
paragraphs_ = paragraphs;
return *this;
}
DialogModelBuilder& WithCheckbox(bool is_checked) {
has_checkbox_ = true;
checkbox_is_checked_ = is_checked;
return *this;
}
DialogModelBuilder& WithIcon(ui::ImageModel icon) {
icon_ = std::move(icon);
return *this;
}
DialogModelBuilder& AddMenuItem(ui::ImageModel icon,
const std::u16string& label) {
menu_items_.emplace_back(std::move(icon), label, base::DoNothing());
return *this;
}
DialogModelBuilder& AddMenuItemWithCallback(
ui::ImageModel icon,
const std::u16string& label,
base::RepeatingClosure callback) {
menu_items_.emplace_back(std::move(icon), label, std::move(callback));
return *this;
}
std::unique_ptr<ui::DialogModel> Build() {
ui::DialogModel::Builder dialog_builder;
dialog_builder.SetTitle(u"title");
if (icon_) {
dialog_builder.SetIcon(*icon_);
}
for (const auto& paragraph_text : paragraphs_) {
dialog_builder.AddParagraph(ui::DialogModelLabel(paragraph_text));
}
for (auto& item : menu_items_) {
dialog_builder.AddMenuItem(
std::move(std::get<0>(item)), std::get<1>(item),
base::BindRepeating([](base::RepeatingClosure callback,
int event_flags) { callback.Run(); },
std::move(std::get<2>(item))));
}
if (has_checkbox_) {
dialog_builder.AddCheckbox(
kCheckboxId, ui::DialogModelLabel(u"checkbox label"),
ui::DialogModelCheckbox::Params().SetIsChecked(
checkbox_is_checked_));
}
dialog_builder.AddOkButton(
std::move(ok_callback_),
ui::DialogModel::Button::Params().SetLabel(u"ok").SetStyle(
ok_button_style_));
if (has_cancel_button_) {
dialog_builder.AddCancelButton(
std::move(cancel_callback_),
ui::DialogModel::Button::Params().SetLabel(u"cancel").SetStyle(
cancel_button_style_));
}
// Capture the pointer to the destruction flag by value. This prevents
// the lambda from holding a dangling reference to the temporary builder
// instance.
bool* flag_ptr = dialog_destroyed_flag_;
dialog_builder.SetCloseActionCallback(std::move(close_callback_))
.SetDialogDestroyingCallback(
base::BindLambdaForTesting([flag_ptr]() { *flag_ptr = true; }));
if (override_button_) {
dialog_builder.OverrideDefaultButton(*override_button_);
}
return dialog_builder.Build();
}
private:
raw_ptr<bool> dialog_destroyed_flag_;
// Default values match the original CreateDialogModel function.
base::OnceClosure ok_callback_ = base::DoNothing();
ui::ButtonStyle ok_button_style_ = ui::ButtonStyle::kDefault;
bool has_cancel_button_ = false;
base::OnceClosure cancel_callback_ = base::DoNothing();
ui::ButtonStyle cancel_button_style_ = ui::ButtonStyle::kDefault;
base::OnceClosure close_callback_ = base::DoNothing();
std::optional<mojom::DialogButton> override_button_;
std::vector<std::u16string> paragraphs_ = {u"paragraph"};
std::vector<
std::tuple<ui::ImageModel, std::u16string, base::RepeatingClosure>>
menu_items_;
bool has_checkbox_ = false;
bool checkbox_is_checked_ = false;
std::optional<ui::ImageModel> icon_;
};
void SetUp() override {
window_ = ui::WindowAndroid::CreateForTesting();
fake_dialog_manager_ = FakeModalDialogManagerBridge::CreateForTab(
window_->get(), /*use_empty_java_presenter=*/false);
}
bool dialog_destroyed_ = false;
std::unique_ptr<ui::WindowAndroid::ScopedWindowAndroidForTesting> window_;
std::unique_ptr<FakeModalDialogManagerBridge> fake_dialog_manager_;
};
TEST_F(ModalDialogWrapperTest, CallOkButton) {
bool ok_called = false;
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_)
.WithOkButton(base::BindLambdaForTesting([&]() { ok_called = true; }))
.Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
fake_dialog_manager_->ClickPositiveButton();
EXPECT_TRUE(ok_called);
EXPECT_TRUE(dialog_destroyed_);
}
TEST_F(ModalDialogWrapperTest, CallCancelButton) {
bool ok_called = false, cancel_called = false;
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_)
.WithOkButton(base::BindLambdaForTesting([&]() { ok_called = true; }))
.WithCancelButton(
base::BindLambdaForTesting([&]() { cancel_called = true; }))
.Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
fake_dialog_manager_->ClickNegativeButton();
EXPECT_FALSE(ok_called);
EXPECT_TRUE(cancel_called);
EXPECT_TRUE(dialog_destroyed_);
}
TEST_F(ModalDialogWrapperTest, CloseDialogFromNative) {
bool ok_called = false, cancel_called = false, closed = false;
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_)
.WithOkButton(base::BindLambdaForTesting([&]() { ok_called = true; }))
.WithCancelButton(
base::BindLambdaForTesting([&]() { cancel_called = true; }))
.WithCloseAction(base::BindLambdaForTesting([&]() { closed = true; }))
.Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
ModalDialogWrapper::GetDialogForTesting()->Close();
EXPECT_FALSE(ok_called);
EXPECT_FALSE(cancel_called);
EXPECT_TRUE(closed);
EXPECT_TRUE(dialog_destroyed_);
}
TEST_F(ModalDialogWrapperTest, ModalButtonsNoProminent) {
auto dialog_model = DialogModelBuilder(&dialog_destroyed_).Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
EXPECT_EQ(static_cast<ui::ModalDialogWrapper::ModalDialogButtonStyles>(
fake_dialog_manager_->GetButtonStyles()),
ui::ModalDialogWrapper::ModalDialogButtonStyles::
kPrimaryOutlineNegativeOutline);
EXPECT_FALSE(dialog_destroyed_);
}
TEST_F(ModalDialogWrapperTest, ModalButtonsPrimaryProminentNoNegative) {
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_)
.WithOkButton(base::DoNothing(), ui::ButtonStyle::kProminent)
.Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
EXPECT_EQ(static_cast<ui::ModalDialogWrapper::ModalDialogButtonStyles>(
fake_dialog_manager_->GetButtonStyles()),
ui::ModalDialogWrapper::ModalDialogButtonStyles::
kPrimaryFilledNoNegative);
EXPECT_FALSE(dialog_destroyed_);
}
TEST_F(ModalDialogWrapperTest, ModalButtonsPrimaryProminent) {
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_)
.WithOkButton(base::DoNothing(), ui::ButtonStyle::kProminent)
.WithCancelButton(base::DoNothing())
.Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
EXPECT_EQ(static_cast<ui::ModalDialogWrapper::ModalDialogButtonStyles>(
fake_dialog_manager_->GetButtonStyles()),
ui::ModalDialogWrapper::ModalDialogButtonStyles::
kPrimaryFilledNegativeOutline);
EXPECT_FALSE(dialog_destroyed_);
}
TEST_F(ModalDialogWrapperTest, ModalButtonsNegativeProminent) {
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_)
.WithCancelButton(base::DoNothing(), ui::ButtonStyle::kProminent)
.Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
EXPECT_EQ(static_cast<ui::ModalDialogWrapper::ModalDialogButtonStyles>(
fake_dialog_manager_->GetButtonStyles()),
ui::ModalDialogWrapper::ModalDialogButtonStyles::
kPrimaryOutlineNegativeFilled);
EXPECT_FALSE(dialog_destroyed_);
}
TEST_F(ModalDialogWrapperTest, ModalButtonsOverriddenNone) {
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_)
.WithOkButton(base::DoNothing(), ui::ButtonStyle::kProminent)
.WithCancelButton(base::DoNothing(), ui::ButtonStyle::kProminent)
.OverrideDefaultButton(ui::mojom::DialogButton::kNone)
.Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
EXPECT_EQ(static_cast<ui::ModalDialogWrapper::ModalDialogButtonStyles>(
fake_dialog_manager_->GetButtonStyles()),
ui::ModalDialogWrapper::ModalDialogButtonStyles::
kPrimaryOutlineNegativeOutline);
EXPECT_FALSE(dialog_destroyed_);
}
TEST_F(ModalDialogWrapperTest, ModalButtonsOverriddenPositive) {
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_)
.WithCancelButton(base::DoNothing(), ui::ButtonStyle::kProminent)
.OverrideDefaultButton(ui::mojom::DialogButton::kOk)
.Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
EXPECT_EQ(static_cast<ui::ModalDialogWrapper::ModalDialogButtonStyles>(
fake_dialog_manager_->GetButtonStyles()),
ui::ModalDialogWrapper::ModalDialogButtonStyles::
kPrimaryFilledNegativeOutline);
EXPECT_FALSE(dialog_destroyed_);
}
TEST_F(ModalDialogWrapperTest, ModalButtonsOverriddenNegative) {
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_)
.WithOkButton(base::DoNothing(), ui::ButtonStyle::kProminent)
.WithCancelButton(base::DoNothing())
.OverrideDefaultButton(ui::mojom::DialogButton::kCancel)
.Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
EXPECT_EQ(static_cast<ui::ModalDialogWrapper::ModalDialogButtonStyles>(
fake_dialog_manager_->GetButtonStyles()),
ui::ModalDialogWrapper::ModalDialogButtonStyles::
kPrimaryOutlineNegativeFilled);
EXPECT_FALSE(dialog_destroyed_);
}
TEST_F(ModalDialogWrapperTest, ParagraphsAreSetAndReplaced) {
std::vector<std::u16string> paragraphs = {u"This is the first paragraph.",
u"This is the second paragraph."};
auto dialog_model_1 =
DialogModelBuilder(&dialog_destroyed_).WithParagraphs(paragraphs).Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model_1), window_->get());
std::vector<std::u16string> displayed_paragraphs_1 =
fake_dialog_manager_->GetMessageParagraphs();
ASSERT_EQ(displayed_paragraphs_1.size(), 2u);
EXPECT_EQ(displayed_paragraphs_1.front(), paragraphs.front());
EXPECT_EQ(displayed_paragraphs_1.back(), paragraphs.back());
// Remove the last element and confirm the behavior.
paragraphs.pop_back();
auto dialog_model_2 =
DialogModelBuilder(&dialog_destroyed_).WithParagraphs(paragraphs).Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model_2), window_->get());
std::vector<std::u16string> displayed_paragraphs_2 =
fake_dialog_manager_->GetMessageParagraphs();
ASSERT_EQ(displayed_paragraphs_2.size(), 1u);
EXPECT_EQ(displayed_paragraphs_2.front(), paragraphs.front());
}
TEST_F(ModalDialogWrapperTest, Checkbox_InitialStateUnchecked) {
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_).WithCheckbox(false).Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
EXPECT_FALSE(fake_dialog_manager_->IsCheckboxChecked());
}
TEST_F(ModalDialogWrapperTest, Checkbox_InitialStateChecked) {
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_).WithCheckbox(true).Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
EXPECT_TRUE(fake_dialog_manager_->IsCheckboxChecked());
}
TEST_F(ModalDialogWrapperTest, Checkbox_StateSynchronizedAfterToggle) {
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_).WithCheckbox(false).Build();
// Get a raw pointer to the model before it's moved.
auto* dialog_model_ptr = dialog_model.get();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
// Verify initial state.
EXPECT_FALSE(fake_dialog_manager_->IsCheckboxChecked());
EXPECT_FALSE(
dialog_model_ptr->GetCheckboxByUniqueId(kCheckboxId)->is_checked());
// Simulate a UI click on the checkbox.
fake_dialog_manager_->ToggleCheckbox();
// Verify both the Java model (via fake manager) and C++ model are updated.
EXPECT_TRUE(fake_dialog_manager_->IsCheckboxChecked());
EXPECT_TRUE(
dialog_model_ptr->GetCheckboxByUniqueId(kCheckboxId)->is_checked());
// Toggle it back.
fake_dialog_manager_->ToggleCheckbox();
EXPECT_FALSE(fake_dialog_manager_->IsCheckboxChecked());
EXPECT_FALSE(
dialog_model_ptr->GetCheckboxByUniqueId(kCheckboxId)->is_checked());
}
TEST_F(ModalDialogWrapperTest, TitleIcon_ShowsVectorIcon) {
auto icon =
ui::ImageModel::FromVectorIcon(kTestIcon, SK_ColorBLACK, kIconDim);
// Convert icon to bitmap for comparison.
ui::ColorProvider color_provider;
color_provider.GenerateColorMapForTesting();
const SkBitmap expected_bitmap = *icon.Rasterize(&color_provider).bitmap();
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_).WithIcon(std::move(icon)).Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
SkBitmap actual_bitmap = fake_dialog_manager_->GetTitleIcon();
EXPECT_TRUE(gfx::test::AreBitmapsEqual(expected_bitmap, actual_bitmap));
EXPECT_FALSE(dialog_destroyed_);
}
TEST_F(ModalDialogWrapperTest, TitleIcon_ShowsGfxImage) {
auto icon = CreateBitmapImage(SK_ColorGREEN);
ui::ColorProvider color_provider;
color_provider.GenerateColorMapForTesting();
const SkBitmap expected_bitmap = *icon.Rasterize(&color_provider).bitmap();
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_).WithIcon(std::move(icon)).Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
SkBitmap actual_bitmap = fake_dialog_manager_->GetTitleIcon();
EXPECT_TRUE(gfx::test::AreBitmapsEqual(expected_bitmap, actual_bitmap));
EXPECT_FALSE(dialog_destroyed_);
}
TEST_F(ModalDialogWrapperTest, ShowsMenuItems) {
// --- Item 1: SkBitmap-based ---
auto icon1 = CreateBitmapImage(SK_ColorRED);
ui::ColorProvider color_provider;
color_provider.GenerateColorMapForTesting();
const SkBitmap expected_bitmap1 = *icon1.Rasterize(&color_provider).bitmap();
const std::u16string label1 = u"Red Bitmap Item";
// --- Item 2: VectorIcon-based ---
auto icon2 =
ui::ImageModel::FromVectorIcon(kTestIcon, SK_ColorBLUE, kIconDim);
const SkBitmap expected_bitmap2 = *icon2.Rasterize(&color_provider).bitmap();
const std::u16string label2 = u"Blue Vector Item";
// --- Build and Show ---
auto dialog_model = DialogModelBuilder(&dialog_destroyed_)
.AddMenuItem(std::move(icon1), label1)
.AddMenuItem(std::move(icon2), label2)
.Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
// --- Assert ---
std::vector<std::u16string> actual_labels =
fake_dialog_manager_->GetMenuItemTexts();
std::vector<SkBitmap> actual_icons = fake_dialog_manager_->GetMenuItemIcons();
// Try a null callback
fake_dialog_manager_->ClickMenuItem(0);
ASSERT_EQ(actual_labels.size(), 2u);
ASSERT_EQ(actual_icons.size(), 2u);
EXPECT_EQ(actual_labels[0], label1);
EXPECT_EQ(actual_labels[1], label2);
EXPECT_TRUE(gfx::test::AreBitmapsEqual(actual_icons[0], expected_bitmap1));
EXPECT_TRUE(gfx::test::AreBitmapsEqual(actual_icons[1], expected_bitmap2));
EXPECT_FALSE(dialog_destroyed_);
}
TEST_F(ModalDialogWrapperTest, MenuItem_Callbacks) {
bool callback1_called = false;
bool callback2_called = false;
auto icon = CreateBitmapImage(SK_ColorRED);
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_)
.AddMenuItemWithCallback(
icon, u"Item 1",
base::BindLambdaForTesting([&]() { callback1_called = true; }))
.AddMenuItemWithCallback(
icon, u"Item 2",
base::BindLambdaForTesting([&]() { callback2_called = true; }))
.Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
fake_dialog_manager_->ClickMenuItem(1);
EXPECT_FALSE(callback1_called);
EXPECT_TRUE(callback2_called);
fake_dialog_manager_->ClickMenuItem(0);
EXPECT_TRUE(callback1_called);
}
TEST_F(ModalDialogWrapperTest, MenuItem_CallbackDismissesDialog) {
auto dialog_model =
DialogModelBuilder(&dialog_destroyed_)
.AddMenuItemWithCallback(
CreateBitmapImage(SK_ColorRED), u"Item",
base::BindLambdaForTesting([&]() {
ModalDialogWrapper::GetDialogForTesting()->Close();
}))
.Build();
ModalDialogWrapper::ShowTabModal(std::move(dialog_model), window_->get());
fake_dialog_manager_->ClickMenuItem(0);
EXPECT_TRUE(dialog_destroyed_);
}
} // namespace ui