blob: e441f0826084d1b303012ad44875545a1881338f [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <array>
#include <memory>
#include <string>
#include "ash/public/cpp/ash_view_ids.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/style/checkbox_group.h"
#include "ash/style/icon_button.h"
#include "ash/style/radio_button_group.h"
#include "ash/style/system_dialog_delegate_view.h"
#include "ash/style/tab_slider.h"
#include "ash/style/tab_slider_button.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/desks/desks_util.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/platform/ax_platform_node.h"
#include "ui/base/interaction/expect_call_in_scope.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/base/ui_base_types.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/view_test_api.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
namespace ash {
namespace {
const gfx::VectorIcon& kTestIcon = kFolderIcon;
enum class TabSliderType {
kIconSlider,
kLabelSlider,
kIconLabelSlider,
};
// Helpers ---------------------------------------------------------------------
std::unique_ptr<views::Widget> CreateSystemDialogWidget(
ui::mojom::ModalType modal_type,
aura::Window* parent_window) {
// Generate a new dialog delegate view.
auto dialog_view = views::Builder<SystemDialogDelegateView>()
.SetIcon(kTestIcon)
.SetTitleText(u"Title")
.SetDescription(u"Dialog description.")
.Build();
dialog_view->SetModalType(modal_type);
// Create a dialog widget.
views::Widget::InitParams dialog_params(
views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
dialog_params.bounds = gfx::Rect(dialog_view->GetPreferredSize());
dialog_params.delegate = dialog_view.release();
dialog_params.parent = parent_window;
auto dialog_widget =
std::make_unique<views::Widget>(std::move(dialog_params));
dialog_widget->Show();
return dialog_widget;
}
} // namespace
// WidgetWithSystemUIComponentView ---------------------------------------------
// A WidgetDelegateView with given component as the only content.
class WidgetWithSystemUIComponentView : public views::WidgetDelegateView {
public:
explicit WidgetWithSystemUIComponentView(
std::unique_ptr<views::View> component) {
SetLayoutManager(std::make_unique<views::FillLayout>());
AddChildView(std::move(component));
}
WidgetWithSystemUIComponentView(const WidgetWithSystemUIComponentView&) =
delete;
WidgetWithSystemUIComponentView& operator=(
const WidgetWithSystemUIComponentView&) = delete;
~WidgetWithSystemUIComponentView() override = default;
};
// Creates a test widget with given component as the only content.
std::unique_ptr<views::Widget> CreateWidgetWithComponent(
std::unique_ptr<views::View> component) {
return AshTestBase::CreateTestWidget(
views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
new WidgetWithSystemUIComponentView(std::move(component)));
}
using SystemComponentsTest = AshTestBase;
// Tests if the tooltip text of PillButton is same with the button text, unless
// the tooltip text is explicitly set.
TEST_F(SystemComponentsTest, PillButtonTooltip) {
// Create a PillButton object.
auto pill_button =
std::make_unique<PillButton>(PillButton::PressedCallback(), u"Default");
// The tooltip text should be same with initial button text.
EXPECT_EQ(pill_button->GetRenderedTooltipText(gfx::Point()), u"Default");
// Changing button text will also update the tooltip text.
pill_button->SetText(u"New Text");
EXPECT_EQ(pill_button->GetRenderedTooltipText(gfx::Point()), u"New Text");
// If the tooltip text is explicitly set, the tooltip text will always be use.
pill_button->SetTooltipText(u"Tooltip");
EXPECT_EQ(pill_button->GetRenderedTooltipText(gfx::Point()), u"Tooltip");
// Updating button text won't change the preset tooltip text.
pill_button->SetText(u"Foo");
EXPECT_EQ(pill_button->GetRenderedTooltipText(gfx::Point()), u"Tooltip");
}
// TODO(crbug.com/40878458): Disable for constant failure.
TEST_F(SystemComponentsTest,
DISABLED_IconButtonWithBackgroundColorIdDoesNotCrash) {
// Create an IconButton with an explicit background color ID.
auto icon_button = std::make_unique<IconButton>(
IconButton::PressedCallback(), IconButton::Type::kSmall, &kTestIcon,
u"button 1",
/*is_toggleable=*/false, /*has_border=*/false);
auto* icon_button_ptr = icon_button.get();
icon_button->SetBackgroundColor(cros_tokens::kCrosSysSystemOnBase);
auto widget = CreateWidgetWithComponent(std::move(icon_button));
// Schedule a paint for the button.
icon_button_ptr->SchedulePaint();
EXPECT_TRUE(views::ViewTestApi(icon_button_ptr).needs_paint());
// Spin the message loop so the button paints.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(views::ViewTestApi(icon_button_ptr).needs_paint());
// No crash.
}
// Tests that when one button is selected in the radio button group, the others
// will be unselected automatically.
TEST_F(SystemComponentsTest, RadioButtonGroup) {
std::unique_ptr<RadioButtonGroup> radio_button_group =
std::make_unique<RadioButtonGroup>(198);
// Add three buttons to the group.
auto* button_1 = radio_button_group->AddButton(RadioButton::PressedCallback(),
u"Test Button1");
auto* button_2 = radio_button_group->AddButton(RadioButton::PressedCallback(),
u"Test Button2");
auto* button_3 = radio_button_group->AddButton(RadioButton::PressedCallback(),
u"Test Button3");
auto* switch_raw_ptr = radio_button_group.get();
auto widget = CreateWidgetWithComponent(std::move(radio_button_group));
// All the buttons should be in unselected state.
EXPECT_FALSE(button_1->selected());
EXPECT_FALSE(button_2->selected());
EXPECT_FALSE(button_3->selected());
// select the first button by using `RadioButton::SetSelected`.
button_1->SetSelected(true);
// Only the first button is selected.
EXPECT_TRUE(button_1->selected());
EXPECT_FALSE(button_2->selected());
EXPECT_FALSE(button_3->selected());
// Select the second button by mouse clicking.
LeftClickOn(button_2);
// Only the second button is selected.
EXPECT_FALSE(button_1->selected());
EXPECT_TRUE(button_2->selected());
EXPECT_FALSE(button_3->selected());
// Select the third button by using `RadioButtonGroup::SelectButtonAtIndex`.
switch_raw_ptr->SelectButtonAtIndex(2);
// Only the third button is selected.
EXPECT_FALSE(button_1->selected());
EXPECT_FALSE(button_2->selected());
EXPECT_TRUE(button_3->selected());
// Using `SetSelected` again on the first button will unselect the other
// buttons.
button_1->SetSelected(true);
// Only the first button is selected.
EXPECT_TRUE(button_1->selected());
EXPECT_FALSE(button_2->selected());
EXPECT_FALSE(button_3->selected());
// Disabling radio button group makes all radio buttons disabled.
switch_raw_ptr->SetEnabled(false);
EXPECT_FALSE(button_1->GetEnabled());
EXPECT_FALSE(button_2->GetEnabled());
EXPECT_FALSE(button_3->GetEnabled());
}
// Tests that all buttons in the checkbox group can be selected / unselected.
// Click on a selected button will unselect, and vice versa。
TEST_F(SystemComponentsTest, CheckboxGroup) {
std::unique_ptr<CheckboxGroup> checkbox_group =
std::make_unique<CheckboxGroup>(198);
// Add four buttons to the group.
auto* button_1 =
checkbox_group->AddButton(Checkbox::PressedCallback(), u"Test Button1");
auto* button_2 =
checkbox_group->AddButton(Checkbox::PressedCallback(), u"Test Button2");
auto* button_3 =
checkbox_group->AddButton(Checkbox::PressedCallback(), u"Test Button3");
auto* button_4 =
checkbox_group->AddButton(Checkbox::PressedCallback(), u"Test Button4");
auto* switch_raw_ptr = checkbox_group.get();
auto widget = CreateWidgetWithComponent(std::move(checkbox_group));
// All the buttons should be in unselected state.
EXPECT_FALSE(button_1->selected());
EXPECT_FALSE(button_2->selected());
EXPECT_FALSE(button_3->selected());
EXPECT_FALSE(button_4->selected());
// select the first button by using `Checkbox::SetSelected`.
button_1->SetSelected(true);
// The first button is selected.
EXPECT_TRUE(button_1->selected());
EXPECT_FALSE(button_2->selected());
EXPECT_FALSE(button_3->selected());
EXPECT_FALSE(button_4->selected());
// Select the second button by mouse clicking.
LeftClickOn(button_2);
// The first and second buttons are selected.
EXPECT_TRUE(button_1->selected());
EXPECT_TRUE(button_2->selected());
EXPECT_FALSE(button_3->selected());
EXPECT_FALSE(button_4->selected());
// Click on the second button again, it should be unselected.
LeftClickOn(button_2);
EXPECT_FALSE(button_2->selected());
// Select the third button by using `CheckboxGroup::SelectButtonAtIndex`.
switch_raw_ptr->SelectButtonAtIndex(2);
// The third button should be selected.
EXPECT_TRUE(button_3->selected());
// Using `SetSelected` on the fourth button.
button_4->SetSelected(true);
// All buttons should be selected except the second one.
EXPECT_TRUE(button_1->selected());
EXPECT_FALSE(button_2->selected());
EXPECT_TRUE(button_3->selected());
EXPECT_TRUE(button_4->selected());
// Disabling radio button group makes all radio buttons disabled.
switch_raw_ptr->SetEnabled(false);
EXPECT_FALSE(button_1->GetEnabled());
EXPECT_FALSE(button_2->GetEnabled());
EXPECT_FALSE(button_3->GetEnabled());
EXPECT_FALSE(button_4->GetEnabled());
}
TEST_F(SystemComponentsTest, AccessibleDefaultActionVerb) {
std::unique_ptr<RadioButtonGroup> radio_button_group =
std::make_unique<RadioButtonGroup>(198);
auto* button = radio_button_group->AddButton(RadioButton::PressedCallback(),
u"Test Button");
ui::AXNodeData data;
// Enabled
button->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(data.GetDefaultActionVerb(), ax::mojom::DefaultActionVerb::kCheck);
button->SetSelected(true);
data = ui::AXNodeData();
button->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(data.GetDefaultActionVerb(),
ax::mojom::DefaultActionVerb::kUncheck);
button->SetSelected(false);
data = ui::AXNodeData();
button->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_EQ(data.GetDefaultActionVerb(), ax::mojom::DefaultActionVerb::kCheck);
// Disabled
button->SetEnabled(false);
button->SetSelected(true);
data = ui::AXNodeData();
button->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_FALSE(
data.HasIntAttribute(ax::mojom::IntAttribute::kDefaultActionVerb));
button->SetSelected(false);
data = ui::AXNodeData();
button->GetViewAccessibility().GetAccessibleNodeData(&data);
EXPECT_FALSE(
data.HasIntAttribute(ax::mojom::IntAttribute::kDefaultActionVerb));
}
struct DialogTestParams {
ui::mojom::ModalType modal_type;
bool parent_to_root;
};
using SystemDialogDelegateViewTest = SystemComponentsTest;
TEST_F(SystemDialogDelegateViewTest, CancelCallback) {
std::unique_ptr<views::Widget> dialog_widget =
CreateSystemDialogWidget(ui::mojom::ModalType::kNone,
/*parent_window=*/Shell::GetPrimaryRootWindow());
UNCALLED_MOCK_CALLBACK(base::OnceClosure, accept_callback);
UNCALLED_MOCK_CALLBACK(base::OnceClosure, cancel_callback);
UNCALLED_MOCK_CALLBACK(base::OnceClosure, close_callback);
auto* const system_dialog_delegate_view =
static_cast<SystemDialogDelegateView*>(dialog_widget->GetContentsView());
system_dialog_delegate_view->SetAcceptCallback(accept_callback.Get());
system_dialog_delegate_view->SetCancelCallback(cancel_callback.Get());
system_dialog_delegate_view->SetCloseCallback(close_callback.Get());
// Close the dialog through the cancel button. Only `cancel_callback` should
// be executed.
EXPECT_CALL_IN_SCOPE(cancel_callback, Run, {
auto* const cancel_button = system_dialog_delegate_view->GetViewByID(
ViewID::VIEW_ID_STYLE_SYSTEM_DIALOG_DELEGATE_CANCEL_BUTTON);
ASSERT_TRUE(cancel_button);
LeftClickOn(cancel_button);
views::test::WidgetDestroyedWaiter(system_dialog_delegate_view->GetWidget())
.Wait();
});
}
// Verifies that the close callback registered on `SystemDialogDelegateView`
// should run when the dialog view is destroyed without clicking any buttons.
TEST_F(SystemDialogDelegateViewTest, CloseCallback) {
std::unique_ptr<views::Widget> dialog_widget =
CreateSystemDialogWidget(ui::mojom::ModalType::kNone,
/*parent_window=*/Shell::GetPrimaryRootWindow());
UNCALLED_MOCK_CALLBACK(base::OnceClosure, accept_callback);
UNCALLED_MOCK_CALLBACK(base::OnceClosure, cancel_callback);
UNCALLED_MOCK_CALLBACK(base::OnceClosure, close_callback);
auto* const system_dialog_delegate_view =
static_cast<SystemDialogDelegateView*>(dialog_widget->GetContentsView());
system_dialog_delegate_view->SetAcceptCallback(accept_callback.Get());
system_dialog_delegate_view->SetCancelCallback(cancel_callback.Get());
system_dialog_delegate_view->SetCloseCallback(close_callback.Get());
EXPECT_CALL_IN_SCOPE(close_callback, Run, dialog_widget.reset());
}
class SystemDialogSizeTest
: public SystemComponentsTest,
public testing::WithParamInterface<DialogTestParams> {
public:
SystemDialogSizeTest() = default;
SystemDialogSizeTest(const SystemDialogSizeTest&) = delete;
SystemDialogSizeTest& operator=(const SystemDialogSizeTest&) = delete;
~SystemDialogSizeTest() override = default;
protected:
// Create a dialog according to the give test parameters. Resize the host
// window with the given host size.
void CreateDialog(const DialogTestParams& params,
const gfx::Size& host_size) {
// Clear existing dialog and host window instances.
dialog_.reset();
host_widget_.reset();
// Resize the display if the dialog is parented to the root window.
// Otherwise, create a host window with the given size.
if (params.parent_to_root) {
UpdateDisplay(host_size.ToString());
} else {
UpdateDisplay("1280x720");
host_widget_ =
CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
nullptr, desks_util::GetActiveDeskContainerId(),
gfx::Rect(host_size), /*show=*/true);
}
dialog_ = CreateSystemDialogWidget(params.modal_type,
params.parent_to_root
? Shell::GetPrimaryRootWindow()
: host_widget_->GetNativeWindow());
}
// Get the dialog size.
int GetDialogWidth() {
CHECK(dialog_);
return dialog_->GetWindowBoundsInScreen().size().width();
}
private:
std::unique_ptr<views::Widget> host_widget_;
std::unique_ptr<views::Widget> dialog_;
};
const DialogTestParams kSystemDialogTestParams[] = {
{ui::mojom::ModalType::kNone, /*parent_to_root=*/false},
{ui::mojom::ModalType::kNone, /*parent_to_root=*/true},
{ui::mojom::ModalType::kWindow, /*parent_to_root=*/false},
{ui::mojom::ModalType::kWindow, /*parent_to_root=*/true},
{ui::mojom::ModalType::kChild, /*parent_to_root=*/false},
{ui::mojom::ModalType::kChild, /*parent_to_root=*/true},
{ui::mojom::ModalType::kSystem, /*parent_to_root=*/false},
{ui::mojom::ModalType::kSystem, /*parent_to_root=*/true},
};
INSTANTIATE_TEST_SUITE_P(SystemDialogSize,
SystemDialogSizeTest,
testing::ValuesIn(kSystemDialogTestParams));
// Tests the dialog sizes with different sizes of host windows.
TEST_P(SystemDialogSizeTest, DialogResponsiveSize) {
DialogTestParams params = GetParam();
// When the width of the host window is no smaller than 672, the width of the
// dialog is 512.
CreateDialog(params, /*host_size=*/gfx::Size(1000, 600));
EXPECT_EQ(512, GetDialogWidth());
CreateDialog(params, /*host_size=*/gfx::Size(672, 600));
EXPECT_EQ(512, GetDialogWidth());
// When the width of the host window is less than 672 and no smaller than 520,
// the dialog has a padding of 80 on both sides.
CreateDialog(params, /*host_size=*/gfx::Size(671, 600));
EXPECT_EQ(511, GetDialogWidth());
CreateDialog(params, /*host_size=*/gfx::Size(520, 600));
EXPECT_EQ(360, GetDialogWidth());
// When the width of the host window is less than 520 and no smaller than 424,
// the width of the dialog is 359.
CreateDialog(params, /*host_size=*/gfx::Size(519, 600));
EXPECT_EQ(359, GetDialogWidth());
CreateDialog(params, /*host_size=*/gfx::Size(424, 600));
EXPECT_EQ(359, GetDialogWidth());
// When the width of the host window is less than 424 and no smaller than 400,
// the dialog has a padding of 32 on both sides.
CreateDialog(params, /*host_size=*/gfx::Size(423, 600));
EXPECT_EQ(359, GetDialogWidth());
CreateDialog(params, /*host_size=*/gfx::Size(400, 600));
EXPECT_EQ(336, GetDialogWidth());
// When the width of the host window is less than 400, the dialog of has
// minimum size of 296.
CreateDialog(params, /*host_size=*/gfx::Size(399, 600));
EXPECT_EQ(296, GetDialogWidth());
CreateDialog(params, /*host_size=*/gfx::Size(300, 600));
EXPECT_EQ(296, GetDialogWidth());
}
class TabSliderTest : public SystemComponentsTest,
public testing::WithParamInterface<
std::tuple<TabSliderType, bool, bool, int>> {
public:
TabSliderTest() = default;
TabSliderTest(const TabSliderTest&) = delete;
TabSliderTest& operator=(const TabSliderTest&) = delete;
~TabSliderTest() override = default;
};
const TabSliderType kTabSliderTypes[] = {TabSliderType::kIconSlider,
TabSliderType::kLabelSlider,
TabSliderType::kIconLabelSlider};
INSTANTIATE_TEST_SUITE_P(
TabSliderStyle,
TabSliderTest,
testing::Combine(
/*tab slider types*/ testing::ValuesIn(kTabSliderTypes),
/*distribute space evenly*/ testing::Bool(),
/*use default layout*/ testing::Bool(),
/*number of tabs*/ testing::Values(2, 3)),
[](const testing::TestParamInfo<TabSliderTest::ParamType>& info) {
std::string test_name;
const TabSliderType type = std::get<0>(info.param);
switch (type) {
case TabSliderType::kIconSlider:
test_name = "IconSlider";
break;
case TabSliderType::kLabelSlider:
test_name = "LabelSlider";
break;
case TabSliderType::kIconLabelSlider:
test_name = "IconLabelSlider";
break;
}
test_name +=
std::get<1>(info.param) ? "EvenlyDistributed" : "UnevenlyDistributed";
test_name += std::get<2>(info.param) ? "DefaultLayout" : "CustomLayout";
test_name += base::NumberToString(std::get<3>(info.param)) + "Buttons";
return test_name;
});
// Tests tab slider layout works properly with different layout settings.
TEST_P(TabSliderTest, TabSliderLayout) {
auto test_params = GetParam();
const TabSliderType type = std::get<0>(test_params);
const bool distribute_space_evenly = std::get<1>(test_params);
const bool use_default_layout = std::get<2>(test_params);
const int tab_num = std::get<3>(test_params);
TabSlider::InitParams params = (type == TabSliderType::kIconLabelSlider)
? IconLabelSliderButton::kSliderParams
: TabSlider::kDefaultParams;
params.distribute_space_evenly = distribute_space_evenly;
if (!use_default_layout) {
params.internal_border_padding = 3;
params.between_buttons_spacing = 5;
}
// Create a tab slider.
auto tab_slider =
std::make_unique<TabSlider>(/*max_tab_num=*/tab_num, params);
// The texts for tabs.
const std::array<std::u16string, 3> labels_text = {
u"one", u"one two three", u"one two three four five"};
// Add slider buttons according to the testing parameters.
std::vector<TabSliderButton*> buttons(tab_num, nullptr);
int max_button_width = 0;
int max_button_height = 0;
for (int i = 0; i < tab_num; i++) {
switch (type) {
case TabSliderType::kIconSlider:
buttons[i] = tab_slider->AddButton<IconSliderButton>(
IconSliderButton::PressedCallback(), &kTestIcon,
u"icon slider button");
break;
case TabSliderType::kLabelSlider:
buttons[i] = tab_slider->AddButton<LabelSliderButton>(
LabelSliderButton::PressedCallback(), labels_text[i],
u"label slider button");
break;
case TabSliderType::kIconLabelSlider:
buttons[i] = tab_slider->AddButton<IconLabelSliderButton>(
IconLabelSliderButton::PressedCallback(), &kTestIcon,
labels_text[i], u"icon label slider button");
break;
}
// Cache the maximum size of the button.
gfx::Size pref_size = buttons[i]->GetPreferredSize();
max_button_width = std::max(max_button_width, pref_size.width());
max_button_height = std::max(max_button_height, pref_size.height());
}
// Attach the tab slider to a widget.
auto widget = CreateWidgetWithComponent(std::move(tab_slider));
int x = params.internal_border_padding;
const int y = params.internal_border_padding;
// Check if the layout works properly.
for (int i = 0; i < tab_num; i++) {
const gfx::Size pref_size = buttons[i]->GetPreferredSize();
const int expect_width =
params.distribute_space_evenly ? max_button_width : pref_size.width();
const int expect_height =
params.distribute_space_evenly ? max_button_height : pref_size.height();
EXPECT_EQ(buttons[i]->bounds(),
gfx::Rect(x, y, expect_width, expect_height));
x += expect_width + params.between_buttons_spacing;
}
}
} // namespace ash