mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
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>
This commit is contained in:
committed by
Keeley Hammond
parent
a839fb94aa
commit
2290cf57c2
@@ -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) - An identifier for the window that enables features such as state persistence.
|
||||
* `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. _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.
|
||||
|
||||
4
docs/api/structures/window-state-persistence.md
Normal file
4
docs/api/structures/window-state-persistence.md
Normal 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.
|
||||
@@ -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 = [
|
||||
|
||||
@@ -170,7 +170,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 +261,7 @@ void BaseWindow::OnWindowWillResize(const gfx::Rect& new_bounds,
|
||||
}
|
||||
|
||||
void BaseWindow::OnWindowResize() {
|
||||
window_->DebouncedSaveWindowState();
|
||||
Emit("resize");
|
||||
}
|
||||
|
||||
@@ -276,6 +277,7 @@ void BaseWindow::OnWindowWillMove(const gfx::Rect& new_bounds,
|
||||
}
|
||||
|
||||
void BaseWindow::OnWindowMove() {
|
||||
window_->DebouncedSaveWindowState();
|
||||
Emit("move");
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -9,22 +9,30 @@
|
||||
#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/views/widget/widget.h"
|
||||
|
||||
#if !BUILDFLAG(IS_MAC)
|
||||
@@ -118,6 +126,29 @@ 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)) {
|
||||
// Other options will be parsed here in the future.
|
||||
window_state_persistence_enabled_ = true;
|
||||
} else if (bool flag; options.Get(options::kWindowStatePersistence, &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()) {
|
||||
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()) {
|
||||
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;
|
||||
@@ -245,7 +276,9 @@ void NativeWindow::InitFromOptions(const gin_helper::Dictionary& options) {
|
||||
SetBackgroundColor(background_color);
|
||||
|
||||
SetTitle(options.ValueOrDefault(options::kTitle, Browser::Get()->GetName()));
|
||||
|
||||
// TODO(nilayarya): Save window state after restoration logic is implemented
|
||||
// here.
|
||||
SaveWindowState();
|
||||
// Then show it.
|
||||
if (options.ValueOrDefault(options::kShow, true))
|
||||
Show();
|
||||
@@ -796,6 +829,66 @@ 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 (!prefs_ || window_name_.empty())
|
||||
return;
|
||||
|
||||
ScopedDictPrefUpdate update(prefs_, electron::kWindowStates);
|
||||
const base::Value::Dict* existing_prefs = update->FindDict(window_name_);
|
||||
|
||||
gfx::Rect bounds = GetBounds();
|
||||
// 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());
|
||||
|
||||
const display::Screen* screen = display::Screen::GetScreen();
|
||||
const display::Display display = screen->GetDisplayMatching(bounds);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
bool NativeWindow::PlatformHasClientFrame() {
|
||||
#if defined(USE_OZONE)
|
||||
|
||||
@@ -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;
|
||||
@@ -430,6 +433,17 @@ 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();
|
||||
|
||||
protected:
|
||||
NativeWindow(int32_t base_window_id,
|
||||
const gin_helper::Dictionary& options,
|
||||
@@ -494,6 +508,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 +570,17 @@ class NativeWindow : public views::WidgetDelegate {
|
||||
|
||||
gfx::Rect overlay_rect_;
|
||||
|
||||
// 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;
|
||||
|
||||
// Timer to debounce window state saving operations.
|
||||
base::OneShotTimer save_window_state_timer_;
|
||||
|
||||
base::WeakPtrFactory<NativeWindow> weak_factory_{this};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -7230,4 +7230,459 @@ describe('BrowserWindow module', () => {
|
||||
await screenCapture.expectColorAtCenterMatches(HexColors.BLUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('draggable regions', () => {
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
ifit(hasCapturableScreen())('should allow the window to be dragged when enabled', async () => {
|
||||
// FIXME: nut-js has been removed from npm; we need to find a replacement
|
||||
// WOA fails to load libnut so we're using require to defer loading only
|
||||
// on supported platforms.
|
||||
// "@nut-tree\libnut-win32\build\Release\libnut.node is not a valid Win32 application."
|
||||
// @ts-ignore: nut-js is an optional dependency so it may not be installed
|
||||
const { mouse, straightTo, centerOf, Region, Button } = require('@nut-tree/nut-js') as typeof import('@nut-tree/nut-js');
|
||||
|
||||
const display = screen.getPrimaryDisplay();
|
||||
|
||||
const w = new BrowserWindow({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: display.bounds.width / 2,
|
||||
height: display.bounds.height / 2,
|
||||
frame: false,
|
||||
titleBarStyle: 'hidden'
|
||||
});
|
||||
|
||||
const overlayHTML = path.join(__dirname, 'fixtures', 'pages', 'overlay.html');
|
||||
w.loadFile(overlayHTML);
|
||||
await once(w, 'ready-to-show');
|
||||
|
||||
const winBounds = w.getBounds();
|
||||
const titleBarHeight = 30;
|
||||
const titleBarRegion = new Region(winBounds.x, winBounds.y, winBounds.width, titleBarHeight);
|
||||
const screenRegion = new Region(display.bounds.x, display.bounds.y, display.bounds.width, display.bounds.height);
|
||||
|
||||
const startPos = w.getPosition();
|
||||
|
||||
await mouse.setPosition(await centerOf(titleBarRegion));
|
||||
await mouse.pressButton(Button.LEFT);
|
||||
await mouse.drag(straightTo(centerOf(screenRegion)));
|
||||
|
||||
// Wait for move to complete
|
||||
await Promise.race([
|
||||
once(w, 'move'),
|
||||
setTimeout(100) // fallback for possible race condition
|
||||
]);
|
||||
|
||||
const endPos = w.getPosition();
|
||||
|
||||
expect(startPos).to.not.deep.equal(endPos);
|
||||
});
|
||||
|
||||
ifit(hasCapturableScreen())('should allow the window to be dragged when no WCO and --webkit-app-region: drag enabled', async () => {
|
||||
// FIXME: nut-js has been removed from npm; we need to find a replacement
|
||||
// @ts-ignore: nut-js is an optional dependency so it may not be installed
|
||||
const { mouse, straightTo, centerOf, Region, Button } = require('@nut-tree/nut-js') as typeof import('@nut-tree/nut-js');
|
||||
|
||||
const display = screen.getPrimaryDisplay();
|
||||
const w = new BrowserWindow({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: display.bounds.width / 2,
|
||||
height: display.bounds.height / 2,
|
||||
frame: false
|
||||
});
|
||||
|
||||
const basePageHTML = path.join(__dirname, 'fixtures', 'pages', 'base-page.html');
|
||||
w.loadFile(basePageHTML);
|
||||
await once(w, 'ready-to-show');
|
||||
|
||||
await w.webContents.executeJavaScript(`
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = \`
|
||||
#titlebar {
|
||||
|
||||
background-color: red;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
-webkit-user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1000000000000;
|
||||
}
|
||||
\`;
|
||||
|
||||
const titleBar = document.createElement('title-bar');
|
||||
titleBar.id = 'titlebar';
|
||||
titleBar.textContent = 'test-titlebar';
|
||||
|
||||
document.body.append(style);
|
||||
document.body.append(titleBar);
|
||||
`);
|
||||
// allow time for titlebar to finish loading
|
||||
await setTimeout(2000);
|
||||
|
||||
const winBounds = w.getBounds();
|
||||
const titleBarHeight = 30;
|
||||
const titleBarRegion = new Region(winBounds.x, winBounds.y, winBounds.width, titleBarHeight);
|
||||
const screenRegion = new Region(display.bounds.x, display.bounds.y, display.bounds.width, display.bounds.height);
|
||||
|
||||
const startPos = w.getPosition();
|
||||
await mouse.setPosition(await centerOf(titleBarRegion));
|
||||
await mouse.pressButton(Button.LEFT);
|
||||
await mouse.drag(straightTo(centerOf(screenRegion)));
|
||||
|
||||
// Wait for move to complete
|
||||
await Promise.race([
|
||||
once(w, 'move'),
|
||||
setTimeout(1000) // fallback for possible race condition
|
||||
]);
|
||||
|
||||
const endPos = w.getPosition();
|
||||
|
||||
expect(startPos).to.not.deep.equal(endPos);
|
||||
});
|
||||
});
|
||||
|
||||
describe('windowStatePersistence', () => {
|
||||
describe('save window state', () => {
|
||||
const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'window-state-save');
|
||||
const sharedUserDataPath = path.join(os.tmpdir(), 'electron-window-state-test');
|
||||
const sharedPreferencesPath = path.join(sharedUserDataPath, 'Local State');
|
||||
|
||||
const getWindowStateFromDisk = (windowName: string, preferencesPath: string) => {
|
||||
if (!fs.existsSync(preferencesPath)) {
|
||||
throw new Error(`Preferences file does not exist at path: ${preferencesPath}. Window state was not saved to disk.`);
|
||||
}
|
||||
const prefsContent = fs.readFileSync(preferencesPath, 'utf8');
|
||||
const prefs = JSON.parse(prefsContent);
|
||||
return prefs?.windowStates?.[windowName] || null;
|
||||
};
|
||||
|
||||
// Clean up before each test
|
||||
beforeEach(() => {
|
||||
if (fs.existsSync(sharedUserDataPath)) {
|
||||
fs.rmSync(sharedUserDataPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('state saving after window operations', () => {
|
||||
it('should save window state with required properties', async () => {
|
||||
const appPath = path.join(fixturesPath, 'schema-check');
|
||||
const appProcess = childProcess.spawn(process.execPath, [appPath]);
|
||||
const [code] = await once(appProcess, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
|
||||
const savedState = getWindowStateFromDisk('test-window-state-schema', sharedPreferencesPath);
|
||||
expect(savedState).to.not.be.null('window state with window name "test-window-state-schema" does not exist');
|
||||
expect(savedState).to.have.property('left');
|
||||
expect(savedState).to.have.property('top');
|
||||
expect(savedState).to.have.property('right');
|
||||
expect(savedState).to.have.property('bottom');
|
||||
expect(savedState).to.have.property('maximized');
|
||||
expect(savedState).to.have.property('fullscreen');
|
||||
expect(savedState).to.have.property('kiosk');
|
||||
expect(savedState).to.have.property('workAreaLeft');
|
||||
expect(savedState).to.have.property('workAreaTop');
|
||||
expect(savedState).to.have.property('workAreaRight');
|
||||
expect(savedState).to.have.property('workAreaBottom');
|
||||
});
|
||||
|
||||
ifit(hasCapturableScreen())('should save window state after window is closed and app exit', async () => {
|
||||
const appPath = path.join(fixturesPath, 'close-save');
|
||||
const appProcess = childProcess.spawn(process.execPath, [appPath]);
|
||||
const [code] = await once(appProcess, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
|
||||
const savedState = getWindowStateFromDisk('test-close-save', sharedPreferencesPath);
|
||||
expect(savedState).to.not.be.null('window state with window name "test-close-save" does not exist');
|
||||
expect(savedState.right - savedState.left).to.equal(400);
|
||||
expect(savedState.bottom - savedState.top).to.equal(300);
|
||||
expect(savedState.maximized).to.equal(false);
|
||||
expect(savedState.fullscreen).to.equal(false);
|
||||
expect(savedState.kiosk).to.equal(false);
|
||||
});
|
||||
|
||||
ifit(hasCapturableScreen())('should save window state after window is resized and app exit', async () => {
|
||||
const appPath = path.join(fixturesPath, 'resize-save');
|
||||
const appProcess = childProcess.spawn(process.execPath, [appPath]);
|
||||
const [code] = await once(appProcess, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
|
||||
const savedState = getWindowStateFromDisk('test-resize-save', sharedPreferencesPath);
|
||||
expect(savedState).to.not.be.null('window state with window name "test-resize-save" does not exist');
|
||||
expect(savedState.right - savedState.left).to.equal(500);
|
||||
expect(savedState.bottom - savedState.top).to.equal(400);
|
||||
expect(savedState.maximized).to.equal(false);
|
||||
expect(savedState.fullscreen).to.equal(false);
|
||||
expect(savedState.kiosk).to.equal(false);
|
||||
});
|
||||
|
||||
ifit(hasCapturableScreen())('should save window state after window is moved and app exit', async () => {
|
||||
const appPath = path.join(fixturesPath, 'move-save');
|
||||
const appProcess = childProcess.spawn(process.execPath, [appPath]);
|
||||
const [code] = await once(appProcess, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
|
||||
const savedState = getWindowStateFromDisk('test-move-save', sharedPreferencesPath);
|
||||
expect(savedState).to.not.be.null('window state with window name "test-move-save" does not exist');
|
||||
expect(savedState.left).to.equal(100);
|
||||
expect(savedState.top).to.equal(150);
|
||||
expect(savedState.maximized).to.equal(false);
|
||||
expect(savedState.fullscreen).to.equal(false);
|
||||
expect(savedState.kiosk).to.equal(false);
|
||||
});
|
||||
|
||||
ifit(hasCapturableScreen())('should save window state after window is fullscreened and app exit', async () => {
|
||||
const appPath = path.join(fixturesPath, 'fullscreen-save');
|
||||
const appProcess = childProcess.spawn(process.execPath, [appPath]);
|
||||
const [code] = await once(appProcess, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
|
||||
const savedState = getWindowStateFromDisk('test-fullscreen-save', sharedPreferencesPath);
|
||||
expect(savedState).to.not.be.null('window state with window name "test-fullscreen-save" does not exist');
|
||||
expect(savedState.fullscreen).to.equal(true);
|
||||
expect(savedState.maximized).to.equal(false);
|
||||
expect(savedState.kiosk).to.equal(false);
|
||||
});
|
||||
|
||||
ifit(hasCapturableScreen())('should save window state after window is maximized and app exit', async () => {
|
||||
const appPath = path.join(fixturesPath, 'maximize-save');
|
||||
const appProcess = childProcess.spawn(process.execPath, [appPath]);
|
||||
const [code] = await once(appProcess, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
|
||||
const savedState = getWindowStateFromDisk('test-maximize-save', sharedPreferencesPath);
|
||||
expect(savedState).to.not.be.null('window state with window name "test-maximize-save" does not exist');
|
||||
expect(savedState.maximized).to.equal(true);
|
||||
expect(savedState.fullscreen).to.equal(false);
|
||||
expect(savedState.kiosk).to.equal(false);
|
||||
});
|
||||
|
||||
ifit(hasCapturableScreen())('should save window state if in a minimized state and app exit', async () => {
|
||||
const appPath = path.join(fixturesPath, 'minimize-save');
|
||||
const appProcess = childProcess.spawn(process.execPath, [appPath]);
|
||||
const [code] = await once(appProcess, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
|
||||
const savedState = getWindowStateFromDisk('test-minimize-save', sharedPreferencesPath);
|
||||
expect(savedState).to.not.be.null('window state with window name "test-minimize-save" does not exist');
|
||||
// Should save the bounds from before minimizing
|
||||
expect(savedState.right - savedState.left).to.equal(400);
|
||||
expect(savedState.bottom - savedState.top).to.equal(300);
|
||||
expect(savedState.maximized).to.equal(false);
|
||||
expect(savedState.fullscreen).to.equal(false);
|
||||
expect(savedState.kiosk).to.equal(false);
|
||||
});
|
||||
|
||||
ifit(hasCapturableScreen())('should save window state after window is kiosked and app exit', async () => {
|
||||
const appPath = path.join(fixturesPath, 'kiosk-save');
|
||||
const appProcess = childProcess.spawn(process.execPath, [appPath]);
|
||||
const [code] = await once(appProcess, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
|
||||
const savedState = getWindowStateFromDisk('test-kiosk-save', sharedPreferencesPath);
|
||||
expect(savedState).to.not.be.null('window state with window name "test-kiosk-save" does not exist');
|
||||
expect(savedState.kiosk).to.equal(true);
|
||||
expect(savedState.fullscreen).to.equal(true);
|
||||
expect(savedState.maximized).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('work area tests', () => {
|
||||
ifit(hasCapturableScreen())('should save valid work area bounds', async () => {
|
||||
const appPath = path.join(fixturesPath, 'schema-check');
|
||||
const appProcess = childProcess.spawn(process.execPath, [appPath]);
|
||||
const [code] = await once(appProcess, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
|
||||
const savedState = getWindowStateFromDisk('test-window-state-schema', sharedPreferencesPath);
|
||||
|
||||
expect(savedState).to.not.be.null('window state with window name "test-window-state-schema" does not exist');
|
||||
expect(savedState.workAreaLeft).to.be.a('number');
|
||||
expect(savedState.workAreaTop).to.be.a('number');
|
||||
expect(savedState.workAreaRight).to.be.a('number');
|
||||
expect(savedState.workAreaBottom).to.be.a('number');
|
||||
|
||||
expect(savedState.workAreaLeft).to.be.lessThan(savedState.workAreaRight);
|
||||
expect(savedState.workAreaTop).to.be.lessThan(savedState.workAreaBottom);
|
||||
});
|
||||
|
||||
ifit(hasCapturableScreen())('should save work area bounds that contain the window bounds on primary display', async () => {
|
||||
// Fixture will center the window on the primary display
|
||||
const appPath = path.join(fixturesPath, 'work-area-primary');
|
||||
const appProcess = childProcess.spawn(process.execPath, [appPath]);
|
||||
const [code] = await once(appProcess, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
|
||||
const savedState = getWindowStateFromDisk('test-work-area-primary', sharedPreferencesPath);
|
||||
expect(savedState).to.not.be.null('window state with window name "test-work-area-primary" does not exist');
|
||||
|
||||
expect(savedState.left).to.be.greaterThanOrEqual(savedState.workAreaLeft);
|
||||
expect(savedState.top).to.be.greaterThanOrEqual(savedState.workAreaTop);
|
||||
expect(savedState.right).to.be.lessThanOrEqual(savedState.workAreaRight);
|
||||
expect(savedState.bottom).to.be.lessThanOrEqual(savedState.workAreaBottom);
|
||||
});
|
||||
});
|
||||
|
||||
describe('asynchronous batching behavior', () => {
|
||||
let w: BrowserWindow;
|
||||
const windowName = 'test-batching-behavior';
|
||||
const preferencesPath = path.join(app.getPath('userData'), 'Local State');
|
||||
|
||||
// Helper to get preferences file modification time
|
||||
const getPrefsModTime = (): Date => {
|
||||
try {
|
||||
return fs.statSync(preferencesPath).mtime;
|
||||
} catch {
|
||||
throw new Error(`Test requires preferences file to exist at path: ${preferencesPath}.`);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to wait for file modification with 20 second default timeout
|
||||
const waitForPrefsUpdate = async (initialModTime: Date, timeoutMs: number = 20000): Promise<void> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (true) {
|
||||
const currentModTime = getPrefsModTime();
|
||||
|
||||
if (currentModTime > initialModTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Date.now() - startTime > timeoutMs) {
|
||||
throw new Error(`Window state was not flushed to disk within ${timeoutMs}ms`);
|
||||
}
|
||||
// Wait for 1 second before checking again
|
||||
await setTimeout(1000);
|
||||
}
|
||||
};
|
||||
|
||||
const waitForPrefsFileCreation = async (preferencesPath: string) => {
|
||||
while (!fs.existsSync(preferencesPath)) {
|
||||
await setTimeout(1000);
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await setTimeout(2000);
|
||||
w = new BrowserWindow({
|
||||
show: false,
|
||||
width: 400,
|
||||
height: 300,
|
||||
name: windowName,
|
||||
windowStatePersistence: true
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(closeAllWindows);
|
||||
|
||||
it('should not immediately save window state to disk when window is moved/resized', async () => {
|
||||
// Wait for preferences file to be created if its the first time we're running the test
|
||||
await waitForPrefsFileCreation(preferencesPath);
|
||||
|
||||
const initialModTime = getPrefsModTime();
|
||||
|
||||
const moved = once(w, 'move');
|
||||
w.setPosition(150, 200);
|
||||
await moved;
|
||||
// Wait for any potential save to occur from the move operation
|
||||
await setTimeout(1000);
|
||||
|
||||
const resized = once(w, 'resize');
|
||||
w.setSize(500, 400);
|
||||
await resized;
|
||||
// Wait for any potential save to occur from the resize operation
|
||||
await setTimeout(1000);
|
||||
|
||||
const afterMoveModTime = getPrefsModTime();
|
||||
|
||||
expect(afterMoveModTime.getTime()).to.equal(initialModTime.getTime());
|
||||
});
|
||||
|
||||
it('should eventually flush window state to disk after batching period', async () => {
|
||||
// Wait for preferences file to be created if its the first time we're running the test
|
||||
await waitForPrefsFileCreation(preferencesPath);
|
||||
|
||||
const initialModTime = getPrefsModTime();
|
||||
|
||||
const resized = once(w, 'resize');
|
||||
w.setSize(500, 400);
|
||||
await resized;
|
||||
|
||||
await waitForPrefsUpdate(initialModTime);
|
||||
|
||||
const savedState = getWindowStateFromDisk(windowName, preferencesPath);
|
||||
expect(savedState).to.not.be.null('window state with window name "test-batching-behavior" does not exist');
|
||||
expect(savedState.right - savedState.left).to.equal(500);
|
||||
expect(savedState.bottom - savedState.top).to.equal(400);
|
||||
});
|
||||
|
||||
it('should batch multiple window operations and save final state', async () => {
|
||||
// Wait for preferences file to be created if its the first time we're running the test
|
||||
await waitForPrefsFileCreation(preferencesPath);
|
||||
|
||||
const initialModTime = getPrefsModTime();
|
||||
|
||||
const resize1 = once(w, 'resize');
|
||||
w.setSize(500, 400);
|
||||
await resize1;
|
||||
// Wait for any potential save to occur
|
||||
await setTimeout(1000);
|
||||
|
||||
const afterFirstResize = getPrefsModTime();
|
||||
|
||||
const resize2 = once(w, 'resize');
|
||||
w.setSize(600, 500);
|
||||
await resize2;
|
||||
// Wait for any potential save to occur
|
||||
await setTimeout(1000);
|
||||
|
||||
const afterSecondResize = getPrefsModTime();
|
||||
|
||||
const resize3 = once(w, 'resize');
|
||||
w.setSize(700, 600);
|
||||
await resize3;
|
||||
// Wait for any potential save to occur
|
||||
await setTimeout(1000);
|
||||
|
||||
const afterThirdResize = getPrefsModTime();
|
||||
|
||||
await waitForPrefsUpdate(initialModTime);
|
||||
|
||||
const savedState = getWindowStateFromDisk(windowName, preferencesPath);
|
||||
expect(savedState).to.not.be.null('window state with window name "test-batching-behavior" does not exist');
|
||||
|
||||
[afterFirstResize, afterSecondResize, afterThirdResize].forEach(time => {
|
||||
expect(time.getTime()).to.equal(initialModTime.getTime());
|
||||
});
|
||||
|
||||
expect(savedState.right - savedState.left).to.equal(700);
|
||||
expect(savedState.bottom - savedState.top).to.equal(600);
|
||||
});
|
||||
|
||||
it('should not save window bounds when main thread is busy', async () => {
|
||||
// Wait for preferences file to be created if its the first time we're running the test
|
||||
await waitForPrefsFileCreation(preferencesPath);
|
||||
|
||||
const initialModTime = getPrefsModTime();
|
||||
|
||||
const moved = once(w, 'move');
|
||||
w.setPosition(100, 100);
|
||||
await moved;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Keep main thread busy for 25 seconds
|
||||
while (Date.now() - startTime < 25000);
|
||||
|
||||
const finalModTime = getPrefsModTime();
|
||||
|
||||
expect(finalModTime.getTime()).to.equal(initialModTime.getTime());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
27
spec/fixtures/api/window-state-save/close-save/index.js
vendored
Normal file
27
spec/fixtures/api/window-state-save/close-save/index.js
vendored
Normal 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);
|
||||
});
|
||||
28
spec/fixtures/api/window-state-save/fullscreen-save/index.js
vendored
Normal file
28
spec/fixtures/api/window-state-save/fullscreen-save/index.js
vendored
Normal 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);
|
||||
});
|
||||
28
spec/fixtures/api/window-state-save/kiosk-save/index.js
vendored
Normal file
28
spec/fixtures/api/window-state-save/kiosk-save/index.js
vendored
Normal 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);
|
||||
});
|
||||
28
spec/fixtures/api/window-state-save/maximize-save/index.js
vendored
Normal file
28
spec/fixtures/api/window-state-save/maximize-save/index.js
vendored
Normal 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);
|
||||
});
|
||||
28
spec/fixtures/api/window-state-save/minimize-save/index.js
vendored
Normal file
28
spec/fixtures/api/window-state-save/minimize-save/index.js
vendored
Normal 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);
|
||||
});
|
||||
23
spec/fixtures/api/window-state-save/move-save/index.js
vendored
Normal file
23
spec/fixtures/api/window-state-save/move-save/index.js
vendored
Normal 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);
|
||||
});
|
||||
23
spec/fixtures/api/window-state-save/resize-save/index.js
vendored
Normal file
23
spec/fixtures/api/window-state-save/resize-save/index.js
vendored
Normal 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);
|
||||
});
|
||||
23
spec/fixtures/api/window-state-save/schema-check/index.js
vendored
Normal file
23
spec/fixtures/api/window-state-save/schema-check/index.js
vendored
Normal 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: false
|
||||
});
|
||||
|
||||
w.close();
|
||||
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 1000);
|
||||
});
|
||||
41
spec/fixtures/api/window-state-save/work-area-primary/index.js
vendored
Normal file
41
spec/fixtures/api/window-state-save/work-area-primary/index.js
vendored
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user