blob: 224ea14d0f17c8ba995aa2eb1f3bba89667c2d31 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ui/views/cocoa/bridged_native_widget.h"
#import <Cocoa/Cocoa.h>
#import "base/mac/mac_util.h"
#import "base/mac/sdk_forward_declarations.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "ui/base/hit_test.h"
#import "ui/base/test/nswindow_fullscreen_notification_waiter.h"
#include "ui/base/test/ui_controls.h"
#import "ui/base/test/windowed_nsnotification_observer.h"
#import "ui/events/test/cocoa_test_event_utils.h"
#include "ui/views/test/views_interactive_ui_test_base.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/widget/native_widget_mac.h"
#include "ui/views/window/native_frame_view.h"
namespace views {
namespace test {
namespace {
// Provide a resizable Widget by default. Starting in 10.11, OSX doesn't
// correctly restore the window size when coming out of fullscreen if the window
// is not user-sizable.
class ResizableDelegateView : public WidgetDelegateView {
public:
ResizableDelegateView() {}
// WidgetDelgate:
bool CanResize() const override { return true; }
private:
DISALLOW_COPY_AND_ASSIGN(ResizableDelegateView);
};
} // namespace
class BridgedNativeWidgetUITest : public test::WidgetTest {
public:
BridgedNativeWidgetUITest() = default;
// testing::Test:
void SetUp() override {
ViewsInteractiveUITestBase::InteractiveSetUp();
WidgetTest::SetUp();
Widget::InitParams init_params =
CreateParams(Widget::InitParams::TYPE_WINDOW);
init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
init_params.bounds = gfx::Rect(100, 100, 300, 200);
init_params.delegate = new ResizableDelegateView;
widget_.reset(new Widget);
widget_->Init(init_params);
}
void TearDown() override {
// Ensures any compositor is removed before ViewsTestBase tears down the
// ContextFactory.
widget_.reset();
WidgetTest::TearDown();
}
NSWindow* test_window() {
return widget_->GetNativeWindow();
}
protected:
std::unique_ptr<Widget> widget_;
private:
DISALLOW_COPY_AND_ASSIGN(BridgedNativeWidgetUITest);
};
// Tests for correct fullscreen tracking, regardless of whether it is initiated
// by the Widget code or elsewhere (e.g. by the user).
TEST_F(BridgedNativeWidgetUITest, FullscreenSynchronousState) {
EXPECT_FALSE(widget_->IsFullscreen());
// Allow user-initiated fullscreen changes on the Window.
[test_window()
setCollectionBehavior:[test_window() collectionBehavior] |
NSWindowCollectionBehaviorFullScreenPrimary];
base::scoped_nsobject<NSWindowFullscreenNotificationWaiter> waiter(
[[NSWindowFullscreenNotificationWaiter alloc]
initWithWindow:test_window()]);
const gfx::Rect restored_bounds = widget_->GetRestoredBounds();
// First show the widget. A user shouldn't be able to initiate fullscreen
// unless the window is visible in the first place.
widget_->Show();
// Simulate a user-initiated fullscreen. Note trying to to this again before
// spinning a runloop will cause Cocoa to emit text to stdio and ignore it.
[test_window() toggleFullScreen:nil];
EXPECT_TRUE(widget_->IsFullscreen());
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
// Note there's now an animation running. While that's happening, toggling the
// state should work as expected, but do "nothing".
widget_->SetFullscreen(false);
EXPECT_FALSE(widget_->IsFullscreen());
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
widget_->SetFullscreen(false); // Same request - should no-op.
EXPECT_FALSE(widget_->IsFullscreen());
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
widget_->SetFullscreen(true);
EXPECT_TRUE(widget_->IsFullscreen());
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
// Always finish out of fullscreen. Otherwise there are 4 NSWindow objects
// that Cocoa creates which don't close themselves and will be seen by the Mac
// test harness on teardown. Note that the test harness will be waiting until
// all animations complete, since these temporary animation windows will not
// be removed from the window list until they do.
widget_->SetFullscreen(false);
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
// Now we must wait for the notifications. Since, if the widget is torn down,
// the NSWindowDelegate is removed, and the pending request to take out of
// fullscreen is lost. Since a message loop has not yet spun up in this test
// we can reliably say there will be one enter and one exit, despite all the
// toggling above.
[waiter waitForEnterCount:1 exitCount:1];
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
}
// Test fullscreen without overlapping calls and without changing collection
// behavior on the test window.
TEST_F(BridgedNativeWidgetUITest, FullscreenEnterAndExit) {
base::scoped_nsobject<NSWindowFullscreenNotificationWaiter> waiter(
[[NSWindowFullscreenNotificationWaiter alloc]
initWithWindow:test_window()]);
EXPECT_FALSE(widget_->IsFullscreen());
const gfx::Rect restored_bounds = widget_->GetRestoredBounds();
EXPECT_FALSE(restored_bounds.IsEmpty());
// Ensure this works without having to change collection behavior as for the
// test above. Also check that making a hidden widget fullscreen shows it.
EXPECT_FALSE(widget_->IsVisible());
widget_->SetFullscreen(true);
EXPECT_TRUE(widget_->IsVisible());
EXPECT_TRUE(widget_->IsFullscreen());
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
// Should be zero until the runloop spins.
EXPECT_EQ(0, [waiter enterCount]);
[waiter waitForEnterCount:1 exitCount:0];
// Verify it hasn't exceeded.
EXPECT_EQ(1, [waiter enterCount]);
EXPECT_EQ(0, [waiter exitCount]);
EXPECT_TRUE(widget_->IsFullscreen());
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
widget_->SetFullscreen(false);
EXPECT_FALSE(widget_->IsFullscreen());
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
[waiter waitForEnterCount:1 exitCount:1];
EXPECT_EQ(1, [waiter enterCount]);
EXPECT_EQ(1, [waiter exitCount]);
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
}
// Test that Widget::Restore exits fullscreen.
TEST_F(BridgedNativeWidgetUITest, FullscreenRestore) {
base::scoped_nsobject<NSWindowFullscreenNotificationWaiter> waiter(
[[NSWindowFullscreenNotificationWaiter alloc]
initWithWindow:test_window()]);
EXPECT_FALSE(widget_->IsFullscreen());
const gfx::Rect restored_bounds = widget_->GetRestoredBounds();
EXPECT_FALSE(restored_bounds.IsEmpty());
widget_->SetFullscreen(true);
EXPECT_TRUE(widget_->IsFullscreen());
[waiter waitForEnterCount:1 exitCount:0];
widget_->Restore();
EXPECT_FALSE(widget_->IsFullscreen());
[waiter waitForEnterCount:1 exitCount:1];
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
}
namespace {
// This is used to wait for reposted events to be seen. We can't just use
// RunPendingMessages() because CGEventPost might not be synchronous.
class HitTestBridgedNativeWidget : public BridgedNativeWidget {
public:
explicit HitTestBridgedNativeWidget(NativeWidgetMac* widget)
: BridgedNativeWidget(widget) {}
// BridgedNativeWidget:
bool ShouldRepostPendingLeftMouseDown(NSEvent* event) override {
did_repost_ = BridgedNativeWidget::ShouldRepostPendingLeftMouseDown(event);
if (run_loop_)
run_loop_->Quit();
return did_repost_;
}
void WaitForShouldRepost() {
base::RunLoop run_loop;
run_loop_ = &run_loop;
run_loop.Run();
run_loop_ = nullptr;
}
bool IsDraggable() { return [ns_view() mouseDownCanMoveWindow]; }
bool did_repost() { return did_repost_; }
private:
base::RunLoop* run_loop_ = nullptr;
bool did_repost_ = false;
DISALLOW_COPY_AND_ASSIGN(HitTestBridgedNativeWidget);
};
// This is used to return a customized result to NonClientHitTest.
class HitTestNonClientFrameView : public NativeFrameView {
public:
explicit HitTestNonClientFrameView(Widget* widget)
: NativeFrameView(widget), hit_test_result_(HTNOWHERE) {}
// NonClientFrameView overrides:
int NonClientHitTest(const gfx::Point& point) override {
return hit_test_result_;
}
void set_hit_test_result(int component) { hit_test_result_ = component; }
private:
int hit_test_result_;
DISALLOW_COPY_AND_ASSIGN(HitTestNonClientFrameView);
};
// This is used to change whether the Widget is resizable.
class HitTestWidgetDelegate : public views::WidgetDelegate {
public:
explicit HitTestWidgetDelegate(views::Widget* widget) : widget_(widget) {}
void set_can_resize(bool can_resize) {
can_resize_ = can_resize;
widget_->OnSizeConstraintsChanged();
}
// views::WidgetDelegate:
bool CanResize() const override { return can_resize_; }
views::Widget* GetWidget() override { return widget_; }
views::Widget* GetWidget() const override { return widget_; }
private:
views::Widget* widget_;
bool can_resize_ = false;
DISALLOW_COPY_AND_ASSIGN(HitTestWidgetDelegate);
};
void WaitForEvent(NSUInteger mask) {
// Pointer because the handler block captures local variables by copying.
base::RunLoop run_loop;
base::RunLoop* run_loop_ref = &run_loop;
id monitor = [NSEvent
addLocalMonitorForEventsMatchingMask:mask
handler:^NSEvent*(NSEvent* ns_event) {
run_loop_ref->Quit();
return ns_event;
}];
run_loop.Run();
[NSEvent removeMonitor:monitor];
}
} // namespace
// This is used to inject test versions of NativeFrameView and
// BridgedNativeWidget.
class HitTestNativeWidgetMac : public NativeWidgetMac {
public:
HitTestNativeWidgetMac(internal::NativeWidgetDelegate* delegate,
NativeFrameView* native_frame_view)
: NativeWidgetMac(delegate), native_frame_view_(native_frame_view) {
NativeWidgetMac::bridge_.reset(new HitTestBridgedNativeWidget(this));
}
HitTestBridgedNativeWidget* bridge() {
return static_cast<HitTestBridgedNativeWidget*>(
NativeWidgetMac::bridge_.get());
}
// internal::NativeWidgetPrivate:
NonClientFrameView* CreateNonClientFrameView() override {
return native_frame_view_;
}
private:
// Owned by Widget.
NativeFrameView* native_frame_view_;
DISALLOW_COPY_AND_ASSIGN(HitTestNativeWidgetMac);
};
// Flaky on macOS 10.12. See http://crbug.com/767299.
TEST_F(BridgedNativeWidgetUITest, DISABLED_HitTest) {
Widget widget;
HitTestNonClientFrameView* frame_view =
new HitTestNonClientFrameView(&widget);
test::HitTestNativeWidgetMac* native_widget =
new test::HitTestNativeWidgetMac(&widget, frame_view);
HitTestWidgetDelegate* widget_delegate = new HitTestWidgetDelegate(&widget);
Widget::InitParams init_params =
CreateParams(Widget::InitParams::TYPE_WINDOW);
init_params.native_widget = native_widget;
init_params.delegate = widget_delegate;
init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
init_params.bounds = gfx::Rect(100, 200, 400, 300);
widget.Init(init_params);
WidgetActivationWaiter activation_waiter(&widget, true);
widget.Show();
activation_waiter.Wait();
// Points inside the resize area.
const NSPoint bottom_right_point = {398, 2};
const NSPoint right_of_bottom_right = {398 + 10, 2};
NSWindow* window = widget.GetNativeWindow();
EXPECT_FALSE([window ignoresMouseEvents]);
// OSX uses both the alpha value of the window and the underlying CALayer to
// decide whether to send mouse events to window, in case [window
// ignoresMouseEvent] is not explicitly initialized. Since, no frames are
// drawn during tests and the underlying CALayer has a transparent background,
// explicitly call setIgnoresMouseEvents: to ensure the window receives the
// mouse events.
[window setIgnoresMouseEvents:NO];
HitTestBridgedNativeWidget* bridge = native_widget->bridge();
const bool using_drag_event_monitor =
BridgedNativeWidget::ShouldUseDragEventMonitor();
// Dragging the window should work.
frame_view->set_hit_test_result(HTCAPTION);
{
EXPECT_EQ(100, [window frame].origin.x);
base::scoped_nsobject<WindowedNSNotificationObserver> will_move_observer(
[[WindowedNSNotificationObserver alloc]
initForNotification:NSWindowWillMoveNotification]);
NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
NSMakePoint(20, 20), window);
EXPECT_FALSE(bridge->IsDraggable());
CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]);
if (using_drag_event_monitor) {
bridge->WaitForShouldRepost();
EXPECT_TRUE(bridge->did_repost());
EXPECT_TRUE(bridge->IsDraggable());
bridge->WaitForShouldRepost();
EXPECT_FALSE(bridge->did_repost());
EXPECT_FALSE(bridge->IsDraggable());
} else {
WaitForEvent(NSLeftMouseDownMask);
}
base::scoped_nsobject<WindowedNSNotificationObserver> did_move_observer(
[[WindowedNSNotificationObserver alloc]
initForNotification:NSWindowDidMoveNotification]);
NSEvent* mouse_drag = cocoa_test_event_utils::MouseEventAtPointInWindow(
NSMakePoint(30, 30), NSLeftMouseDragged, window, 0);
CGEventPost(kCGSessionEventTap, [mouse_drag CGEvent]);
WaitForEvent(NSLeftMouseDraggedMask);
// NSWindowWillMoveNotification should have been observed by the time the
// mouse drag event is received.
EXPECT_EQ(1, [will_move_observer notificationCount]);
EXPECT_TRUE([did_move_observer wait]);
EXPECT_EQ(110, [window frame].origin.x);
NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow(
NSMakePoint(20, 20), NSLeftMouseUp, window, 0);
CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]);
WaitForEvent(NSLeftMouseUpMask);
EXPECT_EQ(110, [window frame].origin.x);
}
// Dragging in the resize area works since the widget is not resizable.
{
EXPECT_EQ(110, [window frame].origin.x);
base::scoped_nsobject<WindowedNSNotificationObserver> will_move_observer(
[[WindowedNSNotificationObserver alloc]
initForNotification:NSWindowWillMoveNotification]);
NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
bottom_right_point, window);
EXPECT_FALSE(bridge->IsDraggable());
CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]);
if (using_drag_event_monitor) {
bridge->WaitForShouldRepost();
EXPECT_TRUE(bridge->did_repost());
EXPECT_TRUE(bridge->IsDraggable());
bridge->WaitForShouldRepost();
EXPECT_FALSE(bridge->did_repost());
EXPECT_FALSE(bridge->IsDraggable());
} else {
WaitForEvent(NSLeftMouseDownMask);
}
base::scoped_nsobject<WindowedNSNotificationObserver> did_move_observer(
[[WindowedNSNotificationObserver alloc]
initForNotification:NSWindowDidMoveNotification]);
NSEvent* mouse_drag = cocoa_test_event_utils::MouseEventAtPointInWindow(
right_of_bottom_right, NSLeftMouseDragged, window, 0);
CGEventPost(kCGSessionEventTap, [mouse_drag CGEvent]);
WaitForEvent(NSLeftMouseDraggedMask);
EXPECT_EQ(1, [will_move_observer notificationCount]);
EXPECT_TRUE([did_move_observer wait]);
EXPECT_EQ(120, [window frame].origin.x);
NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow(
bottom_right_point, NSLeftMouseUp, window, 0);
CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]);
WaitForEvent(NSLeftMouseUpMask);
EXPECT_EQ(120, [window frame].origin.x);
}
// If the widget is resizable, dragging in the resize area should not repost
// (and should resize).
widget_delegate->set_can_resize(true);
{
EXPECT_EQ(400, [window frame].size.width);
NSUInteger x = [window frame].origin.x;
NSUInteger y = [window frame].origin.y;
// Enqueue all mouse events first because AppKit will run its own loop to
// consume them.
base::scoped_nsobject<WindowedNSNotificationObserver> will_move_observer(
[[WindowedNSNotificationObserver alloc]
initForNotification:NSWindowWillMoveNotification]);
NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
bottom_right_point, window);
EXPECT_FALSE(bridge->IsDraggable());
CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]);
base::scoped_nsobject<WindowedNSNotificationObserver> did_resize_observer(
[[WindowedNSNotificationObserver alloc]
initForNotification:NSWindowDidResizeNotification]);
NSEvent* mouse_drag = cocoa_test_event_utils::MouseEventAtPoint(
NSMakePoint(x + 408, y + 2), NSLeftMouseDragged, 0);
CGEventPost(kCGSessionEventTap, [mouse_drag CGEvent]);
NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPoint(
NSMakePoint(x + 408, y + 2), NSLeftMouseUp, 0);
CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]);
if (using_drag_event_monitor) {
// The only event observed by us is the original mouse-down. It should not
// be reposted.
bridge->WaitForShouldRepost();
EXPECT_FALSE(bridge->did_repost());
EXPECT_FALSE(bridge->IsDraggable());
}
EXPECT_TRUE([did_resize_observer wait]);
EXPECT_EQ(0, [will_move_observer notificationCount]);
EXPECT_EQ(410, [window frame].size.width);
// Origin is unchanged.
EXPECT_EQ(x, [window frame].origin.x);
EXPECT_EQ(y, [window frame].origin.y);
}
// Mouse-downs on the window controls should not be intercepted.
{
EXPECT_EQ(120, [window frame].origin.x);
base::scoped_nsobject<WindowedNSNotificationObserver> will_move_observer(
[[WindowedNSNotificationObserver alloc]
initForNotification:NSWindowWillMoveNotification]);
base::scoped_nsobject<WindowedNSNotificationObserver>
did_miniaturize_observer([[WindowedNSNotificationObserver alloc]
initForNotification:NSWindowDidMiniaturizeNotification]);
// Position this on the minimize button.
NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
NSMakePoint(30, 290), window);
CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]);
NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow(
NSMakePoint(30, 290), NSLeftMouseUp, window, 0);
EXPECT_FALSE([window isMiniaturized]);
CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]);
[did_miniaturize_observer wait];
EXPECT_EQ(0, [will_move_observer notificationCount]);
EXPECT_TRUE([window isMiniaturized]);
[window deminiaturize:nil];
// Position unchanged.
EXPECT_EQ(120, [window frame].origin.x);
}
// Non-draggable areas should do nothing.
frame_view->set_hit_test_result(HTCLIENT);
{
EXPECT_EQ(120, [window frame].origin.x);
base::scoped_nsobject<WindowedNSNotificationObserver> will_move_observer(
[[WindowedNSNotificationObserver alloc]
initForNotification:NSWindowWillMoveNotification]);
NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
NSMakePoint(20, 20), window);
CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]);
WaitForEvent(NSLeftMouseDownMask);
NSEvent* mouse_drag = cocoa_test_event_utils::MouseEventAtPointInWindow(
NSMakePoint(30, 30), NSLeftMouseDragged, window, 0);
CGEventPost(kCGSessionEventTap, [mouse_drag CGEvent]);
WaitForEvent(NSLeftMouseDraggedMask);
EXPECT_EQ(0, [will_move_observer notificationCount]);
EXPECT_EQ(120, [window frame].origin.x);
NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow(
NSMakePoint(30, 30), NSLeftMouseUp, window, 0);
CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]);
WaitForEvent(NSLeftMouseUpMask);
EXPECT_EQ(120, [window frame].origin.x);
}
}
} // namespace test
} // namespace views