Compare commits

...

22 Commits

Author SHA1 Message Date
Keeley Hammond
5598bb7286 build: run yarn install 2026-04-02 14:19:40 -07:00
Nilay Arya
f79b2489b2 docs: improve docs for name property and fix typos 2026-03-25 17:50:03 -07:00
Nilay Arya
f4f2e7cb84 refactor: rename BaseWindow.clearWindowState to clearPersistedState 2026-03-25 17:49:48 -07:00
Nilay Arya
accd419b48 refactor: rename event restored-window-state to restored-persisted-state 2026-03-25 17:49:26 -07:00
Nilay Arya
48df7eeff0 docs: better docs for virtual display addon 2026-03-25 17:49:26 -07:00
Nilay Arya
7b4d42f248 docs: better docs for virtual display addon 2026-03-25 17:49:26 -07:00
Nilay Arya
018cdfde54 fix: ensure only single display before multi monitor tests 2026-03-25 17:49:26 -07:00
Nilay Arya
5e75ee34d3 docs: tutorial for windowStatePersistence 2026-03-25 17:46:04 -07:00
Nilay Arya
dd7c4dc207 test: add checks for virtual display creation 2026-03-25 17:46:03 -07:00
Nilay Arya
a9a528c472 docs: differentiate between name and title 2026-03-25 17:41:25 -07:00
Nilay Arya
adfb387767 test: move createWindowAndSave 2026-03-25 17:41:25 -07:00
Nilay Arya
2868c69f4d test: add tests for 'restored-window-state' event emission 2026-03-25 17:41:24 -07:00
Nilay Arya
4c1808eeea feat: add 'restored-window-state' event to BaseWindow 2026-03-25 17:41:24 -07:00
Nilay Arya
ef7a582e16 docs: restored-window-state event 2026-03-25 17:41:24 -07:00
Nilay Arya
ec9efa0b86 test: set show to true 2026-03-25 17:41:24 -07:00
Nilay Arya
07a2541b5a docs: remove inaccurate comment 2026-03-25 17:41:23 -07:00
Nilay Arya
a5c0665bd8 docs: add note on display APIs in CI 2026-03-25 17:41:23 -07:00
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
Nilay Arya
3ce98b60be 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>
2026-03-25 17:25:52 -07:00
Nilay Arya
774c5e52d7 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>
2026-03-25 17:25:07 -07:00
Nilay Arya
0446e7d051 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
2026-03-25 17:24:34 -07:00
Nilay Arya
2290cf57c2 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>
2026-03-25 17:13:11 -07:00
40 changed files with 3674 additions and 15 deletions

View File

@@ -37,10 +37,11 @@ an issue:
* [Represented File for macOS BrowserWindows](tutorial/represented-file.md)
* [Native File Drag & Drop](tutorial/native-file-drag-drop.md)
* [Navigation History](tutorial/navigation-history.md)
* [Window State Persistence](tutorial/window-state-persistence.md)
* [Offscreen Rendering](tutorial/offscreen-rendering.md)
* [Dark Mode](tutorial/dark-mode.md)
* [Web embeds in Electron](tutorial/web-embeds.md)
* [Boilerplates and CLIs](tutorial/boilerplates-and-clis.md)
* [Boilerplates and CLIs](tutorial/boilerplates-and-clis.md)
* [Boilerplate vs CLI](tutorial/boilerplates-and-clis.md#boilerplate-vs-cli)
* [Electron Forge](tutorial/boilerplates-and-clis.md#electron-forge)
* [electron-builder](tutorial/boilerplates-and-clis.md#electron-builder)

View File

@@ -373,6 +373,15 @@ Calling `event.preventDefault()` will prevent the menu from being displayed.
To convert `point` to DIP, use [`screen.screenToDipPoint(point)`](./screen.md#screenscreentodippointpoint-windows-linux).
#### Event: 'restored-persisted-state'
Emitted after the persisted window state has been restored.
Window state includes the window bounds (x, y, height, width) and display mode (maximized, fullscreen, kiosk).
> [!NOTE]
> This event is only emitted when [windowStatePersistence](structures/window-state-persistence.md) is enabled in [BaseWindowConstructorOptions](structures/base-window-options.md) or in [BrowserWindowConstructorOptions](structures/browser-window-options.md).
### Static Methods
The `BaseWindow` class has the following static methods:
@@ -391,6 +400,14 @@ Returns `BaseWindow | null` - The window that is focused in this application, ot
Returns `BaseWindow | null` - The window with the given `id`.
#### `BaseWindow.clearPersistedState(name)`
* `name` string - The window `name` to clear state for (see [BaseWindowConstructorOptions](structures/base-window-options.md)).
Clears the saved state for a window with the given name. This removes all persisted window bounds, display mode, and work area information that was previously saved when `windowStatePersistence` was enabled.
If the window `name` is empty or the window state doesn't exist, the method will log a warning.
### Instance Properties
Objects created with `new BaseWindow` have the following properties:

View File

@@ -42,6 +42,8 @@
Default is `false`.
* `hiddenInMissionControl` boolean (optional) _macOS_ - Whether window should be hidden when the user toggles into mission control.
* `kiosk` boolean (optional) - Whether the window is in kiosk mode. Default is `false`.
* `name` string (optional) - A unique identifier for the window, used internally by Electron to enable features such as state persistence. Each window must have a distinct name. It can only be reused after the corresponding window has been destroyed. An error is thrown if the name is already in use. This is not the visible title shown to users on the title bar.
* `windowStatePersistence` ([WindowStatePersistence](window-state-persistence.md) | boolean) (optional) - Configures or enables the persistence of window state (position, size, maximized state, etc.) across application restarts. Has no effect if window `name` is not provided. Automatically disabled when there is no available display. _Experimental_
* `title` string (optional) - Default window title. Default is `"Electron"`. If the HTML tag `<title>` is defined in the HTML file loaded by `loadURL()`, this property will be ignored.
* `icon` ([NativeImage](../native-image.md) | string) (optional) - The window icon. On Windows it is
recommended to use `ICO` icons to get best visual effects, you can also
@@ -94,7 +96,7 @@
title bar and a full size content window, the traffic light buttons will
display when being hovered over in the top left of the window.
**Note:** This option is currently experimental.
* `titleBarOverlay` Object | Boolean (optional) - When using a frameless window in conjunction with `win.setWindowButtonVisibility(true)` on macOS or using a `titleBarStyle` so that the standard window controls ("traffic lights" on macOS) are visible, this property enables the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and [CSS Environment Variables][overlay-css-env-vars]. Specifying `true` will result in an overlay with default system colors. Default is `false`.
* `titleBarOverlay` Object | boolean (optional) - When using a frameless window in conjunction with `win.setWindowButtonVisibility(true)` on macOS or using a `titleBarStyle` so that the standard window controls ("traffic lights" on macOS) are visible, this property enables the Window Controls Overlay [JavaScript APIs][overlay-javascript-apis] and [CSS Environment Variables][overlay-css-env-vars]. Specifying `true` will result in an overlay with default system colors. Default is `false`.
* `color` String (optional) _Windows_ _Linux_ - The CSS color of the Window Controls Overlay when enabled. Default is the system color.
* `symbolColor` String (optional) _Windows_ _Linux_ - The CSS color of the symbols on the Window Controls Overlay when enabled. Default is the system color.
* `height` Integer (optional) - The height of the title bar and Window Controls Overlay in pixels. Default is system height.

View File

@@ -0,0 +1,4 @@
# WindowStatePersistence Object
* `bounds` boolean (optional) - Whether to persist window position and size across application restarts. Defaults to `true` if not specified.
* `displayMode` boolean (optional) - Whether to persist display modes (fullscreen, kiosk, maximized, etc.) across application restarts. Defaults to `true` if not specified.

View File

@@ -0,0 +1,111 @@
# 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. Due to macOS CoreGraphics quirks, reading the entire guide once before writing tests is recommended.
## Methods
#### `virtualDisplay.create([options])`
Creates a virtual display and returns a display ID.
```js @ts-nocheck
const virtualDisplay = require('@electron-ci/virtual-display')
// Default: 1920×1080 at origin (0, 0)
const displayId = virtualDisplay.create()
```
```js @ts-nocheck
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.
> [!NOTE]
> It is recommended to call [`virtualDisplay.forceCleanup()`](#virtualdisplayforcecleanup) before every test to prevent display creation from failing in that test. 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.
#### `virtualDisplay.forceCleanup()`
Performs a complete cleanup of all virtual displays and resets the macOS CoreGraphics display system.
```js @ts-nocheck
beforeEach(() => {
virtualDisplay.forceCleanup()
})
```
#### `virtualDisplay.destroy(displayId)`
Removes the virtual display.
```js @ts-nocheck
virtualDisplay.destroy(displayId)
```
> [!NOTE]
> Always destroy virtual displays after use to prevent corrupting the macOS CoreGraphics display pool and affecting subsequent tests.
## Recommended usage pattern
```js @ts-nocheck
describe('multi-monitor tests', () => {
const virtualDisplay = require('@electron-ci/virtual-display')
beforeEach(() => {
virtualDisplay.forceCleanup()
})
it('should handle multiple displays', () => {
const display1 = virtualDisplay.create({ width: 1920, height: 1080, x: 0, y: 0 })
const display2 = virtualDisplay.create({ width: 2560, height: 1440, x: 1920, y: 0 })
// Your test logic here
virtualDisplay.destroy(display1)
virtualDisplay.destroy(display2)
})
})
```
## 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.
```js @ts-nocheck
// 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:**
```js @ts-nocheck
// 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:**
```js @ts-nocheck
// 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.

View File

@@ -95,3 +95,11 @@ To configure display scaling:
1. Push the Windows key and search for _Display settings_.
2. Under _Scale and layout_, make sure that the device is set to 100%.
## Multi-Monitor Tests
Some Electron APIs require testing across multiple displays, such as screen detection, window positioning, and display-related events. For contributors working on these features, the `virtualDisplay` native addon enables you to create and position virtual displays programmatically, making it possible to test multi-monitor scenarios without any physical hardware.
For detailed information on using virtual displays in your tests, see [Multi-Monitor Testing](multi-monitor-testing.md).
**Platform support:** macOS only

View File

@@ -0,0 +1,102 @@
# Window State Persistence
## Overview
Window State Persistence allows your Electron application to automatically save and restore a window's position, size, and display modes (such as maximized or fullscreen states) across application restarts.
This feature is particularly useful for applications where users frequently resize, move, or maximize windows and expect them to remain in the same state when reopening the app.
## Usage
### Basic usage
To enable Window State Persistence, simply set `windowStatePersistence: true` in your window constructor options and provide a unique `name` for the window.
```js
const { app, BrowserWindow } = require('electron')
function createWindow () {
const win = new BrowserWindow({
name: 'main-window',
width: 800,
height: 600,
windowStatePersistence: true
})
win.loadFile('index.html')
}
app.whenReady().then(createWindow)
```
With this configuration, Electron will automatically:
1. Restore the window's position, size, and display mode when created (if a previous state exists)
2. Save the window state whenever it changes (position, size, or display mode).
3. Emit a `restored-persisted-state` event after successfully restoring state.
4. Adapt restored window state to multi-monitor setups and display changes automatically.
> [!NOTE]
> Window State Persistence requires that the window has a unique `name` property set in its constructor options. This name serves as the identifier for storing and retrieving the window's saved state.
### Selective persistence
You can control which aspects of the window state are persisted by passing an object with specific options:
```js
const { app, BrowserWindow } = require('electron')
function createWindow () {
const win = new BrowserWindow({
name: 'main-window',
width: 800,
height: 600,
windowStatePersistence: {
bounds: true, // Save position and size (default: true)
displayMode: false // Don't save maximized/fullscreen/kiosk state (default: true)
}
})
win.loadFile('index.html')
}
app.whenReady().then(createWindow)
```
In this example, the window will remember its position and size but will always start in normal mode, even if it was maximized or fullscreened when last closed.
### Clearing persisted state
You can programmatically clear the saved state for a specific window using the static `clearPersistedState` method:
```js
const { BrowserWindow } = require('electron')
// Clear saved state for a specific window
BrowserWindow.clearPersistedState('main-window')
// Now when you create a window with this name,
// it will use the default constructor options
const win = new BrowserWindow({
name: 'main-window',
width: 800,
height: 600,
windowStatePersistence: true
})
```
## API Reference
The Window State Persistence APIs are available on both `BaseWindow` and `BrowserWindow` (since `BrowserWindow` extends `BaseWindow`) and work identically.
For complete API documentation, see:
- [`windowStatePersistence` in BaseWindowConstructorOptions][base-window-options]
- [`WindowStatePersistence` object structure][window-state-persistence-structure]
- [`BaseWindow.clearPersistedState()`][clear-persisted-state]
- [`restored-persisted-state` event][restored-event]
[base-window-options]: ../api/structures/base-window-options.md
[window-state-persistence-structure]: ../api/structures/window-state-persistence.md
[clear-persisted-state]: ../api/base-window.md#basewindowclearpersistedstatename
[restored-event]: ../api/base-window.md#event-restored-persisted-state

View File

@@ -172,6 +172,7 @@ auto_filenames = {
"docs/api/structures/web-source.md",
"docs/api/structures/window-open-handler-response.md",
"docs/api/structures/window-session-end-event.md",
"docs/api/structures/window-state-persistence.md",
]
sandbox_bundle_deps = [

View File

@@ -111,6 +111,8 @@ BrowserWindow.getAllWindows = () => {
return BaseWindow.getAllWindows().filter(isBrowserWindow) as any[] as BWT[];
};
BrowserWindow.clearPersistedState = BaseWindow.clearPersistedState;
BrowserWindow.getFocusedWindow = () => {
for (const window of BrowserWindow.getAllWindows()) {
if (!window.isDestroyed() && window.webContents && !window.webContents.isDestroyed()) {

View File

@@ -10,15 +10,20 @@
#include <vector>
#include "base/task/single_thread_task_runner.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/common/color_parser.h"
#include "electron/buildflags/buildflags.h"
#include "gin/dictionary.h"
#include "shell/browser/api/electron_api_menu.h"
#include "shell/browser/api/electron_api_view.h"
#include "shell/browser/api/electron_api_web_contents.h"
#include "shell/browser/browser_process_impl.h"
#include "shell/browser/electron_browser_main_parts.h"
#include "shell/browser/javascript_environment.h"
#include "shell/browser/native_window.h"
#include "shell/browser/window_list.h"
#include "shell/common/color_util.h"
#include "shell/common/electron_constants.h"
#include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_converters/gfx_converter.h"
@@ -170,7 +175,7 @@ void BaseWindow::OnWindowClosed() {
// We can not call Destroy here because we need to call Emit first, but we
// also do not want any method to be used, so just mark as destroyed here.
MarkDestroyed();
window_->FlushWindowState();
Emit("closed");
parent_window_.Reset();
@@ -261,6 +266,7 @@ void BaseWindow::OnWindowWillResize(const gfx::Rect& new_bounds,
}
void BaseWindow::OnWindowResize() {
window_->DebouncedSaveWindowState();
Emit("resize");
}
@@ -276,6 +282,7 @@ void BaseWindow::OnWindowWillMove(const gfx::Rect& new_bounds,
}
void BaseWindow::OnWindowMove() {
window_->DebouncedSaveWindowState();
Emit("move");
}
@@ -344,6 +351,10 @@ void BaseWindow::OnSystemContextMenu(int x, int y, bool* prevent_default) {
}
}
void BaseWindow::OnWindowStateRestored() {
EmitEventSoon("restored-persisted-state");
}
#if BUILDFLAG(IS_WIN)
void BaseWindow::OnWindowMessage(UINT message, WPARAM w_param, LPARAM l_param) {
if (IsWindowMessageHooked(message)) {
@@ -1152,14 +1163,64 @@ void BaseWindow::SetTitleBarOverlay(const gin_helper::Dictionary& options,
}
#endif
// static
void BaseWindow::ClearPersistedState(const std::string& window_name) {
if (window_name.empty()) {
LOG(WARNING) << "Cannot clear persisted window state: window name is empty";
return;
}
if (auto* browser_process =
electron::ElectronBrowserMainParts::Get()->browser_process()) {
DCHECK(browser_process);
if (auto* prefs = browser_process->local_state()) {
ScopedDictPrefUpdate update(prefs, electron::kWindowStates);
if (!update->Remove(window_name)) {
LOG(WARNING) << "Window state '" << window_name
<< "' not found, nothing to clear";
}
}
}
}
// static
gin_helper::WrappableBase* BaseWindow::New(gin::Arguments* const args) {
auto options = gin_helper::Dictionary::CreateEmpty(args->isolate());
args->GetNext(&options);
std::string error_message;
if (!IsWindowNameValid(options, &error_message)) {
// Window name is already in use throw an error and do not create the window
args->ThrowTypeError(error_message);
return nullptr;
}
return new BaseWindow(args, options);
}
// static
bool BaseWindow::IsWindowNameValid(const gin_helper::Dictionary& options,
std::string* error_message) {
std::string window_name;
if (options.Get(options::kName, &window_name) && !window_name.empty()) {
// Check if window name is already in use by another window
// Window names must be unique for state persistence to work correctly
const auto& windows = electron::WindowList::GetWindows();
bool name_in_use = std::any_of(windows.begin(), windows.end(),
[&window_name](const auto* const window) {
return window->GetName() == window_name;
});
if (name_in_use) {
*error_message = "Window name '" + window_name +
"' is already in use. Window names must be unique.";
return false;
}
}
return true;
}
// static
void BaseWindow::BuildPrototype(v8::Isolate* isolate,
v8::Local<v8::FunctionTemplate> prototype) {
@@ -1351,6 +1412,8 @@ void Initialize(v8::Local<v8::Object> exports,
.ToLocalChecked());
constructor.SetMethod("fromId", &BaseWindow::FromWeakMapID);
constructor.SetMethod("getAllWindows", &BaseWindow::GetAll);
constructor.SetMethod("clearPersistedState",
&BaseWindow::ClearPersistedState);
gin_helper::Dictionary dict(isolate, exports);
dict.Set("BaseWindow", constructor);

View File

@@ -44,6 +44,13 @@ class BaseWindow : public gin_helper::TrackableObject<BaseWindow>,
static void BuildPrototype(v8::Isolate* isolate,
v8::Local<v8::FunctionTemplate> prototype);
// Clears window state from the Local State JSON file in
// app.getPath('userData') via PrefService.
static void ClearPersistedState(const std::string& window_name);
static bool IsWindowNameValid(const gin_helper::Dictionary& options,
std::string* error_message);
const NativeWindow* window() const { return window_.get(); }
NativeWindow* window() { return window_.get(); }
@@ -95,6 +102,7 @@ class BaseWindow : public gin_helper::TrackableObject<BaseWindow>,
const base::DictValue& details) override;
void OnNewWindowForTab() override;
void OnSystemContextMenu(int x, int y, bool* prevent_default) override;
void OnWindowStateRestored() override;
#if BUILDFLAG(IS_WIN)
void OnWindowMessage(UINT message, WPARAM w_param, LPARAM l_param) override;
#endif

View File

@@ -322,6 +322,13 @@ gin_helper::WrappableBase* BrowserWindow::New(gin_helper::ErrorThrower thrower,
options = gin::Dictionary::CreateEmpty(args->isolate());
}
std::string error_message;
if (!IsWindowNameValid(options, &error_message)) {
// Window name is already in use throw an error and do not create the window
thrower.ThrowError(error_message);
return nullptr;
}
return new BrowserWindow(args, options);
}

View File

@@ -40,6 +40,7 @@
#include "services/device/public/cpp/geolocation/geolocation_system_permission_manager.h"
#include "services/network/public/cpp/network_switches.h"
#include "shell/browser/net/resolve_proxy_helper.h"
#include "shell/common/electron_constants.h"
#include "shell/common/electron_paths.h"
#include "shell/common/thread_restrictions.h"
@@ -149,12 +150,12 @@ void BrowserProcessImpl::PostEarlyInitialization() {
pref_registry.get());
#endif
pref_registry->RegisterDictionaryPref(electron::kWindowStates);
in_memory_pref_store_ = base::MakeRefCounted<ValueMapPrefStore>();
ApplyProxyModeFromCommandLine(in_memory_pref_store());
prefs_factory.set_command_line_prefs(in_memory_pref_store());
// Only use a persistent prefs store when cookie encryption is enabled as that
// is the only key that needs it
base::FilePath prefs_path;
CHECK(base::PathService::Get(electron::DIR_SESSION_DATA, &prefs_path));
prefs_path = prefs_path.Append(FILE_PATH_LITERAL("Local State"));

View File

@@ -9,22 +9,31 @@
#include <vector>
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/web_contents_user_data.h"
#include "include/core/SkColor.h"
#include "shell/browser/background_throttling_source.h"
#include "shell/browser/browser.h"
#include "shell/browser/browser_process_impl.h"
#include "shell/browser/draggable_region_provider.h"
#include "shell/browser/electron_browser_main_parts.h"
#include "shell/browser/native_window_features.h"
#include "shell/browser/ui/drag_util.h"
#include "shell/browser/window_list.h"
#include "shell/common/color_util.h"
#include "shell/common/electron_constants.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/persistent_dictionary.h"
#include "shell/common/options_switches.h"
#include "ui/base/hit_test.h"
#include "ui/compositor/compositor.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/types/display_constants.h"
#include "ui/views/widget/widget.h"
#if !BUILDFLAG(IS_MAC)
@@ -94,6 +103,12 @@ gfx::Size GetExpandedWindowSize(const NativeWindow* window,
}
#endif
// Check if display is fake (default display ID) or has invalid dimensions
bool hasInvalidDisplay(const display::Display& display) {
return display.id() == display::kDefaultDisplayId ||
display.size().width() == 0 || display.size().height() == 0;
}
} // namespace
NativeWindow::NativeWindow(const int32_t base_window_id,
@@ -118,6 +133,38 @@ NativeWindow::NativeWindow(const int32_t base_window_id,
options.Get(options::kVibrancyType, &vibrancy_);
#endif
options.Get(options::kName, &window_name_);
if (gin_helper::Dictionary persistence_options;
options.Get(options::kWindowStatePersistence, &persistence_options)) {
// Restore bounds by default
restore_bounds_ = true;
persistence_options.Get(options::kBounds, &restore_bounds_);
// Restore display mode by default
restore_display_mode_ = true;
persistence_options.Get(options::kDisplayMode, &restore_display_mode_);
window_state_persistence_enabled_ = true;
} else if (bool flag; options.Get(options::kWindowStatePersistence, &flag)) {
restore_bounds_ = flag;
restore_display_mode_ = flag;
window_state_persistence_enabled_ = flag;
}
// Initialize prefs_ to save/restore window bounds if we have a valid window
// name and window state persistence is enabled.
if (window_state_persistence_enabled_ && !window_name_.empty()) {
// Move this out if there's a need to initialize prefs_ for other features
if (auto* browser_process =
electron::ElectronBrowserMainParts::Get()->browser_process()) {
DCHECK(browser_process);
prefs_ = browser_process->local_state();
}
} else if (window_state_persistence_enabled_ && window_name_.empty()) {
window_state_persistence_enabled_ = false;
LOG(WARNING) << "Window state persistence enabled but no window name "
"provided. Window state will not be persisted.";
}
if (gin_helper::Dictionary dict;
options.Get(options::ktitleBarOverlay, &dict)) {
titlebar_overlay_ = true;
@@ -216,7 +263,14 @@ void NativeWindow::InitFromOptions(const gin_helper::Dictionary& options) {
options.Get(options::kFullScreenable, &fullscreenable);
SetFullScreenable(fullscreenable);
if (fullscreen)
// Restore window state (bounds and display mode) at this point in
// initialization. We deliberately restore bounds before display modes
// (fullscreen/kiosk) since the target display for these states depends on the
// window's initial bounds. Also, restoring here ensures we respect min/max
// width/height and fullscreenable constraints.
RestoreWindowState(options);
if (fullscreen && !restore_display_mode_)
SetFullScreen(true);
if (bool val; options.Get(options::kResizable, &val))
@@ -225,7 +279,8 @@ void NativeWindow::InitFromOptions(const gin_helper::Dictionary& options) {
if (bool val; options.Get(options::kSkipTaskbar, &val))
SetSkipTaskbar(val);
if (bool val; options.Get(options::kKiosk, &val) && val)
if (bool val;
options.Get(options::kKiosk, &val) && val && !restore_display_mode_)
SetKiosk(val);
#if BUILDFLAG(IS_MAC)
@@ -245,7 +300,9 @@ void NativeWindow::InitFromOptions(const gin_helper::Dictionary& options) {
SetBackgroundColor(background_color);
SetTitle(options.ValueOrDefault(options::kTitle, Browser::Get()->GetName()));
// Save updated window state after restoration adjustments are complete if
// any.
SaveWindowState();
// Then show it.
if (options.ValueOrDefault(options::kShow, true))
Show();
@@ -658,6 +715,10 @@ void NativeWindow::NotifyLayoutWindowControlsOverlay() {
*bounds);
}
void NativeWindow::NotifyWindowStateRestored() {
observers_.Notify(&NativeWindowObserver::OnWindowStateRestored);
}
#if BUILDFLAG(IS_WIN)
void NativeWindow::NotifyWindowMessage(UINT message,
WPARAM w_param,
@@ -760,10 +821,14 @@ void NativeWindow::SetAccessibleTitle(const std::string& title) {
WidgetDelegate::SetAccessibleTitle(base::UTF8ToUTF16(title));
}
std::string NativeWindow::GetAccessibleTitle() {
std::string NativeWindow::GetAccessibleTitle() const {
return base::UTF16ToUTF8(GetAccessibleWindowTitle());
}
std::string NativeWindow::GetName() const {
return window_name_;
}
void NativeWindow::HandlePendingFullscreenTransitions() {
if (pending_transitions_.empty()) {
set_fullscreen_transition_type(FullScreenTransitionType::kNone);
@@ -796,6 +861,252 @@ bool NativeWindow::IsTranslucent() const {
return false;
}
void NativeWindow::DebouncedSaveWindowState() {
save_window_state_timer_.Start(
FROM_HERE, base::Milliseconds(200),
base::BindOnce(&NativeWindow::SaveWindowState, base::Unretained(this)));
}
void NativeWindow::SaveWindowState() {
if (!window_state_persistence_enabled_ || is_being_restored_)
return;
gfx::Rect bounds = GetBounds();
if (bounds.width() == 0 || bounds.height() == 0) {
LOG(WARNING) << "Window state not saved - window bounds are invalid";
return;
}
const display::Screen* screen = display::Screen::Get();
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(bounds);
// Skip window state persistence when display has invalid dimensions (0x0) or
// is fake (ID 0xFF). Invalid displays could cause incorrect window bounds to
// be saved, leading to positioning issues during restoration.
// https://source.chromium.org/chromium/chromium/src/+/main:ui/display/types/display_constants.h;l=28;drc=e4f1aef5f3ec30a28950d766612cc2c04c822c71
if (hasInvalidDisplay(display)) {
LOG(WARNING)
<< "Window state not saved - no physical display attached or current "
"display has invalid bounds";
return;
}
ScopedDictPrefUpdate update(prefs_, electron::kWindowStates);
const base::Value::Dict* existing_prefs = update->FindDict(window_name_);
// When the window is in a special display mode (fullscreen, kiosk, or
// maximized), save the previously stored window bounds instead of
// the current bounds. This ensures that when the window is restored, it can
// be restored to its original position and size if display mode is not
// preserved via windowStatePersistence.
if (!IsNormal() && existing_prefs) {
std::optional<int> left = existing_prefs->FindInt(electron::kLeft);
std::optional<int> top = existing_prefs->FindInt(electron::kTop);
std::optional<int> right = existing_prefs->FindInt(electron::kRight);
std::optional<int> bottom = existing_prefs->FindInt(electron::kBottom);
if (left && top && right && bottom) {
bounds = gfx::Rect(*left, *top, *right - *left, *bottom - *top);
}
}
base::Value::Dict window_preferences;
window_preferences.Set(electron::kLeft, bounds.x());
window_preferences.Set(electron::kTop, bounds.y());
window_preferences.Set(electron::kRight, bounds.right());
window_preferences.Set(electron::kBottom, bounds.bottom());
window_preferences.Set(electron::kMaximized, IsMaximized());
window_preferences.Set(electron::kFullscreen, IsFullscreen());
window_preferences.Set(electron::kKiosk, IsKiosk());
gfx::Rect work_area = display.work_area();
window_preferences.Set(electron::kWorkAreaLeft, work_area.x());
window_preferences.Set(electron::kWorkAreaTop, work_area.y());
window_preferences.Set(electron::kWorkAreaRight, work_area.right());
window_preferences.Set(electron::kWorkAreaBottom, work_area.bottom());
update->Set(window_name_, std::move(window_preferences));
}
void NativeWindow::FlushWindowState() {
if (save_window_state_timer_.IsRunning()) {
save_window_state_timer_.FireNow();
} else {
SaveWindowState();
}
}
void NativeWindow::RestoreWindowState(const gin_helper::Dictionary& options) {
if (!window_state_persistence_enabled_)
return;
const base::Value& value = prefs_->GetValue(electron::kWindowStates);
const base::Value::Dict* window_preferences =
value.is_dict() ? value.GetDict().FindDict(window_name_) : nullptr;
if (!window_preferences)
return;
std::optional<int> saved_left = window_preferences->FindInt(electron::kLeft);
std::optional<int> saved_top = window_preferences->FindInt(electron::kTop);
std::optional<int> saved_right =
window_preferences->FindInt(electron::kRight);
std::optional<int> saved_bottom =
window_preferences->FindInt(electron::kBottom);
std::optional<int> work_area_left =
window_preferences->FindInt(electron::kWorkAreaLeft);
std::optional<int> work_area_top =
window_preferences->FindInt(electron::kWorkAreaTop);
std::optional<int> work_area_right =
window_preferences->FindInt(electron::kWorkAreaRight);
std::optional<int> work_area_bottom =
window_preferences->FindInt(electron::kWorkAreaBottom);
if (!saved_left || !saved_top || !saved_right || !saved_bottom ||
!work_area_left || !work_area_top || !work_area_right ||
!work_area_bottom) {
LOG(WARNING) << "Window state not restored - corrupted values found";
return;
}
gfx::Rect saved_bounds =
gfx::Rect(*saved_left, *saved_top, *saved_right - *saved_left,
*saved_bottom - *saved_top);
display::Screen* screen = display::Screen::Get();
DCHECK(screen);
// 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)
// could cause incorrect window positioning when later moved to real displays.
// https://source.chromium.org/chromium/chromium/src/+/main:ui/display/types/display_constants.h;l=28;drc=e4f1aef5f3ec30a28950d766612cc2c04c822c71
if (hasInvalidDisplay(display)) {
LOG(WARNING) << "Window state not restored - no physical display attached "
"or current display has invalid bounds";
return;
}
gfx::Rect saved_work_area = gfx::Rect(*work_area_left, *work_area_top,
*work_area_right - *work_area_left,
*work_area_bottom - *work_area_top);
// Set this to true before RestoreBounds to prevent SaveWindowState from being
// inadvertently triggered during the restoration process.
is_being_restored_ = true;
if (restore_bounds_) {
RestoreBounds(display, saved_work_area, saved_bounds);
}
if (restore_display_mode_) {
restore_display_mode_callback_ = base::BindOnce(
[](NativeWindow* window, base::Value::Dict prefs) {
if (auto kiosk = prefs.FindBool(electron::kKiosk); kiosk && *kiosk) {
window->SetKiosk(true);
} else if (auto fs = prefs.FindBool(electron::kFullscreen);
fs && *fs) {
window->SetFullScreen(true);
} else if (auto max = prefs.FindBool(electron::kMaximized);
max && *max) {
window->Maximize();
}
},
base::Unretained(this), window_preferences->Clone());
}
is_being_restored_ = false;
NotifyWindowStateRestored();
}
void NativeWindow::FlushPendingDisplayMode() {
if (restore_display_mode_callback_) {
std::move(restore_display_mode_callback_).Run();
}
}
// This function is similar to Chromium's window bounds adjustment logic
// https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/window_sizer/window_sizer.cc;l=350;drc=0ec56065ba588552f21633aa47280ba02c3cd160
void NativeWindow::RestoreBounds(const display::Display& display,
const gfx::Rect& saved_work_area,
gfx::Rect& saved_bounds) {
if (saved_bounds.width() == 0 || saved_bounds.height() == 0) {
LOG(WARNING) << "Window bounds not restored - values are invalid";
return;
}
// Ensure that the window is at least kMinVisibleHeight * kMinVisibleWidth.
saved_bounds.set_height(std::max(kMinVisibleHeight, saved_bounds.height()));
saved_bounds.set_width(std::max(kMinVisibleWidth, saved_bounds.width()));
const gfx::Rect work_area = display.work_area();
// Ensure that the title bar is not above the work area.
if (saved_bounds.y() < work_area.y()) {
saved_bounds.set_y(work_area.y());
}
// Reposition and resize the bounds if the saved_work_area is different from
// the current work area and the current work area doesn't completely contain
// the bounds.
if (!saved_work_area.IsEmpty() && saved_work_area != work_area &&
!work_area.Contains(saved_bounds)) {
saved_bounds.AdjustToFit(work_area);
}
#if BUILDFLAG(IS_MAC)
// On mac, we want to be aggressive about repositioning windows that are
// partially offscreen. If the window is partially offscreen horizontally,
// snap to the nearest edge of the work area. This call also adjusts the
// height, width if needed to make the window fully visible.
saved_bounds.AdjustToFit(work_area);
#else
// On non-Mac platforms, we are less aggressive about repositioning. Simply
// ensure that at least kMinVisibleWidth * kMinVisibleHeight is visible
const int min_y = work_area.y() + kMinVisibleHeight - saved_bounds.height();
const int min_x = work_area.x() + kMinVisibleWidth - saved_bounds.width();
const int max_y = work_area.bottom() - kMinVisibleHeight;
const int max_x = work_area.right() - kMinVisibleWidth;
// Reposition and resize the bounds to make it fully visible inside the work
// area. `min_x >= max_x` happens when work area and bounds are both small.
if (min_x >= max_x || min_y >= max_y) {
saved_bounds.AdjustToFit(work_area);
} else {
saved_bounds.set_y(std::clamp(saved_bounds.y(), min_y, max_y));
saved_bounds.set_x(std::clamp(saved_bounds.x(), min_x, max_x));
}
#endif // BUILDFLAG(IS_MAC)
SetBounds(saved_bounds);
}
// static
bool NativeWindow::PlatformHasClientFrame() {
#if defined(USE_OZONE)

View File

@@ -17,6 +17,8 @@
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/strings/cstring_view.h"
#include "base/supports_user_data.h"
#include "base/timer/timer.h"
#include "content/public/browser/desktop_media_id.h"
#include "content/public/browser/web_contents_user_data.h"
#include "extensions/browser/app_window/size_constraints.h"
@@ -26,6 +28,7 @@
class SkRegion;
class DraggableRegionProvider;
class PrefService;
namespace input {
struct NativeWebKeyboardEvent;
@@ -164,9 +167,11 @@ class NativeWindow : public views::WidgetDelegate {
void SetTitle(std::string_view title);
[[nodiscard]] std::string GetTitle() const;
[[nodiscard]] std::string GetName() const;
// Ability to augment the window title for the screen readers.
void SetAccessibleTitle(const std::string& title);
std::string GetAccessibleTitle();
[[nodiscard]] std::string GetAccessibleTitle() const;
virtual void FlashFrame(bool flash) = 0;
virtual void SetSkipTaskbar(bool skip) = 0;
@@ -339,6 +344,7 @@ class NativeWindow : public views::WidgetDelegate {
void NotifyNewWindowForTab();
void NotifyWindowSystemContextMenu(int x, int y, bool* prevent_default);
void NotifyLayoutWindowControlsOverlay();
void NotifyWindowStateRestored();
#if BUILDFLAG(IS_WIN)
void NotifyWindowMessage(UINT message, WPARAM w_param, LPARAM l_param);
@@ -430,6 +436,28 @@ class NativeWindow : public views::WidgetDelegate {
[[nodiscard]] auto base_window_id() const { return base_window_id_; }
// Saves current window state to the Local State JSON file in
// app.getPath('userData') via PrefService.
// This does NOT immediately write to disk - it updates the in-memory
// preference store and queues an asynchronous write operation. The actual
// disk write is batched and flushed later.
void SaveWindowState();
void DebouncedSaveWindowState();
// Flushes save_window_state_timer_ that was queued by
// DebouncedSaveWindowState. This does NOT flush the actual disk write.
void FlushWindowState();
// Restores window state - bounds first and then display mode.
void RestoreWindowState(const gin_helper::Dictionary& options);
// Applies saved bounds to the window.
void RestoreBounds(const display::Display& display,
const gfx::Rect& saved_work_area,
gfx::Rect& saved_bounds);
// Flushes pending display mode restoration (fullscreen, maximized, kiosk)
// that was deferred during initialization to respect show=false. This
// consumes and clears the restore_display_mode_callback_.
void FlushPendingDisplayMode();
protected:
NativeWindow(int32_t base_window_id,
const gin_helper::Dictionary& options,
@@ -494,6 +522,10 @@ class NativeWindow : public views::WidgetDelegate {
// ID of the api::BaseWindow that owns this NativeWindow.
const int32_t base_window_id_;
// Identifier for the window provided by the application.
// Used by Electron internally for features such as state persistence.
std::string window_name_;
// The "titleBarStyle" option.
const TitleBarStyle title_bar_style_;
@@ -552,6 +584,32 @@ class NativeWindow : public views::WidgetDelegate {
gfx::Rect overlay_rect_;
// Flag to prevent SaveWindowState calls during window restoration.
bool is_being_restored_ = false;
// The boolean parsing of the "windowStatePersistence" option
bool window_state_persistence_enabled_ = false;
// PrefService is used to persist window bounds and state.
// Only populated when windowStatePersistence is enabled and window has a
// valid name.
raw_ptr<PrefService> prefs_ = nullptr;
// Whether to restore bounds.
bool restore_bounds_ = false;
// Whether to restore display mode.
bool restore_display_mode_ = false;
// Callback to restore display mode.
base::OnceCallback<void()> restore_display_mode_callback_;
// Timer to debounce window state saving operations.
base::OneShotTimer save_window_state_timer_;
// Minimum height of the visible part of a window.
const int kMinVisibleHeight = 100;
// Minimum width of the visible part of a window.
const int kMinVisibleWidth = 100;
base::WeakPtrFactory<NativeWindow> weak_factory_{this};
};

View File

@@ -468,6 +468,8 @@ void NativeWindowMac::Show() {
return;
}
FlushPendingDisplayMode();
set_wants_to_be_visible(true);
// Reattach the window to the parent to actually show it.

View File

@@ -110,6 +110,8 @@ class NativeWindowObserver : public base::CheckedObserver {
virtual void OnExecuteAppCommand(std::string_view command_name) {}
virtual void UpdateWindowControlsOverlay(const gfx::Rect& bounding_rect) {}
virtual void OnWindowStateRestored() {}
};
} // namespace electron

View File

@@ -571,6 +571,8 @@ void NativeWindowViews::Show() {
if (is_modal() && NativeWindow::parent() && !widget()->IsVisible())
static_cast<NativeWindowViews*>(parent())->IncrementChildModals();
FlushPendingDisplayMode();
widget()->native_widget_private()->Show(GetRestoredState(), gfx::Rect());
// explicitly focus the window

View File

@@ -21,6 +21,23 @@ inline constexpr std::string_view kDeviceVendorIdKey = "vendorId";
inline constexpr std::string_view kDeviceProductIdKey = "productId";
inline constexpr std::string_view kDeviceSerialNumberKey = "serialNumber";
// Window state preference keys
inline constexpr std::string_view kLeft = "left";
inline constexpr std::string_view kTop = "top";
inline constexpr std::string_view kRight = "right";
inline constexpr std::string_view kBottom = "bottom";
inline constexpr std::string_view kMaximized = "maximized";
inline constexpr std::string_view kFullscreen = "fullscreen";
inline constexpr std::string_view kKiosk = "kiosk";
inline constexpr std::string_view kWorkAreaLeft = "workAreaLeft";
inline constexpr std::string_view kWorkAreaTop = "workAreaTop";
inline constexpr std::string_view kWorkAreaRight = "workAreaRight";
inline constexpr std::string_view kWorkAreaBottom = "workAreaBottom";
inline constexpr std::string_view kWindowStates = "windowStates";
inline constexpr base::cstring_view kRunAsNode = "ELECTRON_RUN_AS_NODE";
// Per-profile UUID to distinguish global shortcut sessions for

View File

@@ -107,6 +107,19 @@ inline constexpr std::string_view kFocusable = "focusable";
// The WebPreferences.
inline constexpr std::string_view kWebPreferences = "webPreferences";
// Window state persistence for BaseWindow
inline constexpr std::string_view kWindowStatePersistence =
"windowStatePersistence";
// Identifier for the window provided by the application
inline constexpr std::string_view kName = "name";
// Whether to save the window bounds
inline constexpr std::string_view kBounds = "bounds";
// Whether to save the window display mode
inline constexpr std::string_view kDisplayMode = "displayMode";
// Add a vibrancy effect to the browser window
inline constexpr std::string_view kVibrancyType = "vibrancy";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
const { app, BrowserWindow } = require('electron');
const os = require('node:os');
const path = require('node:path');
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
app.setPath('userData', sharedUserData);
app.whenReady().then(() => {
const w = new BrowserWindow({
width: 400,
height: 300,
name: 'test-close-save',
windowStatePersistence: true,
show: false
});
w.on('close', () => {
app.quit();
});
w.close();
// Timeout of 10s to ensure app exits
setTimeout(() => {
app.quit();
}, 10000);
});

View File

@@ -0,0 +1,28 @@
const { app, BrowserWindow } = require('electron');
const os = require('node:os');
const path = require('node:path');
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
app.setPath('userData', sharedUserData);
app.whenReady().then(() => {
const w = new BrowserWindow({
width: 400,
height: 300,
name: 'test-fullscreen-save',
windowStatePersistence: true
});
w.on('enter-full-screen', () => {
setTimeout(() => {
app.quit();
}, 1000);
});
w.setFullScreen(true);
// Timeout of 10s to ensure app exits
setTimeout(() => {
app.quit();
}, 10000);
});

View File

@@ -0,0 +1,28 @@
const { app, BrowserWindow } = require('electron');
const os = require('node:os');
const path = require('node:path');
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
app.setPath('userData', sharedUserData);
app.whenReady().then(() => {
const w = new BrowserWindow({
width: 400,
height: 300,
name: 'test-kiosk-save',
windowStatePersistence: true
});
w.on('enter-full-screen', () => {
setTimeout(() => {
app.quit();
}, 1000);
});
w.setKiosk(true);
// Timeout of 10s to ensure app exits
setTimeout(() => {
app.quit();
}, 10000);
});

View File

@@ -0,0 +1,28 @@
const { app, BrowserWindow } = require('electron');
const os = require('node:os');
const path = require('node:path');
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
app.setPath('userData', sharedUserData);
app.whenReady().then(() => {
const w = new BrowserWindow({
width: 400,
height: 300,
name: 'test-maximize-save',
windowStatePersistence: true
});
w.on('maximize', () => {
setTimeout(() => {
app.quit();
}, 1000);
});
w.maximize();
// Timeout of 10s to ensure app exits
setTimeout(() => {
app.quit();
}, 10000);
});

View File

@@ -0,0 +1,28 @@
const { app, BrowserWindow } = require('electron');
const os = require('node:os');
const path = require('node:path');
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
app.setPath('userData', sharedUserData);
app.whenReady().then(() => {
const w = new BrowserWindow({
width: 400,
height: 300,
name: 'test-minimize-save',
windowStatePersistence: true
});
w.on('minimize', () => {
setTimeout(() => {
app.quit();
}, 1000);
});
w.minimize();
// Timeout of 10s to ensure app exits
setTimeout(() => {
app.quit();
}, 10000);
});

View File

@@ -0,0 +1,23 @@
const { app, BrowserWindow } = require('electron');
const os = require('node:os');
const path = require('node:path');
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
app.setPath('userData', sharedUserData);
app.whenReady().then(() => {
const w = new BrowserWindow({
width: 400,
height: 300,
name: 'test-move-save',
windowStatePersistence: true,
show: false
});
w.setPosition(100, 150);
setTimeout(() => {
app.quit();
}, 1000);
});

View File

@@ -0,0 +1,23 @@
const { app, BrowserWindow } = require('electron');
const os = require('node:os');
const path = require('node:path');
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
app.setPath('userData', sharedUserData);
app.whenReady().then(async () => {
const w = new BrowserWindow({
width: 400,
height: 300,
name: 'test-resize-save',
windowStatePersistence: true,
show: false
});
w.setSize(500, 400);
setTimeout(() => {
app.quit();
}, 1000);
});

View File

@@ -0,0 +1,23 @@
const { app, BrowserWindow } = require('electron');
const os = require('node:os');
const path = require('node:path');
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
app.setPath('userData', sharedUserData);
app.whenReady().then(() => {
const w = new BrowserWindow({
width: 400,
height: 300,
name: 'test-window-state-schema',
windowStatePersistence: true,
show: true
});
w.close();
setTimeout(() => {
app.quit();
}, 1000);
});

View File

@@ -0,0 +1,41 @@
const { app, BrowserWindow, screen } = require('electron');
const os = require('node:os');
const path = require('node:path');
const sharedUserData = path.join(os.tmpdir(), 'electron-window-state-test');
app.setPath('userData', sharedUserData);
app.whenReady().then(async () => {
const primaryDisplay = screen.getPrimaryDisplay();
const workArea = primaryDisplay.workArea;
const maxWidth = Math.max(200, Math.floor(workArea.width * 0.8));
const maxHeight = Math.max(150, Math.floor(workArea.height * 0.8));
const windowWidth = Math.min(400, maxWidth);
const windowHeight = Math.min(300, maxHeight);
const w = new BrowserWindow({
width: windowWidth,
height: windowHeight,
name: 'test-work-area-primary',
windowStatePersistence: true
});
// Center the window on the primary display to prevent overflow
const centerX = workArea.x + Math.floor((workArea.width - windowWidth) / 2);
const centerY = workArea.y + Math.floor((workArea.height - windowHeight) / 2);
w.setPosition(centerX, centerY);
w.on('close', () => {
app.quit();
});
w.close();
// Timeout of 10s to ensure app exits
setTimeout(() => {
app.quit();
}, 10000);
});

View File

@@ -0,0 +1,90 @@
{
"targets": [{
"target_name": "virtual_display",
"conditions": [
['OS=="mac"', {
"sources": [
"src/addon.mm",
"src/VirtualDisplayBridge.m"
],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")",
"include",
"build_swift"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"libraries": [
"<(PRODUCT_DIR)/libVirtualDisplay.dylib"
],
"defines": [
"NODE_ADDON_API_CPP_EXCEPTIONS"
],
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"xcode_settings": {
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
"CLANG_ENABLE_OBJC_ARC": "YES",
"CLANG_CXX_LIBRARY": "libc++",
"SWIFT_OBJC_BRIDGING_HEADER": "include/VirtualDisplayBridge.h",
"SWIFT_VERSION": "5.0",
"SWIFT_OBJC_INTERFACE_HEADER_NAME": "virtual_display-Swift.h",
"MACOSX_DEPLOYMENT_TARGET": "11.0",
"OTHER_CFLAGS": [
"-ObjC++",
"-fobjc-arc"
],
"OTHER_LDFLAGS": [
"-lswiftCore",
"-lswiftFoundation",
"-lswiftObjectiveC",
"-lswiftDarwin",
"-lswiftDispatch",
"-L/usr/lib/swift",
"-Wl,-rpath,/usr/lib/swift",
"-Wl,-rpath,@loader_path"
]
},
"actions": [
{
"action_name": "build_swift",
"inputs": [
"src/VirtualDisplay.swift",
"src/Dummy.swift",
"include/VirtualDisplayBridge.h"
],
"outputs": [
"build_swift/libVirtualDisplay.dylib",
"build_swift/virtual_display-Swift.h"
],
"action": [
"swiftc",
"src/VirtualDisplay.swift",
"src/Dummy.swift",
"-import-objc-header", "include/VirtualDisplayBridge.h",
"-emit-objc-header-path", "./build_swift/virtual_display-Swift.h",
"-emit-library", "-o", "./build_swift/libVirtualDisplay.dylib",
"-emit-module", "-module-name", "virtual_display",
"-module-link-name", "VirtualDisplay"
]
},
{
"action_name": "copy_swift_lib",
"inputs": [
"<(module_root_dir)/build_swift/libVirtualDisplay.dylib"
],
"outputs": [
"<(PRODUCT_DIR)/libVirtualDisplay.dylib"
],
"action": [
"sh",
"-c",
"cp -f <(module_root_dir)/build_swift/libVirtualDisplay.dylib <(PRODUCT_DIR)/libVirtualDisplay.dylib && install_name_tool -id @rpath/libVirtualDisplay.dylib <(PRODUCT_DIR)/libVirtualDisplay.dylib"
]
}
]
}]
]
}]
}

View File

@@ -0,0 +1,121 @@
#ifndef VirtualDisplayBridge_h
#define VirtualDisplayBridge_h
#import <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h>
@interface VirtualDisplayBridge : NSObject
+ (NSInteger)create:(int)width height:(int)height x:(int)x y:(int)y;
+ (BOOL)destroy:(NSInteger)displayId;
+ (BOOL)forceCleanup;
@end
@interface CGVirtualDisplay : NSObject {
unsigned int _vendorID;
unsigned int _productID;
unsigned int _serialNum;
NSString* _name;
struct CGSize _sizeInMillimeters;
unsigned int _maxPixelsWide;
unsigned int _maxPixelsHigh;
struct CGPoint _redPrimary;
struct CGPoint _greenPrimary;
struct CGPoint _bluePrimary;
struct CGPoint _whitePoint;
id _queue;
id _terminationHandler;
void* _client;
unsigned int _displayID;
unsigned int _hiDPI;
NSArray* _modes;
unsigned int _serverRPC_port;
unsigned int _proxyRPC_port;
unsigned int _clientHandler_port;
}
@property(readonly, nonatomic) NSArray* modes;
@property(readonly, nonatomic) unsigned int hiDPI;
@property(readonly, nonatomic) unsigned int displayID;
@property(readonly, nonatomic) id terminationHandler;
@property(readonly, nonatomic) id queue;
@property(readonly, nonatomic) struct CGPoint whitePoint;
@property(readonly, nonatomic) struct CGPoint bluePrimary;
@property(readonly, nonatomic) struct CGPoint greenPrimary;
@property(readonly, nonatomic) struct CGPoint redPrimary;
@property(readonly, nonatomic) unsigned int maxPixelsHigh;
@property(readonly, nonatomic) unsigned int maxPixelsWide;
@property(readonly, nonatomic) struct CGSize sizeInMillimeters;
@property(readonly, nonatomic) NSString* name;
@property(readonly, nonatomic) unsigned int serialNum;
@property(readonly, nonatomic) unsigned int productID;
@property(readonly, nonatomic) unsigned int vendorID;
- (BOOL)applySettings:(id)arg1;
- (void)dealloc;
- (id)initWithDescriptor:(id)arg1;
@end
@interface CGVirtualDisplayDescriptor : NSObject {
unsigned int _vendorID;
unsigned int _productID;
unsigned int _serialNum;
NSString* _name;
struct CGSize _sizeInMillimeters;
unsigned int _maxPixelsWide;
unsigned int _maxPixelsHigh;
struct CGPoint _redPrimary;
struct CGPoint _greenPrimary;
struct CGPoint _bluePrimary;
struct CGPoint _whitePoint;
id _queue;
id _terminationHandler;
}
@property(retain, nonatomic) id queue;
@property(retain, nonatomic) NSString* name;
@property(nonatomic) struct CGPoint whitePoint;
@property(nonatomic) struct CGPoint bluePrimary;
@property(nonatomic) struct CGPoint greenPrimary;
@property(nonatomic) struct CGPoint redPrimary;
@property(nonatomic) unsigned int maxPixelsHigh;
@property(nonatomic) unsigned int maxPixelsWide;
@property(nonatomic) struct CGSize sizeInMillimeters;
@property(nonatomic) unsigned int serialNum;
@property(nonatomic) unsigned int productID;
@property(nonatomic) unsigned int vendorID;
- (void)dealloc;
- (id)init;
@property(copy, nonatomic) id terminationHandler;
@end
@interface CGVirtualDisplayMode : NSObject {
unsigned int _width;
unsigned int _height;
double _refreshRate;
}
@property(readonly, nonatomic) double refreshRate;
@property(readonly, nonatomic) unsigned int height;
@property(readonly, nonatomic) unsigned int width;
- (id)initWithWidth:(unsigned int)arg1
height:(unsigned int)arg2
refreshRate:(double)arg3;
@end
@interface CGVirtualDisplaySettings : NSObject {
NSArray* _modes;
unsigned int _hiDPI;
}
@property(nonatomic) unsigned int hiDPI;
- (void)dealloc;
- (id)init;
@property(retain, nonatomic) NSArray* modes;
@end
#endif

View File

@@ -0,0 +1,7 @@
module.exports = process.platform === 'darwin'
? require('../build/Release/virtual_display.node')
: {
create: () => { throw new Error('Virtual displays only supported on macOS'); },
destroy: () => { throw new Error('Virtual displays only supported on macOS'); },
forceCleanup: () => { throw new Error('Virtual displays only supported on macOS'); }
};

View File

@@ -0,0 +1,20 @@
{
"name": "@electron-ci/virtual-display",
"version": "1.0.0",
"description": "Virtual display for multi-monitor testing",
"main": "./lib/virtual-display.js",
"scripts": {
"clean": "rm -rf build",
"build-electron": "electron-rebuild",
"build": "node-gyp configure && node-gyp build"
},
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"node-addon-api": "^8.3.0"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"node-gyp": "^11.1.0"
}
}

View File

@@ -0,0 +1,181 @@
import Foundation
import Cocoa
import os.log
class DummyManager {
struct DefinedDummy {
var dummy: Dummy
}
static var definedDummies: [Int: DefinedDummy] = [:]
static var dummyCounter: Int = 0
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
}
static func discardDummyByNumber(_ number: Int) {
if let definedDummy = self.definedDummies[number] {
if definedDummy.dummy.isConnected {
definedDummy.dummy.disconnect()
}
}
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 {
let aspectWidth, aspectHeight, multiplierStep, minMultiplier, maxMultiplier: Int
let refreshRates: [Double]
let description: String
let addSeparatorAfter: Bool
init(_ aspectWidth: Int, _ aspectHeight: Int, _ step: Int, _ refreshRates: [Double], _ description: String, _ addSeparatorAfter: Bool = false) {
let minX: Int = 720
let minY: Int = 720
let maxX: Int = 8192
let maxY: Int = 8192
let minMultiplier = max(Int(ceil(Float(minX) / (Float(aspectWidth) * Float(step)))), Int(ceil(Float(minY) / (Float(aspectHeight) * Float(step)))))
let maxMultiplier = min(Int(floor(Float(maxX) / (Float(aspectWidth) * Float(step)))), Int(floor(Float(maxY) / (Float(aspectHeight) * Float(step)))))
self.aspectWidth = aspectWidth
self.aspectHeight = aspectHeight
self.minMultiplier = minMultiplier
self.maxMultiplier = maxMultiplier
self.multiplierStep = step
self.refreshRates = refreshRates
self.description = description
self.addSeparatorAfter = addSeparatorAfter
}
}
class Dummy: Equatable {
var virtualDisplay: CGVirtualDisplay?
var dummyDefinition: DummyDefinition
let serialNum: UInt32
var isConnected: Bool = false
var displayIdentifier: CGDirectDisplayID = 0
static func == (lhs: Dummy, rhs: Dummy) -> Bool {
lhs.serialNum == rhs.serialNum
}
init(dummyDefinition: DummyDefinition, serialNum: UInt32 = 0, doConnect: Bool = true) {
var storedSerialNum: UInt32 = serialNum
if storedSerialNum == 0 {
storedSerialNum = UInt32.random(in: 0 ... UInt32.max)
}
self.dummyDefinition = dummyDefinition
self.serialNum = storedSerialNum
if doConnect {
_ = self.connect()
}
}
func getName() -> String {
"Dummy \(self.dummyDefinition.description.components(separatedBy: " ").first ?? self.dummyDefinition.description)"
}
func connect() -> Bool {
if self.virtualDisplay != nil || self.isConnected {
self.disconnect()
}
let name: String = self.getName()
if let virtualDisplay = Dummy.createVirtualDisplay(self.dummyDefinition, name: name, serialNum: self.serialNum) {
self.virtualDisplay = virtualDisplay
self.displayIdentifier = virtualDisplay.displayID
self.isConnected = true
print("[Dummy.connect:\(#line)] Successfully connected virtual display: \(name)")
return true
} else {
print("[Dummy.connect:\(#line)] Failed to connect virtual display: \(name)")
return false
}
}
func disconnect() {
self.virtualDisplay = nil
self.isConnected = false
print("[Dummy.disconnect:\(#line)] Disconnected virtual display: \(self.getName())")
}
private static func waitForDisplayRegistration(_ displayId: CGDirectDisplayID) -> Bool {
for _ in 0..<20 {
var count: UInt32 = 0, displays = [CGDirectDisplayID](repeating: 0, count: 32)
if CGGetActiveDisplayList(32, &displays, &count) == .success && displays[0..<Int(count)].contains(displayId) {
return true
}
usleep(100000)
}
print("[Dummy.waitForDisplayRegistration:\(#line)] Failed to register virtual display: \(displayId)")
return false
}
static func createVirtualDisplay(_ definition: DummyDefinition, name: String, serialNum: UInt32, hiDPI: Bool = false) -> CGVirtualDisplay? {
if let descriptor = CGVirtualDisplayDescriptor() {
descriptor.queue = DispatchQueue.global(qos: .userInteractive)
descriptor.name = name
descriptor.whitePoint = CGPoint(x: 0.950, y: 1.000)
descriptor.redPrimary = CGPoint(x: 0.454, y: 0.242)
descriptor.greenPrimary = CGPoint(x: 0.353, y: 0.674)
descriptor.bluePrimary = CGPoint(x: 0.157, y: 0.084)
descriptor.maxPixelsWide = UInt32(definition.aspectWidth * definition.multiplierStep * definition.maxMultiplier)
descriptor.maxPixelsHigh = UInt32(definition.aspectHeight * definition.multiplierStep * definition.maxMultiplier)
let diagonalSizeRatio: Double = (24 * 25.4) / sqrt(Double(definition.aspectWidth * definition.aspectWidth + definition.aspectHeight * definition.aspectHeight))
descriptor.sizeInMillimeters = CGSize(width: Double(definition.aspectWidth) * diagonalSizeRatio, height: Double(definition.aspectHeight) * diagonalSizeRatio)
descriptor.serialNum = serialNum
descriptor.productID = UInt32(min(definition.aspectWidth - 1, 255) * 256 + min(definition.aspectHeight - 1, 255))
descriptor.vendorID = UInt32(0xF0F0)
if let display = CGVirtualDisplay(descriptor: descriptor) {
var modes = [CGVirtualDisplayMode?](repeating: nil, count: definition.maxMultiplier - definition.minMultiplier + 1)
for multiplier in definition.minMultiplier ... definition.maxMultiplier {
for refreshRate in definition.refreshRates {
let width = UInt32(definition.aspectWidth * multiplier * definition.multiplierStep)
let height = UInt32(definition.aspectHeight * multiplier * definition.multiplierStep)
modes[multiplier - definition.minMultiplier] = CGVirtualDisplayMode(width: width, height: height, refreshRate: refreshRate)!
}
}
if let settings = CGVirtualDisplaySettings() {
settings.hiDPI = hiDPI ? 1 : 0
settings.modes = modes as [Any]
if display.applySettings(settings) {
return waitForDisplayRegistration(display.displayID) ? display : nil
}
}
}
}
return nil
}
}

View File

@@ -0,0 +1,59 @@
import Foundation
import Cocoa
import os.log
@objc public class VirtualDisplay: NSObject {
@objc public static func create(width: Int, height: Int, x: Int, y: Int) -> Int {
let refreshRates: [Double] = [60.0] // Always 60Hz default
let description = "\(width)x\(height) Display"
let definition = DummyDefinition(width, height, 1, refreshRates, description, false)
let displayId = DummyManager.createDummy(definition) ?? 0
positionDisplay(displayId: displayId, x: x, y: y)
return displayId
}
@objc public static func destroy(id: Int) -> Bool {
DummyManager.discardDummyByNumber(id)
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 {
os_log("VirtualDisplay: Cannot position display %{public}@: display not found or not connected", type: .error, "\(displayId)")
return
}
let cgDisplayId = definedDummy.dummy.displayIdentifier
var config: CGDisplayConfigRef? = nil
let beginResult = CGBeginDisplayConfiguration(&config)
if beginResult != .success {
os_log("VirtualDisplay: Cannot position display, failed to begin display configuration via CGBeginDisplayConfiguration: error %{public}@", type: .error, "\(beginResult.rawValue)")
return
}
let configResult = CGConfigureDisplayOrigin(config, cgDisplayId, Int32(x), Int32(y))
if configResult != .success {
os_log("VirtualDisplay: Cannot position display, failed to configure display origin via CGConfigureDisplayOrigin: error %{public}@", type: .error, "\(configResult.rawValue)")
CGCancelDisplayConfiguration(config)
return
}
let completeResult = CGCompleteDisplayConfiguration(config, .permanently)
if completeResult == .success {
os_log("VirtualDisplay: Successfully positioned display %{public}@ at (%{public}@, %{public}@)", type: .info, "\(displayId)", "\(x)", "\(y)")
} else {
os_log("VirtualDisplay: Cannot position display, failed to complete display configuration via CGCompleteDisplayConfiguration: error %{public}@", type: .error, "\(completeResult.rawValue)")
}
}
}

View File

@@ -0,0 +1,18 @@
#import "VirtualDisplayBridge.h"
#import "../build_swift/virtual_display-Swift.h"
@implementation VirtualDisplayBridge
+ (NSInteger)create:(int)width height:(int)height x:(int)x y:(int)y {
return [VirtualDisplay createWithWidth:width height:height x:x y:y];
}
+ (BOOL)destroy:(NSInteger)displayId {
return [VirtualDisplay destroyWithId:(int)displayId];
}
+ (BOOL)forceCleanup {
return [VirtualDisplay forceCleanup];
}
@end

View File

@@ -0,0 +1,197 @@
#include <js_native_api.h>
#include <node_api.h>
#include "VirtualDisplayBridge.h"
namespace {
typedef struct {
const char* name;
int default_val;
int* ptr;
} PropertySpec;
// Helper function to get an integer property from an object
bool GetIntProperty(napi_env env,
napi_value object,
const char* prop_name,
int* result,
int default_value) {
*result = default_value;
bool has_prop;
if (napi_has_named_property(env, object, prop_name, &has_prop) != napi_ok ||
!has_prop) {
return true;
}
napi_value prop_value;
if (napi_get_named_property(env, object, prop_name, &prop_value) != napi_ok) {
return false;
}
if (napi_get_value_int32(env, prop_value, result) != napi_ok) {
return false;
}
return true;
}
// Helper function to validate and parse object properties
bool ParseObjectProperties(napi_env env,
napi_value object,
PropertySpec props[],
size_t prop_count) {
// Process all properties
for (size_t i = 0; i < prop_count; i++) {
if (!GetIntProperty(env, object, props[i].name, props[i].ptr,
props[i].default_val)) {
char error_msg[50];
snprintf(error_msg, sizeof(error_msg), "%s must be a number",
props[i].name);
napi_throw_error(env, NULL, error_msg);
return false;
}
}
// Check for unknown properties
napi_value prop_names;
uint32_t count;
napi_get_property_names(env, object, &prop_names);
napi_get_array_length(env, prop_names, &count);
for (uint32_t i = 0; i < count; i++) {
napi_value prop_name;
napi_get_element(env, prop_names, i, &prop_name);
size_t len;
char name[20];
napi_get_value_string_utf8(env, prop_name, name, sizeof(name), &len);
bool found = false;
for (size_t j = 0; j < prop_count; j++) {
if (strcmp(name, props[j].name) == 0) {
found = true;
break;
}
}
if (!found) {
napi_throw_error(env, NULL, "Object contains unknown properties");
return false;
}
}
return true;
}
// virtualDisplay.create()
napi_value create(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1];
if (napi_get_cb_info(env, info, &argc, args, NULL, NULL) != napi_ok) {
return NULL;
}
int width = 1920, height = 1080, x = 0, y = 0;
PropertySpec props[] = {{"width", 1920, &width},
{"height", 1080, &height},
{"x", 0, &x},
{"y", 0, &y}};
if (argc >= 1) {
napi_valuetype valuetype;
if (napi_typeof(env, args[0], &valuetype) != napi_ok) {
napi_throw_error(env, NULL, "Failed to get argument type");
return NULL;
}
if (valuetype == napi_object) {
if (!ParseObjectProperties(env, args[0], props,
sizeof(props) / sizeof(props[0]))) {
return NULL;
}
} else {
napi_throw_error(env, NULL, "Expected an object as the argument");
return NULL;
}
}
NSInteger displayId = [VirtualDisplayBridge create:width
height:height
x:x
y:y];
if (displayId == 0) {
napi_throw_error(env, NULL, "Failed to create virtual display");
return NULL;
}
napi_value result;
if (napi_create_int64(env, displayId, &result) != napi_ok) {
return NULL;
}
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;
napi_value args[1];
if (napi_get_cb_info(env, info, &argc, args, NULL, NULL) != napi_ok) {
return NULL;
}
if (argc < 1) {
napi_throw_error(env, NULL, "Expected number argument");
return NULL;
}
int64_t displayId;
if (napi_get_value_int64(env, args[0], &displayId) != napi_ok) {
napi_throw_error(env, NULL, "Expected number argument");
return NULL;
}
BOOL result = [VirtualDisplayBridge destroy:(NSInteger)displayId];
napi_value js_result;
if (napi_get_boolean(env, result, &js_result) != napi_ok) {
return NULL;
}
return js_result;
}
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},
{"forceCleanup", NULL, forceCleanup, NULL, NULL, NULL, napi_default,
NULL}};
if (napi_define_properties(env, exports,
sizeof(descriptors) / sizeof(*descriptors),
descriptors) != napi_ok) {
return NULL;
}
return exports;
}
} // namespace
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -12,6 +12,7 @@
"@electron-ci/is-valid-window": "*",
"@electron-ci/osr-gpu": "*",
"@electron-ci/uv-dlopen": "*",
"@electron-ci/virtual-display": "*",
"@electron/fuses": "^1.8.0",
"@electron/packager": "^18.3.2",
"@types/basic-auth": "^1.1.8",

311
yarn.lock
View File

@@ -345,6 +345,24 @@ __metadata:
languageName: node
linkType: hard
"@babel/code-frame@npm:^7.27.1":
version: 7.29.0
resolution: "@babel/code-frame@npm:7.29.0"
dependencies:
"@babel/helper-validator-identifier": "npm:^7.28.5"
js-tokens: "npm:^4.0.0"
picocolors: "npm:^1.1.1"
checksum: 10c0/d34cc504e7765dfb576a663d97067afb614525806b5cad1a5cc1a7183b916fec8ff57fa233585e3926fd5a9e6b31aae6df91aa81ae9775fb7a28f658d3346f0d
languageName: node
linkType: hard
"@babel/helper-validator-identifier@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/helper-validator-identifier@npm:7.28.5"
checksum: 10c0/42aaebed91f739a41f3d80b72752d1f95fd7c72394e8e4bd7cdd88817e0774d80a432451bcba17c2c642c257c483bf1d409dd4548883429ea9493a3bc4ab0847
languageName: node
linkType: hard
"@datadog/datadog-ci-base@npm:4.1.2":
version: 4.1.2
resolution: "@datadog/datadog-ci-base@npm:4.1.2"
@@ -648,6 +666,17 @@ __metadata:
languageName: unknown
linkType: soft
"@electron-ci/virtual-display@npm:*, @electron-ci/virtual-display@workspace:spec/fixtures/native-addon/virtual-display":
version: 0.0.0-use.local
resolution: "@electron-ci/virtual-display@workspace:spec/fixtures/native-addon/virtual-display"
dependencies:
"@types/jest": "npm:^30.0.0"
bindings: "npm:^1.5.0"
node-addon-api: "npm:^8.3.0"
node-gyp: "npm:^11.1.0"
languageName: unknown
linkType: soft
"@electron/asar@npm:^3.2.13":
version: 3.2.13
resolution: "@electron/asar@npm:3.2.13"
@@ -1136,6 +1165,63 @@ __metadata:
languageName: node
linkType: hard
"@jest/diff-sequences@npm:30.3.0":
version: 30.3.0
resolution: "@jest/diff-sequences@npm:30.3.0"
checksum: 10c0/8922c16a869b839b6c05f677023b3e5a9aa1610ad78a9c5ec8bd6654e35e8136ea1c7b60ad561910e2ad964bfdb0b09b0254ff8dcfacd4562095766f60c63d76
languageName: node
linkType: hard
"@jest/expect-utils@npm:30.3.0":
version: 30.3.0
resolution: "@jest/expect-utils@npm:30.3.0"
dependencies:
"@jest/get-type": "npm:30.1.0"
checksum: 10c0/4bb60fb434cb8ed325735bd39171b61621e110502ecc502089805d203ecb17b9fc5a400aeffb83b41fabcc819628a9c38c955f90a716d6aaff193d10926fc854
languageName: node
linkType: hard
"@jest/get-type@npm:30.1.0":
version: 30.1.0
resolution: "@jest/get-type@npm:30.1.0"
checksum: 10c0/3e65fd5015f551c51ec68fca31bbd25b466be0e8ee8075d9610fa1c686ea1e70a942a0effc7b10f4ea9a338c24337e1ad97ff69d3ebacc4681b7e3e80d1b24ac
languageName: node
linkType: hard
"@jest/pattern@npm:30.0.1":
version: 30.0.1
resolution: "@jest/pattern@npm:30.0.1"
dependencies:
"@types/node": "npm:*"
jest-regex-util: "npm:30.0.1"
checksum: 10c0/32c5a7bfb6c591f004dac0ed36d645002ed168971e4c89bd915d1577031672870032594767557b855c5bc330aa1e39a2f54bf150d2ee88a7a0886e9cb65318bc
languageName: node
linkType: hard
"@jest/schemas@npm:30.0.5":
version: 30.0.5
resolution: "@jest/schemas@npm:30.0.5"
dependencies:
"@sinclair/typebox": "npm:^0.34.0"
checksum: 10c0/449dcd7ec5c6505e9ac3169d1143937e67044ae3e66a729ce4baf31812dfd30535f2b3b2934393c97cfdf5984ff581120e6b38f62b8560c8b5b7cc07f4175f65
languageName: node
linkType: hard
"@jest/types@npm:30.3.0":
version: 30.3.0
resolution: "@jest/types@npm:30.3.0"
dependencies:
"@jest/pattern": "npm:30.0.1"
"@jest/schemas": "npm:30.0.5"
"@types/istanbul-lib-coverage": "npm:^2.0.6"
"@types/istanbul-reports": "npm:^3.0.4"
"@types/node": "npm:*"
"@types/yargs": "npm:^17.0.33"
chalk: "npm:^4.1.2"
checksum: 10c0/c3e3f4de0b77a7ced345f47d3687b1094c1b6c1521529a7ca66a76f9a80194f79179a1dbc32d6761a5b67914a8f78be1e65d1408107efcb1f252c4a63b5ddd92
languageName: node
linkType: hard
"@jridgewell/gen-mapping@npm:^0.3.5":
version: 0.3.5
resolution: "@jridgewell/gen-mapping@npm:0.3.5"
@@ -1684,6 +1770,13 @@ __metadata:
languageName: node
linkType: hard
"@sinclair/typebox@npm:^0.34.0":
version: 0.34.48
resolution: "@sinclair/typebox@npm:0.34.48"
checksum: 10c0/e09f26d8ad471a07ee64004eea7c4ec185349a1f61c03e87e71ea33cbe98e97959940076c2e52968a955ffd4c215bf5ba7035e77079511aac7935f25e989e29d
languageName: node
linkType: hard
"@sindresorhus/is@npm:^4.0.0":
version: 4.6.0
resolution: "@sindresorhus/is@npm:4.6.0"
@@ -1963,6 +2056,41 @@ __metadata:
languageName: node
linkType: hard
"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.6":
version: 2.0.6
resolution: "@types/istanbul-lib-coverage@npm:2.0.6"
checksum: 10c0/3948088654f3eeb45363f1db158354fb013b362dba2a5c2c18c559484d5eb9f6fd85b23d66c0a7c2fcfab7308d0a585b14dadaca6cc8bf89ebfdc7f8f5102fb7
languageName: node
linkType: hard
"@types/istanbul-lib-report@npm:*":
version: 3.0.3
resolution: "@types/istanbul-lib-report@npm:3.0.3"
dependencies:
"@types/istanbul-lib-coverage": "npm:*"
checksum: 10c0/247e477bbc1a77248f3c6de5dadaae85ff86ac2d76c5fc6ab1776f54512a745ff2a5f791d22b942e3990ddbd40f3ef5289317c4fca5741bedfaa4f01df89051c
languageName: node
linkType: hard
"@types/istanbul-reports@npm:^3.0.4":
version: 3.0.4
resolution: "@types/istanbul-reports@npm:3.0.4"
dependencies:
"@types/istanbul-lib-report": "npm:*"
checksum: 10c0/1647fd402aced5b6edac87274af14ebd6b3a85447ef9ad11853a70fd92a98d35f81a5d3ea9fcb5dbb5834e800c6e35b64475e33fcae6bfa9acc70d61497c54ee
languageName: node
linkType: hard
"@types/jest@npm:^30.0.0":
version: 30.0.0
resolution: "@types/jest@npm:30.0.0"
dependencies:
expect: "npm:^30.0.0"
pretty-format: "npm:^30.0.0"
checksum: 10c0/20c6ce574154bc16f8dd6a97afacca4b8c4921a819496a3970382031c509ebe87a1b37b152a1b8475089b82d8ca951a9e95beb4b9bf78fbf579b1536f0b65969
languageName: node
linkType: hard
"@types/json-buffer@npm:~3.0.0":
version: 3.0.0
resolution: "@types/json-buffer@npm:3.0.0"
@@ -2197,6 +2325,13 @@ __metadata:
languageName: node
linkType: hard
"@types/stack-utils@npm:^2.0.3":
version: 2.0.3
resolution: "@types/stack-utils@npm:2.0.3"
checksum: 10c0/1f4658385ae936330581bcb8aa3a066df03867d90281cdf89cc356d404bd6579be0f11902304e1f775d92df22c6dd761d4451c804b0a4fba973e06211e9bd77c
languageName: node
linkType: hard
"@types/stream-chain@npm:*":
version: 2.0.0
resolution: "@types/stream-chain@npm:2.0.0"
@@ -2283,6 +2418,22 @@ __metadata:
languageName: node
linkType: hard
"@types/yargs-parser@npm:*":
version: 21.0.3
resolution: "@types/yargs-parser@npm:21.0.3"
checksum: 10c0/e71c3bd9d0b73ca82e10bee2064c384ab70f61034bbfb78e74f5206283fc16a6d85267b606b5c22cb2a3338373586786fed595b2009825d6a9115afba36560a0
languageName: node
linkType: hard
"@types/yargs@npm:^17.0.33":
version: 17.0.35
resolution: "@types/yargs@npm:17.0.35"
dependencies:
"@types/yargs-parser": "npm:*"
checksum: 10c0/609557826a6b85e73ccf587923f6429850d6dc70e420b455bab4601b670bfadf684b09ae288bccedab042c48ba65f1666133cf375814204b544009f57d6eef63
languageName: node
linkType: hard
"@types/yauzl@npm:^2.9.1":
version: 2.10.0
resolution: "@types/yauzl@npm:2.10.0"
@@ -2909,6 +3060,13 @@ __metadata:
languageName: node
linkType: hard
"ansi-styles@npm:^5.2.0":
version: 5.2.0
resolution: "ansi-styles@npm:5.2.0"
checksum: 10c0/9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df
languageName: node
linkType: hard
"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1":
version: 6.2.1
resolution: "ansi-styles@npm:6.2.1"
@@ -3356,7 +3514,7 @@ __metadata:
languageName: node
linkType: hard
"bindings@npm:^1.2.1":
"bindings@npm:^1.2.1, bindings@npm:^1.5.0":
version: 1.5.0
resolution: "bindings@npm:1.5.0"
dependencies:
@@ -3769,7 +3927,7 @@ __metadata:
languageName: node
linkType: hard
"chalk@npm:^4.1.0, chalk@npm:^4.1.1":
"chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2":
version: 4.1.2
resolution: "chalk@npm:4.1.2"
dependencies:
@@ -3921,6 +4079,13 @@ __metadata:
languageName: node
linkType: hard
"ci-info@npm:^4.2.0":
version: 4.4.0
resolution: "ci-info@npm:4.4.0"
checksum: 10c0/44156201545b8dde01aa8a09ee2fe9fc7a73b1bef9adbd4606c9f61c8caeeb73fb7a575c88b0443f7b4edb5ee45debaa59ed54ba5f99698339393ca01349eb3a
languageName: node
linkType: hard
"cli-cursor@npm:^3.1.0":
version: 3.1.0
resolution: "cli-cursor@npm:3.1.0"
@@ -4750,6 +4915,7 @@ __metadata:
"@electron-ci/is-valid-window": "npm:*"
"@electron-ci/osr-gpu": "npm:*"
"@electron-ci/uv-dlopen": "npm:*"
"@electron-ci/virtual-display": "npm:*"
"@electron/fuses": "npm:^1.8.0"
"@electron/packager": "npm:^18.3.2"
"@types/basic-auth": "npm:^1.1.8"
@@ -5285,6 +5451,13 @@ __metadata:
languageName: node
linkType: hard
"escape-string-regexp@npm:^2.0.0":
version: 2.0.0
resolution: "escape-string-regexp@npm:2.0.0"
checksum: 10c0/2530479fe8db57eace5e8646c9c2a9c80fa279614986d16dcc6bcaceb63ae77f05a851ba6c43756d816c61d7f4534baf56e3c705e3e0d884818a46808811c507
languageName: node
linkType: hard
"escape-string-regexp@npm:^4.0.0":
version: 4.0.0
resolution: "escape-string-regexp@npm:4.0.0"
@@ -5845,6 +6018,20 @@ __metadata:
languageName: node
linkType: hard
"expect@npm:^30.0.0":
version: 30.3.0
resolution: "expect@npm:30.3.0"
dependencies:
"@jest/expect-utils": "npm:30.3.0"
"@jest/get-type": "npm:30.1.0"
jest-matcher-utils: "npm:30.3.0"
jest-message-util: "npm:30.3.0"
jest-mock: "npm:30.3.0"
jest-util: "npm:30.3.0"
checksum: 10c0/a07a157a0c8b3f1e29bfe5ccbf03a3add2c69fe60d1af8a0980053bb6403d721d5f5e4616f1ea5833b747913f8c880c79ce4d98c23a71a2f0c27cf7273892576
languageName: node
linkType: hard
"exponential-backoff@npm:^3.1.1":
version: 3.1.3
resolution: "exponential-backoff@npm:3.1.3"
@@ -8070,6 +8257,79 @@ __metadata:
languageName: node
linkType: hard
"jest-diff@npm:30.3.0":
version: 30.3.0
resolution: "jest-diff@npm:30.3.0"
dependencies:
"@jest/diff-sequences": "npm:30.3.0"
"@jest/get-type": "npm:30.1.0"
chalk: "npm:^4.1.2"
pretty-format: "npm:30.3.0"
checksum: 10c0/573a2a1a155b95fbde547d8ee33a5375179a8d03d4586025478dac16d695e4614aef075c3afa57e0f3a96cea8f638fa68a55c1e625f6e86b4f5b9e5850311ffb
languageName: node
linkType: hard
"jest-matcher-utils@npm:30.3.0":
version: 30.3.0
resolution: "jest-matcher-utils@npm:30.3.0"
dependencies:
"@jest/get-type": "npm:30.1.0"
chalk: "npm:^4.1.2"
jest-diff: "npm:30.3.0"
pretty-format: "npm:30.3.0"
checksum: 10c0/4c5f4b6435964110e64c4b5b42e3553fffe303ecdd68021147a7bcc72914aec3a899867c50db22b250c72aded53e3f7a9f64d83c9dca2e65ce27f36d23c6ca78
languageName: node
linkType: hard
"jest-message-util@npm:30.3.0":
version: 30.3.0
resolution: "jest-message-util@npm:30.3.0"
dependencies:
"@babel/code-frame": "npm:^7.27.1"
"@jest/types": "npm:30.3.0"
"@types/stack-utils": "npm:^2.0.3"
chalk: "npm:^4.1.2"
graceful-fs: "npm:^4.2.11"
picomatch: "npm:^4.0.3"
pretty-format: "npm:30.3.0"
slash: "npm:^3.0.0"
stack-utils: "npm:^2.0.6"
checksum: 10c0/6ce611caef76394872b23a111286b48e56f42655d14a5fbd0629d9b7437ed892e85ad96b15864bc22185c24ef670afb6665c57b9729458a36d50ffe8310f0926
languageName: node
linkType: hard
"jest-mock@npm:30.3.0":
version: 30.3.0
resolution: "jest-mock@npm:30.3.0"
dependencies:
"@jest/types": "npm:30.3.0"
"@types/node": "npm:*"
jest-util: "npm:30.3.0"
checksum: 10c0/9d95d550c6c998a85887c48ff5ee26de4bca18be91462ea8a8135d6023d591132465756f74981ca39b60f8708dfe38213a55bd4b619798a7b9438ca10d718099
languageName: node
linkType: hard
"jest-regex-util@npm:30.0.1":
version: 30.0.1
resolution: "jest-regex-util@npm:30.0.1"
checksum: 10c0/f30c70524ebde2d1012afe5ffa5691d5d00f7d5ba9e43d588f6460ac6fe96f9e620f2f9b36a02d0d3e7e77bc8efb8b3450ae3b80ac53c8be5099e01bf54f6728
languageName: node
linkType: hard
"jest-util@npm:30.3.0":
version: 30.3.0
resolution: "jest-util@npm:30.3.0"
dependencies:
"@jest/types": "npm:30.3.0"
"@types/node": "npm:*"
chalk: "npm:^4.1.2"
ci-info: "npm:^4.2.0"
graceful-fs: "npm:^4.2.11"
picomatch: "npm:^4.0.3"
checksum: 10c0/eea6f39e52a8cb2b1a28bb315a90dc6a8e450fffed73bb5ef4489d02d86f7d91be600d83f1dcba22956b8ac5fefa8f1b250e636c8402d3e8b50a5eec8b5963b2
languageName: node
linkType: hard
"jest-worker@npm:^27.4.5":
version: 27.5.1
resolution: "jest-worker@npm:27.5.1"
@@ -8081,7 +8341,7 @@ __metadata:
languageName: node
linkType: hard
"js-tokens@npm:^3.0.0 || ^4.0.0":
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
version: 4.0.0
resolution: "js-tokens@npm:4.0.0"
checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed
@@ -9654,6 +9914,15 @@ __metadata:
languageName: node
linkType: hard
"node-addon-api@npm:^8.3.0":
version: 8.6.0
resolution: "node-addon-api@npm:8.6.0"
dependencies:
node-gyp: "npm:latest"
checksum: 10c0/869fe4fd13aef4feed3e4ca042136fd677675c061b13cde3b720dcd8e60439efe2538fbab841ed273ab8d5b077e1a0af66011141796589a5db0b5e6b183e2191
languageName: node
linkType: hard
"node-fetch@npm:^2.6.1":
version: 2.6.8
resolution: "node-fetch@npm:2.6.8"
@@ -9693,7 +9962,7 @@ __metadata:
languageName: node
linkType: hard
"node-gyp@npm:^11.4.2, node-gyp@npm:latest":
"node-gyp@npm:^11.1.0, node-gyp@npm:^11.4.2, node-gyp@npm:latest":
version: 11.5.0
resolution: "node-gyp@npm:11.5.0"
dependencies:
@@ -10614,6 +10883,17 @@ __metadata:
languageName: node
linkType: hard
"pretty-format@npm:30.3.0, pretty-format@npm:^30.0.0":
version: 30.3.0
resolution: "pretty-format@npm:30.3.0"
dependencies:
"@jest/schemas": "npm:30.0.5"
ansi-styles: "npm:^5.2.0"
react-is: "npm:^18.3.1"
checksum: 10c0/719b27d70cd8b01013485054c5d094e1fe85e093b09ee73553e3b19302da3cf54fbd6a7ea9577d6471aeff8d372200e56979ffc4c831e2133520bd18060895fb
languageName: node
linkType: hard
"pretty-ms@npm:^9.1.0":
version: 9.1.0
resolution: "pretty-ms@npm:9.1.0"
@@ -10836,6 +11116,13 @@ __metadata:
languageName: node
linkType: hard
"react-is@npm:^18.3.1":
version: 18.3.1
resolution: "react-is@npm:18.3.1"
checksum: 10c0/f2f1e60010c683479e74c63f96b09fb41603527cd131a9959e2aee1e5a8b0caf270b365e5ca77d4a6b18aae659b60a86150bb3979073528877029b35aecd2072
languageName: node
linkType: hard
"read-pkg-up@npm:^2.0.0":
version: 2.0.0
resolution: "read-pkg-up@npm:2.0.0"
@@ -11738,6 +12025,13 @@ __metadata:
languageName: node
linkType: hard
"slash@npm:^3.0.0":
version: 3.0.0
resolution: "slash@npm:3.0.0"
checksum: 10c0/e18488c6a42bdfd4ac5be85b2ced3ccd0224773baae6ad42cfbb9ec74fc07f9fa8396bd35ee638084ead7a2a0818eb5e7151111544d4731ce843019dab4be47b
languageName: node
linkType: hard
"slash@npm:^5.1.0":
version: 5.1.0
resolution: "slash@npm:5.1.0"
@@ -11931,6 +12225,15 @@ __metadata:
languageName: node
linkType: hard
"stack-utils@npm:^2.0.6":
version: 2.0.6
resolution: "stack-utils@npm:2.0.6"
dependencies:
escape-string-regexp: "npm:^2.0.0"
checksum: 10c0/651c9f87667e077584bbe848acaecc6049bc71979f1e9a46c7b920cad4431c388df0f51b8ad7cfd6eed3db97a2878d0fc8b3122979439ea8bac29c61c95eec8a
languageName: node
linkType: hard
"standard-engine@npm:^15.0.0":
version: 15.0.0
resolution: "standard-engine@npm:15.0.0"