402 lines
16 KiB
C++
402 lines
16 KiB
C++
/* -*- 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());
|
|
|
|
Preferences::SetBool(PREF_DISPLAY_STATE, true);
|
|
Preferences::SetBool(PREF_SESSION_LOCK, true);
|
|
}
|
|
|
|
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<MockWinWidget> CreateTrackedWindowWithBounds(
|
|
LayoutDeviceIntRect aBounds) {
|
|
RefPtr<MockWinWidget> 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<MockWinWidget> mMockWinWidget;
|
|
};
|
|
|
|
// Simple test completely covering a tracked window with a native window.
|
|
TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleOcclusion) {
|
|
RefPtr<MockWinWidget> 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<MockWinWidget> 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<MockWinWidget> 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<MockWinWidget> 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<MockWinWidget> 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<MockWinWidget> 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<MockWinWidget> 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 WinEventWindow to listen for WM_WTSSESSION_CHANGE messages, but
|
|
// actually locking the screen isn't feasible.
|
|
DWORD currentSessionId = 0;
|
|
::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId);
|
|
::PostMessage(WinEventWindow::GetHwndForTestingOnly(), WM_WTSSESSION_CHANGE,
|
|
WTS_SESSION_LOCK, currentSessionId);
|
|
|
|
while (window->IsExpectingCall()) {
|
|
WinWindowOcclusionTracker::Get()->TriggerCalculation();
|
|
|
|
MSG msg;
|
|
bool gotMessage = ::PeekMessageW(
|
|
&msg, WinEventWindow::GetHwndForTestingOnly(), 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<MockWinWidget> 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 WinEventWindow to listen for WM_WTSSESSION_CHANGE messages, but
|
|
// actually locking the screen isn't feasible.
|
|
DWORD currentSessionId = 0;
|
|
::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId);
|
|
PostMessage(WinEventWindow::GetHwndForTestingOnly(), WM_WTSSESSION_CHANGE,
|
|
WTS_SESSION_LOCK, currentSessionId);
|
|
|
|
while (window->IsExpectingCall()) {
|
|
WinWindowOcclusionTracker::Get()->TriggerCalculation();
|
|
|
|
MSG msg;
|
|
bool gotMessage = ::PeekMessageW(
|
|
&msg, WinEventWindow::GetHwndForTestingOnly(), 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<MockWinWidget> 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(WinEventWindow::GetHwndForTestingOnly(), 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, WinEventWindow::GetHwndForTestingOnly(), 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<MockWinWidget> 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());
|
|
}
|