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());
154 // This is used to wait for reposted events to be seen. We can't just use
155 // RunPendingMessages() because CGEventPost might not be synchronous.
156 class HitTestBridgedNativeWidget : public BridgedNativeWidget {
158 explicit HitTestBridgedNativeWidget(NativeWidgetMac* widget)
159 : BridgedNativeWidget(widget) {}
161 // BridgedNativeWidget:
162 bool ShouldRepostPendingLeftMouseDown(NSPoint location_in_window) override {
163 bool draggable_before = [ns_view() mouseDownCanMoveWindow];
164 bool should_repost = BridgedNativeWidget::ShouldRepostPendingLeftMouseDown(
166 bool draggable_after = [ns_view() mouseDownCanMoveWindow];
168 if (run_loop_ && draggable_before && !draggable_after)
171 return should_repost;
174 void WaitForIsDraggableChange() {
175 base::RunLoop run_loop;
176 run_loop_ = &run_loop;
182 base::RunLoop* run_loop_;
184 DISALLOW_COPY_AND_ASSIGN(HitTestBridgedNativeWidget);
187 // This is used to return a customized result to NonClientHitTest.
188 class HitTestNonClientFrameView : public NativeFrameView {
190 explicit HitTestNonClientFrameView(Widget* widget)
191 : NativeFrameView(widget), hit_test_result_(HTNOWHERE) {}
193 // NonClientFrameView overrides:
194 int NonClientHitTest(const gfx::Point& point) override {
195 return hit_test_result_;
198 void set_hit_test_result(int component) { hit_test_result_ = component; }
201 int hit_test_result_;
203 DISALLOW_COPY_AND_ASSIGN(HitTestNonClientFrameView);
206 void WaitForEvent(NSUInteger mask) {
207 // Pointer because the handler block captures local variables by copying.
208 base::RunLoop run_loop;
209 base::RunLoop* run_loop_ref = &run_loop;
210 id monitor = [NSEvent
211 addLocalMonitorForEventsMatchingMask:mask
212 handler:^NSEvent*(NSEvent* ns_event) {
213 run_loop_ref->Quit();
217 [NSEvent removeMonitor:monitor];
222 // This is used to inject test versions of NativeFrameView and
223 // BridgedNativeWidget.
224 class HitTestNativeWidgetMac : public NativeWidgetMac {
226 HitTestNativeWidgetMac(internal::NativeWidgetDelegate* delegate,
227 NativeFrameView* native_frame_view)
228 : NativeWidgetMac(delegate), native_frame_view_(native_frame_view) {
229 NativeWidgetMac::bridge_.reset(new HitTestBridgedNativeWidget(this));
232 HitTestBridgedNativeWidget* bridge() {
233 return static_cast<HitTestBridgedNativeWidget*>(
234 NativeWidgetMac::bridge_.get());
237 // internal::NativeWidgetPrivate:
238 NonClientFrameView* CreateNonClientFrameView() override {
239 return native_frame_view_;
244 NativeFrameView* native_frame_view_;
246 DISALLOW_COPY_AND_ASSIGN(HitTestNativeWidgetMac);
249 TEST_F(BridgedNativeWidgetUITest, HitTest) {
251 HitTestNonClientFrameView* frame_view =
252 new HitTestNonClientFrameView(&widget);
253 test::HitTestNativeWidgetMac* native_widget =
254 new test::HitTestNativeWidgetMac(&widget, frame_view);
255 Widget::InitParams init_params =
256 CreateParams(Widget::InitParams::TYPE_WINDOW);
257 init_params.native_widget = native_widget;
258 init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
259 init_params.bounds = gfx::Rect(100, 200, 400, 300);
260 widget.Init(init_params);
263 // Dragging the window should work.
264 frame_view->set_hit_test_result(HTCAPTION);
266 EXPECT_EQ(100, [widget.GetNativeWindow() frame].origin.x);
268 NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
269 NSMakePoint(10, 10), widget.GetNativeWindow());
270 CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]);
271 native_widget->bridge()->WaitForIsDraggableChange();
273 base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer(
274 [[WindowedNSNotificationObserver alloc]
275 initForNotification:NSWindowDidMoveNotification]);
276 NSEvent* mouse_drag = cocoa_test_event_utils::MouseEventAtPointInWindow(
277 NSMakePoint(110, 110), NSLeftMouseDragged, widget.GetNativeWindow(), 0);
278 CGEventPost(kCGSessionEventTap, [mouse_drag CGEvent]);
280 EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
282 NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow(
283 NSMakePoint(10, 10), NSLeftMouseUp, widget.GetNativeWindow(), 0);
284 CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]);
285 WaitForEvent(NSLeftMouseUpMask);
286 EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
289 // Mouse-downs on the window controls should not be intercepted.
291 EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
293 base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer(
294 [[WindowedNSNotificationObserver alloc]
295 initForNotification:NSWindowDidMiniaturizeNotification]);
297 // Position this on the minimize button.
298 NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
299 NSMakePoint(30, 290), widget.GetNativeWindow());
300 CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]);
302 NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow(
303 NSMakePoint(30, 290), NSLeftMouseUp, widget.GetNativeWindow(), 0);
304 EXPECT_FALSE([widget.GetNativeWindow() isMiniaturized]);
305 CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]);
307 EXPECT_TRUE([widget.GetNativeWindow() isMiniaturized]);
308 [widget.GetNativeWindow() deminiaturize:nil];
310 // Position unchanged.
311 EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
314 // Non-draggable areas should do nothing.
315 frame_view->set_hit_test_result(HTCLIENT);
317 EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
319 NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
320 NSMakePoint(10, 10), widget.GetNativeWindow());
321 CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]);
322 WaitForEvent(NSLeftMouseDownMask);
324 NSEvent* mouse_drag = cocoa_test_event_utils::MouseEventAtPointInWindow(
325 NSMakePoint(110, 110), NSLeftMouseDragged, widget.GetNativeWindow(), 0);
326 CGEventPost(kCGSessionEventTap, [mouse_drag CGEvent]);
327 WaitForEvent(NSLeftMouseDraggedMask);
328 EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);
330 NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow(
331 NSMakePoint(110, 110), NSLeftMouseUp, widget.GetNativeWindow(), 0);
332 CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]);
333 WaitForEvent(NSLeftMouseUpMask);
334 EXPECT_EQ(200, [widget.GetNativeWindow() frame].origin.x);