blob: 1764b1537a8764f4be33ea71b6f22352f5b1bb41 [file] [log] [blame]
// Copyright 2012 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/views/window/dialog_client_view.h"
#include <algorithm>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/ui_base_types.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/gesture_event_details.h"
#include "ui/events/pointer_details.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/metrics.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/test/button_test_api.h"
#include "ui/views/test/test_layout_provider.h"
#include "ui/views/test/test_views.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/views_features.h"
#include "ui/views/widget/unique_widget_ptr.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_delegate.h"
using ui::mojom::DialogButton;
namespace views {
class DialogClientViewTest;
class DialogClientViewTestDelegate : public DialogDelegateView {
public:
explicit DialogClientViewTestDelegate(DialogClientViewTest* parent);
// DialogDelegateView:
gfx::Size CalculatePreferredSize(
const SizeBounds& available_size) const override;
gfx::Size GetMinimumSize() const override;
gfx::Size GetMaximumSize() const override;
private:
const raw_ptr<DialogClientViewTest> parent_;
};
// Base class for tests. Also acts as the dialog delegate and contents view for
// TestDialogClientView.
class DialogClientViewTest : public test::WidgetTest {
public:
DialogClientViewTest()
: test::WidgetTest(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
DialogClientViewTest(const DialogClientViewTest&) = delete;
DialogClientViewTest& operator=(const DialogClientViewTest&) = delete;
// testing::Test:
void SetUp() override {
WidgetTest::SetUp();
delegate_ = new DialogClientViewTestDelegate(this);
delegate_->set_use_custom_frame(false);
delegate_->SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
// Note: not using DialogDelegate::CreateDialogWidget(..), since that can
// alter the frame type according to the platform.
widget_ = new Widget;
Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
params.delegate = delegate_;
widget_->Init(std::move(params));
layout_provider_ = std::make_unique<test::TestLayoutProvider>();
layout_provider_->SetDistanceMetric(DISTANCE_BUTTON_MAX_LINKABLE_WIDTH,
200);
}
void TearDown() override {
delegate_ = nullptr;
widget_.ExtractAsDangling()->CloseNow();
WidgetTest::TearDown();
}
gfx::Size preferred_size() const { return preferred_size_; }
gfx::Size min_size() const { return min_size_; }
gfx::Size max_size() const { return max_size_; }
protected:
gfx::Rect GetUpdatedClientBounds() {
SizeAndLayoutWidget();
return client_view()->bounds();
}
void SizeAndLayoutWidget() {
Widget* dialog = widget();
dialog->SetSize(dialog->GetContentsView()->GetPreferredSize({}));
views::test::RunScheduledLayout(dialog);
}
// Makes sure that the content view is sized correctly. Width must be at least
// the requested amount, but height should always match exactly.
void CheckContentsIsSetToPreferredSize() {
const gfx::Rect client_bounds = GetUpdatedClientBounds();
const gfx::Size preferred_size = delegate_->GetPreferredSize({});
EXPECT_EQ(preferred_size.height(), delegate_->bounds().height());
EXPECT_LE(preferred_size.width(), delegate_->bounds().width());
EXPECT_EQ(gfx::Point(), delegate_->origin());
EXPECT_EQ(client_bounds.width(), delegate_->width());
}
// Sets the buttons to show in the dialog and refreshes the dialog.
void SetDialogButtons(int dialog_buttons) {
delegate_->SetButtons(dialog_buttons);
delegate_->DialogModelChanged();
}
void SetDialogButtonLabel(ui::mojom::DialogButton button,
const std::u16string& label) {
delegate_->SetButtonLabel(button, label);
delegate_->DialogModelChanged();
}
// Sets the view to provide to DisownExtraView() and updates the dialog. This
// can only be called a single time because DialogClientView caches the result
// of DisownExtraView() and never calls it again.
template <typename T>
T* SetExtraView(std::unique_ptr<T> view) {
T* passed_view = delegate_->SetExtraView(std::move(view));
delegate_->DialogModelChanged();
return passed_view;
}
void SetFixedWidth(int width) {
delegate_->set_fixed_width(width);
delegate_->DialogModelChanged();
}
void SetSizeConstraints(const gfx::Size& min_size,
const gfx::Size& preferred_size,
const gfx::Size& max_size) {
min_size_ = min_size;
preferred_size_ = preferred_size;
max_size_ = max_size;
}
void SetAllowVerticalButtons(bool allow) {
delegate_->set_allow_vertical_buttons(allow);
delegate_->DialogModelChanged();
}
void SetThreeWideButtonConfiguration() {
// Ensure the wide button label will be wider than fixed dialog width.
constexpr int kFixedWidth = 100;
const std::u16string kLongLabel(kFixedWidth, 'a');
SetAllowVerticalButtons(true);
SetFixedWidth(kFixedWidth);
SetDialogButtons(static_cast<int>(DialogButton::kCancel) |
static_cast<int>(DialogButton::kOk));
SetExtraView(
std::make_unique<LabelButton>(Button::PressedCallback(), u"extra"));
SetDialogButtonLabel(ui::mojom::DialogButton::kOk, kLongLabel);
}
View* FocusableViewAfter(View* view) {
const bool dont_loop = false;
const bool reverse = false;
return delegate_->GetFocusManager()->GetNextFocusableView(
view, delegate_->GetWidget(), reverse, dont_loop);
}
// Set a longer than normal Cancel label so that the minimum button width is
// exceeded. The resulting width is around 160 pixels, but depends on system
// fonts.
void SetLongCancelLabel() {
delegate_->SetButtonLabel(ui::mojom::DialogButton::kCancel,
u"Cancel Cancel Cancel");
delegate_->DialogModelChanged();
}
DialogClientView* client_view() {
return static_cast<DialogClientView*>(widget_->client_view());
}
DialogDelegateView* delegate() { return delegate_; }
Widget* widget() { return widget_; }
test::TestLayoutProvider* layout_provider() { return layout_provider_.get(); }
private:
// The dialog Widget.
std::unique_ptr<test::TestLayoutProvider> layout_provider_;
raw_ptr<Widget> widget_ = nullptr;
raw_ptr<DialogDelegateView> delegate_ = nullptr;
gfx::Size preferred_size_;
gfx::Size min_size_;
gfx::Size max_size_;
};
DialogClientViewTestDelegate::DialogClientViewTestDelegate(
DialogClientViewTest* parent)
: parent_(parent) {}
gfx::Size DialogClientViewTestDelegate::CalculatePreferredSize(
const SizeBounds& available_size) const {
return parent_->preferred_size();
}
gfx::Size DialogClientViewTestDelegate::GetMinimumSize() const {
return parent_->min_size();
}
gfx::Size DialogClientViewTestDelegate::GetMaximumSize() const {
return parent_->max_size();
}
TEST_F(DialogClientViewTest, UpdateButtons) {
// Make sure this test runs on all platforms. Mac doesn't allow 0 size
// windows. Test only makes sure the size changes based on whether the buttons
// exist or not. The initial size should not matter.
SetSizeConstraints(gfx::Size(200, 100), gfx::Size(300, 200),
gfx::Size(400, 300));
// This dialog should start with no buttons.
EXPECT_EQ(delegate()->buttons(),
static_cast<int>(ui::mojom::DialogButton::kNone));
EXPECT_EQ(nullptr, client_view()->ok_button());
EXPECT_EQ(nullptr, client_view()->cancel_button());
const int height_without_buttons = GetUpdatedClientBounds().height();
// Update to use both buttons.
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kOk) |
static_cast<int>(ui::mojom::DialogButton::kCancel));
EXPECT_TRUE(client_view()->ok_button()->GetIsDefault());
EXPECT_FALSE(client_view()->cancel_button()->GetIsDefault());
const int height_with_buttons = GetUpdatedClientBounds().height();
EXPECT_GT(height_with_buttons, height_without_buttons);
// Remove the dialog buttons.
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
EXPECT_EQ(nullptr, client_view()->ok_button());
EXPECT_EQ(nullptr, client_view()->cancel_button());
EXPECT_EQ(GetUpdatedClientBounds().height(), height_without_buttons);
// Reset with just an ok button.
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kOk));
EXPECT_TRUE(client_view()->ok_button()->GetIsDefault());
EXPECT_EQ(nullptr, client_view()->cancel_button());
EXPECT_EQ(GetUpdatedClientBounds().height(), height_with_buttons);
// Reset with just a cancel button.
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel));
EXPECT_EQ(nullptr, client_view()->ok_button());
EXPECT_EQ(client_view()->cancel_button()->GetIsDefault(),
PlatformStyle::kDialogDefaultButtonCanBeCancel);
EXPECT_EQ(GetUpdatedClientBounds().height(), height_with_buttons);
}
TEST_F(DialogClientViewTest, RemoveAndUpdateButtons) {
// Removing buttons from another context should clear the local pointer.
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kOk) |
static_cast<int>(ui::mojom::DialogButton::kCancel));
delete client_view()->ok_button();
EXPECT_EQ(nullptr, client_view()->ok_button());
delete client_view()->cancel_button();
EXPECT_EQ(nullptr, client_view()->cancel_button());
// Updating should restore the requested buttons properly.
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kOk) |
static_cast<int>(ui::mojom::DialogButton::kCancel));
EXPECT_TRUE(client_view()->ok_button()->GetIsDefault());
EXPECT_FALSE(client_view()->cancel_button()->GetIsDefault());
}
// Test that views inside the dialog client view have the correct focus order.
TEST_F(DialogClientViewTest, SetupFocusChain) {
delegate()->GetContentsView()->SetFocusBehavior(View::FocusBehavior::ALWAYS);
// Initially the dialog client view only contains the content view.
EXPECT_EQ(delegate()->GetContentsView(),
FocusableViewAfter(delegate()->GetContentsView()));
// Add OK and cancel buttons.
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kOk) |
static_cast<int>(ui::mojom::DialogButton::kCancel));
if constexpr (PlatformStyle::kIsOkButtonLeading) {
EXPECT_EQ(client_view()->ok_button(),
FocusableViewAfter(delegate()->GetContentsView()));
EXPECT_EQ(client_view()->cancel_button(),
FocusableViewAfter(client_view()->ok_button()));
EXPECT_EQ(delegate()->GetContentsView(),
FocusableViewAfter(client_view()->cancel_button()));
} else {
EXPECT_EQ(client_view()->cancel_button(),
FocusableViewAfter(delegate()->GetContentsView()));
EXPECT_EQ(client_view()->ok_button(),
FocusableViewAfter(client_view()->cancel_button()));
EXPECT_EQ(delegate()->GetContentsView(),
FocusableViewAfter(client_view()->ok_button()));
}
// Add extra view and remove OK button.
View* extra_view =
SetExtraView(std::make_unique<StaticSizedView>(gfx::Size(200, 200)));
extra_view->SetFocusBehavior(View::FocusBehavior::ALWAYS);
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel));
EXPECT_EQ(extra_view, FocusableViewAfter(delegate()->GetContentsView()));
EXPECT_EQ(client_view()->cancel_button(), FocusableViewAfter(extra_view));
EXPECT_EQ(delegate()->GetContentsView(), FocusableViewAfter(client_view()));
// Add a dummy view to the contents view. Consult the FocusManager for the
// traversal order since it now spans different levels of the view hierarchy.
View* dummy_view = new StaticSizedView(gfx::Size(200, 200));
dummy_view->SetFocusBehavior(View::FocusBehavior::ALWAYS);
delegate()->GetContentsView()->SetFocusBehavior(View::FocusBehavior::NEVER);
delegate()->GetContentsView()->AddChildViewRaw(dummy_view);
EXPECT_EQ(dummy_view, FocusableViewAfter(client_view()->cancel_button()));
EXPECT_EQ(extra_view, FocusableViewAfter(dummy_view));
EXPECT_EQ(client_view()->cancel_button(), FocusableViewAfter(extra_view));
// Views are added to the contents view, not the client view, so the focus
// chain within the client view is not affected.
// NOTE: The TableLayout requires a view to be in every cell. "Dummy" non-
// focusable views are inserted to satisfy this requirement.
EXPECT_TRUE(!client_view()->cancel_button()->GetNextFocusableView() ||
client_view()
->cancel_button()
->GetNextFocusableView()
->GetFocusBehavior() == View::FocusBehavior::NEVER);
}
// Test that the contents view gets its preferred size in the basic dialog
// configuration.
TEST_F(DialogClientViewTest, ContentsSize) {
// On Mac the size cannot be 0, so we give it a preferred size.
SetSizeConstraints(gfx::Size(200, 100), gfx::Size(300, 200),
gfx::Size(400, 300));
CheckContentsIsSetToPreferredSize();
EXPECT_EQ(delegate()->GetContentsView()->size(), client_view()->size());
EXPECT_EQ(gfx::Size(300, 200), client_view()->size());
}
// Test the effect of the button strip on layout.
TEST_F(DialogClientViewTest, LayoutWithButtons) {
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kOk) |
static_cast<int>(ui::mojom::DialogButton::kCancel));
CheckContentsIsSetToPreferredSize();
EXPECT_LT(delegate()->GetContentsView()->bounds().bottom(),
client_view()->bounds().bottom());
const gfx::Size no_extra_view_size = client_view()->bounds().size();
View* extra_view =
SetExtraView(std::make_unique<StaticSizedView>(gfx::Size(200, 200)));
CheckContentsIsSetToPreferredSize();
EXPECT_GT(client_view()->bounds().height(), no_extra_view_size.height());
// The dialog is bigger with the extra view than without it.
const gfx::Size with_extra_view_size = client_view()->size();
EXPECT_NE(no_extra_view_size, with_extra_view_size);
// Hiding the extra view removes it.
extra_view->SetVisible(false);
CheckContentsIsSetToPreferredSize();
EXPECT_EQ(no_extra_view_size, client_view()->size());
// Making it visible again adds it back.
extra_view->SetVisible(true);
CheckContentsIsSetToPreferredSize();
EXPECT_EQ(with_extra_view_size, client_view()->size());
// Leave |extra_view| hidden. It should still have a parent, to ensure it is
// owned by a View hierarchy and gets deleted.
extra_view->SetVisible(false);
EXPECT_TRUE(extra_view->parent());
}
// Ensure the minimum, maximum and preferred sizes of the contents view are
// respected by the client view, and that the client view includes the button
// row in its minimum and preferred size calculations.
TEST_F(DialogClientViewTest, MinMaxPreferredSize) {
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kOk) |
static_cast<int>(ui::mojom::DialogButton::kCancel));
const gfx::Size buttons_size = client_view()->GetPreferredSize({});
EXPECT_FALSE(buttons_size.IsEmpty());
// When the contents view has no preference, just fit the buttons. The
// maximum size should be unconstrained in both directions.
EXPECT_EQ(buttons_size, client_view()->GetMinimumSize());
EXPECT_EQ(gfx::Size(), client_view()->GetMaximumSize());
// Ensure buttons are between these widths, for the constants below.
EXPECT_LT(20, buttons_size.width());
EXPECT_GT(300, buttons_size.width());
// With no buttons, client view should match the contents view.
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
SetSizeConstraints(gfx::Size(10, 15), gfx::Size(20, 25), gfx::Size(300, 350));
EXPECT_EQ(gfx::Size(10, 15), client_view()->GetMinimumSize());
EXPECT_EQ(gfx::Size(20, 25), client_view()->GetPreferredSize({}));
EXPECT_EQ(gfx::Size(300, 350), client_view()->GetMaximumSize());
// With buttons, size should increase vertically only.
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kOk) |
static_cast<int>(ui::mojom::DialogButton::kCancel));
EXPECT_EQ(gfx::Size(buttons_size.width(), 15 + buttons_size.height()),
client_view()->GetMinimumSize());
EXPECT_EQ(gfx::Size(buttons_size.width(), 25 + buttons_size.height()),
client_view()->GetPreferredSize({}));
EXPECT_EQ(gfx::Size(300, 350 + buttons_size.height()),
client_view()->GetMaximumSize());
// If the contents view gets bigger, it should take over the width.
SetSizeConstraints(gfx::Size(400, 450), gfx::Size(500, 550),
gfx::Size(600, 650));
EXPECT_EQ(gfx::Size(400, 450 + buttons_size.height()),
client_view()->GetMinimumSize());
EXPECT_EQ(gfx::Size(500, 550 + buttons_size.height()),
client_view()->GetPreferredSize({}));
EXPECT_EQ(gfx::Size(600, 650 + buttons_size.height()),
client_view()->GetMaximumSize());
}
// Ensure button widths are linked under MD.
TEST_F(DialogClientViewTest, LinkedWidthDoesLink) {
SetLongCancelLabel();
// Ensure there is no default button since getting a bold font can throw off
// the cached sizes.
delegate()->SetDefaultButton(
static_cast<int>(ui::mojom::DialogButton::kNone));
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kOk));
CheckContentsIsSetToPreferredSize();
const int ok_button_only_width = client_view()->ok_button()->width();
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel));
CheckContentsIsSetToPreferredSize();
const int cancel_button_width = client_view()->cancel_button()->width();
EXPECT_LT(cancel_button_width, 200);
// Ensure the single buttons have different preferred widths when alone, and
// that the Cancel button is bigger (so that it dominates the size).
EXPECT_GT(cancel_button_width, ok_button_only_width);
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
CheckContentsIsSetToPreferredSize();
// Cancel button shouldn't have changed widths.
EXPECT_EQ(cancel_button_width, client_view()->cancel_button()->width());
// OK button should now match the bigger, cancel button.
EXPECT_EQ(cancel_button_width, client_view()->ok_button()->width());
// But not when the size of the cancel button exceeds the max linkable width.
layout_provider()->SetDistanceMetric(DISTANCE_BUTTON_MAX_LINKABLE_WIDTH, 100);
EXPECT_GT(cancel_button_width, 100);
delegate()->DialogModelChanged();
CheckContentsIsSetToPreferredSize();
EXPECT_EQ(ok_button_only_width, client_view()->ok_button()->width());
layout_provider()->SetDistanceMetric(DISTANCE_BUTTON_MAX_LINKABLE_WIDTH, 200);
// The extra view should also match, if it's a matching button type.
View* extra_button = SetExtraView(std::make_unique<LabelButton>(
Button::PressedCallback(), std::u16string()));
CheckContentsIsSetToPreferredSize();
EXPECT_EQ(cancel_button_width, extra_button->width());
}
TEST_F(DialogClientViewTest, LinkedWidthDoesntLink) {
SetLongCancelLabel();
// Ensure there is no default button since getting a bold font can throw off
// the cached sizes.
delegate()->SetDefaultButton(
static_cast<int>(ui::mojom::DialogButton::kNone));
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kOk));
CheckContentsIsSetToPreferredSize();
const int ok_button_only_width = client_view()->ok_button()->width();
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel));
CheckContentsIsSetToPreferredSize();
const int cancel_button_width = client_view()->cancel_button()->width();
EXPECT_LT(cancel_button_width, 200);
// Ensure the single buttons have different preferred widths when alone, and
// that the Cancel button is bigger (so that it dominates the size).
EXPECT_GT(cancel_button_width, ok_button_only_width);
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
CheckContentsIsSetToPreferredSize();
// Cancel button shouldn't have changed widths.
EXPECT_EQ(cancel_button_width, client_view()->cancel_button()->width());
// OK button should now match the bigger, cancel button.
EXPECT_EQ(cancel_button_width, client_view()->ok_button()->width());
// But not when the size of the cancel button exceeds the max linkable width.
layout_provider()->SetDistanceMetric(DISTANCE_BUTTON_MAX_LINKABLE_WIDTH, 100);
EXPECT_GT(cancel_button_width, 100);
delegate()->DialogModelChanged();
CheckContentsIsSetToPreferredSize();
EXPECT_EQ(ok_button_only_width, client_view()->ok_button()->width());
layout_provider()->SetDistanceMetric(DISTANCE_BUTTON_MAX_LINKABLE_WIDTH, 200);
// Checkbox extends LabelButton, but it should not participate in linking.
View* extra_button =
SetExtraView(std::make_unique<Checkbox>(std::u16string()));
CheckContentsIsSetToPreferredSize();
EXPECT_NE(cancel_button_width, extra_button->width());
}
TEST_F(DialogClientViewTest, ButtonPosition) {
constexpr int button_row_inset = 13;
client_view()->SetButtonRowInsets(gfx::Insets(button_row_inset));
constexpr int contents_height = 37;
constexpr int contents_width = 222;
SetSizeConstraints(gfx::Size(), gfx::Size(contents_width, contents_height),
gfx::Size(666, 666));
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kOk));
SizeAndLayoutWidget();
EXPECT_EQ(contents_width - button_row_inset,
client_view()->ok_button()->bounds().right());
EXPECT_EQ(contents_height + button_row_inset,
delegate()->height() + client_view()->ok_button()->y());
}
// Ensures that the focus of the button remains after a dialog update.
TEST_F(DialogClientViewTest, FocusUpdate) {
// Test with just an ok button.
widget()->Show();
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kOk));
EXPECT_FALSE(client_view()->ok_button()->HasFocus());
client_view()->ok_button()->RequestFocus(); // Set focus.
EXPECT_TRUE(client_view()->ok_button()->HasFocus());
delegate()->DialogModelChanged();
EXPECT_TRUE(client_view()->ok_button()->HasFocus());
}
// Ensures that the focus of the button remains after a dialog update that
// contains multiple buttons.
TEST_F(DialogClientViewTest, FocusMultipleButtons) {
// Test with ok and cancel buttons.
widget()->Show();
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
EXPECT_FALSE(client_view()->ok_button()->HasFocus());
EXPECT_FALSE(client_view()->cancel_button()->HasFocus());
client_view()->cancel_button()->RequestFocus(); // Set focus.
EXPECT_FALSE(client_view()->ok_button()->HasFocus());
EXPECT_TRUE(client_view()->cancel_button()->HasFocus());
delegate()->DialogModelChanged();
EXPECT_TRUE(client_view()->cancel_button()->HasFocus());
}
// Ensures that the focus persistence works correctly when buttons are removed.
TEST_F(DialogClientViewTest, FocusChangingButtons) {
// Start with ok and cancel buttons.
widget()->Show();
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
client_view()->cancel_button()->RequestFocus(); // Set focus.
FocusManager* focus_manager = delegate()->GetFocusManager();
EXPECT_EQ(client_view()->cancel_button(), focus_manager->GetFocusedView());
// Remove buttons.
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
EXPECT_EQ(nullptr, focus_manager->GetFocusedView());
}
// Ensures that clicks are ignored for short time after view has been shown.
TEST_F(DialogClientViewTest, IgnorePossiblyUnintendedClicks_ClickAfterShown) {
widget()->Show();
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
// Should ignore clicks right after the dialog is shown.
ui::MouseEvent mouse_event(ui::EventType::kMousePressed, gfx::PointF(),
gfx::PointF(), ui::EventTimeForNow(), ui::EF_NONE,
ui::EF_NONE);
test::ButtonTestApi(client_view()->ok_button()).NotifyClick(mouse_event);
test::ButtonTestApi cancel_button(client_view()->cancel_button());
cancel_button.NotifyClick(mouse_event);
EXPECT_FALSE(widget()->IsClosed());
cancel_button.NotifyClick(
ui::MouseEvent(ui::EventType::kMousePressed, gfx::PointF(), gfx::PointF(),
ui::EventTimeForNow() + GetDoubleClickInterval(),
ui::EF_NONE, ui::EF_NONE));
EXPECT_TRUE(widget()->IsClosed());
}
// Ensures that key events are not ignored for short time, after view has been
// shown.
TEST_F(DialogClientViewTest, DoesNotIgnoreKeyEvents_ReturnKeyAfterShown) {
widget()->Show();
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
// Should not ignore key events right after the dialog is shown.
ui::KeyEvent press_enter(ui::EventType::kKeyPressed, ui::VKEY_RETURN,
ui::EF_NONE, ui::EventTimeForNow());
test::ButtonTestApi(client_view()->ok_button()).NotifyClick(press_enter);
EXPECT_TRUE(widget()->IsClosed());
}
// Ensures that taps are ignored for a short time after the view has been shown.
TEST_F(DialogClientViewTest, IgnorePossiblyUnintendedClicks_TapAfterShown) {
widget()->Show();
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
// Should ignore taps right after the dialog is shown.
ui::GestureEvent tap_event(
0, 0, 0, ui::EventTimeForNow(),
ui::GestureEventDetails(ui::EventType::kGestureTap));
test::ButtonTestApi(client_view()->ok_button()).NotifyClick(tap_event);
test::ButtonTestApi cancel_button(client_view()->cancel_button());
cancel_button.NotifyClick(tap_event);
EXPECT_FALSE(widget()->IsClosed());
ui::GestureEvent tap_event2(
0, 0, 0, ui::EventTimeForNow() + GetDoubleClickInterval(),
ui::GestureEventDetails(ui::EventType::kGestureTap));
cancel_button.NotifyClick(tap_event2);
EXPECT_TRUE(widget()->IsClosed());
}
// Ensures that touch events are ignored for a short time after the view has
// been shown.
TEST_F(DialogClientViewTest, IgnorePossiblyUnintendedClicks_TouchAfterShown) {
widget()->Show();
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
// Should ignore touches right after the dialog is shown.
ui::TouchEvent touch_event(ui::EventType::kTouchPressed, gfx::PointF(),
gfx::PointF(), ui::EventTimeForNow(),
ui::PointerDetails(ui::EventPointerType::kTouch));
test::ButtonTestApi(client_view()->ok_button()).NotifyClick(touch_event);
test::ButtonTestApi cancel_button(client_view()->cancel_button());
cancel_button.NotifyClick(touch_event);
EXPECT_FALSE(widget()->IsClosed());
ui::TouchEvent touch_event2(ui::EventType::kTouchPressed, gfx::PointF(),
gfx::PointF(),
ui::EventTimeForNow() + GetDoubleClickInterval(),
ui::PointerDetails(ui::EventPointerType::kTouch));
cancel_button.NotifyClick(touch_event2);
EXPECT_TRUE(widget()->IsClosed());
}
// TODO(crbug.com/40269697): investigate the tests on ChromeOS and
// fuchsia
#if !BUILDFLAG(IS_CHROMEOS) && !BUILDFLAG(IS_FUCHSIA)
class DesktopDialogClientViewTest : public DialogClientViewTest {
public:
void SetUp() override {
set_native_widget_type(NativeWidgetType::kDesktop);
DialogClientViewTest::SetUp();
}
};
// Ensures that unintended clicks are protected properly when a root window's
// bound has been changed.
TEST_F(DesktopDialogClientViewTest,
IgnorePossiblyUnintendedClicks_TopLevelWindowBoundsChanged) {
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
SizeAndLayoutWidget();
widget()->Show();
task_environment()->FastForwardBy(GetDoubleClickInterval() * 2);
// Create another widget on top, change window's bounds, click event to the
// old widget should be ignored.
auto* widget1 = CreateTopLevelNativeWidget();
widget1->SetBounds(gfx::Rect(50, 50, 100, 100));
ui::MouseEvent mouse_event(ui::EventType::kMousePressed, gfx::Point(),
gfx::Point(), ui::EventTimeForNow(), ui::EF_NONE,
ui::EF_NONE);
test::ButtonTestApi(client_view()->ok_button()).NotifyClick(mouse_event);
test::ButtonTestApi cancel_button(client_view()->cancel_button());
cancel_button.NotifyClick(mouse_event);
EXPECT_FALSE(widget()->IsClosed());
cancel_button.NotifyClick(
ui::MouseEvent(ui::EventType::kMousePressed, gfx::Point(), gfx::Point(),
ui::EventTimeForNow() + GetDoubleClickInterval(),
ui::EF_NONE, ui::EF_NONE));
EXPECT_TRUE(widget()->IsClosed());
widget1->CloseNow();
}
// Ensures that unintended clicks are protected properly when a root window has
// been closed.
TEST_F(DesktopDialogClientViewTest,
IgnorePossiblyUnintendedClicks_CloseRootWindow) {
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
SizeAndLayoutWidget();
widget()->Show();
task_environment()->FastForwardBy(GetDoubleClickInterval() * 2);
// Create another widget on top, close the top window, click event to the old
// widget should be ignored.
auto* widget1 = CreateTopLevelNativeWidget();
widget1->CloseNow();
ui::MouseEvent mouse_event(ui::EventType::kMousePressed, gfx::Point(),
gfx::Point(), ui::EventTimeForNow(), ui::EF_NONE,
ui::EF_NONE);
test::ButtonTestApi(client_view()->ok_button()).NotifyClick(mouse_event);
test::ButtonTestApi cancel_button(client_view()->cancel_button());
cancel_button.NotifyClick(mouse_event);
EXPECT_FALSE(widget()->IsClosed());
cancel_button.NotifyClick(
ui::MouseEvent(ui::EventType::kMousePressed, gfx::Point(), gfx::Point(),
ui::EventTimeForNow() + GetDoubleClickInterval(),
ui::EF_NONE, ui::EF_NONE));
EXPECT_TRUE(widget()->IsClosed());
}
#endif // !BUILDFLAG(IS_CHROMEOS) && !BUILDFLAG(IS_FUCHSIA)
#if BUILDFLAG(ENABLE_DESKTOP_AURA)
TEST_F(DialogClientViewTest,
IgnorePossiblyUnintendedClicks_ClickAfterClosingTooltip) {
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
SizeAndLayoutWidget();
widget()->Show();
task_environment()->FastForwardBy(GetDoubleClickInterval() * 2);
UniqueWidgetPtr widget1(std::make_unique<Widget>());
Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_TOOLTIP);
widget1->Init(std::move(params));
widget1->CloseNow();
ui::MouseEvent mouse_event(ui::EventType::kMousePressed, gfx::Point(),
gfx::Point(), ui::EventTimeForNow(), ui::EF_NONE,
ui::EF_NONE);
test::ButtonTestApi(client_view()->ok_button()).NotifyClick(mouse_event);
test::ButtonTestApi cancel_button(client_view()->cancel_button());
cancel_button.NotifyClick(mouse_event);
EXPECT_TRUE(widget()->IsClosed());
}
#endif // BUILDFLAG(ENABLE_DESKTOP_AURA)
// Ensures that repeated clicks with short intervals after view has been shown
// are also ignored.
TEST_F(DialogClientViewTest, IgnorePossiblyUnintendedClicks_RepeatedClicks) {
widget()->Show();
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
const base::TimeTicks kNow = ui::EventTimeForNow();
const base::TimeDelta kShortClickInterval = GetDoubleClickInterval();
// Should ignore clicks right after the dialog is shown.
ui::MouseEvent mouse_event(ui::EventType::kMousePressed, gfx::Point(),
gfx::Point(), kNow, ui::EF_NONE, ui::EF_NONE);
test::ButtonTestApi(client_view()->ok_button()).NotifyClick(mouse_event);
test::ButtonTestApi cancel_button(client_view()->cancel_button());
cancel_button.NotifyClick(mouse_event);
EXPECT_FALSE(widget()->IsClosed());
// Should ignore repeated clicks with short intervals, even though enough time
// has passed since the dialog was shown.
const base::TimeDelta kRepeatedClickInterval = kShortClickInterval / 2;
const size_t kNumClicks = 4;
ASSERT_TRUE(kNumClicks * kRepeatedClickInterval > kShortClickInterval);
base::TimeTicks event_time = kNow;
for (size_t i = 0; i < kNumClicks; i++) {
cancel_button.NotifyClick(
ui::MouseEvent(ui::EventType::kMousePressed, gfx::Point(), gfx::Point(),
event_time, ui::EF_NONE, ui::EF_NONE));
EXPECT_FALSE(widget()->IsClosed());
event_time += kRepeatedClickInterval;
}
// Sufficient time passed, events are now allowed.
event_time += kShortClickInterval;
cancel_button.NotifyClick(
ui::MouseEvent(ui::EventType::kMousePressed, gfx::Point(), gfx::Point(),
event_time, ui::EF_NONE, ui::EF_NONE));
EXPECT_TRUE(widget()->IsClosed());
}
TEST_F(DialogClientViewTest, ButtonLayoutWithExtra) {
// The dialog button row's layout should look like:
// | <inset> [extra] <flex-margin> [cancel] <margin> [ok] <inset> |
// Where:
// 1) The two insets are linkable
// 2) The ok & cancel buttons have their width linked
// 3) The extra button has its width linked to the other two
// 4) The margin should be invariant as the dialog changes width
// 5) The flex margin should change as the dialog changes width
//
// Note that cancel & ok may swap order depending on
// PlatformStyle::kIsOkButtonLeading; these invariants hold for either order.
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kOk) |
static_cast<int>(ui::mojom::DialogButton::kCancel));
SetDialogButtonLabel(ui::mojom::DialogButton::kOk, u"ok");
SetDialogButtonLabel(ui::mojom::DialogButton::kCancel, u"cancel");
SetExtraView(
std::make_unique<LabelButton>(Button::PressedCallback(), u"extra"));
widget()->Show();
View* ok = client_view()->ok_button();
View* cancel = client_view()->cancel_button();
View* extra = client_view()->extra_view();
ASSERT_NE(ok, cancel);
ASSERT_NE(ok, extra);
ASSERT_NE(cancel, extra);
SizeAndLayoutWidget();
auto bounds_left = [](View* v) { return v->GetBoundsInScreen().x(); };
auto bounds_right = [](View* v) { return v->GetBoundsInScreen().right(); };
// (1): left inset == right inset (and they shouldn't be 0):
int left_inset = bounds_left(extra) - bounds_left(delegate());
int right_inset = bounds_right(delegate()) -
std::max(bounds_right(ok), bounds_right(cancel));
EXPECT_EQ(left_inset, right_inset);
EXPECT_GT(left_inset, 0);
// (2) & (3): All three buttons have their widths linked:
EXPECT_EQ(ok->width(), cancel->width());
EXPECT_EQ(ok->width(), extra->width());
EXPECT_GT(ok->width(), 0);
// (4): Margin between ok & cancel should be invariant as dialog width
// changes:
auto get_margin = [&]() {
return std::max(bounds_left(ok), bounds_left(cancel)) -
std::min(bounds_right(ok), bounds_right(cancel));
};
// (5): Flex margin between ok/cancel and extra should vary with dialog width
// (it should absorb 100% of the change in width)
auto get_flex_margin = [&]() {
return std::min(bounds_left(ok), bounds_left(cancel)) - bounds_right(extra);
};
int old_margin = get_margin();
int old_flex_margin = get_flex_margin();
SetSizeConstraints(gfx::Size(), gfx::Size(delegate()->width() + 100, 0),
gfx::Size());
SizeAndLayoutWidget();
EXPECT_EQ(old_margin, get_margin());
EXPECT_EQ(old_flex_margin + 100, get_flex_margin());
}
TEST_F(DialogClientViewTest, LayoutWithHiddenExtraView) {
SetDialogButtons(static_cast<int>(ui::mojom::DialogButton::kCancel) |
static_cast<int>(ui::mojom::DialogButton::kOk));
SetDialogButtonLabel(ui::mojom::DialogButton::kOk, u"ok");
SetDialogButtonLabel(ui::mojom::DialogButton::kCancel, u"cancel");
SetExtraView(
std::make_unique<LabelButton>(Button::PressedCallback(), u"extra"));
widget()->Show();
SizeAndLayoutWidget();
View* ok = client_view()->ok_button();
View* cancel = client_view()->cancel_button();
View* extra = client_view()->extra_view();
int ok_left = ok->bounds().x();
int cancel_left = cancel->bounds().x();
extra->SetVisible(false);
// Re-layout but do not resize the widget. If we resized it without the extra
// view, it would get narrower and the other buttons would move.
EXPECT_TRUE(widget()->GetContentsView()->needs_layout());
views::test::RunScheduledLayout(widget());
EXPECT_EQ(ok_left, ok->bounds().x());
EXPECT_EQ(cancel_left, cancel->bounds().x());
}
MATCHER(HasHorizontalButtons, "") {
const auto ok_bounds = arg->ok_button()->bounds();
const auto cancel_bounds = arg->cancel_button()->bounds();
EXPECT_EQ(ok_bounds.CenterPoint().y(), cancel_bounds.CenterPoint().y());
// Order from the top is always Extra, Cancel, Ok (unlike horizontal
// platform-specific ordering).
if (arg->extra_view()) {
const auto extra_bounds = arg->extra_view()->bounds();
EXPECT_EQ(ok_bounds.CenterPoint().y(), extra_bounds.CenterPoint().y());
}
return true;
}
MATCHER(HasVerticalButtons, "") {
EXPECT_NE(arg->extra_view(), nullptr);
if (!arg->extra_view()) {
return false;
}
const auto ok_bounds = arg->ok_button()->bounds();
const auto cancel_bounds = arg->cancel_button()->bounds();
const auto extra_bounds = arg->extra_view()->bounds();
// Buttons should have the same width and be vertically-aligned.
EXPECT_EQ(ok_bounds.width(), cancel_bounds.width());
EXPECT_EQ(ok_bounds.width(), extra_bounds.width());
EXPECT_EQ(ok_bounds.x(), cancel_bounds.x());
EXPECT_EQ(ok_bounds.x(), extra_bounds.x());
// Order from the top is always Extra, Cancel, Ok (unlike horizontal
// platform-specific ordering).
EXPECT_LT(extra_bounds.y(), cancel_bounds.y());
EXPECT_LT(cancel_bounds.y(), ok_bounds.y());
return true;
}
TEST_F(DialogClientViewTest, WideButtonsRenderVertically) {
SetThreeWideButtonConfiguration();
widget()->Show();
SizeAndLayoutWidget();
EXPECT_THAT(client_view(), HasVerticalButtons());
}
TEST_F(DialogClientViewTest, WideButtonsStayHorizontalIfFeatureDisabled) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(
views::features::kDialogVerticalButtonFallback);
SetThreeWideButtonConfiguration();
widget()->Show();
SizeAndLayoutWidget();
EXPECT_THAT(client_view(), HasHorizontalButtons());
}
TEST_F(DialogClientViewTest, WideButtonsStayHorizontalIfNotFixedWidth) {
SetThreeWideButtonConfiguration();
SetFixedWidth(0);
widget()->Show();
SizeAndLayoutWidget();
EXPECT_THAT(client_view(), HasHorizontalButtons());
}
TEST_F(DialogClientViewTest, WideButtonsStayHorizontalIfNoExtraButton) {
SetThreeWideButtonConfiguration();
SetExtraView(std::unique_ptr<View>());
widget()->Show();
SizeAndLayoutWidget();
EXPECT_THAT(client_view(), HasHorizontalButtons());
}
TEST_F(DialogClientViewTest, WideButtonsStayHorizontalIfVerticalNotAllowed) {
SetThreeWideButtonConfiguration();
SetAllowVerticalButtons(false);
widget()->Show();
SizeAndLayoutWidget();
EXPECT_THAT(client_view(), HasHorizontalButtons());
}
struct IsPossiblyUnintendedInteractionTestCase {
enum class EventType {
kKey,
kMouse,
};
std::string test_name;
EventType event_type;
bool is_delayed_interaction;
bool allow_key_events;
bool is_possibly_unintended_interaction;
};
class InteractionTest : public DialogClientViewTest,
public testing::WithParamInterface<
IsPossiblyUnintendedInteractionTestCase> {
public:
InteractionTest() = default;
InteractionTest(const InteractionTest&) = delete;
InteractionTest& operator=(const InteractionTest&) = delete;
std::unique_ptr<ui::KeyEvent> KeyEventNow() {
return std::make_unique<ui::KeyEvent>(ui::EventType::kKeyPressed,
ui::VKEY_RETURN, ui::EF_NONE,
ui::EventTimeForNow());
}
std::unique_ptr<ui::KeyEvent> KeyEventDelayed() {
return std::make_unique<ui::KeyEvent>(
ui::EventType::kKeyPressed, ui::VKEY_RETURN, ui::EF_NONE,
ui::EventTimeForNow() + GetDoubleClickInterval());
}
std::unique_ptr<ui::MouseEvent> MouseEventNow() {
return std::make_unique<ui::MouseEvent>(
ui::EventType::kMousePressed, gfx::PointF(), gfx::PointF(),
ui::EventTimeForNow(), ui::EF_NONE, ui::EF_NONE);
}
std::unique_ptr<ui::MouseEvent> MouseEventDelayed() {
return std::make_unique<ui::MouseEvent>(
ui::EventType::kMousePressed, gfx::PointF(), gfx::PointF(),
ui::EventTimeForNow() + GetDoubleClickInterval(), ui::EF_NONE,
ui::EF_NONE);
}
};
TEST_P(InteractionTest, IsPossiblyUnintendedInteraction) {
const IsPossiblyUnintendedInteractionTestCase& test_case = GetParam();
widget()->Show();
std::unique_ptr<ui::Event> event;
switch (test_case.event_type) {
case IsPossiblyUnintendedInteractionTestCase::EventType::kKey:
event =
test_case.is_delayed_interaction ? KeyEventDelayed() : KeyEventNow();
break;
case IsPossiblyUnintendedInteractionTestCase::EventType::kMouse:
event = test_case.is_delayed_interaction ? MouseEventDelayed()
: MouseEventNow();
break;
}
ASSERT_NE(event, nullptr);
EXPECT_EQ(client_view()->IsPossiblyUnintendedInteraction(
*event, test_case.allow_key_events),
test_case.is_possibly_unintended_interaction);
}
INSTANTIATE_TEST_SUITE_P(
AllInteractions,
InteractionTest,
testing::ValuesIn<IsPossiblyUnintendedInteractionTestCase>({
{
.test_name = "NotPermissionRelevantKeyEventNow",
.event_type =
IsPossiblyUnintendedInteractionTestCase::EventType::kKey,
.is_delayed_interaction = false,
.allow_key_events = true,
.is_possibly_unintended_interaction = false,
},
{
.test_name = "PermissionRelevantKeyEventNow",
.event_type =
IsPossiblyUnintendedInteractionTestCase::EventType::kKey,
.is_delayed_interaction = false,
.allow_key_events = false,
.is_possibly_unintended_interaction = true,
},
{
.test_name = "NotPermissionRelevantKeyEventDelayed",
.event_type =
IsPossiblyUnintendedInteractionTestCase::EventType::kKey,
.is_delayed_interaction = true,
.allow_key_events = true,
.is_possibly_unintended_interaction = false,
},
{
.test_name = "PermissionRelevantKeyEventDelayed",
.event_type =
IsPossiblyUnintendedInteractionTestCase::EventType::kKey,
.is_delayed_interaction = true,
.allow_key_events = false,
.is_possibly_unintended_interaction = false,
},
{
.test_name = "NotPermissionRelevantMouseEventNow",
.event_type =
IsPossiblyUnintendedInteractionTestCase::EventType::kMouse,
.is_delayed_interaction = false,
.allow_key_events = true,
.is_possibly_unintended_interaction = true,
},
{
.test_name = "PermissionRelevantMouseEventNow",
.event_type =
IsPossiblyUnintendedInteractionTestCase::EventType::kMouse,
.is_delayed_interaction = false,
.allow_key_events = false,
.is_possibly_unintended_interaction = true,
},
{
.test_name = "NotPermissionRelevantMouseEventDelayed",
.event_type =
IsPossiblyUnintendedInteractionTestCase::EventType::kMouse,
.is_delayed_interaction = true,
.allow_key_events = true,
.is_possibly_unintended_interaction = false,
},
{
.test_name = "PermissionRelevantMouseEventDelayed",
.event_type =
IsPossiblyUnintendedInteractionTestCase::EventType::kMouse,
.is_delayed_interaction = true,
.allow_key_events = false,
.is_possibly_unintended_interaction = false,
},
}),
[](const testing::TestParamInfo<InteractionTest::ParamType>& info) {
return info.param.test_name;
});
} // namespace views