/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "gtest/gtest.h" #include "MockWinWidget.h" #include "mozilla/Preferences.h" #include "mozilla/widget/WinEventObserver.h" #include "mozilla/widget/WinWindowOcclusionTracker.h" #include "nsThreadUtils.h" #include "Units.h" #include "WinUtils.h" using namespace mozilla; using namespace mozilla::widget; #define PREF_DISPLAY_STATE \ "widget.windows.window_occlusion_tracking_display_state.enabled" #define PREF_SESSION_LOCK \ "widget.windows.window_occlusion_tracking_session_lock.enabled" class WinWindowOcclusionTrackerInteractiveTest : public ::testing::Test { protected: void SetUp() override { // Shut down WinWindowOcclusionTracker if it exists. // This could happen when WinWindowOcclusionTracker was initialized by other // gtest if (WinWindowOcclusionTracker::Get()) { WinWindowOcclusionTracker::ShutDown(); } EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get()); WinWindowOcclusionTracker::Ensure(); EXPECT_NE(nullptr, WinWindowOcclusionTracker::Get()); WinWindowOcclusionTracker::Get()->EnsureDisplayStatusObserver(); WinWindowOcclusionTracker::Get()->EnsureSessionChangeObserver(); } void TearDown() override { WinWindowOcclusionTracker::ShutDown(); EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get()); } void SetNativeWindowBounds(HWND aHWnd, const LayoutDeviceIntRect aBounds) { RECT wr; wr.left = aBounds.X(); wr.top = aBounds.Y(); wr.right = aBounds.XMost(); wr.bottom = aBounds.YMost(); ::AdjustWindowRectEx(&wr, ::GetWindowLong(aHWnd, GWL_STYLE), FALSE, ::GetWindowLong(aHWnd, GWL_EXSTYLE)); // Make sure to keep the window onscreen, as AdjustWindowRectEx() may have // moved part of it offscreen. But, if the original requested bounds are // offscreen, don't adjust the position. LayoutDeviceIntRect windowBounds(wr.left, wr.top, wr.right - wr.left, wr.bottom - wr.top); if (aBounds.X() >= 0) { windowBounds.x = std::max(0, windowBounds.X()); } if (aBounds.Y() >= 0) { windowBounds.y = std::max(0, windowBounds.Y()); } ::SetWindowPos(aHWnd, HWND_TOP, windowBounds.X(), windowBounds.Y(), windowBounds.Width(), windowBounds.Height(), SWP_NOREPOSITION); EXPECT_TRUE(::UpdateWindow(aHWnd)); } void CreateNativeWindowWithBounds(LayoutDeviceIntRect aBounds) { mMockWinWidget = MockWinWidget::Create( WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle = */ 0, aBounds); EXPECT_NE(nullptr, mMockWinWidget.get()); HWND hwnd = mMockWinWidget->GetWnd(); SetNativeWindowBounds(hwnd, aBounds); HRGN region = ::CreateRectRgn(0, 0, 0, 0); EXPECT_NE(nullptr, region); if (::GetWindowRgn(hwnd, region) == COMPLEXREGION) { // On Windows 7, the newly created window has a complex region, which // means it will be ignored during the occlusion calculation. So, force // it to have a simple region so that we get test coverage on win 7. RECT boundingRect; EXPECT_TRUE(::GetWindowRect(hwnd, &boundingRect)); HRGN rectangularRegion = ::CreateRectRgnIndirect(&boundingRect); EXPECT_NE(nullptr, rectangularRegion); ::SetWindowRgn(hwnd, rectangularRegion, /* bRedraw = */ TRUE); ::DeleteObject(rectangularRegion); } ::DeleteObject(region); ::ShowWindow(hwnd, SW_SHOWNORMAL); EXPECT_TRUE(UpdateWindow(hwnd)); } RefPtr CreateTrackedWindowWithBounds( LayoutDeviceIntRect aBounds) { RefPtr window = MockWinWidget::Create( WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle */ 0, aBounds); EXPECT_NE(nullptr, window.get()); HWND hwnd = window->GetWnd(); ::ShowWindow(hwnd, SW_SHOWNORMAL); WinWindowOcclusionTracker::Get()->Enable(window, window->GetWnd()); return window; } int GetNumVisibleRootWindows() { return WinWindowOcclusionTracker::Get()->mNumVisibleRootWindows; } void OnDisplayStateChanged(bool aDisplayOn) { WinWindowOcclusionTracker::Get()->OnDisplayStateChanged(aDisplayOn); } RefPtr mMockWinWidget; }; // Simple test completely covering a tracked window with a native window. TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleOcclusion) { RefPtr window = CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); window->SetExpectation(widget::OcclusionState::OCCLUDED); CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } EXPECT_FALSE(window->IsExpectingCall()); } // Simple test partially covering a tracked window with a native window. TEST_F(WinWindowOcclusionTrackerInteractiveTest, PartialOcclusion) { RefPtr window = CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200)); window->SetExpectation(widget::OcclusionState::VISIBLE); CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50)); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } EXPECT_FALSE(window->IsExpectingCall()); } // Simple test that a partly off screen tracked window, with the on screen part // occluded by a native window, is considered occluded. TEST_F(WinWindowOcclusionTrackerInteractiveTest, OffscreenOcclusion) { RefPtr window = CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); // Move the tracked window 50 pixels offscreen to the left. int screenLeft = ::GetSystemMetrics(SM_XVIRTUALSCREEN); ::SetWindowPos(window->GetWnd(), HWND_TOP, screenLeft - 50, 0, 100, 100, SWP_NOZORDER | SWP_NOSIZE); // Create a native window that covers the onscreen part of the tracked window. CreateNativeWindowWithBounds(LayoutDeviceIntRect(screenLeft, 0, 50, 100)); window->SetExpectation(widget::OcclusionState::OCCLUDED); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } EXPECT_FALSE(window->IsExpectingCall()); } // Simple test with a tracked window and native window that do not overlap. TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleVisible) { RefPtr window = CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); window->SetExpectation(widget::OcclusionState::VISIBLE); CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100)); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } EXPECT_FALSE(window->IsExpectingCall()); } // Simple test with a minimized tracked window and native window. TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleHidden) { RefPtr window = CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100)); // Iconify the tracked window and check that its occlusion state is HIDDEN. ::CloseWindow(window->GetWnd()); window->SetExpectation(widget::OcclusionState::HIDDEN); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } EXPECT_FALSE(window->IsExpectingCall()); } // Test that minimizing and restoring a tracked window results in the occlusion // tracker re-registering for win events and detecting that a native window // occludes the tracked window. TEST_F(WinWindowOcclusionTrackerInteractiveTest, OcclusionAfterVisibilityToggle) { RefPtr window = CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); window->SetExpectation(widget::OcclusionState::VISIBLE); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } window->SetExpectation(widget::OcclusionState::HIDDEN); WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged( window, /* aVisible = */ false); // This makes the window iconic. ::CloseWindow(window->GetWnd()); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } // HIDDEN state is set synchronously by OnWindowVsiblityChanged notification, // before occlusion is calculated, so the above expectation will be met w/o an // occlusion calculation. // Loop until an occlusion calculation has run with no non-hidden windows. while (GetNumVisibleRootWindows() != 0) { // Need to pump events in order for UpdateOcclusionState to get called, and // update the number of non hidden windows. When that number is 0, // occlusion has been calculated with no visible tracked windows. NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } window->SetExpectation(widget::OcclusionState::VISIBLE); WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged( window, /* aVisible = */ true); // This opens the window made iconic above. ::OpenIcon(window->GetWnd()); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } // Open a native window that occludes the visible tracked window. window->SetExpectation(widget::OcclusionState::OCCLUDED); CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } EXPECT_FALSE(window->IsExpectingCall()); } // Test that locking the screen causes visible windows to become occluded. TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenVisibleOcclusion) { RefPtr window = CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); window->SetExpectation(widget::OcclusionState::VISIBLE); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } window->SetExpectation(widget::OcclusionState::OCCLUDED); // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker // uses SessionChangeObserver to listen for WM_WTSSESSION_CHANGE messages, but // actually locking the screen isn't feasible. DWORD currentSessionId = 0; ::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId); ::PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE, WTS_SESSION_LOCK, currentSessionId); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); MSG msg; bool gotMessage = ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE); if (gotMessage) { ::TranslateMessage(&msg); ::DispatchMessageW(&msg); } NS_ProcessNextEvent(nullptr, /* aMayWait = */ false); PR_Sleep(PR_INTERVAL_NO_WAIT); } EXPECT_FALSE(window->IsExpectingCall()); } // Test that locking the screen leaves hidden windows as hidden. TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenHiddenOcclusion) { RefPtr window = CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); // Iconify the tracked window and check that its occlusion state is HIDDEN. ::CloseWindow(window->GetWnd()); window->SetExpectation(widget::OcclusionState::HIDDEN); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } // Force the state to VISIBLE. window->NotifyOcclusionState(widget::OcclusionState::VISIBLE); window->SetExpectation(widget::OcclusionState::HIDDEN); // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker // uses SessionChangeObserver to listen for WM_WTSSESSION_CHANGE messages, but // actually locking the screen isn't feasible. DWORD currentSessionId = 0; ::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId); PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE, WTS_SESSION_LOCK, currentSessionId); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); MSG msg; bool gotMessage = ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE); if (gotMessage) { ::TranslateMessage(&msg); ::DispatchMessageW(&msg); } NS_ProcessNextEvent(nullptr, /* aMayWait = */ false); PR_Sleep(PR_INTERVAL_NO_WAIT); } EXPECT_FALSE(window->IsExpectingCall()); } // Test that locking the screen from a different session doesn't mark window // as occluded. TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenDifferentSession) { RefPtr window = CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200)); window->SetExpectation(widget::OcclusionState::VISIBLE); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } // Force the state to OCCLUDED. window->NotifyOcclusionState(widget::OcclusionState::OCCLUDED); // Generate a session change lock screen with a session id that's not // currentSessionId. DWORD currentSessionId = 0; ::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId); ::PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE, WTS_SESSION_LOCK, currentSessionId + 1); window->SetExpectation(widget::OcclusionState::VISIBLE); // Create a native window to trigger occlusion calculation. CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50)); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); MSG msg; bool gotMessage = ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE); if (gotMessage) { ::TranslateMessage(&msg); ::DispatchMessageW(&msg); } NS_ProcessNextEvent(nullptr, /* aMayWait = */ false); PR_Sleep(PR_INTERVAL_NO_WAIT); } EXPECT_FALSE(window->IsExpectingCall()); } // Test that display off & on power state notification causes visible windows to // become occluded, then visible. TEST_F(WinWindowOcclusionTrackerInteractiveTest, DisplayOnOffHandling) { RefPtr window = CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); window->SetExpectation(widget::OcclusionState::VISIBLE); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } window->SetExpectation(widget::OcclusionState::OCCLUDED); // Turning display off and on isn't feasible, so send a notification. OnDisplayStateChanged(/* aDisplayOn */ false); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } window->SetExpectation(widget::OcclusionState::VISIBLE); OnDisplayStateChanged(/* aDisplayOn */ true); while (window->IsExpectingCall()) { WinWindowOcclusionTracker::Get()->TriggerCalculation(); NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); } EXPECT_FALSE(window->IsExpectingCall()); }