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