mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-08-26 09:02:23 +00:00
402 lines
11 KiB
TypeScript
402 lines
11 KiB
TypeScript
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
captureWindowState,
|
|
fitPreferredWindowSize,
|
|
getWindowStatePath,
|
|
loadWindowStartupState,
|
|
parseWindowState,
|
|
persistWindowState,
|
|
readWindowState,
|
|
resolveWindowStartupState,
|
|
WINDOW_STATE_VERSION,
|
|
type PersistedWindowStateV2,
|
|
type WindowDisplayLike,
|
|
type WindowStateIO,
|
|
} from '../../../electron/main/windowState';
|
|
|
|
const LARGE_PRIMARY: WindowDisplayLike = {
|
|
id: 1,
|
|
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
|
|
workArea: { x: 0, y: 0, width: 1920, height: 1080 },
|
|
};
|
|
|
|
const NEGATIVE_SECONDARY: WindowDisplayLike = {
|
|
id: 2,
|
|
bounds: { x: -1600, y: 0, width: 1600, height: 900 },
|
|
workArea: { x: -1600, y: 23, width: 1600, height: 877 },
|
|
};
|
|
|
|
function persisted(
|
|
bounds: PersistedWindowStateV2['bounds'],
|
|
isMaximized = false
|
|
): PersistedWindowStateV2 {
|
|
return { version: WINDOW_STATE_VERSION, bounds, isMaximized };
|
|
}
|
|
|
|
function createMemoryIO(initialFiles: Record<string, string> = {}) {
|
|
const files = new Map(Object.entries(initialFiles));
|
|
const operations: string[] = [];
|
|
let renameError: Error | null = null;
|
|
|
|
const io: WindowStateIO = {
|
|
readText(filePath) {
|
|
operations.push(`read:${filePath}`);
|
|
const contents = files.get(filePath);
|
|
if (contents === undefined) throw new Error('ENOENT');
|
|
return contents;
|
|
},
|
|
writeText(filePath, contents) {
|
|
operations.push(`write:${filePath}`);
|
|
files.set(filePath, contents);
|
|
},
|
|
rename(fromPath, toPath) {
|
|
operations.push(`rename:${fromPath}->${toPath}`);
|
|
if (renameError) throw renameError;
|
|
const contents = files.get(fromPath);
|
|
if (contents === undefined) throw new Error('Missing temporary file');
|
|
files.set(toPath, contents);
|
|
files.delete(fromPath);
|
|
},
|
|
remove(filePath) {
|
|
operations.push(`remove:${filePath}`);
|
|
files.delete(filePath);
|
|
},
|
|
};
|
|
|
|
return {
|
|
io,
|
|
files,
|
|
operations,
|
|
failRename(error: Error) {
|
|
renameError = error;
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('window state schema', () => {
|
|
it('accepts a versioned state and rounds integer-like values', () => {
|
|
expect(
|
|
parseWindowState({
|
|
version: 2,
|
|
bounds: {
|
|
x: -1200.0004,
|
|
y: 20.0004,
|
|
width: 1280.0004,
|
|
height: 960.0004,
|
|
},
|
|
isMaximized: true,
|
|
})
|
|
).toEqual({
|
|
version: 2,
|
|
bounds: { x: -1200, y: 20, width: 1280, height: 960 },
|
|
isMaximized: true,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
version: 1,
|
|
bounds: { x: 0, y: 0, width: 100, height: 100 },
|
|
isMaximized: false,
|
|
},
|
|
{
|
|
version: 2,
|
|
bounds: { x: 0.25, y: 0, width: 100, height: 100 },
|
|
isMaximized: false,
|
|
},
|
|
{
|
|
version: 2,
|
|
bounds: { x: 0, y: 0, width: 0, height: 100 },
|
|
isMaximized: false,
|
|
},
|
|
{
|
|
version: 2,
|
|
bounds: { x: 0, y: 0, width: Number.NaN, height: 100 },
|
|
isMaximized: false,
|
|
},
|
|
{
|
|
version: 2,
|
|
bounds: { x: 0, y: 0, width: 100, height: Number.POSITIVE_INFINITY },
|
|
isMaximized: false,
|
|
},
|
|
{
|
|
version: 2,
|
|
bounds: { x: '0', y: 0, width: 100, height: 100 },
|
|
isMaximized: false,
|
|
},
|
|
{
|
|
version: 2,
|
|
bounds: { x: 0, y: 0, width: 100, height: 100 },
|
|
isMaximized: 'false',
|
|
},
|
|
])('rejects invalid state %#', (value) => {
|
|
expect(parseWindowState(value)).toBeNull();
|
|
});
|
|
|
|
it('treats missing, malformed, and unknown-version files as no state', () => {
|
|
const statePath = getWindowStatePath('/user-data');
|
|
const missing = createMemoryIO();
|
|
const malformed = createMemoryIO({ [statePath]: '{bad json' });
|
|
const unknown = createMemoryIO({
|
|
[statePath]: JSON.stringify({
|
|
version: 99,
|
|
bounds: { x: 0, y: 0, width: 1200, height: 800 },
|
|
isMaximized: false,
|
|
}),
|
|
});
|
|
|
|
expect(readWindowState('/user-data', missing.io)).toBeNull();
|
|
expect(readWindowState('/user-data', malformed.io)).toBeNull();
|
|
expect(readWindowState('/user-data', unknown.io)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('window startup placement', () => {
|
|
it('uses 1440x810 whenever there are no persisted bounds', () => {
|
|
const startup = resolveWindowStartupState({
|
|
persistedState: null,
|
|
displays: [LARGE_PRIMARY],
|
|
primaryDisplay: LARGE_PRIMARY,
|
|
});
|
|
|
|
expect(startup).toEqual({
|
|
bounds: { x: 240, y: 135, width: 1440, height: 810 },
|
|
minimumSize: { width: 1100, height: 700 },
|
|
shouldMaximize: false,
|
|
source: 'default',
|
|
});
|
|
});
|
|
|
|
it('caps defaults near 94% and lets fitting shrink achievable minimums', () => {
|
|
const mediumWorkArea = { x: 0, y: 0, width: 1200, height: 800 };
|
|
expect(
|
|
fitPreferredWindowSize({ width: 1440, height: 810 }, mediumWorkArea)
|
|
).toEqual({
|
|
size: { width: 1128, height: 634 },
|
|
minimumSize: { width: 1100, height: 634 },
|
|
});
|
|
|
|
const tinyDisplay: WindowDisplayLike = {
|
|
id: 3,
|
|
bounds: { x: 50, y: 25, width: 1000, height: 650 },
|
|
workArea: { x: 50, y: 25, width: 1000, height: 650 },
|
|
};
|
|
const startup = resolveWindowStartupState({
|
|
persistedState: null,
|
|
displays: [tinyDisplay],
|
|
primaryDisplay: tinyDisplay,
|
|
});
|
|
|
|
expect(startup.bounds).toEqual({
|
|
x: 80,
|
|
y: 86,
|
|
width: 940,
|
|
height: 528,
|
|
});
|
|
expect(startup.minimumSize).toEqual({ width: 940, height: 528 });
|
|
});
|
|
|
|
it('allows persisted user bounds to occupy the full work area', () => {
|
|
const startup = resolveWindowStartupState({
|
|
persistedState: persisted({
|
|
x: 0,
|
|
y: 0,
|
|
width: 1920,
|
|
height: 1080,
|
|
}),
|
|
displays: [LARGE_PRIMARY],
|
|
primaryDisplay: LARGE_PRIMARY,
|
|
});
|
|
|
|
expect(startup.bounds).toEqual({
|
|
x: 0,
|
|
y: 0,
|
|
width: 1920,
|
|
height: 1080,
|
|
});
|
|
});
|
|
|
|
it('selects a negative-coordinate display and clamps the full window into its work area', () => {
|
|
const startup = resolveWindowStartupState({
|
|
persistedState: persisted({
|
|
x: -1700,
|
|
y: -50,
|
|
width: 1300,
|
|
height: 900,
|
|
}),
|
|
displays: [LARGE_PRIMARY, NEGATIVE_SECONDARY],
|
|
primaryDisplay: LARGE_PRIMARY,
|
|
});
|
|
|
|
expect(startup).toEqual({
|
|
bounds: { x: -1600, y: 23, width: 1300, height: 877 },
|
|
minimumSize: { width: 1100, height: 700 },
|
|
shouldMaximize: false,
|
|
source: 'persisted',
|
|
});
|
|
});
|
|
|
|
it('centers state from a disconnected display on the primary work area', () => {
|
|
const startup = resolveWindowStartupState({
|
|
persistedState: persisted(
|
|
{ x: 5000, y: 4000, width: 1200, height: 800 },
|
|
true
|
|
),
|
|
displays: [LARGE_PRIMARY, NEGATIVE_SECONDARY],
|
|
primaryDisplay: LARGE_PRIMARY,
|
|
});
|
|
|
|
expect(startup.bounds).toEqual({
|
|
x: 360,
|
|
y: 140,
|
|
width: 1200,
|
|
height: 800,
|
|
});
|
|
expect(startup.shouldMaximize).toBe(true);
|
|
expect(startup.source).toBe('persisted');
|
|
});
|
|
|
|
it('expands undersized saved bounds to the effective minimum without overflowing', () => {
|
|
const smallDisplay: WindowDisplayLike = {
|
|
id: 4,
|
|
bounds: { x: -1000, y: -650, width: 1000, height: 650 },
|
|
workArea: { x: -1000, y: -650, width: 1000, height: 650 },
|
|
};
|
|
const startup = resolveWindowStartupState({
|
|
persistedState: persisted({
|
|
x: -900,
|
|
y: -600,
|
|
width: 400,
|
|
height: 300,
|
|
}),
|
|
displays: [smallDisplay],
|
|
primaryDisplay: smallDisplay,
|
|
});
|
|
|
|
expect(startup.bounds).toEqual({
|
|
x: -1000,
|
|
y: -650,
|
|
width: 1000,
|
|
height: 650,
|
|
});
|
|
expect(startup.minimumSize).toEqual({ width: 1000, height: 650 });
|
|
});
|
|
|
|
it('loads valid disk state and falls back to 1440x810 when disk state is corrupt', () => {
|
|
const statePath = getWindowStatePath('/user-data');
|
|
const validIO = createMemoryIO({
|
|
[statePath]: JSON.stringify(
|
|
persisted({ x: 100, y: 120, width: 1300, height: 800 })
|
|
),
|
|
});
|
|
const corruptIO = createMemoryIO({ [statePath]: 'not-json' });
|
|
|
|
expect(
|
|
loadWindowStartupState({
|
|
userDataPath: '/user-data',
|
|
io: validIO.io,
|
|
displays: [LARGE_PRIMARY],
|
|
primaryDisplay: LARGE_PRIMARY,
|
|
}).source
|
|
).toBe('persisted');
|
|
expect(
|
|
loadWindowStartupState({
|
|
userDataPath: '/user-data',
|
|
io: corruptIO.io,
|
|
displays: [LARGE_PRIMARY],
|
|
primaryDisplay: LARGE_PRIMARY,
|
|
})
|
|
).toMatchObject({
|
|
bounds: { width: 1440, height: 810 },
|
|
source: 'default',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('window state persistence', () => {
|
|
it('captures normal bounds and maximized state without minimized/fullscreen fields', () => {
|
|
const source = {
|
|
getNormalBounds: vi.fn(() => ({
|
|
x: -100.0004,
|
|
y: 20,
|
|
width: 1280,
|
|
height: 960,
|
|
})),
|
|
isMaximized: vi.fn(() => true),
|
|
isMinimized: vi.fn(() => true),
|
|
isFullScreen: vi.fn(() => true),
|
|
};
|
|
|
|
const state = captureWindowState(source);
|
|
|
|
expect(state).toEqual({
|
|
version: 2,
|
|
bounds: { x: -100, y: 20, width: 1280, height: 960 },
|
|
isMaximized: true,
|
|
});
|
|
expect(state).not.toHaveProperty('isMinimized');
|
|
expect(state).not.toHaveProperty('isFullScreen');
|
|
});
|
|
|
|
it('writes a versioned JSON file through temp then rename', () => {
|
|
const memory = createMemoryIO();
|
|
const state = persisted({ x: 10, y: 20, width: 1280, height: 960 });
|
|
|
|
const saved = persistWindowState(
|
|
'/user-data',
|
|
{
|
|
getNormalBounds: () => state.bounds,
|
|
isMaximized: () => state.isMaximized,
|
|
},
|
|
memory.io
|
|
);
|
|
|
|
const statePath = getWindowStatePath('/user-data');
|
|
expect(saved).toEqual(state);
|
|
expect(memory.operations).toEqual([
|
|
`write:${statePath}.tmp`,
|
|
`rename:${statePath}.tmp->${statePath}`,
|
|
]);
|
|
expect(memory.files.has(`${statePath}.tmp`)).toBe(false);
|
|
expect(JSON.parse(memory.files.get(statePath)!)).toEqual(state);
|
|
});
|
|
|
|
it('removes a temporary file and preserves the original state when rename fails', () => {
|
|
const statePath = getWindowStatePath('/user-data');
|
|
const memory = createMemoryIO({ [statePath]: 'original-state' });
|
|
memory.failRename(new Error('rename failed'));
|
|
|
|
expect(() =>
|
|
persistWindowState(
|
|
'/user-data',
|
|
{
|
|
getNormalBounds: () => ({
|
|
x: 10,
|
|
y: 20,
|
|
width: 1280,
|
|
height: 960,
|
|
}),
|
|
isMaximized: () => false,
|
|
},
|
|
memory.io
|
|
)
|
|
).toThrow('rename failed');
|
|
expect(memory.files.get(statePath)).toBe('original-state');
|
|
expect(memory.files.has(`${statePath}.tmp`)).toBe(false);
|
|
expect(memory.operations.at(-1)).toBe(`remove:${statePath}.tmp`);
|
|
});
|
|
});
|