Files
electron/docs/development/multi-monitor-testing.md
Nilay Arya 8a5c5a4fe2 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>
2026-03-25 17:26:02 -07:00

3.4 KiB
Raw Blame History

Multi-Monitor Testing

The virtualDisplay addon leverages macOS CoreGraphics APIs to create virtual displays, allowing you to write and run multi-monitor tests without the need for physical monitors.

Methods

virtualDisplay.create([options])

Creates a virtual display and returns a display ID.

const virtualDisplay = require('@electron-ci/virtual-display')
// Default: 1920×1080 at origin (0, 0)
const displayId = virtualDisplay.create()
const virtualDisplay = require('@electron-ci/virtual-display')
// Custom options (all parameters optional and have default values)
const displayId = virtualDisplay.create({
  width: 2560, // Display width in pixels
  height: 1440, // Display height in pixels
  x: 1920, // X position (top-left corner)
  y: 0 // Y position (top-left corner)
})

Returns: number - Unique display ID used to identify the display. Returns 0 on failure to create display.

virtualDisplay.destroy(displayId)

Removes the virtual display.

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.

// Recommended test pattern
beforeEach(() => {
  virtualDisplay.forceCleanup()
})

Returns: boolean - Success status

Display Constraints

Size Limits

Virtual displays are constrained to 720×720 pixels minimum and 8192×8192 pixels maximum. Actual limits may vary depending on your Mac's graphics capabilities, so sizes outside this range (like 9000×6000) may fail on some systems.

// Safe sizes for testing
virtualDisplay.create({ width: 1920, height: 1080 }) // Full HD
virtualDisplay.create({ width: 3840, height: 2160 }) // 4K

Positioning Behavior

macOS maintains a contiguous desktop space by automatically adjusting display positions if there are any overlaps or gaps. In case of either, the placement of the new origin is as close as possible to the requested location, without overlapping or leaving a gap between displays.

Overlap:

// Requested positions
const display1 = virtualDisplay.create({ x: 0, y: 0, width: 1920, height: 1080 })
const display2 = virtualDisplay.create({ x: 500, y: 0, width: 1920, height: 1080 })

// macOS automatically repositions display2 to x: 1920 to prevent overlap
const actualBounds = screen.getAllDisplays().map(d => d.bounds)
// Result: [{ x: 0, y: 0, width: 1920, height: 1080 },
//          { x: 1920, y: 0, width: 1920, height: 1080 }]

Gap:

// Requested: gap between displays
const display1 = virtualDisplay.create({ width: 1920, height: 1080, x: 0, y: 0 })
const display2 = virtualDisplay.create({ width: 1920, height: 1080, x: 2000, y: 0 })
// macOS snaps display2 to x: 1920 (eliminates 80px gap)

Note

Always verify actual positions with screen.getAllDisplays() after creation, as macOS may adjust coordinates from the set values.