@lobehub/lobehub 2.0.0-next.140 → 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 +50 -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 +18 -0
- package/docs/development/database-schema.dbml +1 -2
- 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/database/migrations/0055_rename_phone_number_to_phone.sql +4 -0
- package/packages/database/migrations/meta/0055_snapshot.json +8396 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/core/migrations.json +11 -0
- package/packages/database/src/schemas/user.ts +1 -2
- 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,415 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { App as AppCore } from '../../App';
|
|
4
|
+
import { BrowserManager } from '../BrowserManager';
|
|
5
|
+
|
|
6
|
+
// Use vi.hoisted to define mocks before hoisting
|
|
7
|
+
const { MockBrowser, mockAppBrowsers, mockWindowTemplates } = vi.hoisted(() => {
|
|
8
|
+
const createMockBrowserWindow = () => ({
|
|
9
|
+
isMaximized: vi.fn().mockReturnValue(false),
|
|
10
|
+
maximize: vi.fn(),
|
|
11
|
+
minimize: vi.fn(),
|
|
12
|
+
on: vi.fn(),
|
|
13
|
+
unmaximize: vi.fn(),
|
|
14
|
+
webContents: { id: Math.random() },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const MockBrowser = vi.fn().mockImplementation((options: any) => {
|
|
18
|
+
const browserWindow = createMockBrowserWindow();
|
|
19
|
+
return {
|
|
20
|
+
broadcast: vi.fn(),
|
|
21
|
+
browserWindow,
|
|
22
|
+
close: vi.fn(),
|
|
23
|
+
handleAppThemeChange: vi.fn(),
|
|
24
|
+
hide: vi.fn(),
|
|
25
|
+
identifier: options.identifier,
|
|
26
|
+
loadUrl: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
options,
|
|
28
|
+
show: vi.fn(),
|
|
29
|
+
webContents: browserWindow.webContents,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
MockBrowser,
|
|
35
|
+
mockAppBrowsers: {
|
|
36
|
+
chat: {
|
|
37
|
+
identifier: 'chat',
|
|
38
|
+
keepAlive: true,
|
|
39
|
+
path: '/chat',
|
|
40
|
+
},
|
|
41
|
+
settings: {
|
|
42
|
+
identifier: 'settings',
|
|
43
|
+
keepAlive: false,
|
|
44
|
+
path: '/settings',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
mockWindowTemplates: {
|
|
48
|
+
popup: {
|
|
49
|
+
baseIdentifier: 'popup',
|
|
50
|
+
height: 400,
|
|
51
|
+
width: 600,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Mock Browser class
|
|
58
|
+
vi.mock('../Browser', () => ({
|
|
59
|
+
default: MockBrowser,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// Mock appBrowsers config
|
|
63
|
+
vi.mock('../../../appBrowsers', () => ({
|
|
64
|
+
appBrowsers: mockAppBrowsers,
|
|
65
|
+
windowTemplates: mockWindowTemplates,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
// Mock logger
|
|
69
|
+
vi.mock('@/utils/logger', () => ({
|
|
70
|
+
createLogger: () => ({
|
|
71
|
+
debug: vi.fn(),
|
|
72
|
+
error: vi.fn(),
|
|
73
|
+
info: vi.fn(),
|
|
74
|
+
warn: vi.fn(),
|
|
75
|
+
}),
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
describe('BrowserManager', () => {
|
|
79
|
+
let manager: BrowserManager;
|
|
80
|
+
let mockApp: AppCore;
|
|
81
|
+
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
vi.clearAllMocks();
|
|
84
|
+
|
|
85
|
+
// Reset MockBrowser
|
|
86
|
+
MockBrowser.mockClear();
|
|
87
|
+
|
|
88
|
+
// Create mock App
|
|
89
|
+
mockApp = {} as unknown as AppCore;
|
|
90
|
+
|
|
91
|
+
manager = new BrowserManager(mockApp);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('constructor', () => {
|
|
95
|
+
it('should initialize with empty browsers Map', () => {
|
|
96
|
+
expect(manager.browsers.size).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should store app reference', () => {
|
|
100
|
+
expect(manager.app).toBe(mockApp);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('getMainWindow', () => {
|
|
105
|
+
it('should return chat window', () => {
|
|
106
|
+
const mainWindow = manager.getMainWindow();
|
|
107
|
+
|
|
108
|
+
expect(mainWindow.identifier).toBe('chat');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('showMainWindow', () => {
|
|
113
|
+
it('should show the main window', () => {
|
|
114
|
+
manager.showMainWindow();
|
|
115
|
+
|
|
116
|
+
const chatBrowser = manager.browsers.get('chat');
|
|
117
|
+
expect(chatBrowser?.show).toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('retrieveByIdentifier', () => {
|
|
122
|
+
it('should return existing browser', () => {
|
|
123
|
+
// First call creates the browser
|
|
124
|
+
const browser1 = manager.retrieveByIdentifier('chat');
|
|
125
|
+
// Second call should return same instance
|
|
126
|
+
const browser2 = manager.retrieveByIdentifier('chat');
|
|
127
|
+
|
|
128
|
+
expect(browser1).toBe(browser2);
|
|
129
|
+
expect(MockBrowser).toHaveBeenCalledTimes(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should create static browser when not exists', () => {
|
|
133
|
+
const browser = manager.retrieveByIdentifier('chat');
|
|
134
|
+
|
|
135
|
+
expect(MockBrowser).toHaveBeenCalledWith(mockAppBrowsers.chat, mockApp);
|
|
136
|
+
expect(browser.identifier).toBe('chat');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should throw error for non-static browser that does not exist', () => {
|
|
140
|
+
expect(() => manager.retrieveByIdentifier('non-existent')).toThrow(
|
|
141
|
+
'Browser non-existent not found and is not a static browser',
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('createMultiInstanceWindow', () => {
|
|
147
|
+
it('should create window from template', () => {
|
|
148
|
+
const result = manager.createMultiInstanceWindow('popup' as any, '/popup/path');
|
|
149
|
+
|
|
150
|
+
expect(result.browser).toBeDefined();
|
|
151
|
+
expect(result.identifier).toMatch(/^popup_/);
|
|
152
|
+
expect(MockBrowser).toHaveBeenCalledWith(
|
|
153
|
+
expect.objectContaining({
|
|
154
|
+
baseIdentifier: 'popup',
|
|
155
|
+
height: 400,
|
|
156
|
+
path: '/popup/path',
|
|
157
|
+
width: 600,
|
|
158
|
+
}),
|
|
159
|
+
mockApp,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should use provided uniqueId', () => {
|
|
164
|
+
const result = manager.createMultiInstanceWindow(
|
|
165
|
+
'popup' as any,
|
|
166
|
+
'/popup/path',
|
|
167
|
+
'my-custom-id',
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(result.identifier).toBe('my-custom-id');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should throw error for non-existent template', () => {
|
|
174
|
+
expect(() => manager.createMultiInstanceWindow('nonexistent' as any, '/path')).toThrow(
|
|
175
|
+
'Window template nonexistent not found',
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should generate unique identifier when not provided', () => {
|
|
180
|
+
const result1 = manager.createMultiInstanceWindow('popup' as any, '/path1');
|
|
181
|
+
const result2 = manager.createMultiInstanceWindow('popup' as any, '/path2');
|
|
182
|
+
|
|
183
|
+
expect(result1.identifier).not.toBe(result2.identifier);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('getWindowsByTemplate', () => {
|
|
188
|
+
it('should return windows matching template prefix', () => {
|
|
189
|
+
manager.createMultiInstanceWindow('popup' as any, '/path1', 'popup_1');
|
|
190
|
+
manager.createMultiInstanceWindow('popup' as any, '/path2', 'popup_2');
|
|
191
|
+
manager.retrieveByIdentifier('chat'); // This should not be included
|
|
192
|
+
|
|
193
|
+
const popupWindows = manager.getWindowsByTemplate('popup');
|
|
194
|
+
|
|
195
|
+
expect(popupWindows).toContain('popup_1');
|
|
196
|
+
expect(popupWindows).toContain('popup_2');
|
|
197
|
+
expect(popupWindows).not.toContain('chat');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should return empty array when no matching windows', () => {
|
|
201
|
+
const windows = manager.getWindowsByTemplate('nonexistent');
|
|
202
|
+
|
|
203
|
+
expect(windows).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('closeWindowsByTemplate', () => {
|
|
208
|
+
it('should close all windows matching template', () => {
|
|
209
|
+
const { browser: browser1 } = manager.createMultiInstanceWindow(
|
|
210
|
+
'popup' as any,
|
|
211
|
+
'/path1',
|
|
212
|
+
'popup_1',
|
|
213
|
+
);
|
|
214
|
+
const { browser: browser2 } = manager.createMultiInstanceWindow(
|
|
215
|
+
'popup' as any,
|
|
216
|
+
'/path2',
|
|
217
|
+
'popup_2',
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
manager.closeWindowsByTemplate('popup');
|
|
221
|
+
|
|
222
|
+
expect(browser1.close).toHaveBeenCalled();
|
|
223
|
+
expect(browser2.close).toHaveBeenCalled();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('initializeBrowsers', () => {
|
|
228
|
+
it('should initialize keepAlive browsers', () => {
|
|
229
|
+
manager.initializeBrowsers();
|
|
230
|
+
|
|
231
|
+
// chat has keepAlive: true, settings has keepAlive: false
|
|
232
|
+
expect(manager.browsers.has('chat')).toBe(true);
|
|
233
|
+
expect(manager.browsers.has('settings')).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('broadcastToAllWindows', () => {
|
|
238
|
+
it('should broadcast to all browsers', () => {
|
|
239
|
+
manager.retrieveByIdentifier('chat');
|
|
240
|
+
manager.retrieveByIdentifier('settings');
|
|
241
|
+
|
|
242
|
+
manager.broadcastToAllWindows('updateAvailable' as any, { version: '1.0.0' } as any);
|
|
243
|
+
|
|
244
|
+
const chatBrowser = manager.browsers.get('chat');
|
|
245
|
+
const settingsBrowser = manager.browsers.get('settings');
|
|
246
|
+
|
|
247
|
+
expect(chatBrowser?.broadcast).toHaveBeenCalledWith('updateAvailable', { version: '1.0.0' });
|
|
248
|
+
expect(settingsBrowser?.broadcast).toHaveBeenCalledWith('updateAvailable', {
|
|
249
|
+
version: '1.0.0',
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('broadcastToWindow', () => {
|
|
255
|
+
it('should broadcast to specific window', () => {
|
|
256
|
+
manager.retrieveByIdentifier('chat');
|
|
257
|
+
manager.retrieveByIdentifier('settings');
|
|
258
|
+
|
|
259
|
+
const chatBrowser = manager.browsers.get('chat');
|
|
260
|
+
const settingsBrowser = manager.browsers.get('settings');
|
|
261
|
+
|
|
262
|
+
manager.broadcastToWindow('chat', 'updateAvailable' as any, { version: '1.0.0' } as any);
|
|
263
|
+
|
|
264
|
+
expect(chatBrowser?.broadcast).toHaveBeenCalledWith('updateAvailable', { version: '1.0.0' });
|
|
265
|
+
expect(settingsBrowser?.broadcast).not.toHaveBeenCalled();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should safely handle non-existent window', () => {
|
|
269
|
+
expect(() =>
|
|
270
|
+
manager.broadcastToWindow('nonexistent', 'updateAvailable' as any, {} as any),
|
|
271
|
+
).not.toThrow();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('redirectToPage', () => {
|
|
276
|
+
it('should load URL and show window', async () => {
|
|
277
|
+
const browser = await manager.redirectToPage('chat', 'agent');
|
|
278
|
+
|
|
279
|
+
expect(browser.hide).toHaveBeenCalled();
|
|
280
|
+
expect(browser.loadUrl).toHaveBeenCalledWith('/chat/agent');
|
|
281
|
+
expect(browser.show).toHaveBeenCalled();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should handle subPath correctly', async () => {
|
|
285
|
+
const browser = await manager.redirectToPage('chat', 'settings/profile');
|
|
286
|
+
|
|
287
|
+
expect(browser.loadUrl).toHaveBeenCalledWith('/chat/settings/profile');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should handle search parameters', async () => {
|
|
291
|
+
const browser = await manager.redirectToPage('chat', 'agent', 'id=123');
|
|
292
|
+
|
|
293
|
+
expect(browser.loadUrl).toHaveBeenCalledWith('/chat/agent?id=123');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should handle search parameters starting with ?', async () => {
|
|
297
|
+
const browser = await manager.redirectToPage('chat', undefined, '?id=123');
|
|
298
|
+
|
|
299
|
+
expect(browser.loadUrl).toHaveBeenCalledWith('/chat?id=123');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should handle no subPath', async () => {
|
|
303
|
+
const browser = await manager.redirectToPage('chat');
|
|
304
|
+
|
|
305
|
+
expect(browser.loadUrl).toHaveBeenCalledWith('/chat');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should throw error on failure', async () => {
|
|
309
|
+
const mockError = new Error('Load failed');
|
|
310
|
+
MockBrowser.mockImplementationOnce((options: any) => ({
|
|
311
|
+
broadcast: vi.fn(),
|
|
312
|
+
browserWindow: { on: vi.fn(), webContents: { id: 1 } },
|
|
313
|
+
close: vi.fn(),
|
|
314
|
+
handleAppThemeChange: vi.fn(),
|
|
315
|
+
hide: vi.fn(),
|
|
316
|
+
identifier: options.identifier,
|
|
317
|
+
loadUrl: vi.fn().mockRejectedValue(mockError),
|
|
318
|
+
options: { path: '/chat' },
|
|
319
|
+
show: vi.fn(),
|
|
320
|
+
webContents: { id: 1 },
|
|
321
|
+
}));
|
|
322
|
+
|
|
323
|
+
// Clear the browser cache
|
|
324
|
+
manager.browsers.clear();
|
|
325
|
+
|
|
326
|
+
await expect(manager.redirectToPage('chat', 'agent')).rejects.toThrow('Load failed');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('window operations', () => {
|
|
331
|
+
describe('closeWindow', () => {
|
|
332
|
+
it('should close specified window', () => {
|
|
333
|
+
manager.retrieveByIdentifier('chat');
|
|
334
|
+
|
|
335
|
+
manager.closeWindow('chat');
|
|
336
|
+
|
|
337
|
+
const browser = manager.browsers.get('chat');
|
|
338
|
+
expect(browser?.close).toHaveBeenCalled();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should safely handle non-existent window', () => {
|
|
342
|
+
expect(() => manager.closeWindow('nonexistent')).not.toThrow();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('minimizeWindow', () => {
|
|
347
|
+
it('should minimize specified window', () => {
|
|
348
|
+
manager.retrieveByIdentifier('chat');
|
|
349
|
+
|
|
350
|
+
manager.minimizeWindow('chat');
|
|
351
|
+
|
|
352
|
+
const browser = manager.browsers.get('chat');
|
|
353
|
+
expect(browser?.browserWindow.minimize).toHaveBeenCalled();
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe('maximizeWindow', () => {
|
|
358
|
+
it('should maximize when not maximized', () => {
|
|
359
|
+
manager.retrieveByIdentifier('chat');
|
|
360
|
+
const browser = manager.browsers.get('chat');
|
|
361
|
+
browser!.browserWindow.isMaximized = vi.fn().mockReturnValue(false);
|
|
362
|
+
|
|
363
|
+
manager.maximizeWindow('chat');
|
|
364
|
+
|
|
365
|
+
expect(browser?.browserWindow.maximize).toHaveBeenCalled();
|
|
366
|
+
expect(browser?.browserWindow.unmaximize).not.toHaveBeenCalled();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should unmaximize when already maximized', () => {
|
|
370
|
+
manager.retrieveByIdentifier('chat');
|
|
371
|
+
const browser = manager.browsers.get('chat');
|
|
372
|
+
browser!.browserWindow.isMaximized = vi.fn().mockReturnValue(true);
|
|
373
|
+
|
|
374
|
+
manager.maximizeWindow('chat');
|
|
375
|
+
|
|
376
|
+
expect(browser?.browserWindow.unmaximize).toHaveBeenCalled();
|
|
377
|
+
expect(browser?.browserWindow.maximize).not.toHaveBeenCalled();
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe('getIdentifierByWebContents', () => {
|
|
383
|
+
it('should return identifier for known webContents', () => {
|
|
384
|
+
const browser = manager.retrieveByIdentifier('chat');
|
|
385
|
+
const webContents = browser.browserWindow.webContents;
|
|
386
|
+
|
|
387
|
+
const identifier = manager.getIdentifierByWebContents(webContents as any);
|
|
388
|
+
|
|
389
|
+
expect(identifier).toBe('chat');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should return null for unknown webContents', () => {
|
|
393
|
+
const unknownWebContents = { id: 999 };
|
|
394
|
+
|
|
395
|
+
const identifier = manager.getIdentifierByWebContents(unknownWebContents as any);
|
|
396
|
+
|
|
397
|
+
expect(identifier).toBeNull();
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe('handleAppThemeChange', () => {
|
|
402
|
+
it('should notify all browsers of theme change', () => {
|
|
403
|
+
manager.retrieveByIdentifier('chat');
|
|
404
|
+
manager.retrieveByIdentifier('settings');
|
|
405
|
+
|
|
406
|
+
manager.handleAppThemeChange();
|
|
407
|
+
|
|
408
|
+
const chatBrowser = manager.browsers.get('chat');
|
|
409
|
+
const settingsBrowser = manager.browsers.get('settings');
|
|
410
|
+
|
|
411
|
+
expect(chatBrowser?.handleAppThemeChange).toHaveBeenCalled();
|
|
412
|
+
expect(settingsBrowser?.handleAppThemeChange).toHaveBeenCalled();
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
});
|