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"
21 class BridgedNativeWidgetUITest : public test::WidgetTest {
23 BridgedNativeWidgetUITest() {}
26 void SetUp() override {
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);
35 void TearDown() override {
36 // Ensures any compositor is removed before ViewsTestBase tears down the
39 WidgetTest::TearDown();
42 NSWindow* test_window() {
43 return widget_->GetNativeWindow();
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())
57 // Allow user-initiated fullscreen changes on the 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.
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
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());
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())
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];
170 EXPECT_FALSE(widget_->IsFullscreen());
171 [waiter waitForEnterCount:1 exitCount:1];
172 EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
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 {
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(
189 bool draggable_after = [ns_view() mouseDownCanMoveWindow];
191 if (run_loop_ && draggable_before && !draggable_after)
194 return should_repost;
197 void WaitForIsDraggableChange() {
198 base::RunLoop run_loop;
199 run_loop_ = &run_loop;
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 {
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_;
221 void set_hit_test_result(int component) { hit_test_result_ = component; }
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();
240 [NSEvent removeMonitor:monitor];
245 // This is used to inject test versions of NativeFrameView and
246 // BridgedNativeWidget.
247 class HitTestNativeWidgetMac : public NativeWidgetMac {
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));
255 HitTestBridgedNativeWidget* bridge() {
256 return static_cast<HitTestBridgedNativeWidget*>(
257 NativeWidgetMac::bridge_.get());
260 // internal::NativeWidgetPrivate:
261 NonClientFrameView* CreateNonClientFrameView() override {
262 return native_frame_view_;
267 NativeFrameView* native_frame_view_;
269 DISALLOW_COPY_AND_ASSIGN(HitTestNativeWidgetMac);
272 TEST_F(BridgedNativeWidgetUITest, HitTest) {
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);
286 // Dragging the window should work.
287 frame_view->set_hit_test_result(HTCAPTION);
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]);
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);
312 // Mouse-downs on the window controls should not be intercepted.
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]);
330 EXPECT_TRUE([widget.GetNativeWindow() isMiniaturized]);
331 [widget.GetNativeWindow() deminiaturize:nil];
333 // Position unchanged.
334 EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
337 // Non-draggable areas should do nothing.
338 frame_view->set_hit_test_result(HTCLIENT);
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);