Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / views / cocoa / bridged_native_widget_interactive_uitest.mm
blob6b4db3498d062009aabbb2f6e27299e46fb80298
1 // Copyright 2014 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.
5 #import "ui/views/cocoa/bridged_native_widget.h"
7 #import <Cocoa/Cocoa.h>
9 #import "base/mac/mac_util.h"
10 #import "base/mac/sdk_forward_declarations.h"
11 #import "ui/base/test/nswindow_fullscreen_notification_waiter.h"
12 #include "ui/base/hit_test.h"
13 #import "ui/base/test/windowed_nsnotification_observer.h"
14 #import "ui/events/test/cocoa_test_event_utils.h"
15 #include "ui/views/test/widget_test.h"
16 #include "ui/views/window/native_frame_view.h"
18 namespace views {
19 namespace test {
21 class BridgedNativeWidgetUITest : public test::WidgetTest {
22  public:
23   BridgedNativeWidgetUITest() {}
25   // testing::Test:
26   void SetUp() override {
27     WidgetTest::SetUp();
28     Widget::InitParams init_params =
29         CreateParams(Widget::InitParams::TYPE_WINDOW);
30     init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
31     widget_.reset(new Widget);
32     widget_->Init(init_params);
33   }
35   void TearDown() override {
36     // Ensures any compositor is removed before ViewsTestBase tears down the
37     // ContextFactory.
38     widget_.reset();
39     WidgetTest::TearDown();
40   }
42   NSWindow* test_window() {
43     return widget_->GetNativeWindow();
44   }
46  protected:
47   scoped_ptr<Widget> widget_;
50 // Tests for correct fullscreen tracking, regardless of whether it is initiated
51 // by the Widget code or elsewhere (e.g. by the user).
52 TEST_F(BridgedNativeWidgetUITest, FullscreenSynchronousState) {
53   EXPECT_FALSE(widget_->IsFullscreen());
54   if (base::mac::IsOSSnowLeopard())
55     return;
57   // Allow user-initiated fullscreen changes on the Window.
58   [test_window()
59       setCollectionBehavior:[test_window() collectionBehavior] |
60                             NSWindowCollectionBehaviorFullScreenPrimary];
62   base::scoped_nsobject<NSWindowFullscreenNotificationWaiter> waiter(
63       [[NSWindowFullscreenNotificationWaiter alloc]
64           initWithWindow:test_window()]);
65   const gfx::Rect restored_bounds = widget_->GetRestoredBounds();
67   // First show the widget. A user shouldn't be able to initiate fullscreen
68   // unless the window is visible in the first place.
69   widget_->Show();
71   // Simulate a user-initiated fullscreen. Note trying to to this again before
72   // spinning a runloop will cause Cocoa to emit text to stdio and ignore it.
73   [test_window() toggleFullScreen:nil];
74   EXPECT_TRUE(widget_->IsFullscreen());
75   EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
77   // Note there's now an animation running. While that's happening, toggling the
78   // state should work as expected, but do "nothing".
79   widget_->SetFullscreen(false);
80   EXPECT_FALSE(widget_->IsFullscreen());
81   EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
82   widget_->SetFullscreen(false);  // Same request - should no-op.
83   EXPECT_FALSE(widget_->IsFullscreen());
84   EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
86   widget_->SetFullscreen(true);
87   EXPECT_TRUE(widget_->IsFullscreen());
88   EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
90   // Always finish out of fullscreen. Otherwise there are 4 NSWindow objects
91   // that Cocoa creates which don't close themselves and will be seen by the Mac
92   // test harness on teardown. Note that the test harness will be waiting until
93   // all animations complete, since these temporary animation windows will not
94   // be removed from the window list until they do.
95   widget_->SetFullscreen(false);
96   EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
98   // Now we must wait for the notifications. Since, if the widget is torn down,
99   // the NSWindowDelegate is removed, and the pending request to take out of
100   // fullscreen is lost. Since a message loop has not yet spun up in this test
101   // we can reliably say there will be one enter and one exit, despite all the
102   // toggling above.
103   [waiter waitForEnterCount:1 exitCount:1];
104   EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
107 // Test fullscreen without overlapping calls and without changing collection
108 // behavior on the test window.
109 TEST_F(BridgedNativeWidgetUITest, FullscreenEnterAndExit) {
110   base::scoped_nsobject<NSWindowFullscreenNotificationWaiter> waiter(
111       [[NSWindowFullscreenNotificationWaiter alloc]
112           initWithWindow:test_window()]);
114   EXPECT_FALSE(widget_->IsFullscreen());
115   const gfx::Rect restored_bounds = widget_->GetRestoredBounds();
116   EXPECT_FALSE(restored_bounds.IsEmpty());
118   // Ensure this works without having to change collection behavior as for the
119   // test above. Also check that making a hidden widget fullscreen shows it.
120   EXPECT_FALSE(widget_->IsVisible());
121   widget_->SetFullscreen(true);
122   EXPECT_TRUE(widget_->IsVisible());
123   if (base::mac::IsOSSnowLeopard()) {
124     // On Snow Leopard, SetFullscreen() isn't implemented. But shouldn't crash.
125     EXPECT_FALSE(widget_->IsFullscreen());
126     return;
127   }
129   EXPECT_TRUE(widget_->IsFullscreen());
130   EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
132   // Should be zero until the runloop spins.
133   EXPECT_EQ(0, [waiter enterCount]);
134   [waiter waitForEnterCount:1 exitCount:0];
136   // Verify it hasn't exceeded.
137   EXPECT_EQ(1, [waiter enterCount]);
138   EXPECT_EQ(0, [waiter exitCount]);
139   EXPECT_TRUE(widget_->IsFullscreen());
140   EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
142   widget_->SetFullscreen(false);
143   EXPECT_FALSE(widget_->IsFullscreen());
144   EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
146   [waiter waitForEnterCount:1 exitCount:1];
147   EXPECT_EQ(1, [waiter enterCount]);
148   EXPECT_EQ(1, [waiter exitCount]);
149   EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
152 // Test that Widget::Restore exits fullscreen.
153 TEST_F(BridgedNativeWidgetUITest, FullscreenRestore) {
154   if (base::mac::IsOSSnowLeopard())
155     return;
157   base::scoped_nsobject<NSWindowFullscreenNotificationWaiter> waiter(
158       [[NSWindowFullscreenNotificationWaiter alloc]
159           initWithWindow:test_window()]);
161   EXPECT_FALSE(widget_->IsFullscreen());
162   const gfx::Rect restored_bounds = widget_->GetRestoredBounds();
163   EXPECT_FALSE(restored_bounds.IsEmpty());
165   widget_->SetFullscreen(true);
166   EXPECT_TRUE(widget_->IsFullscreen());
167   [waiter waitForEnterCount:1 exitCount:0];
169   widget_->Restore();
170   EXPECT_FALSE(widget_->IsFullscreen());
171   [waiter waitForEnterCount:1 exitCount:1];
172   EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
175 namespace {
177 // This is used to wait for reposted events to be seen. We can't just use
178 // RunPendingMessages() because CGEventPost might not be synchronous.
179 class HitTestBridgedNativeWidget : public BridgedNativeWidget {
180  public:
181   explicit HitTestBridgedNativeWidget(NativeWidgetMac* widget)
182       : BridgedNativeWidget(widget) {}
184   // BridgedNativeWidget:
185   bool ShouldRepostPendingLeftMouseDown(NSPoint location_in_window) override {
186     bool draggable_before = [ns_view() mouseDownCanMoveWindow];
187     bool should_repost = BridgedNativeWidget::ShouldRepostPendingLeftMouseDown(
188         location_in_window);
189     bool draggable_after = [ns_view() mouseDownCanMoveWindow];
191     if (run_loop_ && draggable_before && !draggable_after)
192       run_loop_->Quit();
194     return should_repost;
195   }
197   void WaitForIsDraggableChange() {
198     base::RunLoop run_loop;
199     run_loop_ = &run_loop;
200     run_loop.Run();
201     run_loop_ = nullptr;
202   }
204  private:
205   base::RunLoop* run_loop_;
207   DISALLOW_COPY_AND_ASSIGN(HitTestBridgedNativeWidget);
210 // This is used to return a customized result to NonClientHitTest.
211 class HitTestNonClientFrameView : public NativeFrameView {
212  public:
213   explicit HitTestNonClientFrameView(Widget* widget)
214       : NativeFrameView(widget), hit_test_result_(HTNOWHERE) {}
216   // NonClientFrameView overrides:
217   int NonClientHitTest(const gfx::Point& point) override {
218     return hit_test_result_;
219   }
221   void set_hit_test_result(int component) { hit_test_result_ = component; }
223  private:
224   int hit_test_result_;
226   DISALLOW_COPY_AND_ASSIGN(HitTestNonClientFrameView);
229 void WaitForEvent(NSUInteger mask) {
230   // Pointer because the handler block captures local variables by copying.
231   base::RunLoop run_loop;
232   base::RunLoop* run_loop_ref = &run_loop;
233   id monitor = [NSEvent
234       addLocalMonitorForEventsMatchingMask:mask
235                                    handler:^NSEvent*(NSEvent* ns_event) {
236                                      run_loop_ref->Quit();
237                                      return ns_event;
238                                    }];
239   run_loop.Run();
240   [NSEvent removeMonitor:monitor];
243 }  // namespace
245 // This is used to inject test versions of NativeFrameView and
246 // BridgedNativeWidget.
247 class HitTestNativeWidgetMac : public NativeWidgetMac {
248  public:
249   HitTestNativeWidgetMac(internal::NativeWidgetDelegate* delegate,
250                          NativeFrameView* native_frame_view)
251       : NativeWidgetMac(delegate), native_frame_view_(native_frame_view) {
252     NativeWidgetMac::bridge_.reset(new HitTestBridgedNativeWidget(this));
253   }
255   HitTestBridgedNativeWidget* bridge() {
256     return static_cast<HitTestBridgedNativeWidget*>(
257         NativeWidgetMac::bridge_.get());
258   }
260   // internal::NativeWidgetPrivate:
261   NonClientFrameView* CreateNonClientFrameView() override {
262     return native_frame_view_;
263   }
265  private:
266   // Owned by Widget.
267   NativeFrameView* native_frame_view_;
269   DISALLOW_COPY_AND_ASSIGN(HitTestNativeWidgetMac);
272 TEST_F(BridgedNativeWidgetUITest, HitTest) {
273   Widget widget;
274   HitTestNonClientFrameView* frame_view =
275       new HitTestNonClientFrameView(&widget);
276   test::HitTestNativeWidgetMac* native_widget =
277       new test::HitTestNativeWidgetMac(&widget, frame_view);
278   Widget::InitParams init_params =
279       CreateParams(Widget::InitParams::TYPE_WINDOW);
280   init_params.native_widget = native_widget;
281   init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
282   init_params.bounds = gfx::Rect(100, 200, 400, 300);
283   widget.Init(init_params);
284   widget.Show();
286   // Dragging the window should work.
287   frame_view->set_hit_test_result(HTCAPTION);
288   {
289     EXPECT_EQ(100, [widget.GetNativeWindow() frame].origin.x);
291     NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
292         NSMakePoint(10, 10), widget.GetNativeWindow());
293     CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]);
294     native_widget->bridge()->WaitForIsDraggableChange();
296     base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer(
297         [[WindowedNSNotificationObserver alloc]
298             initForNotification:NSWindowDidMoveNotification]);
299     NSEvent* mouse_drag = cocoa_test_event_utils::MouseEventAtPointInWindow(
300         NSMakePoint(110, 110), NSLeftMouseDragged, widget.GetNativeWindow(), 0);
301     CGEventPost(kCGSessionEventTap, [mouse_drag CGEvent]);
302     [ns_observer wait];
303     EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
305     NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow(
306         NSMakePoint(10, 10), NSLeftMouseUp, widget.GetNativeWindow(), 0);
307     CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]);
308     WaitForEvent(NSLeftMouseUpMask);
309     EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
310   }
312   // Mouse-downs on the window controls should not be intercepted.
313   {
314     EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
316     base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer(
317         [[WindowedNSNotificationObserver alloc]
318             initForNotification:NSWindowDidMiniaturizeNotification]);
320     // Position this on the minimize button.
321     NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
322         NSMakePoint(30, 290), widget.GetNativeWindow());
323     CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]);
325     NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow(
326         NSMakePoint(30, 290), NSLeftMouseUp, widget.GetNativeWindow(), 0);
327     EXPECT_FALSE([widget.GetNativeWindow() isMiniaturized]);
328     CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]);
329     [ns_observer wait];
330     EXPECT_TRUE([widget.GetNativeWindow() isMiniaturized]);
331     [widget.GetNativeWindow() deminiaturize:nil];
333     // Position unchanged.
334     EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
335   }
337   // Non-draggable areas should do nothing.
338   frame_view->set_hit_test_result(HTCLIENT);
339   {
340     EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
342     NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
343         NSMakePoint(10, 10), widget.GetNativeWindow());
344     CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]);
345     WaitForEvent(NSLeftMouseDownMask);
347     NSEvent* mouse_drag = cocoa_test_event_utils::MouseEventAtPointInWindow(
348         NSMakePoint(110, 110), NSLeftMouseDragged, widget.GetNativeWindow(), 0);
349     CGEventPost(kCGSessionEventTap, [mouse_drag CGEvent]);
350     WaitForEvent(NSLeftMouseDraggedMask);
351     EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
353     NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow(
354         NSMakePoint(110, 110), NSLeftMouseUp, widget.GetNativeWindow(), 0);
355     CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]);
356     WaitForEvent(NSLeftMouseUpMask);
357     EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
358   }
361 }  // namespace test
362 }  // namespace views