// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#import "base/apple/foundation_util.h"
#include "base/apple/scoped_objc_class_swizzler.h"
#import "base/mac/mac_util.h"
#import "base/task/single_thread_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
#import "content/app_shim_remote_cocoa/web_contents_occlusion_checker_mac.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
using remote_cocoa::mojom::DraggingInfo;
using remote_cocoa::mojom::DraggingInfoPtr;
using remote_cocoa::mojom::SelectionDirection;
using content::DropData;
namespace {
const int kNeverCalled = -100;
struct FeatureState {
bool enhanced_occlusion_detection_enabled = false;
};
struct Version {
int packed_version;
bool supported;
};
} // namespace
// An NSWindow subclass that enables programmatic setting of macOS occlusion and
// miniaturize states.
@interface WebContentsHostWindowForOcclusionTesting : NSWindow {
BOOL _miniaturizedForTesting;
}
@property(assign, nonatomic) BOOL occludedForTesting;
@property(assign, nonatomic) BOOL modifyingChildWindowList;
@end
@implementation WebContentsHostWindowForOcclusionTesting
@synthesize occludedForTesting = _occludedForTesting;
@synthesize modifyingChildWindowList = _modifyingChildWindowList;
- (NSWindowOcclusionState)occlusionState {
return _occludedForTesting ? 0 : NSWindowOcclusionStateVisible;
}
- (void)miniaturize:(id)sender {
// Miniaturizing a window doesn't immediately take effect (isMiniaturized
// returns false) so fake it with a flag and removal from window list.
_miniaturizedForTesting = YES;
[self orderOut:nil];
}
- (void)deminiaturize:(id)sender {
_miniaturizedForTesting = NO;
[self orderFront:nil];
}
- (BOOL)isMiniaturized {
return _miniaturizedForTesting;
}
- (void)addChildWindow:(NSWindow*)childWindow
ordered:(NSWindowOrderingMode)place {
_modifyingChildWindowList = YES;
[super addChildWindow:childWindow ordered:place];
_modifyingChildWindowList = NO;
}
- (void)removeChildWindow:(NSWindow*)childWindow {
_modifyingChildWindowList = YES;
[super removeChildWindow:childWindow];
_modifyingChildWindowList = NO;
}
@end
@interface WebContentsViewCocoaForOcclusionTesting : WebContentsViewCocoa
@end
@implementation WebContentsViewCocoaForOcclusionTesting
- (void)updateWebContentsVisibility:
(remote_cocoa::mojom::Visibility)windowVisibility {
WebContentsHostWindowForOcclusionTesting* hostWindow =
base::apple::ObjCCast<WebContentsHostWindowForOcclusionTesting>(
[self window]);
EXPECT_FALSE([hostWindow modifyingChildWindowList]);
[super updateWebContentsVisibility:windowVisibility];
}
@end
// A class that waits for invocations of the private
// -performOcclusionStateUpdates method in
// WebContentsOcclusionCheckerMac to complete.
@interface WebContentVisibilityUpdateWatcher : NSObject
@end
@implementation WebContentVisibilityUpdateWatcher
+ (std::unique_ptr<base::apple::ScopedObjCClassSwizzler>&)
performOcclusionStateUpdatesSwizzler {
// The swizzler needs to be generally available (i.e. not stored in an
// instance variable) because we want to call the original
// -performOcclusionStateUpdates from the swapped-in version
// defined below. At the point where the swapped-in version is
// called, the callee is an instance of WebContentsOcclusionCheckerMac,
// not WebContentVisibilityUpdateWatcher, so it has no access to any
// instance variables we define for WebContentVisibilityUpdateWatcher.
// Storing the swizzler in a static makes it available to any caller.
static base::NoDestructor<
std::unique_ptr<base::apple::ScopedObjCClassSwizzler>>
performOcclusionStateUpdatesSwizzler;
return *performOcclusionStateUpdatesSwizzler;
}
+ (std::unique_ptr<base::apple::ScopedObjCClassSwizzler>&)
setWebContentsOccludedSwizzler {
static base::NoDestructor<
std::unique_ptr<base::apple::ScopedObjCClassSwizzler>>
setWebContentsOccludedSwizzler;
return *setWebContentsOccludedSwizzler;
}
// A global place to stash the runLoop.
+ (base::RunLoop**)runLoop {
static base::RunLoop* runLoop = nullptr;
return &runLoop;
}
- (instancetype)init {
self = [super init];
// The tests should access WebContentsOcclusionCheckerMac directly, rather
// than through NSClassFromString(). See crbug.com/1450724 .
[WebContentVisibilityUpdateWatcher performOcclusionStateUpdatesSwizzler] =
std::make_unique<base::apple::ScopedObjCClassSwizzler>(
NSClassFromString(@"WebContentsOcclusionCheckerMac"),
[WebContentVisibilityUpdateWatcher class],
@selector(performOcclusionStateUpdates));
[WebContentVisibilityUpdateWatcher setWebContentsOccludedSwizzler] =
std::make_unique<base::apple::ScopedObjCClassSwizzler>(
NSClassFromString(@"WebContentsViewCocoa"),
[WebContentVisibilityUpdateWatcher class],
@selector(performDelayedSetWebContentsOccluded));
return self;
}
- (void)dealloc {
[WebContentVisibilityUpdateWatcher performOcclusionStateUpdatesSwizzler]
.reset();
[WebContentVisibilityUpdateWatcher setWebContentsOccludedSwizzler].reset();
}
- (void)waitForOcclusionUpdate:(NSTimeInterval)delayInMilliseconds {
// -performOcclusionStateUpdates is invoked by
// -performSelector:afterDelay: which means it will only get called after
// a turn of the run loop. So, we don't have to worry that it might have
// already been called, which would block us here until the test timed out.
base::RunLoop runLoop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, runLoop.QuitClosure(),
base::Milliseconds(delayInMilliseconds));
(*[WebContentVisibilityUpdateWatcher runLoop]) = &runLoop;
runLoop.Run();
(*[WebContentVisibilityUpdateWatcher runLoop]) = nullptr;
}
- (void)performOcclusionStateUpdates {
// Proceed with the notification.
[WebContentVisibilityUpdateWatcher performOcclusionStateUpdatesSwizzler]
->InvokeOriginal<void>(self, @selector(performOcclusionStateUpdates));
if (*[WebContentVisibilityUpdateWatcher runLoop]) {
(*[WebContentVisibilityUpdateWatcher runLoop])->Quit();
}
}
- (void)performDelayedSetWebContentsOccluded {
// Proceed with the notification.
[WebContentVisibilityUpdateWatcher setWebContentsOccludedSwizzler]
->InvokeOriginal<void>(self,
@selector(performDelayedSetWebContentsOccluded));
if (*[WebContentVisibilityUpdateWatcher runLoop]) {
(*[WebContentVisibilityUpdateWatcher runLoop])->Quit();
}
}
@end
// A class that counts invocations of the public
// -scheduleOcclusionStateUpdates method in WebContentsOcclusionCheckerMac.
@interface WebContentVisibilityUpdateCounter : NSObject
@end
@implementation WebContentVisibilityUpdateCounter
+ (std::unique_ptr<base::apple::ScopedObjCClassSwizzler>&)swizzler {
static base::NoDestructor<
std::unique_ptr<base::apple::ScopedObjCClassSwizzler>>
swizzler;
return *swizzler;
}
+ (NSInteger&)methodInvocationCount {
static NSInteger invocationCount = 0;
return invocationCount;
}
+ (BOOL)methodNeverCalled {
return
[WebContentVisibilityUpdateCounter methodInvocationCount] == kNeverCalled;
}
- (instancetype)init {
self = [super init];
// Set up the swizzling.
[WebContentVisibilityUpdateCounter swizzler] =
std::make_unique<base::apple::ScopedObjCClassSwizzler>(
NSClassFromString(@"WebContentsOcclusionCheckerMac"),
[WebContentVisibilityUpdateCounter class],
@selector(scheduleOcclusionStateUpdates));
[WebContentVisibilityUpdateCounter methodInvocationCount] = kNeverCalled;
return self;
}
- (void)dealloc {
[WebContentVisibilityUpdateCounter methodInvocationCount] = 0;
}
- (void)scheduleOcclusionStateUpdates {
// Proceed with the scheduling.
[WebContentVisibilityUpdateCounter swizzler]->InvokeOriginal<void>(
self, @selector(scheduleOcclusionStateUpdates));
NSInteger count = [WebContentVisibilityUpdateCounter methodInvocationCount];
if (count < 0) {
count = 0;
}
[WebContentVisibilityUpdateCounter methodInvocationCount] = count + 1;
}
@end
namespace content {
// A stub class for WebContentsNSViewHost.
class WebContentsNSViewHostStub
: public remote_cocoa::mojom::WebContentsNSViewHost {
public:
WebContentsNSViewHostStub() = default;
void OnMouseEvent(std::unique_ptr<ui::Event> event) override {}
void OnBecameFirstResponder(SelectionDirection direction) override {}
void OnWindowVisibilityChanged(
remote_cocoa::mojom::Visibility visibility) override {
_visibility = visibility;
}
remote_cocoa::mojom::Visibility WebContentsVisibility() {
return _visibility;
}
void SetDropData(const ::content::DropData& drop_data) override {}
bool DraggingEntered(DraggingInfoPtr dragging_info,
uint32_t* out_result) override {
return false;
}
void DraggingEntered(DraggingInfoPtr dragging_info,
DraggingEnteredCallback callback) override {}
void DraggingExited() override {}
void DraggingUpdated(DraggingInfoPtr dragging_info,
DraggingUpdatedCallback callback) override {}
bool PerformDragOperation(DraggingInfoPtr dragging_info,
bool* out_result) override {
return false;
}
void PerformDragOperation(DraggingInfoPtr dragging_info,
PerformDragOperationCallback callback) override {}
bool DragPromisedFileTo(const ::base::FilePath& file_path,
const ::content::DropData& drop_data,
const ::GURL& download_url,
const ::url::Origin& source_origin,
::base::FilePath* out_file_path) override {
return false;
}
void DragPromisedFileTo(const ::base::FilePath& file_path,
const ::content::DropData& drop_data,
const ::GURL& download_url,
const ::url::Origin& source_origin,
DragPromisedFileToCallback callback) override {}
void EndDrag(uint32_t drag_operation,
const ::gfx::PointF& local_point,
const ::gfx::PointF& screen_point) override {}
private:
remote_cocoa::mojom::Visibility _visibility;
};
// Sets up occlusion tests.
class WindowOcclusionBrowserTestMac
: public ::testing::WithParamInterface<FeatureState>,
public ContentBrowserTest {
public:
WindowOcclusionBrowserTestMac() {
if (GetParam().enhanced_occlusion_detection_enabled) {
base::FieldTrialParams params;
params["EnhancedWindowOcclusionDetection"] = "true";
features_.InitAndEnableFeatureWithParameters(
features::kMacWebContentsOcclusion, params);
} else {
features_.InitAndDisableFeature(features::kMacWebContentsOcclusion);
}
}
void SetUp() override {
if (![NSClassFromString(@"WebContentsOcclusionCheckerMac")
manualOcclusionDetectionSupportedForCurrentMacOSVersion]) {
GTEST_SKIP()
<< "Manual window occlusion detection is broken on macOS 13.0-13.2.";
}
ContentBrowserTest::SetUp();
}
~WindowOcclusionBrowserTestMac() override {
[NSClassFromString(@"WebContentsOcclusionCheckerMac")
resetSharedInstanceForTesting];
}
bool WebContentsAwaitingUpdates() {
NSMutableArray<WebContentsViewCocoa*>* allWebContentsViewCocoa =
[NSMutableArray array];
[allWebContentsViewCocoa
addObjectsFromArray:[window_a_ webContentsViewCocoa]];
[allWebContentsViewCocoa
addObjectsFromArray:[window_b_ webContentsViewCocoa]];
// Add these explicitly, in case they've been removed from their host
// windows.
if (window_a_web_contents_view_cocoa_ &&
![allWebContentsViewCocoa
containsObject:window_a_web_contents_view_cocoa_]) {
[allWebContentsViewCocoa addObject:window_a_web_contents_view_cocoa_];
}
if (window_b_web_contents_view_cocoa_ &&
![allWebContentsViewCocoa
containsObject:window_b_web_contents_view_cocoa_]) {
[allWebContentsViewCocoa addObject:window_b_web_contents_view_cocoa_];
}
for (WebContentsViewCocoa* webContentsViewCocoa in
allWebContentsViewCocoa) {
if ([webContentsViewCocoa
willSetWebContentsOccludedAfterDelayForTesting]) {
return true;
}
}
return false;
}
void WaitForOcclusionUpdate() {
if (!base::FeatureList::IsEnabled(features::kMacWebContentsOcclusion))
return;
while ([[NSClassFromString(@"WebContentsOcclusionCheckerMac")
sharedInstance] occlusionStateUpdatesAreScheduledForTesting] ||
WebContentsAwaitingUpdates()) {
WebContentVisibilityUpdateWatcher* watcher =
[[WebContentVisibilityUpdateWatcher alloc] init];
[watcher waitForOcclusionUpdate:1200];
}
}
struct WindowAndWebContents {
WebContentsHostWindowForOcclusionTesting* __strong window;
WebContentsViewCocoaForOcclusionTesting* __strong web_contents_view;
};
static WindowAndWebContents MakeWindowAndWebContents(
NSRect contentRect,
NSWindowStyleMask styleMask = NSWindowStyleMaskClosable) {
WebContentsHostWindowForOcclusionTesting* window =
[[WebContentsHostWindowForOcclusionTesting alloc]
initWithContentRect:contentRect
styleMask:styleMask
backing:NSBackingStoreBuffered
defer:YES];
NSRect window_frame = [NSWindow frameRectForContentRect:contentRect
styleMask:styleMask];
window_frame.origin = NSMakePoint(20.0, 200.0);
[window setFrame:window_frame display:NO];
window.releasedWhenClosed = NO;
const NSRect kWebContentsFrame = NSMakeRect(0.0, 0.0, 10.0, 10.0);
WebContentsViewCocoaForOcclusionTesting* web_contents_view =
[[WebContentsViewCocoaForOcclusionTesting alloc]
initWithFrame:kWebContentsFrame];
[window.contentView addSubview:web_contents_view];
return {.window = window, .web_contents_view = web_contents_view};
}
// Creates |window_a| with a visible (i.e. unoccluded) WebContentsViewCocoa.
void InitWindowA() {
const NSRect kWindowAContentRect = NSMakeRect(0.0, 0.0, 80.0, 60.0);
WindowAndWebContents window_and_web_contents =
MakeWindowAndWebContents(kWindowAContentRect);
window_a_ = window_and_web_contents.window;
window_a_web_contents_view_cocoa_ =
window_and_web_contents.web_contents_view;
window_a_.title = @"window_a";
// Set up a fake host so we can check the occlusion status.
[window_a_web_contents_view_cocoa_ setHost:&host_a_];
// Bring the browser window onscreen.
OrderWindowFront(window_a_);
// Init visibility state.
SetWindowAWebContentsVisibility(remote_cocoa::mojom::Visibility::kVisible);
}
void InitWindowB(NSRect window_frame = NSZeroRect) {
const NSRect kWindowBContentRect = NSMakeRect(0.0, 0.0, 40.0, 40.0);
WindowAndWebContents window_and_web_contents =
MakeWindowAndWebContents(kWindowBContentRect);
window_b_ = window_and_web_contents.window;
window_b_web_contents_view_cocoa_ =
window_and_web_contents.web_contents_view;
window_b_.title = @"window_b";
if (NSIsEmptyRect(window_frame)) {
window_frame.size = [NSWindow frameRectForContentRect:kWindowBContentRect
styleMask:window_b_.styleMask]
.size;
}
[window_b_ setFrame:window_frame display:NO];
OrderWindowFront(window_b_);
}
void OrderWindowFront(NSWindow* window) {
[[maybe_unused]] WebContentVisibilityUpdateCounter* watcher;
if (!kEnhancedWindowOcclusionDetection.Get()) {
watcher = [[WebContentVisibilityUpdateCounter alloc] init];
}
[window orderFront:nil];
ASSERT_TRUE([window isVisible]);
if (kEnhancedWindowOcclusionDetection.Get()) {
WaitForOcclusionUpdate();
}
}
void OrderWindowOut(NSWindow* window) {
[window orderOut:nil];
ASSERT_FALSE(window.visible);
WaitForOcclusionUpdate();
}
void CloseWindow(NSWindow* window) {
[window close];
ASSERT_FALSE(window.visible);
WaitForOcclusionUpdate();
}
void MiniaturizeWindow(NSWindow* window) {
[window miniaturize:nil];
WaitForOcclusionUpdate();
}
void DeminiaturizeWindow(NSWindow* window) {
[window deminiaturize:nil];
WaitForOcclusionUpdate();
}
void AddSubviewOfView(NSView* subview, NSView* view) {
[view addSubview:subview];
WaitForOcclusionUpdate();
}
void SetViewHidden(NSView* view, BOOL hidden) {
view.hidden = hidden;
WaitForOcclusionUpdate();
}
void RemoveViewFromSuperview(NSView* view) {
[view removeFromSuperview];
WaitForOcclusionUpdate();
}
void PostNotification(NSString* notification_name, id object = nil) {
[NSNotificationCenter.defaultCenter postNotificationName:notification_name
object:object
userInfo:nil];
WaitForOcclusionUpdate();
}
void PostWorkspaceNotification(NSString* notification_name) {
ASSERT_TRUE(NSWorkspace.sharedWorkspace.notificationCenter);
[NSWorkspace.sharedWorkspace.notificationCenter
postNotificationName:notification_name
object:nil
userInfo:nil];
WaitForOcclusionUpdate();
}
remote_cocoa::mojom::Visibility WindowAWebContentsVisibility() {
return host_a_.WebContentsVisibility();
}
void SetWindowAWebContentsVisibility(
remote_cocoa::mojom::Visibility visibility) {
host_a_.OnWindowVisibilityChanged(visibility);
}
void TearDownInProcessBrowserTestFixture() override {
[window_a_web_contents_view_cocoa_ setHost:nullptr];
}
WebContentsHostWindowForOcclusionTesting* __strong window_a_;
WebContentsViewCocoa* __strong window_a_web_contents_view_cocoa_;
WebContentsHostWindowForOcclusionTesting* __strong window_b_;
WebContentsViewCocoa* __strong window_b_web_contents_view_cocoa_;
private:
base::test::ScopedFeatureList features_;
WebContentsNSViewHostStub host_a_;
};
using WindowOcclusionBrowserTestMacWithoutOcclusionFeature =
WindowOcclusionBrowserTestMac;
using WindowOcclusionBrowserTestMacWithOcclusionDetectionFeature =
WindowOcclusionBrowserTestMac;
// Tests that should only work without the occlusion detection feature.
INSTANTIATE_TEST_SUITE_P(NoFeature,
WindowOcclusionBrowserTestMacWithoutOcclusionFeature,
::testing::Values(FeatureState{
.enhanced_occlusion_detection_enabled = false}));
// Tests that should work with or without the occlusion detection feature.
INSTANTIATE_TEST_SUITE_P(
Common,
WindowOcclusionBrowserTestMac,
::testing::Values(
FeatureState{.enhanced_occlusion_detection_enabled = false},
FeatureState{.enhanced_occlusion_detection_enabled = true}));
// Tests that require enhanced window occlusion detection.
INSTANTIATE_TEST_SUITE_P(
EnhancedWindowOcclusionDetection,
WindowOcclusionBrowserTestMacWithOcclusionDetectionFeature,
::testing::Values(FeatureState{
.enhanced_occlusion_detection_enabled = true}));
// Tests that we correctly disallow unsupported macOS versions.
IN_PROC_BROWSER_TEST_P(WindowOcclusionBrowserTestMac, MacOSVersionChecking) {
Class WebContentsOcclusionCheckerMac =
NSClassFromString(@"WebContentsOcclusionCheckerMac");
std::vector<Version> versions = {
{11'00'00, true}, {12'00'00, true}, {12'09'00, true}, {13'00'00, false},
{13'01'00, false}, {13'02'00, false}, {13'03'00, true}, {14'00'00, true}};
for (const auto& version : versions) {
bool supported = [WebContentsOcclusionCheckerMac
manualOcclusionDetectionSupportedForPackedVersion:version
.packed_version];
EXPECT_EQ(supported, version.supported);
}
}
// Tests that enhanced occlusion detection isn't triggered if the feature's
// not enabled.
IN_PROC_BROWSER_TEST_P(WindowOcclusionBrowserTestMacWithoutOcclusionFeature,
ManualOcclusionDetectionDisabled) {
InitWindowA();
// Create a second window and place it exactly over window_a. The window
// should still be considered visible.
InitWindowB([window_a_ frame]);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
}
// Test that display sleep and app hide detection don't work if the feature's
// not enabled.
IN_PROC_BROWSER_TEST_P(WindowOcclusionBrowserTestMacWithoutOcclusionFeature,
OcclusionDetectionOnDisplaySleepDisabled) {
InitWindowA();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
// Fake a display sleep notification.
ASSERT_TRUE(NSWorkspace.sharedWorkspace.notificationCenter);
[[maybe_unused]] WebContentVisibilityUpdateCounter* watcher =
[[WebContentVisibilityUpdateCounter alloc] init];
[NSWorkspace.sharedWorkspace.notificationCenter
postNotificationName:NSWorkspaceScreensDidSleepNotification
object:nil
userInfo:nil];
EXPECT_TRUE([WebContentVisibilityUpdateCounter methodNeverCalled]);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
}
// Test that we properly handle occlusion notifications from macOS.
IN_PROC_BROWSER_TEST_P(WindowOcclusionBrowserTestMac,
MacOSOcclusionNotifications) {
InitWindowA();
[window_a_ setOccludedForTesting:YES];
PostNotification(NSWindowDidChangeOcclusionStateNotification, window_a_);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
[window_a_ setOccludedForTesting:NO];
PostNotification(NSWindowDidChangeOcclusionStateNotification, window_a_);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
}
IN_PROC_BROWSER_TEST_P(
WindowOcclusionBrowserTestMacWithOcclusionDetectionFeature,
ManualOcclusionDetection) {
InitWindowA();
// Create a second window and place it exactly over window_a. Unlike macOS,
// our manual occlusion detection will determine window_a is occluded.
InitWindowB(window_a_.frame);
WaitForOcclusionUpdate();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
// Move window_b slightly in different directions and check the occlusion
// state of window_a's web contents.
const NSSize window_offsets[] = {
{1.0, 0.0}, {-1.0, 0.0}, {0.0, 1.0}, {0.0, -1.0}};
NSRect window_b_frame = window_b_.frame;
for (auto window_offset : window_offsets) {
// Move window b so that it no longer completely covers
// window_a's webcontents.
NSRect offset_window_frame =
NSOffsetRect(window_b_frame, window_offset.width, window_offset.height);
[window_b_ setFrame:offset_window_frame display:YES];
WaitForOcclusionUpdate();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
// Move it back.
[window_b_ setFrame:window_b_frame display:YES];
WaitForOcclusionUpdate();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
}
}
// Checks manual occlusion detection as windows change display order.
IN_PROC_BROWSER_TEST_P(
WindowOcclusionBrowserTestMacWithOcclusionDetectionFeature,
ManualOcclusionDetectionOnWindowOrderChange) {
InitWindowA();
// Size and position the second window so that it exactly covers the
// first.
InitWindowB(window_a_.frame);
WaitForOcclusionUpdate();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
OrderWindowFront(window_a_);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
OrderWindowFront(window_b_);
WaitForOcclusionUpdate();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
}
// Checks that window_a, occluded by window_b, transitions to kVisible while the
// user resizes window_b.
IN_PROC_BROWSER_TEST_P(
WindowOcclusionBrowserTestMacWithOcclusionDetectionFeature,
ManualOcclusionDetectionOnWindowLiveResize) {
InitWindowA();
// Size and position the second window so that it exactly covers the
// first.
InitWindowB(window_a_.frame);
WaitForOcclusionUpdate();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
// Fake the start of a live resize. window_a's web contents should
// become kVisible because resizing window_b may expose whatever's
// behind it.
PostNotification(NSWindowWillStartLiveResizeNotification, window_b_);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
// Fake the resize end, which should return window_a to kOccluded because
// it's still completely covered by window_b.
PostNotification(NSWindowDidEndLiveResizeNotification, window_b_);
WaitForOcclusionUpdate();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
}
// Checks that window_a, occluded by window_b, transitions to kVisible when
// window_b is set to close.
IN_PROC_BROWSER_TEST_P(
WindowOcclusionBrowserTestMacWithOcclusionDetectionFeature,
ManualOcclusionDetectionOnWindowClose) {
InitWindowA();
// Size and position the second window so that it exactly covers the
// first.
InitWindowB(window_a_.frame);
WaitForOcclusionUpdate();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
// Close window b.
CloseWindow(window_b_);
// window_a's web contents should be kVisible, so that it's properly
// updated when window_b goes offscreen.
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
}
// Checks that window_a, occluded by window_b and window_c, remains kOccluded
// when window_b is set to close.
IN_PROC_BROWSER_TEST_P(
WindowOcclusionBrowserTestMacWithOcclusionDetectionFeature,
ManualOcclusionDetectionOnMiddleWindowClose) {
InitWindowA();
// Size and position the second window so that it exactly covers the
// first.
InitWindowB(window_a_.frame);
WaitForOcclusionUpdate();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
// Create a window_c on top of them both.
const NSRect kWindowCContentRect = NSMakeRect(0.0, 0.0, 80.0, 60.0);
WindowAndWebContents window_and_web_contents =
MakeWindowAndWebContents(kWindowCContentRect);
NSWindow* window_c = window_and_web_contents.window;
window_c.title = @"window_c";
// Configure it for the test.
[window_c setFrame:window_a_.frame display:NO];
OrderWindowFront(window_c);
// Close window_b.
CloseWindow(window_b_);
WaitForOcclusionUpdate();
// window_a's web contents should remain kOccluded because of window_c.
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
}
// Checks that web contents are marked kHidden on display sleep.
IN_PROC_BROWSER_TEST_P(
WindowOcclusionBrowserTestMacWithOcclusionDetectionFeature,
OcclusionDetectionOnDisplaySleep) {
InitWindowA();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
// Fake a display sleep notification.
PostWorkspaceNotification(NSWorkspaceScreensDidSleepNotification);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
// Fake a display wake notification.
PostWorkspaceNotification(NSWorkspaceScreensDidWakeNotification);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
}
// Checks that occlusion updates are ignored in between fullscreen transition
// notifications.
IN_PROC_BROWSER_TEST_P(
WindowOcclusionBrowserTestMac,
// WindowOcclusionBrowserTestMacWithOcclusionDetectionFeature,
IgnoreOcclusionUpdatesBetweenWindowFullscreenTransitionNotifications) {
InitWindowA();
[window_a_ setOccluded:NO];
[window_a_ setOccludedForTesting:NO];
// Fake a fullscreen transition notification.
PostNotification(NSWindowWillEnterFullScreenNotification, window_a_);
// An occlusion change should have no effect while in transition.
[window_a_ setOccludedForTesting:YES];
PostNotification(NSWindowDidChangeOcclusionStateNotification, window_a_);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
// End the transition.
PostNotification(NSWindowDidExitFullScreenNotification, window_a_);
PostNotification(NSWindowDidChangeOcclusionStateNotification, window_a_);
WaitForOcclusionUpdate();
// Check the web contents visibility state rather than the window's occlusion
// state because -isOccluded, added by a category, does not ever return YES
// unless manual window occlusion is enabled.
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
// Reset.
[window_a_ setOccluded:NO];
[window_a_ setOccludedForTesting:NO];
PostNotification(NSWindowDidChangeOcclusionStateNotification, window_a_);
WaitForOcclusionUpdate();
ASSERT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
// Fake the exit transition start.
PostNotification(NSWindowWillExitFullScreenNotification, window_a_);
[window_a_ setOccludedForTesting:YES];
PostNotification(NSWindowDidChangeOcclusionStateNotification, window_a_);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
// End the transition.
PostNotification(NSWindowDidExitFullScreenNotification, window_a_);
PostNotification(NSWindowDidChangeOcclusionStateNotification, window_a_);
WaitForOcclusionUpdate();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
// EXPECT_TRUE([window_a isOccluded]);
}
// Tests that each web contents in a window receives an updated occlusion
// state updated.
IN_PROC_BROWSER_TEST_P(
WindowOcclusionBrowserTestMacWithOcclusionDetectionFeature,
OcclusionDetectionForMultipleWebContents) {
InitWindowA();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
// Create a second web contents.
const NSRect kWebContentsBFrame = NSMakeRect(0.0, 0.0, 10.0, 10.0);
WebContentsViewCocoa* web_contents_b =
[[WebContentsViewCocoaForOcclusionTesting alloc]
initWithFrame:kWebContentsBFrame];
[window_a_.contentView addSubview:web_contents_b];
WebContentsNSViewHostStub host_2;
[web_contents_b setHost:&host_2];
host_2.OnWindowVisibilityChanged(remote_cocoa::mojom::Visibility::kVisible);
const NSRect kWebContentsCFrame = NSMakeRect(0.0, 20.0, 10.0, 10.0);
WebContentsViewCocoa* web_contents_c =
[[WebContentsViewCocoaForOcclusionTesting alloc]
initWithFrame:kWebContentsCFrame];
[window_a_.contentView addSubview:web_contents_c];
WebContentsNSViewHostStub host_3;
[web_contents_c setHost:&host_3];
host_3.OnWindowVisibilityChanged(remote_cocoa::mojom::Visibility::kVisible);
// Add window_b to occlude window_a and its web contentses.
InitWindowB(window_a_.frame);
WaitForOcclusionUpdate();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
EXPECT_EQ(host_2.WebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
EXPECT_EQ(host_3.WebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
// Close window b, which should expose the web contentses.
CloseWindow(window_b_);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
EXPECT_EQ(host_2.WebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
EXPECT_EQ(host_3.WebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
[web_contents_b setHost:nullptr];
[web_contents_c setHost:nullptr];
}
// Checks that web contentses are marked kHidden on WebContentsViewCocoa hide.
IN_PROC_BROWSER_TEST_P(WindowOcclusionBrowserTestMac,
OcclusionDetectionOnWebContentsViewCocoaHide) {
InitWindowA();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
SetViewHidden(window_a_web_contents_view_cocoa_, YES);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kHidden);
SetViewHidden(window_a_web_contents_view_cocoa_, NO);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
// Hiding the superview should have the same effect.
SetViewHidden(window_a_web_contents_view_cocoa_.superview, YES);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kHidden);
SetViewHidden(window_a_web_contents_view_cocoa_.superview, NO);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
}
// Checks that web contentses are marked kHidden on WebContentsViewCocoa removal
// from the view hierarchy.
IN_PROC_BROWSER_TEST_P(
WindowOcclusionBrowserTestMac,
OcclusionDetectionOnWebContentsViewCocoaRemoveFromSuperview) {
InitWindowA();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
RemoveViewFromSuperview(window_a_web_contents_view_cocoa_);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kHidden);
// Adding it back should make it visible.
AddSubviewOfView(window_a_web_contents_view_cocoa_, window_a_.contentView);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
// Try the same with its superview.
const NSRect kTmpViewFrame = NSMakeRect(0.0, 0.0, 10.0, 10.0);
NSView* tmpView = [[NSView alloc] initWithFrame:kTmpViewFrame];
[window_a_.contentView addSubview:tmpView];
AddSubviewOfView(tmpView, window_a_.contentView);
RemoveViewFromSuperview(window_a_web_contents_view_cocoa_);
AddSubviewOfView(window_a_web_contents_view_cocoa_, tmpView);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
RemoveViewFromSuperview(tmpView);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kHidden);
AddSubviewOfView(tmpView, [window_a_ contentView]);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
}
// Checks that web contentses are marked kHidden on window miniaturize.
IN_PROC_BROWSER_TEST_P(
WindowOcclusionBrowserTestMacWithOcclusionDetectionFeature,
OcclusionDetectionOnWindowMiniaturize) {
InitWindowA();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
MiniaturizeWindow(window_a_);
EXPECT_TRUE([window_a_ isMiniaturized]);
EXPECT_NE(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
DeminiaturizeWindow(window_a_);
EXPECT_FALSE([window_a_ isMiniaturized]);
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
}
// Tests that occlusion updates only occur after a child window has been
// added to or removed from a parent. In Chrome, some webcontents visibility
// watchers add child windows (bubbles) when visibility changes. We want to
// avoid the situation where a browser component adds a child window,
// triggering a visibility update, which causes a visibility watcher to add
// a second child window (while we're still inside AppKit code adding the
// first).
IN_PROC_BROWSER_TEST_P(
WindowOcclusionBrowserTestMacWithOcclusionDetectionFeature,
ChildWindowListMutationDuringManualOcclusionDetection) {
InitWindowA();
const NSRect kContentRect = NSMakeRect(0.0, 0.0, 20.0, 20.0);
WindowAndWebContents window_and_web_contents =
MakeWindowAndWebContents(kContentRect, NSWindowStyleMaskBorderless);
// Clear out any pending occlusion updates from the window creation.
WaitForOcclusionUpdate();
// Add the window with the webcontents as a child. The child window coming
// onscreen should not trigger a visibility update (at least not from us).
// A check inside the webcontents will also ensure no updates occur while
// the window modifies its child window list.
[window_a_ addChildWindow:window_and_web_contents.window
ordered:NSWindowAbove];
EXPECT_FALSE([[NSClassFromString(@"WebContentsOcclusionCheckerMac")
sharedInstance] occlusionStateUpdatesAreScheduledForTesting]);
// Modify the child window list by removing a child window.
[window_a_ removeChildWindow:window_and_web_contents.window];
EXPECT_FALSE([[NSClassFromString(@"WebContentsOcclusionCheckerMac")
sharedInstance] occlusionStateUpdatesAreScheduledForTesting]);
}
// Tests that when a window becomes a child, if the occlusion system
// previously marked it occluded, the window transitions to visible.
IN_PROC_BROWSER_TEST_P(
WindowOcclusionBrowserTestMacWithOcclusionDetectionFeature,
WindowMadeChildForcedVisible) {
InitWindowA();
// Create a second window that occludes window_a.
InitWindowB(window_a_.frame);
WaitForOcclusionUpdate();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kOccluded);
// Make window_a a child of window_b. The occlusion system ignores
// child windows, so ensure window_a's occlusion state changes back
// to visible.
[window_b_ addChildWindow:window_a_ ordered:NSWindowAbove];
WaitForOcclusionUpdate();
EXPECT_EQ(WindowAWebContentsVisibility(),
remote_cocoa::mojom::Visibility::kVisible);
}
} // namespace content