1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ui/views/window/dialog_client_view.h"
6
7 #include <algorithm>
8 #include <map>
9 #include <memory>
10 #include <string>
11 #include <utility>
12
13 #include "base/macros.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "base/time/time.h"
16 #include "build/build_config.h"
17 #include "ui/base/ui_base_types.h"
18 #include "ui/events/base_event_utils.h"
19 #include "ui/events/event.h"
20 #include "ui/gfx/geometry/point.h"
21 #include "ui/views/controls/button/checkbox.h"
22 #include "ui/views/controls/button/image_button.h"
23 #include "ui/views/controls/button/label_button.h"
24 #include "ui/views/metrics.h"
25 #include "ui/views/style/platform_style.h"
26 #include "ui/views/test/button_test_api.h"
27 #include "ui/views/test/test_layout_provider.h"
28 #include "ui/views/test/test_views.h"
29 #include "ui/views/test/widget_test.h"
30 #include "ui/views/widget/widget.h"
31 #include "ui/views/window/dialog_delegate.h"
32
33 namespace views {
34
35 // Base class for tests. Also acts as the dialog delegate and contents view for
36 // TestDialogClientView.
37 class DialogClientViewTest : public test::WidgetTest,
38 public DialogDelegateView {
39 public:
40 DialogClientViewTest() = default;
41
42 // testing::Test:
SetUp()43 void SetUp() override {
44 WidgetTest::SetUp();
45
46 DialogDelegate::set_use_custom_frame(false);
47 DialogDelegate::SetButtons(ui::DIALOG_BUTTON_NONE);
48
49 // Note: not using DialogDelegate::CreateDialogWidget(..), since that can
50 // alter the frame type according to the platform.
51 widget_ = new Widget;
52 Widget::InitParams params = CreateParams(Widget::InitParams::TYPE_WINDOW);
53 params.delegate = this;
54 widget_->Init(std::move(params));
55 EXPECT_EQ(this, GetContentsView());
56 layout_provider_ = std::make_unique<test::TestLayoutProvider>();
57 layout_provider_->SetDistanceMetric(DISTANCE_BUTTON_MAX_LINKABLE_WIDTH,
58 200);
59 }
60
TearDown()61 void TearDown() override {
62 widget_->CloseNow();
63 WidgetTest::TearDown();
64 }
65
66 // DialogDelegateView:
CalculatePreferredSize() const67 gfx::Size CalculatePreferredSize() const override { return preferred_size_; }
GetMinimumSize() const68 gfx::Size GetMinimumSize() const override { return min_size_; }
GetMaximumSize() const69 gfx::Size GetMaximumSize() const override { return max_size_; }
70
DeleteDelegate()71 void DeleteDelegate() override {
72 // DialogDelegateView would delete this, but |this| is owned by the test.
73 }
74
75 protected:
GetUpdatedClientBounds()76 gfx::Rect GetUpdatedClientBounds() {
77 client_view()->SizeToPreferredSize();
78 client_view()->Layout();
79 return client_view()->bounds();
80 }
81
82 // Makes sure that the content view is sized correctly. Width must be at least
83 // the requested amount, but height should always match exactly.
CheckContentsIsSetToPreferredSize()84 void CheckContentsIsSetToPreferredSize() {
85 const gfx::Rect client_bounds = GetUpdatedClientBounds();
86 const gfx::Size preferred_size = this->GetPreferredSize();
87 EXPECT_EQ(preferred_size.height(), this->bounds().height());
88 EXPECT_LE(preferred_size.width(), this->bounds().width());
89 EXPECT_EQ(gfx::Point(), this->origin());
90 EXPECT_EQ(client_bounds.width(), this->width());
91 }
92
93 // Sets the buttons to show in the dialog and refreshes the dialog.
SetDialogButtons(int dialog_buttons)94 void SetDialogButtons(int dialog_buttons) {
95 DialogDelegate::SetButtons(dialog_buttons);
96 DialogModelChanged();
97 }
98
SetDialogButtonLabel(ui::DialogButton button,const std::string & label)99 void SetDialogButtonLabel(ui::DialogButton button, const std::string& label) {
100 DialogDelegate::SetButtonLabel(button, base::UTF8ToUTF16(label));
101 DialogModelChanged();
102 }
103
104 // Sets the view to provide to DisownExtraView() and updates the dialog. This
105 // can only be called a single time because DialogClientView caches the result
106 // of DisownExtraView() and never calls it again.
107 template <typename T>
SetExtraView(std::unique_ptr<T> view)108 T* SetExtraView(std::unique_ptr<T> view) {
109 T* passed_view = DialogDelegate::SetExtraView(std::move(view));
110 DialogModelChanged();
111 return passed_view;
112 }
113
SetSizeConstraints(const gfx::Size & min_size,const gfx::Size & preferred_size,const gfx::Size & max_size)114 void SetSizeConstraints(const gfx::Size& min_size,
115 const gfx::Size& preferred_size,
116 const gfx::Size& max_size) {
117 min_size_ = min_size;
118 preferred_size_ = preferred_size;
119 max_size_ = max_size;
120 }
121
FocusableViewAfter(View * view)122 View* FocusableViewAfter(View* view) {
123 const bool dont_loop = false;
124 const bool reverse = false;
125 return GetFocusManager()->GetNextFocusableView(view, GetWidget(), reverse,
126 dont_loop);
127 }
128
129 // Set a longer than normal Cancel label so that the minimum button width is
130 // exceeded. The resulting width is around 160 pixels, but depends on system
131 // fonts.
SetLongCancelLabel()132 void SetLongCancelLabel() {
133 DialogDelegate::SetButtonLabel(
134 ui::DIALOG_BUTTON_CANCEL, base::ASCIIToUTF16("Cancel Cancel Cancel"));
135 DialogModelChanged();
136 }
137
GetButtonByAccessibleName(View * root,const base::string16 & name)138 Button* GetButtonByAccessibleName(View* root, const base::string16& name) {
139 Button* button = Button::AsButton(root);
140 if (button && button->GetAccessibleName() == name)
141 return button;
142 for (auto* child : root->children()) {
143 button = GetButtonByAccessibleName(child, name);
144 if (button)
145 return button;
146 }
147 return nullptr;
148 }
149
GetButtonByAccessibleName(const std::string & label)150 Button* GetButtonByAccessibleName(const std::string& label) {
151 return GetButtonByAccessibleName(widget_->GetRootView(),
152 base::UTF8ToUTF16(label));
153 }
154
client_view()155 DialogClientView* client_view() {
156 return static_cast<DialogClientView*>(widget_->client_view());
157 }
158
widget()159 Widget* widget() { return widget_; }
layout_provider()160 test::TestLayoutProvider* layout_provider() { return layout_provider_.get(); }
161
162 private:
163 // The dialog Widget.
164 std::unique_ptr<test::TestLayoutProvider> layout_provider_;
165 Widget* widget_ = nullptr;
166
167 gfx::Size preferred_size_;
168 gfx::Size min_size_;
169 gfx::Size max_size_;
170
171 DISALLOW_COPY_AND_ASSIGN(DialogClientViewTest);
172 };
173
TEST_F(DialogClientViewTest,UpdateButtons)174 TEST_F(DialogClientViewTest, UpdateButtons) {
175 // This dialog should start with no buttons.
176 EXPECT_EQ(GetDialogButtons(), ui::DIALOG_BUTTON_NONE);
177 EXPECT_EQ(nullptr, client_view()->ok_button());
178 EXPECT_EQ(nullptr, client_view()->cancel_button());
179 const int height_without_buttons = GetUpdatedClientBounds().height();
180
181 // Update to use both buttons.
182 SetDialogButtons(ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL);
183 EXPECT_TRUE(client_view()->ok_button()->GetIsDefault());
184 EXPECT_FALSE(client_view()->cancel_button()->GetIsDefault());
185 const int height_with_buttons = GetUpdatedClientBounds().height();
186 EXPECT_GT(height_with_buttons, height_without_buttons);
187
188 // Remove the dialog buttons.
189 SetDialogButtons(ui::DIALOG_BUTTON_NONE);
190 EXPECT_EQ(nullptr, client_view()->ok_button());
191 EXPECT_EQ(nullptr, client_view()->cancel_button());
192 EXPECT_EQ(GetUpdatedClientBounds().height(), height_without_buttons);
193
194 // Reset with just an ok button.
195 SetDialogButtons(ui::DIALOG_BUTTON_OK);
196 EXPECT_TRUE(client_view()->ok_button()->GetIsDefault());
197 EXPECT_EQ(nullptr, client_view()->cancel_button());
198 EXPECT_EQ(GetUpdatedClientBounds().height(), height_with_buttons);
199
200 // Reset with just a cancel button.
201 SetDialogButtons(ui::DIALOG_BUTTON_CANCEL);
202 EXPECT_EQ(nullptr, client_view()->ok_button());
203 EXPECT_EQ(client_view()->cancel_button()->GetIsDefault(),
204 PlatformStyle::kDialogDefaultButtonCanBeCancel);
205 EXPECT_EQ(GetUpdatedClientBounds().height(), height_with_buttons);
206 }
207
TEST_F(DialogClientViewTest,RemoveAndUpdateButtons)208 TEST_F(DialogClientViewTest, RemoveAndUpdateButtons) {
209 // Removing buttons from another context should clear the local pointer.
210 SetDialogButtons(ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL);
211 delete client_view()->ok_button();
212 EXPECT_EQ(nullptr, client_view()->ok_button());
213 delete client_view()->cancel_button();
214 EXPECT_EQ(nullptr, client_view()->cancel_button());
215
216 // Updating should restore the requested buttons properly.
217 SetDialogButtons(ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL);
218 EXPECT_TRUE(client_view()->ok_button()->GetIsDefault());
219 EXPECT_FALSE(client_view()->cancel_button()->GetIsDefault());
220 }
221
222 // Test that views inside the dialog client view have the correct focus order.
TEST_F(DialogClientViewTest,SetupFocusChain)223 TEST_F(DialogClientViewTest, SetupFocusChain) {
224 const bool kIsOkButtonOnLeftSide = PlatformStyle::kIsOkButtonLeading;
225
226 GetContentsView()->SetFocusBehavior(View::FocusBehavior::ALWAYS);
227 // Initially the dialog client view only contains the content view.
228 EXPECT_EQ(GetContentsView(), FocusableViewAfter(GetContentsView()));
229
230 // Add OK and cancel buttons.
231 SetDialogButtons(ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL);
232
233 if (kIsOkButtonOnLeftSide) {
234 EXPECT_EQ(client_view()->ok_button(),
235 FocusableViewAfter(GetContentsView()));
236 EXPECT_EQ(client_view()->cancel_button(),
237 FocusableViewAfter(client_view()->ok_button()));
238 EXPECT_EQ(GetContentsView(),
239 FocusableViewAfter(client_view()->cancel_button()));
240 } else {
241 EXPECT_EQ(client_view()->cancel_button(),
242 FocusableViewAfter(GetContentsView()));
243 EXPECT_EQ(client_view()->ok_button(),
244 FocusableViewAfter(client_view()->cancel_button()));
245 EXPECT_EQ(GetContentsView(),
246 FocusableViewAfter(client_view()->ok_button()));
247 }
248
249 // Add extra view and remove OK button.
250 View* extra_view =
251 SetExtraView(std::make_unique<StaticSizedView>(gfx::Size(200, 200)));
252 extra_view->SetFocusBehavior(View::FocusBehavior::ALWAYS);
253 SetDialogButtons(ui::DIALOG_BUTTON_CANCEL);
254
255 EXPECT_EQ(extra_view, FocusableViewAfter(GetContentsView()));
256 EXPECT_EQ(client_view()->cancel_button(), FocusableViewAfter(extra_view));
257 EXPECT_EQ(GetContentsView(), FocusableViewAfter(client_view()));
258
259 // Add a dummy view to the contents view. Consult the FocusManager for the
260 // traversal order since it now spans different levels of the view hierarchy.
261 View* dummy_view = new StaticSizedView(gfx::Size(200, 200));
262 dummy_view->SetFocusBehavior(View::FocusBehavior::ALWAYS);
263 GetContentsView()->SetFocusBehavior(View::FocusBehavior::NEVER);
264 GetContentsView()->AddChildView(dummy_view);
265 EXPECT_EQ(dummy_view, FocusableViewAfter(client_view()->cancel_button()));
266 EXPECT_EQ(extra_view, FocusableViewAfter(dummy_view));
267 EXPECT_EQ(client_view()->cancel_button(), FocusableViewAfter(extra_view));
268
269 // Views are added to the contents view, not the client view, so the focus
270 // chain within the client view is not affected.
271 EXPECT_EQ(nullptr, client_view()->cancel_button()->GetNextFocusableView());
272 }
273
274 // Test that the contents view gets its preferred size in the basic dialog
275 // configuration.
TEST_F(DialogClientViewTest,ContentsSize)276 TEST_F(DialogClientViewTest, ContentsSize) {
277 CheckContentsIsSetToPreferredSize();
278 EXPECT_EQ(GetContentsView()->size(), client_view()->size());
279 // There's nothing in the contents view (i.e. |this|), so it should be 0x0.
280 EXPECT_EQ(gfx::Size(), client_view()->size());
281 }
282
283 // Test the effect of the button strip on layout.
TEST_F(DialogClientViewTest,LayoutWithButtons)284 TEST_F(DialogClientViewTest, LayoutWithButtons) {
285 SetDialogButtons(ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL);
286 CheckContentsIsSetToPreferredSize();
287
288 EXPECT_LT(GetContentsView()->bounds().bottom(),
289 client_view()->bounds().bottom());
290 const gfx::Size no_extra_view_size = client_view()->bounds().size();
291
292 View* extra_view =
293 SetExtraView(std::make_unique<StaticSizedView>(gfx::Size(200, 200)));
294 CheckContentsIsSetToPreferredSize();
295 EXPECT_GT(client_view()->bounds().height(), no_extra_view_size.height());
296
297 // The dialog is bigger with the extra view than without it.
298 const gfx::Size with_extra_view_size = client_view()->size();
299 EXPECT_NE(no_extra_view_size, with_extra_view_size);
300
301 // Hiding the extra view removes it.
302 extra_view->SetVisible(false);
303 CheckContentsIsSetToPreferredSize();
304 EXPECT_EQ(no_extra_view_size, client_view()->size());
305
306 // Making it visible again adds it back.
307 extra_view->SetVisible(true);
308 CheckContentsIsSetToPreferredSize();
309 EXPECT_EQ(with_extra_view_size, client_view()->size());
310
311 // Leave |extra_view| hidden. It should still have a parent, to ensure it is
312 // owned by a View hierarchy and gets deleted.
313 extra_view->SetVisible(false);
314 EXPECT_TRUE(extra_view->parent());
315 }
316
317 // Ensure the minimum, maximum and preferred sizes of the contents view are
318 // respected by the client view, and that the client view includes the button
319 // row in its minimum and preferred size calculations.
TEST_F(DialogClientViewTest,MinMaxPreferredSize)320 TEST_F(DialogClientViewTest, MinMaxPreferredSize) {
321 SetDialogButtons(ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL);
322 const gfx::Size buttons_size = client_view()->GetPreferredSize();
323 EXPECT_FALSE(buttons_size.IsEmpty());
324
325 // When the contents view has no preference, just fit the buttons. The
326 // maximum size should be unconstrained in both directions.
327 EXPECT_EQ(buttons_size, client_view()->GetMinimumSize());
328 EXPECT_EQ(gfx::Size(), client_view()->GetMaximumSize());
329
330 // Ensure buttons are between these widths, for the constants below.
331 EXPECT_LT(20, buttons_size.width());
332 EXPECT_GT(300, buttons_size.width());
333
334 // With no buttons, client view should match the contents view.
335 SetDialogButtons(ui::DIALOG_BUTTON_NONE);
336 SetSizeConstraints(gfx::Size(10, 15), gfx::Size(20, 25), gfx::Size(300, 350));
337 EXPECT_EQ(gfx::Size(10, 15), client_view()->GetMinimumSize());
338 EXPECT_EQ(gfx::Size(20, 25), client_view()->GetPreferredSize());
339 EXPECT_EQ(gfx::Size(300, 350), client_view()->GetMaximumSize());
340
341 // With buttons, size should increase vertically only.
342 SetDialogButtons(ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL);
343 EXPECT_EQ(gfx::Size(buttons_size.width(), 15 + buttons_size.height()),
344 client_view()->GetMinimumSize());
345 EXPECT_EQ(gfx::Size(buttons_size.width(), 25 + buttons_size.height()),
346 client_view()->GetPreferredSize());
347 EXPECT_EQ(gfx::Size(300, 350 + buttons_size.height()),
348 client_view()->GetMaximumSize());
349
350 // If the contents view gets bigger, it should take over the width.
351 SetSizeConstraints(gfx::Size(400, 450), gfx::Size(500, 550),
352 gfx::Size(600, 650));
353 EXPECT_EQ(gfx::Size(400, 450 + buttons_size.height()),
354 client_view()->GetMinimumSize());
355 EXPECT_EQ(gfx::Size(500, 550 + buttons_size.height()),
356 client_view()->GetPreferredSize());
357 EXPECT_EQ(gfx::Size(600, 650 + buttons_size.height()),
358 client_view()->GetMaximumSize());
359 }
360
361 // Ensure button widths are linked under MD.
TEST_F(DialogClientViewTest,LinkedWidthDoesLink)362 TEST_F(DialogClientViewTest, LinkedWidthDoesLink) {
363 SetLongCancelLabel();
364
365 // Ensure there is no default button since getting a bold font can throw off
366 // the cached sizes.
367 SetDefaultButton(ui::DIALOG_BUTTON_NONE);
368
369 SetDialogButtons(ui::DIALOG_BUTTON_OK);
370 CheckContentsIsSetToPreferredSize();
371 const int ok_button_only_width = client_view()->ok_button()->width();
372
373 SetDialogButtons(ui::DIALOG_BUTTON_CANCEL);
374 CheckContentsIsSetToPreferredSize();
375 const int cancel_button_width = client_view()->cancel_button()->width();
376 EXPECT_LT(cancel_button_width, 200);
377
378 // Ensure the single buttons have different preferred widths when alone, and
379 // that the Cancel button is bigger (so that it dominates the size).
380 EXPECT_GT(cancel_button_width, ok_button_only_width);
381
382 SetDialogButtons(ui::DIALOG_BUTTON_CANCEL | ui::DIALOG_BUTTON_OK);
383 CheckContentsIsSetToPreferredSize();
384
385 // Cancel button shouldn't have changed widths.
386 EXPECT_EQ(cancel_button_width, client_view()->cancel_button()->width());
387
388 // OK button should now match the bigger, cancel button.
389 EXPECT_EQ(cancel_button_width, client_view()->ok_button()->width());
390
391 // But not when the size of the cancel button exceeds the max linkable width.
392 layout_provider()->SetDistanceMetric(DISTANCE_BUTTON_MAX_LINKABLE_WIDTH, 100);
393 EXPECT_GT(cancel_button_width, 100);
394
395 DialogModelChanged();
396 CheckContentsIsSetToPreferredSize();
397 EXPECT_EQ(ok_button_only_width, client_view()->ok_button()->width());
398 layout_provider()->SetDistanceMetric(DISTANCE_BUTTON_MAX_LINKABLE_WIDTH, 200);
399
400 // The extra view should also match, if it's a matching button type.
401 View* extra_button = SetExtraView(std::make_unique<LabelButton>(
402 Button::PressedCallback(), base::string16()));
403 CheckContentsIsSetToPreferredSize();
404 EXPECT_EQ(cancel_button_width, extra_button->width());
405 }
406
TEST_F(DialogClientViewTest,LinkedWidthDoesntLink)407 TEST_F(DialogClientViewTest, LinkedWidthDoesntLink) {
408 SetLongCancelLabel();
409
410 // Ensure there is no default button since getting a bold font can throw off
411 // the cached sizes.
412 SetDefaultButton(ui::DIALOG_BUTTON_NONE);
413
414 SetDialogButtons(ui::DIALOG_BUTTON_OK);
415 CheckContentsIsSetToPreferredSize();
416 const int ok_button_only_width = client_view()->ok_button()->width();
417
418 SetDialogButtons(ui::DIALOG_BUTTON_CANCEL);
419 CheckContentsIsSetToPreferredSize();
420 const int cancel_button_width = client_view()->cancel_button()->width();
421 EXPECT_LT(cancel_button_width, 200);
422
423 // Ensure the single buttons have different preferred widths when alone, and
424 // that the Cancel button is bigger (so that it dominates the size).
425 EXPECT_GT(cancel_button_width, ok_button_only_width);
426
427 SetDialogButtons(ui::DIALOG_BUTTON_CANCEL | ui::DIALOG_BUTTON_OK);
428 CheckContentsIsSetToPreferredSize();
429
430 // Cancel button shouldn't have changed widths.
431 EXPECT_EQ(cancel_button_width, client_view()->cancel_button()->width());
432
433 // OK button should now match the bigger, cancel button.
434 EXPECT_EQ(cancel_button_width, client_view()->ok_button()->width());
435
436 // But not when the size of the cancel button exceeds the max linkable width.
437 layout_provider()->SetDistanceMetric(DISTANCE_BUTTON_MAX_LINKABLE_WIDTH, 100);
438 EXPECT_GT(cancel_button_width, 100);
439
440 DialogModelChanged();
441 CheckContentsIsSetToPreferredSize();
442 EXPECT_EQ(ok_button_only_width, client_view()->ok_button()->width());
443 layout_provider()->SetDistanceMetric(DISTANCE_BUTTON_MAX_LINKABLE_WIDTH, 200);
444
445 // Checkbox extends LabelButton, but it should not participate in linking.
446 View* extra_button =
447 SetExtraView(std::make_unique<Checkbox>(base::string16()));
448 CheckContentsIsSetToPreferredSize();
449 EXPECT_NE(cancel_button_width, extra_button->width());
450 }
451
TEST_F(DialogClientViewTest,ButtonPosition)452 TEST_F(DialogClientViewTest, ButtonPosition) {
453 constexpr int button_row_inset = 13;
454 client_view()->SetButtonRowInsets(gfx::Insets(button_row_inset));
455 constexpr int contents_height = 37;
456 constexpr int contents_width = 222;
457 SetSizeConstraints(gfx::Size(), gfx::Size(contents_width, contents_height),
458 gfx::Size(666, 666));
459 SetDialogButtons(ui::DIALOG_BUTTON_OK);
460 client_view()->SizeToPreferredSize();
461 client_view()->Layout();
462 EXPECT_EQ(contents_width - button_row_inset,
463 client_view()->ok_button()->bounds().right());
464 EXPECT_EQ(contents_height + button_row_inset,
465 height() + client_view()->ok_button()->y());
466 }
467
468 // Ensures that the focus of the button remains after a dialog update.
TEST_F(DialogClientViewTest,FocusUpdate)469 TEST_F(DialogClientViewTest, FocusUpdate) {
470 // Test with just an ok button.
471 widget()->Show();
472 SetDialogButtons(ui::DIALOG_BUTTON_OK);
473 EXPECT_FALSE(client_view()->ok_button()->HasFocus());
474 client_view()->ok_button()->RequestFocus(); // Set focus.
475 EXPECT_TRUE(client_view()->ok_button()->HasFocus());
476 DialogModelChanged();
477 EXPECT_TRUE(client_view()->ok_button()->HasFocus());
478 }
479
480 // Ensures that the focus of the button remains after a dialog update that
481 // contains multiple buttons.
TEST_F(DialogClientViewTest,FocusMultipleButtons)482 TEST_F(DialogClientViewTest, FocusMultipleButtons) {
483 // Test with ok and cancel buttons.
484 widget()->Show();
485 SetDialogButtons(ui::DIALOG_BUTTON_CANCEL | ui::DIALOG_BUTTON_OK);
486 EXPECT_FALSE(client_view()->ok_button()->HasFocus());
487 EXPECT_FALSE(client_view()->cancel_button()->HasFocus());
488 client_view()->cancel_button()->RequestFocus(); // Set focus.
489 EXPECT_FALSE(client_view()->ok_button()->HasFocus());
490 EXPECT_TRUE(client_view()->cancel_button()->HasFocus());
491 DialogModelChanged();
492 EXPECT_TRUE(client_view()->cancel_button()->HasFocus());
493 }
494
495 // Ensures that the focus persistence works correctly when buttons are removed.
TEST_F(DialogClientViewTest,FocusChangingButtons)496 TEST_F(DialogClientViewTest, FocusChangingButtons) {
497 // Start with ok and cancel buttons.
498 widget()->Show();
499 SetDialogButtons(ui::DIALOG_BUTTON_CANCEL | ui::DIALOG_BUTTON_OK);
500 client_view()->cancel_button()->RequestFocus(); // Set focus.
501 FocusManager* focus_manager = GetFocusManager();
502 EXPECT_EQ(client_view()->cancel_button(), focus_manager->GetFocusedView());
503
504 // Remove buttons.
505 SetDialogButtons(ui::DIALOG_BUTTON_NONE);
506 EXPECT_EQ(nullptr, focus_manager->GetFocusedView());
507 }
508
509 // Ensures that clicks are ignored for short time after view has been shown.
TEST_F(DialogClientViewTest,IgnorePossiblyUnintendedClicks_ClickAfterShown)510 TEST_F(DialogClientViewTest, IgnorePossiblyUnintendedClicks_ClickAfterShown) {
511 widget()->Show();
512 SetDialogButtons(ui::DIALOG_BUTTON_CANCEL | ui::DIALOG_BUTTON_OK);
513
514 // Should ignore clicks right after the dialog is shown.
515 ui::MouseEvent mouse_event(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
516 ui::EventTimeForNow(), ui::EF_NONE, ui::EF_NONE);
517 test::ButtonTestApi(client_view()->ok_button()).NotifyClick(mouse_event);
518 test::ButtonTestApi cancel_button(client_view()->cancel_button());
519 cancel_button.NotifyClick(mouse_event);
520 EXPECT_FALSE(widget()->IsClosed());
521
522 cancel_button.NotifyClick(ui::MouseEvent(
523 ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
524 ui::EventTimeForNow() +
525 base::TimeDelta::FromMilliseconds(GetDoubleClickInterval()),
526 ui::EF_NONE, ui::EF_NONE));
527 EXPECT_TRUE(widget()->IsClosed());
528 }
529
530 // Ensures that repeated clicks with short intervals after view has been shown
531 // are also ignored.
TEST_F(DialogClientViewTest,IgnorePossiblyUnintendedClicks_RepeatedClicks)532 TEST_F(DialogClientViewTest, IgnorePossiblyUnintendedClicks_RepeatedClicks) {
533 widget()->Show();
534 SetDialogButtons(ui::DIALOG_BUTTON_CANCEL | ui::DIALOG_BUTTON_OK);
535
536 const base::TimeTicks kNow = ui::EventTimeForNow();
537 const base::TimeDelta kShortClickInterval =
538 base::TimeDelta::FromMilliseconds(GetDoubleClickInterval());
539
540 // Should ignore clicks right after the dialog is shown.
541 ui::MouseEvent mouse_event(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
542 kNow, ui::EF_NONE, ui::EF_NONE);
543 test::ButtonTestApi(client_view()->ok_button()).NotifyClick(mouse_event);
544 test::ButtonTestApi cancel_button(client_view()->cancel_button());
545 cancel_button.NotifyClick(mouse_event);
546 EXPECT_FALSE(widget()->IsClosed());
547
548 // Should ignore repeated clicks with short intervals, even though enough time
549 // has passed since the dialog was shown.
550 const base::TimeDelta kRepeatedClickInterval = kShortClickInterval / 2;
551 const size_t kNumClicks = 4;
552 ASSERT_TRUE(kNumClicks * kRepeatedClickInterval > kShortClickInterval);
553 base::TimeTicks event_time = kNow;
554 for (size_t i = 0; i < kNumClicks; i++) {
555 cancel_button.NotifyClick(ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(),
556 gfx::Point(), event_time,
557 ui::EF_NONE, ui::EF_NONE));
558 EXPECT_FALSE(widget()->IsClosed());
559 event_time += kRepeatedClickInterval;
560 }
561
562 // Sufficient time passed, events are now allowed.
563 event_time += kShortClickInterval;
564 cancel_button.NotifyClick(ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(),
565 gfx::Point(), event_time,
566 ui::EF_NONE, ui::EF_NONE));
567 EXPECT_TRUE(widget()->IsClosed());
568 }
569
TEST_F(DialogClientViewTest,ButtonLayoutWithExtra)570 TEST_F(DialogClientViewTest, ButtonLayoutWithExtra) {
571 // The dialog button row's layout should look like:
572 // | <inset> [extra] <flex-margin> [cancel] <margin> [ok] <inset> |
573 // Where:
574 // 1) The two insets are linkable
575 // 2) The ok & cancel buttons have their width linked
576 // 3) The extra button has its width linked to the other two
577 // 4) The margin should be invariant as the dialog changes width
578 // 5) The flex margin should change as the dialog changes width
579 //
580 // Note that cancel & ok may swap order depending on
581 // PlatformStyle::kIsOkButtonLeading; these invariants hold for either order.
582 SetDialogButtons(ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL);
583 SetDialogButtonLabel(ui::DIALOG_BUTTON_OK, "ok");
584 SetDialogButtonLabel(ui::DIALOG_BUTTON_CANCEL, "cancel");
585 SetExtraView(std::make_unique<LabelButton>(Button::PressedCallback(),
586 base::UTF8ToUTF16("extra")));
587
588 widget()->Show();
589
590 Button* ok = GetButtonByAccessibleName("ok");
591 Button* cancel = GetButtonByAccessibleName("cancel");
592 Button* extra = GetButtonByAccessibleName("extra");
593
594 ASSERT_NE(ok, cancel);
595 ASSERT_NE(ok, extra);
596 ASSERT_NE(cancel, extra);
597
598 client_view()->SizeToPreferredSize();
599 client_view()->Layout();
600
601 auto bounds_left = [](View* v) { return v->GetBoundsInScreen().x(); };
602 auto bounds_right = [](View* v) { return v->GetBoundsInScreen().right(); };
603
604 // (1): left inset == right inset (and they shouldn't be 0):
605 int left_inset = bounds_left(extra) - bounds_left(this);
606 int right_inset =
607 bounds_right(this) - std::max(bounds_right(ok), bounds_right(cancel));
608 EXPECT_EQ(left_inset, right_inset);
609 EXPECT_GT(left_inset, 0);
610
611 // (2) & (3): All three buttons have their widths linked:
612 EXPECT_EQ(ok->width(), cancel->width());
613 EXPECT_EQ(ok->width(), extra->width());
614 EXPECT_GT(ok->width(), 0);
615
616 // (4): Margin between ok & cancel should be invariant as dialog width
617 // changes:
618 auto get_margin = [&]() {
619 return std::max(bounds_left(ok), bounds_left(cancel)) -
620 std::min(bounds_right(ok), bounds_right(cancel));
621 };
622
623 // (5): Flex margin between ok/cancel and extra should vary with dialog width
624 // (it should absorb 100% of the change in width)
625 auto get_flex_margin = [&]() {
626 return std::min(bounds_left(ok), bounds_left(cancel)) - bounds_right(extra);
627 };
628
629 int old_margin = get_margin();
630 int old_flex_margin = get_flex_margin();
631
632 SetSizeConstraints(gfx::Size(), gfx::Size(width() + 100, 0), gfx::Size());
633 client_view()->SizeToPreferredSize();
634 client_view()->Layout();
635
636 EXPECT_EQ(old_margin, get_margin());
637 EXPECT_EQ(old_flex_margin + 100, get_flex_margin());
638 }
639
640 } // namespace views
641