// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
#import <Cocoa/Cocoa.h>
#import "base/mac/mac_util.h"
#include "base/run_loop.h"
#import "ui/base/cocoa/nswindow_test_util.h"
#include "ui/base/hit_test.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/cocoa/native_widget_mac_ns_window_host.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::test {
class BridgedNativeWidgetUITest : public WidgetTest {
public:
BridgedNativeWidgetUITest() = default;
BridgedNativeWidgetUITest(const BridgedNativeWidgetUITest&) = delete;
BridgedNativeWidgetUITest& operator=(const BridgedNativeWidgetUITest&) =
delete;
// testing::Test:
void SetUp() override {
SetUpForInteractiveTests();
WidgetTest::SetUp();
widget_delegate_ = std::make_unique<WidgetDelegate>();
Widget::InitParams init_params =
CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET,
Widget::InitParams::TYPE_WINDOW);
init_params.bounds = gfx::Rect(100, 100, 300, 200);
init_params.delegate = widget_delegate_.get();
// Provide a resizable Widget by default, as macOS doesn't correctly restore
// the window size when coming out of fullscreen if the window is not
// user-sizable.
init_params.delegate->SetCanResize(true);
widget_ = std::make_unique<Widget>();
widget_->Init(std::move(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().GetNativeNSWindow();
}
protected:
std::unique_ptr<WidgetDelegate> widget_delegate_;
std::unique_ptr<Widget> widget_;
};
// 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];
ui::NSWindowFullscreenNotificationWaiter waiter(widget_->GetNativeWindow());
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. Wait only for the exit notification (the enter
// notification will be swallowed, because the exit will have been requested
// before the enter completes).
waiter.WaitForEnterAndExitCount(0, 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) {
ui::NSWindowFullscreenNotificationWaiter waiter(widget_->GetNativeWindow());
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.enter_count());
waiter.WaitForEnterAndExitCount(1, 0);
// Verify it hasn't exceeded.
EXPECT_EQ(1, waiter.enter_count());
EXPECT_EQ(0, waiter.exit_count());
EXPECT_TRUE(widget_->IsFullscreen());
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
widget_->SetFullscreen(false);
EXPECT_FALSE(widget_->IsFullscreen());
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
waiter.WaitForEnterAndExitCount(1, 1);
EXPECT_EQ(1, waiter.enter_count());
EXPECT_EQ(1, waiter.exit_count());
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
}
// Test that Widget::Restore exits fullscreen.
TEST_F(BridgedNativeWidgetUITest, FullscreenRestore) {
ui::NSWindowFullscreenNotificationWaiter waiter(widget_->GetNativeWindow());
EXPECT_FALSE(widget_->IsFullscreen());
const gfx::Rect restored_bounds = widget_->GetRestoredBounds();
EXPECT_FALSE(restored_bounds.IsEmpty());
widget_->SetFullscreen(true);
EXPECT_TRUE(widget_->IsFullscreen());
waiter.WaitForEnterAndExitCount(1, 0);
widget_->Restore();
EXPECT_FALSE(widget_->IsFullscreen());
waiter.WaitForEnterAndExitCount(1, 1);
EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds());
}
} // namespace views::test