1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "gtest/gtest.h"
8
9 #include "MockWinWidget.h"
10 #include "mozilla/Preferences.h"
11 #include "mozilla/widget/WinEventObserver.h"
12 #include "mozilla/widget/WinWindowOcclusionTracker.h"
13 #include "nsThreadUtils.h"
14 #include "Units.h"
15 #include "WinUtils.h"
16
17 using namespace mozilla;
18 using namespace mozilla::widget;
19
20 #define PREF_DISPLAY_STATE \
21 "widget.windows.window_occlusion_tracking_display_state.enabled"
22 #define PREF_SESSION_LOCK \
23 "widget.windows.window_occlusion_tracking_session_lock.enabled"
24
25 class WinWindowOcclusionTrackerInteractiveTest : public ::testing::Test {
26 protected:
SetUp()27 void SetUp() override {
28 // Shut down WinWindowOcclusionTracker if it exists.
29 // This could happen when WinWindowOcclusionTracker was initialized by other
30 // gtest
31 if (WinWindowOcclusionTracker::Get()) {
32 WinWindowOcclusionTracker::ShutDown();
33 }
34 EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get());
35
36 WinWindowOcclusionTracker::Ensure();
37 EXPECT_NE(nullptr, WinWindowOcclusionTracker::Get());
38
39 WinWindowOcclusionTracker::Get()->EnsureDisplayStatusObserver();
40 WinWindowOcclusionTracker::Get()->EnsureSessionChangeObserver();
41 }
42
TearDown()43 void TearDown() override {
44 WinWindowOcclusionTracker::ShutDown();
45 EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get());
46 }
47
SetNativeWindowBounds(HWND aHWnd,const LayoutDeviceIntRect aBounds)48 void SetNativeWindowBounds(HWND aHWnd, const LayoutDeviceIntRect aBounds) {
49 RECT wr;
50 wr.left = aBounds.X();
51 wr.top = aBounds.Y();
52 wr.right = aBounds.XMost();
53 wr.bottom = aBounds.YMost();
54
55 ::AdjustWindowRectEx(&wr, ::GetWindowLong(aHWnd, GWL_STYLE), FALSE,
56 ::GetWindowLong(aHWnd, GWL_EXSTYLE));
57
58 // Make sure to keep the window onscreen, as AdjustWindowRectEx() may have
59 // moved part of it offscreen. But, if the original requested bounds are
60 // offscreen, don't adjust the position.
61 LayoutDeviceIntRect windowBounds(wr.left, wr.top, wr.right - wr.left,
62 wr.bottom - wr.top);
63
64 if (aBounds.X() >= 0) {
65 windowBounds.x = std::max(0, windowBounds.X());
66 }
67 if (aBounds.Y() >= 0) {
68 windowBounds.y = std::max(0, windowBounds.Y());
69 }
70 ::SetWindowPos(aHWnd, HWND_TOP, windowBounds.X(), windowBounds.Y(),
71 windowBounds.Width(), windowBounds.Height(),
72 SWP_NOREPOSITION);
73 EXPECT_TRUE(::UpdateWindow(aHWnd));
74 }
75
CreateNativeWindowWithBounds(LayoutDeviceIntRect aBounds)76 void CreateNativeWindowWithBounds(LayoutDeviceIntRect aBounds) {
77 mMockWinWidget = MockWinWidget::Create(
78 WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle = */ 0, aBounds);
79 EXPECT_NE(nullptr, mMockWinWidget.get());
80 HWND hwnd = mMockWinWidget->GetWnd();
81 SetNativeWindowBounds(hwnd, aBounds);
82 HRGN region = ::CreateRectRgn(0, 0, 0, 0);
83 EXPECT_NE(nullptr, region);
84 if (::GetWindowRgn(hwnd, region) == COMPLEXREGION) {
85 // On Windows 7, the newly created window has a complex region, which
86 // means it will be ignored during the occlusion calculation. So, force
87 // it to have a simple region so that we get test coverage on win 7.
88 RECT boundingRect;
89 EXPECT_TRUE(::GetWindowRect(hwnd, &boundingRect));
90 HRGN rectangularRegion = ::CreateRectRgnIndirect(&boundingRect);
91 EXPECT_NE(nullptr, rectangularRegion);
92 ::SetWindowRgn(hwnd, rectangularRegion, /* bRedraw = */ TRUE);
93 ::DeleteObject(rectangularRegion);
94 }
95 ::DeleteObject(region);
96
97 ::ShowWindow(hwnd, SW_SHOWNORMAL);
98 EXPECT_TRUE(UpdateWindow(hwnd));
99 }
100
CreateTrackedWindowWithBounds(LayoutDeviceIntRect aBounds)101 RefPtr<MockWinWidget> CreateTrackedWindowWithBounds(
102 LayoutDeviceIntRect aBounds) {
103 RefPtr<MockWinWidget> window = MockWinWidget::Create(
104 WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle */ 0, aBounds);
105 EXPECT_NE(nullptr, window.get());
106 HWND hwnd = window->GetWnd();
107 ::ShowWindow(hwnd, SW_SHOWNORMAL);
108 WinWindowOcclusionTracker::Get()->Enable(window, window->GetWnd());
109 return window;
110 }
111
GetNumVisibleRootWindows()112 int GetNumVisibleRootWindows() {
113 return WinWindowOcclusionTracker::Get()->mNumVisibleRootWindows;
114 }
115
OnDisplayStateChanged(bool aDisplayOn)116 void OnDisplayStateChanged(bool aDisplayOn) {
117 WinWindowOcclusionTracker::Get()->OnDisplayStateChanged(aDisplayOn);
118 }
119
120 RefPtr<MockWinWidget> mMockWinWidget;
121 };
122
123 // Simple test completely covering a tracked window with a native window.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,SimpleOcclusion)124 TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleOcclusion) {
125 RefPtr<MockWinWidget> window =
126 CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
127 window->SetExpectation(widget::OcclusionState::OCCLUDED);
128 CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
129 while (window->IsExpectingCall()) {
130 WinWindowOcclusionTracker::Get()->TriggerCalculation();
131 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
132 }
133 EXPECT_FALSE(window->IsExpectingCall());
134 }
135
136 // Simple test partially covering a tracked window with a native window.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,PartialOcclusion)137 TEST_F(WinWindowOcclusionTrackerInteractiveTest, PartialOcclusion) {
138 RefPtr<MockWinWidget> window =
139 CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200));
140 window->SetExpectation(widget::OcclusionState::VISIBLE);
141 CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50));
142 while (window->IsExpectingCall()) {
143 WinWindowOcclusionTracker::Get()->TriggerCalculation();
144 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
145 }
146 EXPECT_FALSE(window->IsExpectingCall());
147 }
148
149 // Simple test that a partly off screen tracked window, with the on screen part
150 // occluded by a native window, is considered occluded.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,OffscreenOcclusion)151 TEST_F(WinWindowOcclusionTrackerInteractiveTest, OffscreenOcclusion) {
152 RefPtr<MockWinWidget> window =
153 CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
154 // Move the tracked window 50 pixels offscreen to the left.
155 int screenLeft = ::GetSystemMetrics(SM_XVIRTUALSCREEN);
156 ::SetWindowPos(window->GetWnd(), HWND_TOP, screenLeft - 50, 0, 100, 100,
157 SWP_NOZORDER | SWP_NOSIZE);
158
159 // Create a native window that covers the onscreen part of the tracked window.
160 CreateNativeWindowWithBounds(LayoutDeviceIntRect(screenLeft, 0, 50, 100));
161 window->SetExpectation(widget::OcclusionState::OCCLUDED);
162 while (window->IsExpectingCall()) {
163 WinWindowOcclusionTracker::Get()->TriggerCalculation();
164 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
165 }
166 EXPECT_FALSE(window->IsExpectingCall());
167 }
168
169 // Simple test with a tracked window and native window that do not overlap.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,SimpleVisible)170 TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleVisible) {
171 RefPtr<MockWinWidget> window =
172 CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
173 window->SetExpectation(widget::OcclusionState::VISIBLE);
174 CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100));
175 while (window->IsExpectingCall()) {
176 WinWindowOcclusionTracker::Get()->TriggerCalculation();
177 WinWindowOcclusionTracker::Get()->TriggerCalculation();
178 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
179 }
180 EXPECT_FALSE(window->IsExpectingCall());
181 }
182
183 // Simple test with a minimized tracked window and native window.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,SimpleHidden)184 TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleHidden) {
185 RefPtr<MockWinWidget> window =
186 CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
187 CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100));
188 // Iconify the tracked window and check that its occlusion state is HIDDEN.
189 ::CloseWindow(window->GetWnd());
190 window->SetExpectation(widget::OcclusionState::HIDDEN);
191 while (window->IsExpectingCall()) {
192 WinWindowOcclusionTracker::Get()->TriggerCalculation();
193 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
194 }
195 EXPECT_FALSE(window->IsExpectingCall());
196 }
197
198 // Test that minimizing and restoring a tracked window results in the occlusion
199 // tracker re-registering for win events and detecting that a native window
200 // occludes the tracked window.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,OcclusionAfterVisibilityToggle)201 TEST_F(WinWindowOcclusionTrackerInteractiveTest,
202 OcclusionAfterVisibilityToggle) {
203 RefPtr<MockWinWidget> window =
204 CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
205 window->SetExpectation(widget::OcclusionState::VISIBLE);
206 while (window->IsExpectingCall()) {
207 WinWindowOcclusionTracker::Get()->TriggerCalculation();
208 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
209 }
210
211 window->SetExpectation(widget::OcclusionState::HIDDEN);
212 WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged(
213 window, /* aVisible = */ false);
214
215 // This makes the window iconic.
216 ::CloseWindow(window->GetWnd());
217
218 while (window->IsExpectingCall()) {
219 WinWindowOcclusionTracker::Get()->TriggerCalculation();
220 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
221 }
222
223 // HIDDEN state is set synchronously by OnWindowVsiblityChanged notification,
224 // before occlusion is calculated, so the above expectation will be met w/o an
225 // occlusion calculation.
226 // Loop until an occlusion calculation has run with no non-hidden windows.
227 while (GetNumVisibleRootWindows() != 0) {
228 // Need to pump events in order for UpdateOcclusionState to get called, and
229 // update the number of non hidden windows. When that number is 0,
230 // occlusion has been calculated with no visible tracked windows.
231 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
232 }
233
234 window->SetExpectation(widget::OcclusionState::VISIBLE);
235 WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged(
236 window, /* aVisible = */ true);
237 // This opens the window made iconic above.
238 ::OpenIcon(window->GetWnd());
239
240 while (window->IsExpectingCall()) {
241 WinWindowOcclusionTracker::Get()->TriggerCalculation();
242 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
243 }
244
245 // Open a native window that occludes the visible tracked window.
246 window->SetExpectation(widget::OcclusionState::OCCLUDED);
247 CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
248 while (window->IsExpectingCall()) {
249 WinWindowOcclusionTracker::Get()->TriggerCalculation();
250 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
251 }
252 EXPECT_FALSE(window->IsExpectingCall());
253 }
254
255 // Test that locking the screen causes visible windows to become occluded.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,LockScreenVisibleOcclusion)256 TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenVisibleOcclusion) {
257 RefPtr<MockWinWidget> window =
258 CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
259 window->SetExpectation(widget::OcclusionState::VISIBLE);
260 while (window->IsExpectingCall()) {
261 WinWindowOcclusionTracker::Get()->TriggerCalculation();
262 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
263 }
264
265 window->SetExpectation(widget::OcclusionState::OCCLUDED);
266 // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker
267 // uses SessionChangeObserver to listen for WM_WTSSESSION_CHANGE messages, but
268 // actually locking the screen isn't feasible.
269 DWORD currentSessionId = 0;
270 ::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId);
271 ::PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE,
272 WTS_SESSION_LOCK, currentSessionId);
273
274 while (window->IsExpectingCall()) {
275 WinWindowOcclusionTracker::Get()->TriggerCalculation();
276
277 MSG msg;
278 bool gotMessage =
279 ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE);
280 if (gotMessage) {
281 ::TranslateMessage(&msg);
282 ::DispatchMessageW(&msg);
283 }
284 NS_ProcessNextEvent(nullptr, /* aMayWait = */ false);
285 PR_Sleep(PR_INTERVAL_NO_WAIT);
286 }
287 EXPECT_FALSE(window->IsExpectingCall());
288 }
289
290 // Test that locking the screen leaves hidden windows as hidden.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,LockScreenHiddenOcclusion)291 TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenHiddenOcclusion) {
292 RefPtr<MockWinWidget> window =
293 CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
294 // Iconify the tracked window and check that its occlusion state is HIDDEN.
295 ::CloseWindow(window->GetWnd());
296 window->SetExpectation(widget::OcclusionState::HIDDEN);
297 while (window->IsExpectingCall()) {
298 WinWindowOcclusionTracker::Get()->TriggerCalculation();
299 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
300 }
301
302 // Force the state to VISIBLE.
303 window->NotifyOcclusionState(widget::OcclusionState::VISIBLE);
304
305 window->SetExpectation(widget::OcclusionState::HIDDEN);
306
307 // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker
308 // uses SessionChangeObserver to listen for WM_WTSSESSION_CHANGE messages, but
309 // actually locking the screen isn't feasible.
310 DWORD currentSessionId = 0;
311 ::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId);
312 PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE,
313 WTS_SESSION_LOCK, currentSessionId);
314
315 while (window->IsExpectingCall()) {
316 WinWindowOcclusionTracker::Get()->TriggerCalculation();
317
318 MSG msg;
319 bool gotMessage =
320 ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE);
321 if (gotMessage) {
322 ::TranslateMessage(&msg);
323 ::DispatchMessageW(&msg);
324 }
325 NS_ProcessNextEvent(nullptr, /* aMayWait = */ false);
326 PR_Sleep(PR_INTERVAL_NO_WAIT);
327 }
328 EXPECT_FALSE(window->IsExpectingCall());
329 }
330
331 // Test that locking the screen from a different session doesn't mark window
332 // as occluded.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,LockScreenDifferentSession)333 TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenDifferentSession) {
334 RefPtr<MockWinWidget> window =
335 CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200));
336 window->SetExpectation(widget::OcclusionState::VISIBLE);
337 while (window->IsExpectingCall()) {
338 WinWindowOcclusionTracker::Get()->TriggerCalculation();
339
340 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
341 }
342
343 // Force the state to OCCLUDED.
344 window->NotifyOcclusionState(widget::OcclusionState::OCCLUDED);
345
346 // Generate a session change lock screen with a session id that's not
347 // currentSessionId.
348 DWORD currentSessionId = 0;
349 ::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId);
350 ::PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE,
351 WTS_SESSION_LOCK, currentSessionId + 1);
352
353 window->SetExpectation(widget::OcclusionState::VISIBLE);
354 // Create a native window to trigger occlusion calculation.
355 CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50));
356 while (window->IsExpectingCall()) {
357 WinWindowOcclusionTracker::Get()->TriggerCalculation();
358
359 MSG msg;
360 bool gotMessage =
361 ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE);
362 if (gotMessage) {
363 ::TranslateMessage(&msg);
364 ::DispatchMessageW(&msg);
365 }
366 NS_ProcessNextEvent(nullptr, /* aMayWait = */ false);
367 PR_Sleep(PR_INTERVAL_NO_WAIT);
368 }
369 EXPECT_FALSE(window->IsExpectingCall());
370 }
371
372 // Test that display off & on power state notification causes visible windows to
373 // become occluded, then visible.
TEST_F(WinWindowOcclusionTrackerInteractiveTest,DisplayOnOffHandling)374 TEST_F(WinWindowOcclusionTrackerInteractiveTest, DisplayOnOffHandling) {
375 RefPtr<MockWinWidget> window =
376 CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100));
377 window->SetExpectation(widget::OcclusionState::VISIBLE);
378 while (window->IsExpectingCall()) {
379 WinWindowOcclusionTracker::Get()->TriggerCalculation();
380
381 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
382 }
383
384 window->SetExpectation(widget::OcclusionState::OCCLUDED);
385
386 // Turning display off and on isn't feasible, so send a notification.
387 OnDisplayStateChanged(/* aDisplayOn */ false);
388 while (window->IsExpectingCall()) {
389 WinWindowOcclusionTracker::Get()->TriggerCalculation();
390
391 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
392 }
393
394 window->SetExpectation(widget::OcclusionState::VISIBLE);
395 OnDisplayStateChanged(/* aDisplayOn */ true);
396 while (window->IsExpectingCall()) {
397 WinWindowOcclusionTracker::Get()->TriggerCalculation();
398
399 NS_ProcessNextEvent(nullptr, /* aMayWait = */ true);
400 }
401 EXPECT_FALSE(window->IsExpectingCall());
402 }
403