test: multi monitor tests for save/restore window state (#48048)

* feat: save window state (#47425)

* feat: save/restore window state

* cleanup

* remove constructor option

* refactor: apply suggestions from code review

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* refactor: forward declare prefservice

* refactor: remove constructor option

* refactor: save window state on move/resize instead of moved/resized

* feat: resave window state after construction

* test: add basic window save tests

* test: add work area tests

* test: asynchronous batching behavior

* docs: add windowStateRestoreOptions to BaseWindowConstructorOptions

* chore: move includes to main block

* Update spec/api-browser-window-spec.ts

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* docs: update docs/api/structures/base-window-options.md

Co-authored-by: Erick Zhao <erick@hotmail.ca>

* fix: preserve original bounds during window state save in special modes

* feat: save kiosk state in window preferences

* chore: remove ts-expect-error

* test: check hasCapturableScreen before running tests

* test: remove multimonitor tests

* test: add missing hasCapturableScreen checks before tests

* docs: add blurb on saving mechanism

* feat: add debounce window of 200ms to saveWindowState

* docs: remove blurb until finalized

* style: convert constants from snake_case to camelCase

* refactor: initialize prefs_ only if window state is configured to be saved/restored

* refactor: rename window states key

* refactor: store in application-level Local State instead of browser context

* refactor: switch to more accurate function names

* fix: add dcheck for browser_process

* fix: flush window state to avoid race condition

* refactor: change stateId to name

* refactor: change windowStateRestoreOptions to windowStatePersistence

* Update docs/api/structures/base-window-options.md

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* fix: add warning when window state persistence enabled without window name

* docs: lowercase capital B for consistency

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>

* feat: restore window state

* feat: flush display modes on show

* refactor: move utility functions to common area

* feat: clear window state

* fix: wait for the prefs to update

* test: clearWindowState extra test

* test: refine clear window state tests

* test: single monitor restore window tests

chore: rebase on gsoc-2025

* refactor: refine clearWindowState test

* fix: revert default_app back to original

* docs: add comment linking AdjustBoundsToBeVisibleOnDisplay to Chromium code

* fix: add correct permalink

* refactor: ci friendly

* fix: disable windowStatePersistence when no display

* refactor: use reference instead pointer

* fix: skip window state persistence for invalid/fake displays

* refactor: better flag placement

* test: add test to verify window state is not saved when no display

* fix: restore display mode inside show()

* feat: support for multimonitor tests

* fix: update yarn.lock file

* feat: support any resolution for new displays

* feat: support display positioning

* docs: multi-monitor tests

* test: remove dummy test

* feat: save window state (#47425)

* feat: save/restore window state

* cleanup

* remove constructor option

* refactor: apply suggestions from code review

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* refactor: forward declare prefservice

* refactor: remove constructor option

* refactor: save window state on move/resize instead of moved/resized

* feat: resave window state after construction

* test: add basic window save tests

* test: add work area tests

* test: asynchronous batching behavior

* docs: add windowStateRestoreOptions to BaseWindowConstructorOptions

* chore: move includes to main block

* Update spec/api-browser-window-spec.ts

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* docs: update docs/api/structures/base-window-options.md

Co-authored-by: Erick Zhao <erick@hotmail.ca>

* fix: preserve original bounds during window state save in special modes

* feat: save kiosk state in window preferences

* chore: remove ts-expect-error

* test: check hasCapturableScreen before running tests

* test: remove multimonitor tests

* test: add missing hasCapturableScreen checks before tests

* docs: add blurb on saving mechanism

* feat: add debounce window of 200ms to saveWindowState

* docs: remove blurb until finalized

* style: convert constants from snake_case to camelCase

* refactor: initialize prefs_ only if window state is configured to be saved/restored

* refactor: rename window states key

* refactor: store in application-level Local State instead of browser context

* refactor: switch to more accurate function names

* fix: add dcheck for browser_process

* fix: flush window state to avoid race condition

* refactor: change stateId to name

* refactor: change windowStateRestoreOptions to windowStatePersistence

* Update docs/api/structures/base-window-options.md

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* fix: add warning when window state persistence enabled without window name

* docs: lowercase capital B for consistency

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>

* test: clear sharedUserPath before and test

* refactor: hasInvalidDisplay function

* debug: add display info logging for CI

* fix: do not save/restore when window is 0x0

* test: support for multimonitor tests (#47911)

* test: support for multimonitor tests

* fix: update yarn.lock file

* test: support any resolution for new displays

* test: support display positioning

* docs: multi-monitor tests

* test: remove dummy test

* fix: native-addon forceCleanup

* docs: add forceCleanup description

* test: add two basic multi-monitor tests

* fix: find the closest display for non-overlapping saved bounds

* test: windowStatePersistence multi-monitor tests

* docs: add note on display APIs in CI

* fix: remove duplicate destroy registration

* feat: enforce unique window names across BaseWindow and BrowserWindow (#47764)

* feat: save window state (#47425)

* feat: save/restore window state

* cleanup

* remove constructor option

* refactor: apply suggestions from code review

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* refactor: forward declare prefservice

* refactor: remove constructor option

* refactor: save window state on move/resize instead of moved/resized

* feat: resave window state after construction

* test: add basic window save tests

* test: add work area tests

* test: asynchronous batching behavior

* docs: add windowStateRestoreOptions to BaseWindowConstructorOptions

* chore: move includes to main block

* Update spec/api-browser-window-spec.ts

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* docs: update docs/api/structures/base-window-options.md

Co-authored-by: Erick Zhao <erick@hotmail.ca>

* fix: preserve original bounds during window state save in special modes

* feat: save kiosk state in window preferences

* chore: remove ts-expect-error

* test: check hasCapturableScreen before running tests

* test: remove multimonitor tests

* test: add missing hasCapturableScreen checks before tests

* docs: add blurb on saving mechanism

* feat: add debounce window of 200ms to saveWindowState

* docs: remove blurb until finalized

* style: convert constants from snake_case to camelCase

* refactor: initialize prefs_ only if window state is configured to be saved/restored

* refactor: rename window states key

* refactor: store in application-level Local State instead of browser context

* refactor: switch to more accurate function names

* fix: add dcheck for browser_process

* fix: flush window state to avoid race condition

* refactor: change stateId to name

* refactor: change windowStateRestoreOptions to windowStatePersistence

* Update docs/api/structures/base-window-options.md

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* fix: add warning when window state persistence enabled without window name

* docs: lowercase capital B for consistency

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>

* feat: enforce unique window names across BaseWindow and BrowserWindow

* docs: update docs for name property

* fix: linter issue with symbol

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>

* docs: remove inaccurate comment

* fix: move expect blocks outside beforeEach

* test: exclude macOS-x64 for now

* test: remove invalid display test

* test: remove invalid display test

* feat: save window state (#47425)

* feat: save/restore window state

* cleanup

* remove constructor option

* refactor: apply suggestions from code review

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* refactor: forward declare prefservice

* refactor: remove constructor option

* refactor: save window state on move/resize instead of moved/resized

* feat: resave window state after construction

* test: add basic window save tests

* test: add work area tests

* test: asynchronous batching behavior

* docs: add windowStateRestoreOptions to BaseWindowConstructorOptions

* chore: move includes to main block

* Update spec/api-browser-window-spec.ts

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* docs: update docs/api/structures/base-window-options.md

Co-authored-by: Erick Zhao <erick@hotmail.ca>

* fix: preserve original bounds during window state save in special modes

* feat: save kiosk state in window preferences

* chore: remove ts-expect-error

* test: check hasCapturableScreen before running tests

* test: remove multimonitor tests

* test: add missing hasCapturableScreen checks before tests

* docs: add blurb on saving mechanism

* feat: add debounce window of 200ms to saveWindowState

* docs: remove blurb until finalized

* style: convert constants from snake_case to camelCase

* refactor: initialize prefs_ only if window state is configured to be saved/restored

* refactor: rename window states key

* refactor: store in application-level Local State instead of browser context

* refactor: switch to more accurate function names

* fix: add dcheck for browser_process

* fix: flush window state to avoid race condition

* refactor: change stateId to name

* refactor: change windowStateRestoreOptions to windowStatePersistence

* Update docs/api/structures/base-window-options.md

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* fix: add warning when window state persistence enabled without window name

* docs: lowercase capital B for consistency

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>

* test: support for multimonitor tests (#47911)

* test: support for multimonitor tests

* fix: update yarn.lock file

* test: support any resolution for new displays

* test: support display positioning

* docs: multi-monitor tests

* test: remove dummy test

* feat: enforce unique window names across BaseWindow and BrowserWindow (#47764)

* feat: save window state (#47425)

* feat: save/restore window state

* cleanup

* remove constructor option

* refactor: apply suggestions from code review

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* refactor: forward declare prefservice

* refactor: remove constructor option

* refactor: save window state on move/resize instead of moved/resized

* feat: resave window state after construction

* test: add basic window save tests

* test: add work area tests

* test: asynchronous batching behavior

* docs: add windowStateRestoreOptions to BaseWindowConstructorOptions

* chore: move includes to main block

* Update spec/api-browser-window-spec.ts

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* docs: update docs/api/structures/base-window-options.md

Co-authored-by: Erick Zhao <erick@hotmail.ca>

* fix: preserve original bounds during window state save in special modes

* feat: save kiosk state in window preferences

* chore: remove ts-expect-error

* test: check hasCapturableScreen before running tests

* test: remove multimonitor tests

* test: add missing hasCapturableScreen checks before tests

* docs: add blurb on saving mechanism

* feat: add debounce window of 200ms to saveWindowState

* docs: remove blurb until finalized

* style: convert constants from snake_case to camelCase

* refactor: initialize prefs_ only if window state is configured to be saved/restored

* refactor: rename window states key

* refactor: store in application-level Local State instead of browser context

* refactor: switch to more accurate function names

* fix: add dcheck for browser_process

* fix: flush window state to avoid race condition

* refactor: change stateId to name

* refactor: change windowStateRestoreOptions to windowStatePersistence

* Update docs/api/structures/base-window-options.md

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* fix: add warning when window state persistence enabled without window name

* docs: lowercase capital B for consistency

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>

* feat: enforce unique window names across BaseWindow and BrowserWindow

* docs: update docs for name property

* fix: linter issue with symbol

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>

* feat: clear and restore window state (#47781)

* feat: save window state (#47425)

* feat: save/restore window state

* cleanup

* remove constructor option

* refactor: apply suggestions from code review

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* refactor: forward declare prefservice

* refactor: remove constructor option

* refactor: save window state on move/resize instead of moved/resized

* feat: resave window state after construction

* test: add basic window save tests

* test: add work area tests

* test: asynchronous batching behavior

* docs: add windowStateRestoreOptions to BaseWindowConstructorOptions

* chore: move includes to main block

* Update spec/api-browser-window-spec.ts

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* docs: update docs/api/structures/base-window-options.md

Co-authored-by: Erick Zhao <erick@hotmail.ca>

* fix: preserve original bounds during window state save in special modes

* feat: save kiosk state in window preferences

* chore: remove ts-expect-error

* test: check hasCapturableScreen before running tests

* test: remove multimonitor tests

* test: add missing hasCapturableScreen checks before tests

* docs: add blurb on saving mechanism

* feat: add debounce window of 200ms to saveWindowState

* docs: remove blurb until finalized

* style: convert constants from snake_case to camelCase

* refactor: initialize prefs_ only if window state is configured to be saved/restored

* refactor: rename window states key

* refactor: store in application-level Local State instead of browser context

* refactor: switch to more accurate function names

* fix: add dcheck for browser_process

* fix: flush window state to avoid race condition

* refactor: change stateId to name

* refactor: change windowStateRestoreOptions to windowStatePersistence

* Update docs/api/structures/base-window-options.md

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* fix: add warning when window state persistence enabled without window name

* docs: lowercase capital B for consistency

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>

* feat: restore window state

* feat: flush display modes on show

* refactor: move utility functions to common area

* feat: clear window state

* fix: wait for the prefs to update

* test: clearWindowState extra test

* test: refine clear window state tests

* test: single monitor restore window tests

chore: rebase on gsoc-2025

* refactor: refine clearWindowState test

* fix: revert default_app back to original

* docs: add comment linking AdjustBoundsToBeVisibleOnDisplay to Chromium code

* fix: add correct permalink

* refactor: ci friendly

* fix: disable windowStatePersistence when no display

* refactor: use reference instead pointer

* fix: skip window state persistence for invalid/fake displays

* refactor: better flag placement

* test: add test to verify window state is not saved when no display

* fix: restore display mode inside show()

* feat: save window state (#47425)

* feat: save/restore window state

* cleanup

* remove constructor option

* refactor: apply suggestions from code review

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* refactor: forward declare prefservice

* refactor: remove constructor option

* refactor: save window state on move/resize instead of moved/resized

* feat: resave window state after construction

* test: add basic window save tests

* test: add work area tests

* test: asynchronous batching behavior

* docs: add windowStateRestoreOptions to BaseWindowConstructorOptions

* chore: move includes to main block

* Update spec/api-browser-window-spec.ts

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* docs: update docs/api/structures/base-window-options.md

Co-authored-by: Erick Zhao <erick@hotmail.ca>

* fix: preserve original bounds during window state save in special modes

* feat: save kiosk state in window preferences

* chore: remove ts-expect-error

* test: check hasCapturableScreen before running tests

* test: remove multimonitor tests

* test: add missing hasCapturableScreen checks before tests

* docs: add blurb on saving mechanism

* feat: add debounce window of 200ms to saveWindowState

* docs: remove blurb until finalized

* style: convert constants from snake_case to camelCase

* refactor: initialize prefs_ only if window state is configured to be saved/restored

* refactor: rename window states key

* refactor: store in application-level Local State instead of browser context

* refactor: switch to more accurate function names

* fix: add dcheck for browser_process

* fix: flush window state to avoid race condition

* refactor: change stateId to name

* refactor: change windowStateRestoreOptions to windowStatePersistence

* Update docs/api/structures/base-window-options.md

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* fix: add warning when window state persistence enabled without window name

* docs: lowercase capital B for consistency

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>

* test: clear sharedUserPath before and test

* refactor: hasInvalidDisplay function

* debug: add display info logging for CI

* fix: do not save/restore when window is 0x0

* test: support for multimonitor tests (#47911)

* test: support for multimonitor tests

* fix: update yarn.lock file

* test: support any resolution for new displays

* test: support display positioning

* docs: multi-monitor tests

* test: remove dummy test

* feat: enforce unique window names across BaseWindow and BrowserWindow (#47764)

* feat: save window state (#47425)

* feat: save/restore window state

* cleanup

* remove constructor option

* refactor: apply suggestions from code review

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* refactor: forward declare prefservice

* refactor: remove constructor option

* refactor: save window state on move/resize instead of moved/resized

* feat: resave window state after construction

* test: add basic window save tests

* test: add work area tests

* test: asynchronous batching behavior

* docs: add windowStateRestoreOptions to BaseWindowConstructorOptions

* chore: move includes to main block

* Update spec/api-browser-window-spec.ts

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* docs: update docs/api/structures/base-window-options.md

Co-authored-by: Erick Zhao <erick@hotmail.ca>

* fix: preserve original bounds during window state save in special modes

* feat: save kiosk state in window preferences

* chore: remove ts-expect-error

* test: check hasCapturableScreen before running tests

* test: remove multimonitor tests

* test: add missing hasCapturableScreen checks before tests

* docs: add blurb on saving mechanism

* feat: add debounce window of 200ms to saveWindowState

* docs: remove blurb until finalized

* style: convert constants from snake_case to camelCase

* refactor: initialize prefs_ only if window state is configured to be saved/restored

* refactor: rename window states key

* refactor: store in application-level Local State instead of browser context

* refactor: switch to more accurate function names

* fix: add dcheck for browser_process

* fix: flush window state to avoid race condition

* refactor: change stateId to name

* refactor: change windowStateRestoreOptions to windowStatePersistence

* Update docs/api/structures/base-window-options.md

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

* fix: add warning when window state persistence enabled without window name

* docs: lowercase capital B for consistency

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>

* feat: enforce unique window names across BaseWindow and BrowserWindow

* docs: update docs for name property

* fix: linter issue with symbol

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>

* test: remove invalid display test

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>
Co-authored-by: Keeley Hammond <vertedinde@electronjs.org>

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>
Co-authored-by: Keeley Hammond <vertedinde@electronjs.org>
This commit is contained in:
Nilay Arya
2025-08-26 21:30:35 -04:00
committed by GitHub
parent 04d4505004
commit 444448507d
8 changed files with 653 additions and 8 deletions

View File

@@ -37,6 +37,21 @@ const success = virtualDisplay.destroy(displayId)
**Returns:** `boolean` - Success status
#### `virtualDisplay.forceCleanup()`
Performs a complete cleanup of all virtual displays and resets the macOS CoreGraphics display system.
It is recommended to call this before every test to prevent test failures. macOS CoreGraphics maintains an internal display ID allocation pool that can become corrupted when virtual displays are created and destroyed rapidly during testing. Without proper cleanup, subsequent display creation may fail with inconsistent display IDs, resulting in test flakiness.
```js @ts-nocheck
// Recommended test pattern
beforeEach(() => {
virtualDisplay.forceCleanup()
})
```
**Returns:** `boolean` - Success status
## Display Constraints
### Size Limits

View File

@@ -976,10 +976,25 @@ void NativeWindow::RestoreWindowState(const gin_helper::Dictionary& options) {
display::Screen* screen = display::Screen::GetScreen();
DCHECK(screen);
// GetDisplayMatching returns a fake display with 1920x1080 resolution at
// (0,0) when no physical displays are attached.
// https://source.chromium.org/chromium/chromium/src/+/main:ui/display/display.cc;l=184;drc=e4f1aef5f3ec30a28950d766612cc2c04c822c71
const display::Display display = screen->GetDisplayMatching(saved_bounds);
// Set the primary display as the target display for restoration.
display::Display display = screen->GetPrimaryDisplay();
// We identify the display with the minimal Manhattan distance to the saved
// bounds and set it as the target display for restoration.
int min_displacement = std::numeric_limits<int>::max();
for (const auto& candidate : screen->GetAllDisplays()) {
gfx::Rect test_bounds = saved_bounds;
test_bounds.AdjustToFit(candidate.work_area());
int displacement = std::abs(test_bounds.x() - saved_bounds.x()) +
std::abs(test_bounds.y() - saved_bounds.y());
if (displacement < min_displacement) {
min_displacement = displacement;
display = candidate;
}
}
// Skip window state restoration if current display has invalid dimensions or
// is fake. Restoring from invalid displays (0x0) or fake displays (ID 0xFF)

View File

@@ -7985,6 +7985,567 @@ describe('BrowserWindow module', () => {
w.destroy();
});
});
// FIXME(nilayarya): Figure out why these tests fail on macOS-x64
// virtualDisplay.create() is creating double displays on macOS-x64
ifdescribe(process.platform === 'darwin' && process.arch === 'arm64')('multi-monitor tests', () => {
const virtualDisplay = require('@electron-ci/virtual-display');
const primaryDisplay = screen.getPrimaryDisplay();
beforeEach(() => {
virtualDisplay.forceCleanup();
});
it('should restore window bounds correctly on a secondary display', async () => {
const targetDisplayX = primaryDisplay.bounds.x + primaryDisplay.bounds.width;
const targetDisplayY = primaryDisplay.bounds.y;
// We expect only the primary display to be present before the tests start
expect(screen.getAllDisplays().length).to.equal(1);
// Create a new virtual target display to the right of the primary display
const targetDisplayId = virtualDisplay.create({
width: 1920,
height: 1080,
x: targetDisplayX,
y: targetDisplayY
});
// Verify the virtual display is created correctly
const targetDisplay = screen.getDisplayNearestPoint({ x: targetDisplayX, y: targetDisplayY });
expect(targetDisplay.bounds.x).to.equal(targetDisplayX);
expect(targetDisplay.bounds.y).to.equal(targetDisplayY);
expect(targetDisplay.bounds.width).to.equal(1920);
expect(targetDisplay.bounds.height).to.equal(1080);
// Bounds for the test window on the virtual target display
const boundsOnTargetDisplay = {
width: 400,
height: 300,
x: targetDisplay.workArea.x + 100,
y: targetDisplay.workArea.y + 100
};
await createAndSaveWindowState(boundsOnTargetDisplay);
// Restore the window state by creating a new window with the same name
const w = new BrowserWindow({
name: windowName,
windowStatePersistence: true,
show: false
});
const restoredBounds = w.getBounds();
expectBoundsEqual(restoredBounds, boundsOnTargetDisplay);
w.destroy();
virtualDisplay.destroy(targetDisplayId);
});
it('should restore window to a visible location when saved display no longer exists', async () => {
const targetDisplayX = primaryDisplay.bounds.x + primaryDisplay.bounds.width;
const targetDisplayY = primaryDisplay.bounds.y;
// We expect only the primary display to be present before the tests start
expect(screen.getAllDisplays().length).to.equal(1);
// Create a new virtual target display to the right of the primary display
const targetDisplayId = virtualDisplay.create({
width: 1920,
height: 1080,
x: targetDisplayX,
y: targetDisplayY
});
// Verify the virtual display is created correctly - single check for targetDisplay.bounds.x
const targetDisplay = screen.getDisplayNearestPoint({ x: targetDisplayX, y: targetDisplayY });
expect(targetDisplay.bounds.x).to.equal(targetDisplayX);
// Bounds for the test window on the virtual target display
const boundsOnTargetDisplay = {
width: 400,
height: 300,
x: targetDisplay.workArea.x + 100,
y: targetDisplay.workArea.y + 100
};
// Save window state on the virtual display
await createAndSaveWindowState(boundsOnTargetDisplay);
virtualDisplay.destroy(targetDisplayId);
// Wait for the target virtual display to be destroyed
while (screen.getAllDisplays().length > 1) await setTimeout(1000);
const w = new BrowserWindow({
name: windowName,
windowStatePersistence: true,
show: false
});
const restoredBounds = w.getBounds();
const primaryWorkArea = primaryDisplay.workArea;
// Window should be fully visible on the primary display
expect(restoredBounds.x).to.be.at.least(primaryWorkArea.x);
expect(restoredBounds.y).to.be.at.least(primaryWorkArea.y);
expect(restoredBounds.x + restoredBounds.width).to.be.at.most(primaryWorkArea.x + primaryWorkArea.width);
expect(restoredBounds.y + restoredBounds.height).to.be.at.most(primaryWorkArea.y + primaryWorkArea.height);
// Window should maintain its original size
expect(restoredBounds.width).to.equal(boundsOnTargetDisplay.width);
expect(restoredBounds.height).to.equal(boundsOnTargetDisplay.height);
w.destroy();
});
it('should fallback to nearest display when saved display no longer exists', async () => {
const targetDisplayX = primaryDisplay.bounds.x + primaryDisplay.bounds.width;
const targetDisplayY = primaryDisplay.bounds.y;
// We expect only the primary display to be present before the tests start
expect(screen.getAllDisplays().length).to.equal(1);
// Create first virtual display to the right of primary
const middleDisplayId = virtualDisplay.create({
width: 1920,
height: 1080,
x: targetDisplayX,
y: targetDisplayY
});
// Create second virtual display to the right of the first (rightmost)
const rightmostDisplayX = targetDisplayX + 1920;
const rightmostDisplayId = virtualDisplay.create({
width: 1920,
height: 1080,
x: rightmostDisplayX,
y: targetDisplayY
});
// Verify the virtual displays are created correctly - single check for origin x values
const middleDisplay = screen.getDisplayNearestPoint({ x: targetDisplayX, y: targetDisplayY });
expect(middleDisplay.bounds.x).to.equal(targetDisplayX);
const rightmostDisplay = screen.getDisplayNearestPoint({ x: rightmostDisplayX, y: targetDisplayY });
expect(rightmostDisplay.bounds.x).to.equal(rightmostDisplayX);
// Bounds for the test window on the rightmost display
const boundsOnRightmostDisplay = {
width: 400,
height: 300,
x: rightmostDisplay.workArea.x + 100,
y: rightmostDisplay.workArea.y + 100
};
// Save window state on the rightmost display
await createAndSaveWindowState(boundsOnRightmostDisplay);
// Destroy the rightmost display (where window was saved)
virtualDisplay.destroy(rightmostDisplayId);
// Wait for the rightmost display to be destroyed
while (screen.getAllDisplays().length > 2) await setTimeout(1000);
const w = new BrowserWindow({
name: windowName,
windowStatePersistence: true,
show: false
});
const restoredBounds = w.getBounds();
// Window should be restored on the middle display (nearest remaining display)
expect(restoredBounds.x).to.be.at.least(middleDisplay.workArea.x);
expect(restoredBounds.y).to.be.at.least(middleDisplay.workArea.y);
expect(restoredBounds.x + restoredBounds.width).to.be.at.most(middleDisplay.workArea.x + middleDisplay.workArea.width);
expect(restoredBounds.y + restoredBounds.height).to.be.at.most(middleDisplay.workArea.y + middleDisplay.workArea.height);
// Window should maintain its original size
expect(restoredBounds.width).to.equal(boundsOnRightmostDisplay.width);
expect(restoredBounds.height).to.equal(boundsOnRightmostDisplay.height);
w.destroy();
virtualDisplay.destroy(middleDisplayId);
});
it('should restore multiple named windows independently across displays', async () => {
const targetDisplayX = primaryDisplay.bounds.x + primaryDisplay.bounds.width;
const targetDisplayY = primaryDisplay.bounds.y;
// We expect only the primary display to be present before the tests start
expect(screen.getAllDisplays().length).to.equal(1);
// Create a first virtual display to the right of the primary display
const targetDisplayId1 = virtualDisplay.create({
width: 1920,
height: 1080,
x: targetDisplayX,
y: targetDisplayY
});
// Create a second virtual display to the right of the first
const targetDisplayId2 = virtualDisplay.create({
width: 1600,
height: 900,
x: targetDisplayX + 1920,
y: targetDisplayY
});
// Verify the virtual displays are created correctly - single check for origin x values
const targetDisplay1 = screen.getDisplayNearestPoint({ x: targetDisplayX, y: targetDisplayY });
expect(targetDisplay1.bounds.x).to.equal(targetDisplayX);
const targetDisplay2 = screen.getDisplayNearestPoint({ x: targetDisplayX + 1920, y: targetDisplayY });
expect(targetDisplay2.bounds.x).to.equal(targetDisplayX + 1920);
// Window 1 on primary display
const window1Name = 'test-multi-window-1';
const bounds1 = {
width: 300,
height: 200,
x: primaryDisplay.workArea.x + 50,
y: primaryDisplay.workArea.y + 50
};
// Window 2 on second display (first virtual)
const window2Name = 'test-multi-window-2';
const bounds2 = {
width: 400,
height: 300,
x: targetDisplay1.workArea.x + 100,
y: targetDisplay1.workArea.y + 100
};
// Window 3 on third display (second virtual)
const window3Name = 'test-multi-window-3';
const bounds3 = {
width: 350,
height: 250,
x: targetDisplay2.workArea.x + 150,
y: targetDisplay2.workArea.y + 150
};
// Clear window state for all three windows from previous tests
BrowserWindow.clearWindowState(window1Name);
BrowserWindow.clearWindowState(window2Name);
BrowserWindow.clearWindowState(window3Name);
// Create and save state for all three windows
const w1 = new BrowserWindow({
name: window1Name,
windowStatePersistence: true,
show: false,
...bounds1
});
const w2 = new BrowserWindow({
name: window2Name,
windowStatePersistence: true,
show: false,
...bounds2
});
const w3 = new BrowserWindow({
name: window3Name,
windowStatePersistence: true,
show: false,
...bounds3
});
w1.destroy();
w2.destroy();
w3.destroy();
await setTimeout(2000);
// Restore all three windows
const restoredW1 = new BrowserWindow({
name: window1Name,
windowStatePersistence: true,
show: false
});
const restoredW2 = new BrowserWindow({
name: window2Name,
windowStatePersistence: true,
show: false
});
const restoredW3 = new BrowserWindow({
name: window3Name,
windowStatePersistence: true,
show: false
});
// Check that each window restored to its correct display and position
expectBoundsEqual(restoredW1.getBounds(), bounds1);
expectBoundsEqual(restoredW2.getBounds(), bounds2);
expectBoundsEqual(restoredW3.getBounds(), bounds3);
restoredW1.destroy();
restoredW2.destroy();
restoredW3.destroy();
virtualDisplay.destroy(targetDisplayId1);
virtualDisplay.destroy(targetDisplayId2);
});
it('should restore fullscreen state on correct display', async () => {
const targetDisplayX = primaryDisplay.bounds.x + primaryDisplay.bounds.width;
const targetDisplayY = primaryDisplay.bounds.y;
// We expect only the primary display to be present before the tests start
expect(screen.getAllDisplays().length).to.equal(1);
// Create a new virtual target display to the right of the primary display
const targetDisplayId = virtualDisplay.create({
width: 1920,
height: 1080,
x: targetDisplayX,
y: targetDisplayY
});
const targetDisplay = screen.getDisplayNearestPoint({ x: targetDisplayX, y: targetDisplayY });
expect(targetDisplay.bounds.x).to.equal(targetDisplayX);
// Create window on target display and set fullscreen
const initialBounds = {
width: 400,
height: 300,
x: targetDisplay.workArea.x + 100,
y: targetDisplay.workArea.y + 100,
fullscreen: true
};
await createAndSaveWindowState(initialBounds);
const w = new BrowserWindow({
name: windowName,
windowStatePersistence: true
});
const enterFullScreen = once(w, 'enter-full-screen');
if (!w.isFullScreen()) await enterFullScreen;
expect(w.isFullScreen()).to.equal(true);
// Check that fullscreen window is on the correct display
const fsBounds = w.getBounds();
expect(fsBounds.x).to.be.at.least(targetDisplay.bounds.x);
expect(fsBounds.y).to.be.at.least(targetDisplay.bounds.y);
expect(fsBounds.x + fsBounds.width).to.be.at.most(targetDisplay.bounds.x + targetDisplay.bounds.width);
expect(fsBounds.y + fsBounds.height).to.be.at.most(targetDisplay.bounds.y + targetDisplay.bounds.height);
w.destroy();
virtualDisplay.destroy(targetDisplayId);
});
it('should restore maximized state on correct display', async () => {
const targetDisplayX = primaryDisplay.bounds.x + primaryDisplay.bounds.width;
const targetDisplayY = primaryDisplay.bounds.y;
// We expect only the primary display to be present before the tests start
expect(screen.getAllDisplays().length).to.equal(1);
// Create a new virtual target display to the right of the primary display
const targetDisplayId = virtualDisplay.create({
width: 1920,
height: 1080,
x: targetDisplayX,
y: targetDisplayY
});
const targetDisplay = screen.getDisplayNearestPoint({ x: targetDisplayX, y: targetDisplayY });
expect(targetDisplay.bounds.x).to.equal(targetDisplayX);
// Create window on target display and maximize it
const w1 = new BrowserWindow({
name: windowName,
windowStatePersistence: {
displayMode: false
},
x: targetDisplay.workArea.x,
y: targetDisplay.workArea.y
});
const maximized = once(w1, 'maximize');
w1.maximize();
if (!w1.isMaximized()) await maximized;
w1.destroy();
await setTimeout(2000);
const w2 = new BrowserWindow({
name: windowName,
windowStatePersistence: true,
show: true
});
const maximized_ = once(w2, 'maximize');
if (!w2.isMaximized()) await maximized_;
expect(w2.isMaximized()).to.equal(true);
// Check that maximized window is on the correct display
const maximizedBounds = w2.getBounds();
expect(maximizedBounds.x).to.be.at.least(targetDisplay.bounds.x);
expect(maximizedBounds.y).to.be.at.least(targetDisplay.bounds.y);
expect(maximizedBounds.x + maximizedBounds.width).to.be.at.most(targetDisplay.bounds.x + targetDisplay.bounds.width);
expect(maximizedBounds.y + maximizedBounds.height).to.be.at.most(targetDisplay.bounds.y + targetDisplay.bounds.height);
w2.destroy();
virtualDisplay.destroy(targetDisplayId);
});
it('should restore kiosk state on correct display', async () => {
const targetDisplayX = primaryDisplay.bounds.x + primaryDisplay.bounds.width;
const targetDisplayY = primaryDisplay.bounds.y;
// We expect only the primary display to be present before the tests start
expect(screen.getAllDisplays().length).to.equal(1);
// Create a new virtual target display to the right of the primary display
const targetDisplayId = virtualDisplay.create({
width: 1920,
height: 1080,
x: targetDisplayX,
y: targetDisplayY
});
const targetDisplay = screen.getDisplayNearestPoint({ x: targetDisplayX, y: targetDisplayY });
// Create window on target display and set kiosk: true
const initialBounds = {
width: 400,
height: 300,
x: targetDisplay.workArea.x + 100,
y: targetDisplay.workArea.y + 100,
kiosk: true
};
await createAndSaveWindowState(initialBounds);
const w = new BrowserWindow({
name: windowName,
windowStatePersistence: true
});
const enterFullScreen = once(w, 'enter-full-screen');
if (!w.isFullScreen()) await enterFullScreen;
expect(w.isFullScreen()).to.equal(true);
expect(w.isKiosk()).to.equal(true);
// Check that kiosk window is on the correct display
const kioskBounds = w.getBounds();
expect(kioskBounds.x).to.be.at.least(targetDisplay.bounds.x);
expect(kioskBounds.y).to.be.at.least(targetDisplay.bounds.y);
expect(kioskBounds.x + kioskBounds.width).to.be.at.most(targetDisplay.bounds.x + targetDisplay.bounds.width);
expect(kioskBounds.y + kioskBounds.height).to.be.at.most(targetDisplay.bounds.y + targetDisplay.bounds.height);
w.destroy();
virtualDisplay.destroy(targetDisplayId);
});
it('should maintain same bounds when target display resolution increases', async () => {
const targetDisplayX = primaryDisplay.bounds.x + primaryDisplay.bounds.width;
const targetDisplayY = primaryDisplay.bounds.y;
// We expect only the primary display to be present before the tests start
expect(screen.getAllDisplays().length).to.equal(1);
// Create initial virtual display
const targetDisplayId = virtualDisplay.create({
width: 1920,
height: 1080,
x: targetDisplayX,
y: targetDisplayY
});
const targetDisplay = screen.getDisplayNearestPoint({ x: targetDisplayX, y: targetDisplayY });
// Create a new virtual display with double the resolution of the target display
const higherResDisplayId = virtualDisplay.create({
width: targetDisplay.bounds.width * 2,
height: targetDisplay.bounds.height * 2,
x: targetDisplayX + targetDisplay.bounds.width,
y: targetDisplayY
});
// Bounds for the test window on the virtual target display
const initialBounds = {
width: 400,
height: 300,
x: targetDisplay.workArea.x + 100,
y: targetDisplay.workArea.y + 100
};
await createAndSaveWindowState(initialBounds);
// Destroy the target display and wait for the higher resolution display to take its place
virtualDisplay.destroy(targetDisplayId);
while (screen.getAllDisplays().length > 2) await setTimeout(1000);
// Restore window
const w = new BrowserWindow({
name: windowName,
windowStatePersistence: true,
show: false
});
const restoredBounds = w.getBounds();
// Window should maintain same x, y, width, height as the display got bigger
expectBoundsEqual(restoredBounds, initialBounds);
w.destroy();
virtualDisplay.destroy(higherResDisplayId);
});
it('should reposition and resize window when target display resolution decreases', async () => {
const targetDisplayX = primaryDisplay.bounds.x + primaryDisplay.bounds.width;
const targetDisplayY = primaryDisplay.bounds.y;
// We expect only the primary display to be present before the tests start
expect(screen.getAllDisplays().length).to.equal(1);
// Create initial virtual display with high resolution
const targetDisplayId = virtualDisplay.create({
width: 2560,
height: 1440,
x: targetDisplayX,
y: targetDisplayY
});
const targetDisplay = screen.getDisplayNearestPoint({ x: targetDisplayX, y: targetDisplayY });
// Create a new virtual display with half the resolution of the target display shifted down
const lowerResDisplayId = virtualDisplay.create({
width: targetDisplay.bounds.width / 2,
height: targetDisplay.bounds.height / 2,
x: targetDisplayX + targetDisplay.bounds.width,
y: targetDisplay.bounds.height / 2
});
// Bounds that would overflow on a smaller display
const initialBounds = {
x: targetDisplay.workArea.x,
y: targetDisplay.workArea.y,
width: targetDisplay.bounds.width,
height: targetDisplay.bounds.height
};
await createAndSaveWindowState(initialBounds);
// Destroy and and wait for the lower resolution display to take its place
virtualDisplay.destroy(targetDisplayId);
while (screen.getAllDisplays().length > 2) await setTimeout(1000);
// We expect the display to be shifted down as we set y: targetDisplay.bounds.height / 2 earlier
const smallerDisplay = screen.getDisplayNearestPoint({ x: targetDisplayX, y: targetDisplay.bounds.height / 2 });
// Restore window
const w = new BrowserWindow({
name: windowName,
windowStatePersistence: true,
show: false
});
const restoredBounds = w.getBounds();
// Window should be repositioned to be entirely visible on smaller display
expect(restoredBounds.x).to.be.at.least(smallerDisplay.workArea.x);
expect(restoredBounds.y).to.be.at.least(smallerDisplay.workArea.y);
expect(restoredBounds.x + restoredBounds.width).to.be.at.most(smallerDisplay.workArea.x + smallerDisplay.workArea.width);
expect(restoredBounds.y + restoredBounds.height).to.be.at.most(smallerDisplay.workArea.y + smallerDisplay.workArea.height);
w.destroy();
virtualDisplay.destroy(lowerResDisplayId);
});
});
});
});
});

View File

@@ -8,6 +8,7 @@
+ (NSInteger)create:(int)width height:(int)height x:(int)x y:(int)y;
+ (BOOL)destroy:(NSInteger)displayId;
+ (BOOL)forceCleanup;
@end

View File

@@ -12,6 +12,11 @@ class DummyManager {
static func createDummy(_ dummyDefinition: DummyDefinition, isPortrait _: Bool = false, serialNum: UInt32 = 0, doConnect: Bool = true) -> Int? {
let dummy = Dummy(dummyDefinition: dummyDefinition, serialNum: serialNum, doConnect: doConnect)
if !dummy.isConnected {
print("[DummyManager.createDummy:\(#line)] Failed to create virtual display - not connected")
return nil
}
self.dummyCounter += 1
self.definedDummies[self.dummyCounter] = DefinedDummy(dummy: dummy)
return self.dummyCounter
@@ -25,6 +30,30 @@ class DummyManager {
}
self.definedDummies[number] = nil
}
static func forceCleanup() {
for (_, definedDummy) in self.definedDummies {
if definedDummy.dummy.isConnected {
definedDummy.dummy.virtualDisplay = nil
definedDummy.dummy.displayIdentifier = 0
definedDummy.dummy.isConnected = false
}
}
self.definedDummies.removeAll()
self.dummyCounter = 0
var config: CGDisplayConfigRef? = nil
if CGBeginDisplayConfiguration(&config) == .success {
CGCompleteDisplayConfiguration(config, .permanently)
}
usleep(2000000)
if CGBeginDisplayConfiguration(&config) == .success {
CGCompleteDisplayConfiguration(config, .forSession)
}
}
}
struct DummyDefinition {
@@ -88,10 +117,10 @@ class Dummy: Equatable {
self.virtualDisplay = virtualDisplay
self.displayIdentifier = virtualDisplay.displayID
self.isConnected = true
os_log("Display %{public}@ successfully connected", type: .info, "\(name)")
print("[Dummy.connect:\(#line)] Successfully connected virtual display: \(name)")
return true
} else {
os_log("Failed to connect display %{public}@", type: .info, "\(name)")
print("[Dummy.connect:\(#line)] Failed to connect virtual display: \(name)")
return false
}
}
@@ -99,7 +128,7 @@ class Dummy: Equatable {
func disconnect() {
self.virtualDisplay = nil
self.isConnected = false
os_log("Disconnected virtual display: %{public}@", type: .info, "\(self.getName())")
print("[Dummy.disconnect:\(#line)] Disconnected virtual display: \(self.getName())")
}
private static func waitForDisplayRegistration(_ displayId: CGDirectDisplayID) -> Bool {
@@ -110,6 +139,7 @@ class Dummy: Equatable {
}
usleep(100000)
}
print("[Dummy.waitForDisplayRegistration:\(#line)] Failed to register virtual display: \(displayId)")
return false
}

View File

@@ -18,6 +18,11 @@ import os.log
return true
}
@objc public static func forceCleanup() -> Bool {
DummyManager.forceCleanup()
return true
}
private static func positionDisplay(displayId: Int, x: Int, y: Int) {
guard let definedDummy = DummyManager.definedDummies[displayId],
definedDummy.dummy.isConnected else {

View File

@@ -11,4 +11,8 @@
return [VirtualDisplay destroyWithId:(int)displayId];
}
+ (BOOL)forceCleanup {
return [VirtualDisplay forceCleanup];
}
@end

View File

@@ -134,6 +134,18 @@ napi_value create(napi_env env, napi_callback_info info) {
return result;
}
// virtualDisplay.forceCleanup()
napi_value forceCleanup(napi_env env, napi_callback_info info) {
BOOL result = [VirtualDisplayBridge forceCleanup];
napi_value js_result;
if (napi_get_boolean(env, result, &js_result) != napi_ok) {
return NULL;
}
return js_result;
}
// virtualDisplay.destroy()
napi_value destroy(napi_env env, napi_callback_info info) {
size_t argc = 1;
@@ -167,7 +179,9 @@ napi_value destroy(napi_env env, napi_callback_info info) {
napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor descriptors[] = {
{"create", NULL, create, NULL, NULL, NULL, napi_default, NULL},
{"destroy", NULL, destroy, NULL, NULL, NULL, napi_default, NULL}};
{"destroy", NULL, destroy, NULL, NULL, NULL, napi_default, NULL},
{"forceCleanup", NULL, forceCleanup, NULL, NULL, NULL, napi_default,
NULL}};
if (napi_define_properties(env, exports,
sizeof(descriptors) / sizeof(*descriptors),