| // 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/public/common/content_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 { |
| int32_t major; |
| int32_t minor; |
| 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(bool motion, bool exited) 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, |
| ::base::FilePath* out_file_path) override { |
| return false; |
| } |
| |
| void DragPromisedFileTo(const ::base::FilePath& file_path, |
| const ::content::DropData& drop_data, |
| const ::GURL& download_url, |
| 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 orderWindow:NSWindowAbove relativeTo:0]; |
| ASSERT_TRUE([window isVisible]); |
| |
| if (kEnhancedWindowOcclusionDetection.Get()) { |
| WaitForOcclusionUpdate(); |
| } |
| } |
| |
| void OrderWindowOut(NSWindow* window) { |
| [window orderWindow:NSWindowOut relativeTo:0]; |
| 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, 0, true}, {12, 0, true}, {12, 9, true}, {13, 0, false}, |
| {13, 1, false}, {13, 2, false}, {13, 3, true}, {14, 0, true}}; |
| |
| for (const auto& version : versions) { |
| bool supported = [WebContentsOcclusionCheckerMac manualOcclusionDetectionSupportedForVersion:version.major |
| :version.minor]; |
| 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 |