@lobehub/lobehub 2.0.0-next.141 → 2.0.0-next.142
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/Dockerfile +2 -0
- package/apps/desktop/src/main/controllers/__tests__/McpInstallCtr.test.ts +286 -0
- package/apps/desktop/src/main/controllers/__tests__/NotificationCtr.test.ts +347 -0
- package/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts +645 -0
- package/apps/desktop/src/main/controllers/__tests__/RemoteServerSyncCtr.test.ts +372 -0
- package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +276 -0
- package/apps/desktop/src/main/controllers/__tests__/UploadFileCtr.test.ts +171 -0
- package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +573 -0
- package/apps/desktop/src/main/core/browser/__tests__/BrowserManager.test.ts +415 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/I18nManager.test.ts +353 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/IoCContainer.test.ts +156 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/ProtocolManager.test.ts +348 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/StaticFileServerManager.test.ts +481 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/StoreManager.test.ts +164 -0
- package/apps/desktop/src/main/core/infrastructure/__tests__/UpdaterManager.test.ts +513 -0
- package/changelog/v1.json +9 -0
- package/docs/self-hosting/environment-variables/model-provider.mdx +31 -0
- package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +30 -0
- package/package.json +1 -1
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +6 -3
- package/src/config/modelProviders/vertexai.ts +1 -1
- package/src/envs/llm.ts +4 -0
- package/src/server/modules/ModelRuntime/index.ts +4 -4
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { App as AppCore } from '../../App';
|
|
4
|
+
import Browser, { BrowserWindowOpts } from '../Browser';
|
|
5
|
+
|
|
6
|
+
// Use vi.hoisted to define mocks before hoisting
|
|
7
|
+
const { mockBrowserWindow, mockNativeTheme, mockIpcMain, mockScreen, MockBrowserWindow } =
|
|
8
|
+
vi.hoisted(() => {
|
|
9
|
+
const mockBrowserWindow = {
|
|
10
|
+
center: vi.fn(),
|
|
11
|
+
close: vi.fn(),
|
|
12
|
+
focus: vi.fn(),
|
|
13
|
+
getBounds: vi.fn().mockReturnValue({ height: 600, width: 800, x: 0, y: 0 }),
|
|
14
|
+
getContentBounds: vi.fn().mockReturnValue({ height: 600, width: 800 }),
|
|
15
|
+
hide: vi.fn(),
|
|
16
|
+
isDestroyed: vi.fn().mockReturnValue(false),
|
|
17
|
+
isFocused: vi.fn().mockReturnValue(true),
|
|
18
|
+
isFullScreen: vi.fn().mockReturnValue(false),
|
|
19
|
+
isMaximized: vi.fn().mockReturnValue(false),
|
|
20
|
+
isVisible: vi.fn().mockReturnValue(true),
|
|
21
|
+
loadFile: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
loadURL: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
maximize: vi.fn(),
|
|
24
|
+
minimize: vi.fn(),
|
|
25
|
+
on: vi.fn(),
|
|
26
|
+
once: vi.fn(),
|
|
27
|
+
setBackgroundColor: vi.fn(),
|
|
28
|
+
setBounds: vi.fn(),
|
|
29
|
+
setFullScreen: vi.fn(),
|
|
30
|
+
setPosition: vi.fn(),
|
|
31
|
+
setTitleBarOverlay: vi.fn(),
|
|
32
|
+
show: vi.fn(),
|
|
33
|
+
unmaximize: vi.fn(),
|
|
34
|
+
webContents: {
|
|
35
|
+
openDevTools: vi.fn(),
|
|
36
|
+
send: vi.fn(),
|
|
37
|
+
session: {
|
|
38
|
+
webRequest: {
|
|
39
|
+
onHeadersReceived: vi.fn(),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
MockBrowserWindow: vi.fn().mockImplementation(() => mockBrowserWindow),
|
|
47
|
+
mockBrowserWindow,
|
|
48
|
+
mockIpcMain: {
|
|
49
|
+
handle: vi.fn(),
|
|
50
|
+
removeHandler: vi.fn(),
|
|
51
|
+
},
|
|
52
|
+
mockNativeTheme: {
|
|
53
|
+
off: vi.fn(),
|
|
54
|
+
on: vi.fn(),
|
|
55
|
+
shouldUseDarkColors: false,
|
|
56
|
+
},
|
|
57
|
+
mockScreen: {
|
|
58
|
+
getDisplayNearestPoint: vi.fn().mockReturnValue({
|
|
59
|
+
workArea: { height: 1080, width: 1920, x: 0, y: 0 },
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Mock electron
|
|
66
|
+
vi.mock('electron', () => ({
|
|
67
|
+
BrowserWindow: MockBrowserWindow,
|
|
68
|
+
ipcMain: mockIpcMain,
|
|
69
|
+
nativeTheme: mockNativeTheme,
|
|
70
|
+
screen: mockScreen,
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
// Mock logger
|
|
74
|
+
vi.mock('@/utils/logger', () => ({
|
|
75
|
+
createLogger: () => ({
|
|
76
|
+
debug: vi.fn(),
|
|
77
|
+
error: vi.fn(),
|
|
78
|
+
info: vi.fn(),
|
|
79
|
+
warn: vi.fn(),
|
|
80
|
+
}),
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
// Mock constants
|
|
84
|
+
vi.mock('@/const/dir', () => ({
|
|
85
|
+
buildDir: '/mock/build',
|
|
86
|
+
preloadDir: '/mock/preload',
|
|
87
|
+
resourcesDir: '/mock/resources',
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
vi.mock('@/const/env', () => ({
|
|
91
|
+
isDev: false,
|
|
92
|
+
isMac: false,
|
|
93
|
+
isWindows: true,
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
vi.mock('@/const/theme', () => ({
|
|
97
|
+
BACKGROUND_DARK: '#1a1a1a',
|
|
98
|
+
BACKGROUND_LIGHT: '#ffffff',
|
|
99
|
+
SYMBOL_COLOR_DARK: '#ffffff',
|
|
100
|
+
SYMBOL_COLOR_LIGHT: '#000000',
|
|
101
|
+
THEME_CHANGE_DELAY: 0,
|
|
102
|
+
TITLE_BAR_HEIGHT: 32,
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
describe('Browser', () => {
|
|
106
|
+
let browser: Browser;
|
|
107
|
+
let mockApp: AppCore;
|
|
108
|
+
let mockStoreManagerGet: ReturnType<typeof vi.fn>;
|
|
109
|
+
let mockStoreManagerSet: ReturnType<typeof vi.fn>;
|
|
110
|
+
let mockNextInterceptor: ReturnType<typeof vi.fn>;
|
|
111
|
+
|
|
112
|
+
const defaultOptions: BrowserWindowOpts = {
|
|
113
|
+
height: 600,
|
|
114
|
+
identifier: 'test-window',
|
|
115
|
+
path: '/test',
|
|
116
|
+
title: 'Test Window',
|
|
117
|
+
width: 800,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
vi.clearAllMocks();
|
|
122
|
+
vi.useFakeTimers();
|
|
123
|
+
|
|
124
|
+
// Reset mock behaviors
|
|
125
|
+
mockBrowserWindow.isDestroyed.mockReturnValue(false);
|
|
126
|
+
mockBrowserWindow.isVisible.mockReturnValue(true);
|
|
127
|
+
mockBrowserWindow.isFocused.mockReturnValue(true);
|
|
128
|
+
mockBrowserWindow.isFullScreen.mockReturnValue(false);
|
|
129
|
+
mockBrowserWindow.loadURL.mockResolvedValue(undefined);
|
|
130
|
+
mockBrowserWindow.loadFile.mockResolvedValue(undefined);
|
|
131
|
+
mockNativeTheme.shouldUseDarkColors = false;
|
|
132
|
+
|
|
133
|
+
// Create mock App
|
|
134
|
+
mockStoreManagerGet = vi.fn().mockReturnValue(undefined);
|
|
135
|
+
mockStoreManagerSet = vi.fn();
|
|
136
|
+
mockNextInterceptor = vi.fn().mockReturnValue(vi.fn());
|
|
137
|
+
|
|
138
|
+
mockApp = {
|
|
139
|
+
browserManager: {
|
|
140
|
+
retrieveByIdentifier: vi.fn(),
|
|
141
|
+
},
|
|
142
|
+
isQuiting: false,
|
|
143
|
+
nextInterceptor: mockNextInterceptor,
|
|
144
|
+
nextServerUrl: 'http://localhost:3000',
|
|
145
|
+
storeManager: {
|
|
146
|
+
get: mockStoreManagerGet,
|
|
147
|
+
set: mockStoreManagerSet,
|
|
148
|
+
},
|
|
149
|
+
} as unknown as AppCore;
|
|
150
|
+
|
|
151
|
+
browser = new Browser(defaultOptions, mockApp);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
vi.useRealTimers();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('constructor', () => {
|
|
159
|
+
it('should set identifier and options', () => {
|
|
160
|
+
expect(browser.identifier).toBe('test-window');
|
|
161
|
+
expect(browser.options).toEqual(defaultOptions);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should create BrowserWindow on construction', () => {
|
|
165
|
+
expect(MockBrowserWindow).toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should setup next interceptor', () => {
|
|
169
|
+
expect(mockNextInterceptor).toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('browserWindow getter', () => {
|
|
174
|
+
it('should return existing window if not destroyed', () => {
|
|
175
|
+
mockBrowserWindow.isDestroyed.mockReturnValue(false);
|
|
176
|
+
|
|
177
|
+
const win1 = browser.browserWindow;
|
|
178
|
+
const win2 = browser.browserWindow;
|
|
179
|
+
|
|
180
|
+
// Should not create a new window
|
|
181
|
+
expect(MockBrowserWindow).toHaveBeenCalledTimes(1);
|
|
182
|
+
expect(win1).toBe(win2);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('webContents getter', () => {
|
|
187
|
+
it('should return webContents when window not destroyed', () => {
|
|
188
|
+
mockBrowserWindow.isDestroyed.mockReturnValue(false);
|
|
189
|
+
|
|
190
|
+
expect(browser.webContents).toBe(mockBrowserWindow.webContents);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should return null when window is destroyed', () => {
|
|
194
|
+
mockBrowserWindow.isDestroyed.mockReturnValue(true);
|
|
195
|
+
|
|
196
|
+
expect(browser.webContents).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('retrieveOrInitialize', () => {
|
|
201
|
+
it('should restore window size from store', () => {
|
|
202
|
+
mockStoreManagerGet.mockImplementation((key: string) => {
|
|
203
|
+
if (key === 'windowSize_test-window') {
|
|
204
|
+
return { height: 700, width: 900 };
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Create new browser to trigger initialization with saved state
|
|
210
|
+
const newBrowser = new Browser(defaultOptions, mockApp);
|
|
211
|
+
|
|
212
|
+
expect(MockBrowserWindow).toHaveBeenCalledWith(
|
|
213
|
+
expect.objectContaining({
|
|
214
|
+
height: 700,
|
|
215
|
+
width: 900,
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should use default size when no saved state', () => {
|
|
221
|
+
mockStoreManagerGet.mockReturnValue(undefined);
|
|
222
|
+
|
|
223
|
+
expect(MockBrowserWindow).toHaveBeenCalledWith(
|
|
224
|
+
expect.objectContaining({
|
|
225
|
+
height: 600,
|
|
226
|
+
width: 800,
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should setup theme listener', () => {
|
|
232
|
+
expect(mockNativeTheme.on).toHaveBeenCalledWith('updated', expect.any(Function));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should setup CORS bypass', () => {
|
|
236
|
+
expect(mockBrowserWindow.webContents.session.webRequest.onHeadersReceived).toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should open devTools when devTools option is true', () => {
|
|
240
|
+
const optionsWithDevTools: BrowserWindowOpts = {
|
|
241
|
+
...defaultOptions,
|
|
242
|
+
devTools: true,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
new Browser(optionsWithDevTools, mockApp);
|
|
246
|
+
|
|
247
|
+
expect(mockBrowserWindow.webContents.openDevTools).toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('theme management', () => {
|
|
252
|
+
describe('getPlatformThemeConfig', () => {
|
|
253
|
+
it('should return Windows dark theme config', () => {
|
|
254
|
+
mockNativeTheme.shouldUseDarkColors = true;
|
|
255
|
+
|
|
256
|
+
// Create browser with dark mode
|
|
257
|
+
const darkBrowser = new Browser(defaultOptions, mockApp);
|
|
258
|
+
|
|
259
|
+
expect(MockBrowserWindow).toHaveBeenCalledWith(
|
|
260
|
+
expect.objectContaining({
|
|
261
|
+
backgroundColor: '#1a1a1a',
|
|
262
|
+
titleBarOverlay: expect.objectContaining({
|
|
263
|
+
color: '#1a1a1a',
|
|
264
|
+
symbolColor: '#ffffff',
|
|
265
|
+
}),
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should return Windows light theme config', () => {
|
|
271
|
+
mockNativeTheme.shouldUseDarkColors = false;
|
|
272
|
+
|
|
273
|
+
expect(MockBrowserWindow).toHaveBeenCalledWith(
|
|
274
|
+
expect.objectContaining({
|
|
275
|
+
backgroundColor: '#ffffff',
|
|
276
|
+
titleBarOverlay: expect.objectContaining({
|
|
277
|
+
color: '#ffffff',
|
|
278
|
+
symbolColor: '#000000',
|
|
279
|
+
}),
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('handleThemeChange', () => {
|
|
286
|
+
it('should reapply visual effects on theme change', () => {
|
|
287
|
+
// Get the theme change handler
|
|
288
|
+
const themeHandler = mockNativeTheme.on.mock.calls.find(
|
|
289
|
+
(call) => call[0] === 'updated',
|
|
290
|
+
)?.[1];
|
|
291
|
+
|
|
292
|
+
expect(themeHandler).toBeDefined();
|
|
293
|
+
|
|
294
|
+
// Trigger theme change
|
|
295
|
+
themeHandler();
|
|
296
|
+
vi.advanceTimersByTime(0);
|
|
297
|
+
|
|
298
|
+
// Should update window background and title bar
|
|
299
|
+
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalled();
|
|
300
|
+
expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalled();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('handleAppThemeChange', () => {
|
|
305
|
+
it('should reapply visual effects', () => {
|
|
306
|
+
browser.handleAppThemeChange();
|
|
307
|
+
vi.advanceTimersByTime(0);
|
|
308
|
+
|
|
309
|
+
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalled();
|
|
310
|
+
expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalled();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('isDarkMode', () => {
|
|
315
|
+
it('should return true when themeMode is dark', () => {
|
|
316
|
+
mockStoreManagerGet.mockImplementation((key: string) => {
|
|
317
|
+
if (key === 'themeMode') return 'dark';
|
|
318
|
+
return undefined;
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const darkBrowser = new Browser(defaultOptions, mockApp);
|
|
322
|
+
// Access private getter through handleAppThemeChange which uses isDarkMode
|
|
323
|
+
darkBrowser.handleAppThemeChange();
|
|
324
|
+
vi.advanceTimersByTime(0);
|
|
325
|
+
|
|
326
|
+
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalledWith('#1a1a1a');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should use system theme when themeMode is auto', () => {
|
|
330
|
+
mockStoreManagerGet.mockImplementation((key: string) => {
|
|
331
|
+
if (key === 'themeMode') return 'auto';
|
|
332
|
+
return undefined;
|
|
333
|
+
});
|
|
334
|
+
mockNativeTheme.shouldUseDarkColors = true;
|
|
335
|
+
|
|
336
|
+
const autoBrowser = new Browser(defaultOptions, mockApp);
|
|
337
|
+
autoBrowser.handleAppThemeChange();
|
|
338
|
+
vi.advanceTimersByTime(0);
|
|
339
|
+
|
|
340
|
+
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalledWith('#1a1a1a');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('loadUrl', () => {
|
|
346
|
+
it('should load full URL successfully', async () => {
|
|
347
|
+
await browser.loadUrl('/test-path');
|
|
348
|
+
|
|
349
|
+
expect(mockBrowserWindow.loadURL).toHaveBeenCalledWith('http://localhost:3000/test-path');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should load error page on failure', async () => {
|
|
353
|
+
mockBrowserWindow.loadURL.mockRejectedValueOnce(new Error('Load failed'));
|
|
354
|
+
|
|
355
|
+
await browser.loadUrl('/test-path');
|
|
356
|
+
|
|
357
|
+
expect(mockBrowserWindow.loadFile).toHaveBeenCalledWith('/mock/resources/error.html');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should setup retry handler on error', async () => {
|
|
361
|
+
mockBrowserWindow.loadURL.mockRejectedValueOnce(new Error('Load failed'));
|
|
362
|
+
|
|
363
|
+
await browser.loadUrl('/test-path');
|
|
364
|
+
|
|
365
|
+
expect(mockIpcMain.removeHandler).toHaveBeenCalledWith('retry-connection');
|
|
366
|
+
expect(mockIpcMain.handle).toHaveBeenCalledWith('retry-connection', expect.any(Function));
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should load fallback HTML when error page fails', async () => {
|
|
370
|
+
mockBrowserWindow.loadURL.mockRejectedValueOnce(new Error('Load failed'));
|
|
371
|
+
mockBrowserWindow.loadFile.mockRejectedValueOnce(new Error('Error page failed'));
|
|
372
|
+
mockBrowserWindow.loadURL.mockResolvedValueOnce(undefined);
|
|
373
|
+
|
|
374
|
+
await browser.loadUrl('/test-path');
|
|
375
|
+
|
|
376
|
+
expect(mockBrowserWindow.loadURL).toHaveBeenCalledWith(
|
|
377
|
+
expect.stringContaining('data:text/html'),
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe('loadPlaceholder', () => {
|
|
383
|
+
it('should load splash screen', async () => {
|
|
384
|
+
await browser.loadPlaceholder();
|
|
385
|
+
|
|
386
|
+
expect(mockBrowserWindow.loadFile).toHaveBeenCalledWith('/mock/resources/splash.html');
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe('window operations', () => {
|
|
391
|
+
describe('show', () => {
|
|
392
|
+
it('should show window', () => {
|
|
393
|
+
browser.show();
|
|
394
|
+
|
|
395
|
+
expect(mockBrowserWindow.show).toHaveBeenCalled();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe('hide', () => {
|
|
400
|
+
it('should hide window', () => {
|
|
401
|
+
mockBrowserWindow.isFullScreen.mockReturnValue(false);
|
|
402
|
+
|
|
403
|
+
browser.hide();
|
|
404
|
+
|
|
405
|
+
expect(mockBrowserWindow.hide).toHaveBeenCalled();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe('close', () => {
|
|
410
|
+
it('should close window', () => {
|
|
411
|
+
browser.close();
|
|
412
|
+
|
|
413
|
+
expect(mockBrowserWindow.close).toHaveBeenCalled();
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe('moveToCenter', () => {
|
|
418
|
+
it('should center window', () => {
|
|
419
|
+
browser.moveToCenter();
|
|
420
|
+
|
|
421
|
+
expect(mockBrowserWindow.center).toHaveBeenCalled();
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe('setWindowSize', () => {
|
|
426
|
+
it('should set window bounds', () => {
|
|
427
|
+
browser.setWindowSize({ height: 700, width: 900 });
|
|
428
|
+
|
|
429
|
+
expect(mockBrowserWindow.setBounds).toHaveBeenCalledWith({
|
|
430
|
+
height: 700,
|
|
431
|
+
width: 900,
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should use current size for missing dimensions', () => {
|
|
436
|
+
mockBrowserWindow.getBounds.mockReturnValue({ height: 600, width: 800 });
|
|
437
|
+
|
|
438
|
+
browser.setWindowSize({ width: 900 });
|
|
439
|
+
|
|
440
|
+
expect(mockBrowserWindow.setBounds).toHaveBeenCalledWith({
|
|
441
|
+
height: 600,
|
|
442
|
+
width: 900,
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe('toggleVisible', () => {
|
|
448
|
+
it('should hide when visible and focused', () => {
|
|
449
|
+
mockBrowserWindow.isVisible.mockReturnValue(true);
|
|
450
|
+
mockBrowserWindow.isFocused.mockReturnValue(true);
|
|
451
|
+
|
|
452
|
+
browser.toggleVisible();
|
|
453
|
+
|
|
454
|
+
expect(mockBrowserWindow.hide).toHaveBeenCalled();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('should show and focus when not visible', () => {
|
|
458
|
+
mockBrowserWindow.isVisible.mockReturnValue(false);
|
|
459
|
+
|
|
460
|
+
browser.toggleVisible();
|
|
461
|
+
|
|
462
|
+
expect(mockBrowserWindow.show).toHaveBeenCalled();
|
|
463
|
+
expect(mockBrowserWindow.focus).toHaveBeenCalled();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('should show and focus when visible but not focused', () => {
|
|
467
|
+
mockBrowserWindow.isVisible.mockReturnValue(true);
|
|
468
|
+
mockBrowserWindow.isFocused.mockReturnValue(false);
|
|
469
|
+
|
|
470
|
+
browser.toggleVisible();
|
|
471
|
+
|
|
472
|
+
expect(mockBrowserWindow.show).toHaveBeenCalled();
|
|
473
|
+
expect(mockBrowserWindow.focus).toHaveBeenCalled();
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
describe('broadcast', () => {
|
|
479
|
+
it('should send message to webContents', () => {
|
|
480
|
+
browser.broadcast('updateAvailable' as any, { version: '1.0.0' } as any);
|
|
481
|
+
|
|
482
|
+
expect(mockBrowserWindow.webContents.send).toHaveBeenCalledWith('updateAvailable', {
|
|
483
|
+
version: '1.0.0',
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should not send when window is destroyed', () => {
|
|
488
|
+
mockBrowserWindow.isDestroyed.mockReturnValue(true);
|
|
489
|
+
|
|
490
|
+
browser.broadcast('updateAvailable' as any);
|
|
491
|
+
|
|
492
|
+
expect(mockBrowserWindow.webContents.send).not.toHaveBeenCalled();
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe('destroy', () => {
|
|
497
|
+
it('should cleanup theme listener', () => {
|
|
498
|
+
browser.destroy();
|
|
499
|
+
|
|
500
|
+
expect(mockNativeTheme.off).toHaveBeenCalledWith('updated', expect.any(Function));
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe('close event handling', () => {
|
|
505
|
+
let closeHandler: (e: any) => void;
|
|
506
|
+
|
|
507
|
+
beforeEach(() => {
|
|
508
|
+
// Get the close handler registered during initialization
|
|
509
|
+
closeHandler = mockBrowserWindow.on.mock.calls.find((call) => call[0] === 'close')?.[1];
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('should save window size and allow close when app is quitting', () => {
|
|
513
|
+
(mockApp as any).isQuiting = true;
|
|
514
|
+
const mockEvent = { preventDefault: vi.fn() };
|
|
515
|
+
|
|
516
|
+
closeHandler(mockEvent);
|
|
517
|
+
|
|
518
|
+
expect(mockStoreManagerSet).toHaveBeenCalledWith('windowSize_test-window', {
|
|
519
|
+
height: 600,
|
|
520
|
+
width: 800,
|
|
521
|
+
});
|
|
522
|
+
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should hide instead of close when keepAlive is true', () => {
|
|
526
|
+
const keepAliveOptions: BrowserWindowOpts = {
|
|
527
|
+
...defaultOptions,
|
|
528
|
+
keepAlive: true,
|
|
529
|
+
};
|
|
530
|
+
const keepAliveBrowser = new Browser(keepAliveOptions, mockApp);
|
|
531
|
+
|
|
532
|
+
// Get the new close handler
|
|
533
|
+
const keepAliveCloseHandler = mockBrowserWindow.on.mock.calls
|
|
534
|
+
.filter((call) => call[0] === 'close')
|
|
535
|
+
.pop()?.[1];
|
|
536
|
+
|
|
537
|
+
const mockEvent = { preventDefault: vi.fn() };
|
|
538
|
+
keepAliveCloseHandler(mockEvent);
|
|
539
|
+
|
|
540
|
+
expect(mockEvent.preventDefault).toHaveBeenCalled();
|
|
541
|
+
expect(mockBrowserWindow.hide).toHaveBeenCalled();
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should save size and allow close when keepAlive is false', () => {
|
|
545
|
+
const mockEvent = { preventDefault: vi.fn() };
|
|
546
|
+
|
|
547
|
+
closeHandler(mockEvent);
|
|
548
|
+
|
|
549
|
+
expect(mockStoreManagerSet).toHaveBeenCalledWith('windowSize_test-window', {
|
|
550
|
+
height: 600,
|
|
551
|
+
width: 800,
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
describe('reapplyVisualEffects', () => {
|
|
557
|
+
it('should apply visual effects', () => {
|
|
558
|
+
browser.reapplyVisualEffects();
|
|
559
|
+
|
|
560
|
+
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalled();
|
|
561
|
+
expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalled();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('should not apply when window is destroyed', () => {
|
|
565
|
+
mockBrowserWindow.isDestroyed.mockReturnValue(true);
|
|
566
|
+
mockBrowserWindow.setBackgroundColor.mockClear();
|
|
567
|
+
|
|
568
|
+
browser.reapplyVisualEffects();
|
|
569
|
+
|
|
570
|
+
expect(mockBrowserWindow.setBackgroundColor).not.toHaveBeenCalled();
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
});
|