@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.
Files changed (24) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/Dockerfile +2 -0
  3. package/apps/desktop/src/main/controllers/__tests__/McpInstallCtr.test.ts +286 -0
  4. package/apps/desktop/src/main/controllers/__tests__/NotificationCtr.test.ts +347 -0
  5. package/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts +645 -0
  6. package/apps/desktop/src/main/controllers/__tests__/RemoteServerSyncCtr.test.ts +372 -0
  7. package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +276 -0
  8. package/apps/desktop/src/main/controllers/__tests__/UploadFileCtr.test.ts +171 -0
  9. package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +573 -0
  10. package/apps/desktop/src/main/core/browser/__tests__/BrowserManager.test.ts +415 -0
  11. package/apps/desktop/src/main/core/infrastructure/__tests__/I18nManager.test.ts +353 -0
  12. package/apps/desktop/src/main/core/infrastructure/__tests__/IoCContainer.test.ts +156 -0
  13. package/apps/desktop/src/main/core/infrastructure/__tests__/ProtocolManager.test.ts +348 -0
  14. package/apps/desktop/src/main/core/infrastructure/__tests__/StaticFileServerManager.test.ts +481 -0
  15. package/apps/desktop/src/main/core/infrastructure/__tests__/StoreManager.test.ts +164 -0
  16. package/apps/desktop/src/main/core/infrastructure/__tests__/UpdaterManager.test.ts +513 -0
  17. package/changelog/v1.json +9 -0
  18. package/docs/self-hosting/environment-variables/model-provider.mdx +31 -0
  19. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +30 -0
  20. package/package.json +1 -1
  21. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +6 -3
  22. package/src/config/modelProviders/vertexai.ts +1 -1
  23. package/src/envs/llm.ts +4 -0
  24. package/src/server/modules/ModelRuntime/index.ts +4 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.142](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.141...v2.0.0-next.142)
6
+
7
+ <sup>Released on **2025-12-01**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Remove internal apiMode param from chat completion API requests.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Remove internal apiMode param from chat completion API requests, closes [#10539](https://github.com/lobehub/lobe-chat/issues/10539) ([9498cc6](https://github.com/lobehub/lobe-chat/commit/9498cc6))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.141](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.140...v2.0.0-next.141)
6
31
 
7
32
  <sup>Released on **2025-12-01**</sup>
package/Dockerfile CHANGED
@@ -231,6 +231,8 @@ ENV \
231
231
  GITHUB_TOKEN="" GITHUB_MODEL_LIST="" \
232
232
  # Google
233
233
  GOOGLE_API_KEY="" GOOGLE_MODEL_LIST="" GOOGLE_PROXY_URL="" \
234
+ # Vertex AI
235
+ VERTEXAI_CREDENTIALS="" VERTEXAI_PROJECT="" VERTEXAI_LOCATION="" VERTEXAI_MODEL_LIST="" \
234
236
  # Groq
235
237
  GROQ_API_KEY="" GROQ_MODEL_LIST="" GROQ_PROXY_URL="" \
236
238
  # Higress
@@ -0,0 +1,286 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import type { App } from '@/core/App';
4
+
5
+ import McpInstallController from '../McpInstallCtr';
6
+
7
+ // Mock logger
8
+ vi.mock('@/utils/logger', () => ({
9
+ createLogger: () => ({
10
+ debug: vi.fn(),
11
+ error: vi.fn(),
12
+ info: vi.fn(),
13
+ warn: vi.fn(),
14
+ }),
15
+ }));
16
+
17
+ // Mock browserManager
18
+ const mockBrowserManager = {
19
+ broadcastToWindow: vi.fn(),
20
+ };
21
+
22
+ const mockApp = {
23
+ browserManager: mockBrowserManager,
24
+ } as unknown as App;
25
+
26
+ describe('McpInstallController', () => {
27
+ let controller: McpInstallController;
28
+
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ controller = new McpInstallController(mockApp);
32
+ });
33
+
34
+ describe('handleInstallRequest', () => {
35
+ const validStdioSchema = {
36
+ identifier: 'test-plugin',
37
+ name: 'Test Plugin',
38
+ author: 'Test Author',
39
+ description: 'A test plugin',
40
+ version: '1.0.0',
41
+ config: {
42
+ type: 'stdio',
43
+ command: 'npx',
44
+ args: ['-y', 'test-mcp-server'],
45
+ },
46
+ };
47
+
48
+ const validHttpSchema = {
49
+ identifier: 'test-http-plugin',
50
+ name: 'Test HTTP Plugin',
51
+ author: 'Test Author',
52
+ description: 'A test HTTP plugin',
53
+ version: '1.0.0',
54
+ config: {
55
+ type: 'http',
56
+ url: 'https://api.example.com/mcp',
57
+ },
58
+ };
59
+
60
+ it('should return false when id is missing', async () => {
61
+ const result = await controller.handleInstallRequest({
62
+ id: '',
63
+ });
64
+
65
+ expect(result).toBe(false);
66
+ expect(mockBrowserManager.broadcastToWindow).not.toHaveBeenCalled();
67
+ });
68
+
69
+ it('should return false when schema is missing for third-party marketplace', async () => {
70
+ const result = await controller.handleInstallRequest({
71
+ id: 'test-plugin',
72
+ marketId: 'third-party',
73
+ });
74
+
75
+ expect(result).toBe(false);
76
+ expect(mockBrowserManager.broadcastToWindow).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it('should succeed for official market without schema', async () => {
80
+ const result = await controller.handleInstallRequest({
81
+ id: 'test-plugin',
82
+ marketId: 'lobehub',
83
+ });
84
+
85
+ expect(result).toBe(true);
86
+ expect(mockBrowserManager.broadcastToWindow).toHaveBeenCalledWith(
87
+ 'chat',
88
+ 'mcpInstallRequest',
89
+ {
90
+ marketId: 'lobehub',
91
+ pluginId: 'test-plugin',
92
+ schema: undefined,
93
+ },
94
+ );
95
+ });
96
+
97
+ it('should return false when schema is invalid JSON', async () => {
98
+ const result = await controller.handleInstallRequest({
99
+ id: 'test-plugin',
100
+ marketId: 'third-party',
101
+ schema: 'invalid json {',
102
+ });
103
+
104
+ expect(result).toBe(false);
105
+ expect(mockBrowserManager.broadcastToWindow).not.toHaveBeenCalled();
106
+ });
107
+
108
+ it('should return false when schema structure is invalid', async () => {
109
+ const invalidSchema = {
110
+ identifier: 'test-plugin',
111
+ // missing required fields
112
+ };
113
+
114
+ const result = await controller.handleInstallRequest({
115
+ id: 'test-plugin',
116
+ marketId: 'third-party',
117
+ schema: JSON.stringify(invalidSchema),
118
+ });
119
+
120
+ expect(result).toBe(false);
121
+ expect(mockBrowserManager.broadcastToWindow).not.toHaveBeenCalled();
122
+ });
123
+
124
+ it('should return false when schema identifier does not match id', async () => {
125
+ const schema = { ...validStdioSchema, identifier: 'different-id' };
126
+
127
+ const result = await controller.handleInstallRequest({
128
+ id: 'test-plugin',
129
+ marketId: 'third-party',
130
+ schema: JSON.stringify(schema),
131
+ });
132
+
133
+ expect(result).toBe(false);
134
+ expect(mockBrowserManager.broadcastToWindow).not.toHaveBeenCalled();
135
+ });
136
+
137
+ it('should succeed with valid stdio schema', async () => {
138
+ const result = await controller.handleInstallRequest({
139
+ id: 'test-plugin',
140
+ marketId: 'third-party',
141
+ schema: JSON.stringify(validStdioSchema),
142
+ });
143
+
144
+ expect(result).toBe(true);
145
+ expect(mockBrowserManager.broadcastToWindow).toHaveBeenCalledWith(
146
+ 'chat',
147
+ 'mcpInstallRequest',
148
+ {
149
+ marketId: 'third-party',
150
+ pluginId: 'test-plugin',
151
+ schema: validStdioSchema,
152
+ },
153
+ );
154
+ });
155
+
156
+ it('should succeed with valid http schema', async () => {
157
+ const result = await controller.handleInstallRequest({
158
+ id: 'test-http-plugin',
159
+ marketId: 'third-party',
160
+ schema: JSON.stringify(validHttpSchema),
161
+ });
162
+
163
+ expect(result).toBe(true);
164
+ expect(mockBrowserManager.broadcastToWindow).toHaveBeenCalledWith(
165
+ 'chat',
166
+ 'mcpInstallRequest',
167
+ {
168
+ marketId: 'third-party',
169
+ pluginId: 'test-http-plugin',
170
+ schema: validHttpSchema,
171
+ },
172
+ );
173
+ });
174
+
175
+ it('should return false when http schema has invalid URL', async () => {
176
+ const invalidHttpSchema = {
177
+ ...validHttpSchema,
178
+ config: {
179
+ type: 'http',
180
+ url: 'not-a-valid-url',
181
+ },
182
+ };
183
+
184
+ const result = await controller.handleInstallRequest({
185
+ id: 'test-http-plugin',
186
+ marketId: 'third-party',
187
+ schema: JSON.stringify(invalidHttpSchema),
188
+ });
189
+
190
+ expect(result).toBe(false);
191
+ expect(mockBrowserManager.broadcastToWindow).not.toHaveBeenCalled();
192
+ });
193
+
194
+ it('should return false when config type is unknown', async () => {
195
+ const unknownTypeSchema = {
196
+ ...validStdioSchema,
197
+ config: {
198
+ type: 'unknown',
199
+ },
200
+ };
201
+
202
+ const result = await controller.handleInstallRequest({
203
+ id: 'test-plugin',
204
+ marketId: 'third-party',
205
+ schema: JSON.stringify(unknownTypeSchema),
206
+ });
207
+
208
+ expect(result).toBe(false);
209
+ expect(mockBrowserManager.broadcastToWindow).not.toHaveBeenCalled();
210
+ });
211
+
212
+ it('should return false when browserManager is not available', async () => {
213
+ const controllerWithoutBrowserManager = new McpInstallController({} as App);
214
+
215
+ const result = await controllerWithoutBrowserManager.handleInstallRequest({
216
+ id: 'test-plugin',
217
+ marketId: 'lobehub',
218
+ });
219
+
220
+ expect(result).toBe(false);
221
+ });
222
+
223
+ it('should handle schema with optional fields', async () => {
224
+ const schemaWithOptionalFields = {
225
+ ...validStdioSchema,
226
+ homepage: 'https://example.com',
227
+ icon: 'https://example.com/icon.png',
228
+ };
229
+
230
+ const result = await controller.handleInstallRequest({
231
+ id: 'test-plugin',
232
+ marketId: 'third-party',
233
+ schema: JSON.stringify(schemaWithOptionalFields),
234
+ });
235
+
236
+ expect(result).toBe(true);
237
+ expect(mockBrowserManager.broadcastToWindow).toHaveBeenCalledWith(
238
+ 'chat',
239
+ 'mcpInstallRequest',
240
+ expect.objectContaining({
241
+ schema: schemaWithOptionalFields,
242
+ }),
243
+ );
244
+ });
245
+
246
+ it('should return false when stdio config missing command', async () => {
247
+ const invalidStdioSchema = {
248
+ ...validStdioSchema,
249
+ config: {
250
+ type: 'stdio',
251
+ // missing command
252
+ },
253
+ };
254
+
255
+ const result = await controller.handleInstallRequest({
256
+ id: 'test-plugin',
257
+ marketId: 'third-party',
258
+ schema: JSON.stringify(invalidStdioSchema),
259
+ });
260
+
261
+ expect(result).toBe(false);
262
+ });
263
+
264
+ it('should handle schema with env configuration', async () => {
265
+ const schemaWithEnv = {
266
+ ...validStdioSchema,
267
+ config: {
268
+ type: 'stdio',
269
+ command: 'npx',
270
+ args: ['-y', 'test-mcp-server'],
271
+ env: {
272
+ API_KEY: 'test-key',
273
+ },
274
+ },
275
+ };
276
+
277
+ const result = await controller.handleInstallRequest({
278
+ id: 'test-plugin',
279
+ marketId: 'third-party',
280
+ schema: JSON.stringify(schemaWithEnv),
281
+ });
282
+
283
+ expect(result).toBe(true);
284
+ });
285
+ });
286
+ });
@@ -0,0 +1,347 @@
1
+ import { ShowDesktopNotificationParams } from '@lobechat/electron-client-ipc';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import type { App } from '@/core/App';
5
+
6
+ import NotificationCtr from '../NotificationCtr';
7
+
8
+ // Mock logger
9
+ vi.mock('@/utils/logger', () => ({
10
+ createLogger: () => ({
11
+ debug: vi.fn(),
12
+ error: vi.fn(),
13
+ info: vi.fn(),
14
+ warn: vi.fn(),
15
+ }),
16
+ }));
17
+
18
+ // Mock electron
19
+ vi.mock('electron', () => {
20
+ const mockNotificationInstance = {
21
+ on: vi.fn(),
22
+ show: vi.fn(),
23
+ };
24
+ const MockNotification = vi.fn(() => mockNotificationInstance) as any;
25
+ MockNotification.isSupported = vi.fn(() => true);
26
+
27
+ return {
28
+ Notification: MockNotification,
29
+ app: {
30
+ setAppUserModelId: vi.fn(),
31
+ },
32
+ };
33
+ });
34
+
35
+ // Mock electron-is
36
+ vi.mock('electron-is', () => ({
37
+ macOS: vi.fn(() => false),
38
+ windows: vi.fn(() => false),
39
+ }));
40
+
41
+ // Mock browserManager
42
+ const mockBrowserWindow = {
43
+ focus: vi.fn(),
44
+ isDestroyed: vi.fn(() => false),
45
+ isFocused: vi.fn(() => true),
46
+ isMinimized: vi.fn(() => false),
47
+ isVisible: vi.fn(() => true),
48
+ };
49
+
50
+ const mockMainWindow = {
51
+ browserWindow: mockBrowserWindow,
52
+ show: vi.fn(),
53
+ };
54
+
55
+ const mockBrowserManager = {
56
+ getMainWindow: vi.fn(() => mockMainWindow),
57
+ };
58
+
59
+ const mockApp = {
60
+ browserManager: mockBrowserManager,
61
+ } as unknown as App;
62
+
63
+ describe('NotificationCtr', () => {
64
+ let controller: NotificationCtr;
65
+
66
+ beforeEach(() => {
67
+ vi.clearAllMocks();
68
+ vi.useFakeTimers();
69
+ controller = new NotificationCtr(mockApp);
70
+ });
71
+
72
+ afterEach(() => {
73
+ vi.useRealTimers();
74
+ });
75
+
76
+ describe('afterAppReady', () => {
77
+ it('should setup notifications when supported', async () => {
78
+ const { Notification } = await import('electron');
79
+ vi.mocked(Notification.isSupported).mockReturnValue(true);
80
+
81
+ controller.afterAppReady();
82
+
83
+ expect(Notification.isSupported).toHaveBeenCalled();
84
+ });
85
+
86
+ it('should not setup when notifications are not supported', async () => {
87
+ const { Notification } = await import('electron');
88
+ vi.mocked(Notification.isSupported).mockReturnValue(false);
89
+
90
+ controller.afterAppReady();
91
+
92
+ expect(Notification.isSupported).toHaveBeenCalled();
93
+ });
94
+
95
+ it('should set app user model ID on Windows', async () => {
96
+ const { windows } = await import('electron-is');
97
+ const { app, Notification } = await import('electron');
98
+ vi.mocked(windows).mockReturnValue(true);
99
+ vi.mocked(Notification.isSupported).mockReturnValue(true);
100
+
101
+ controller.afterAppReady();
102
+
103
+ expect(app.setAppUserModelId).toHaveBeenCalledWith('com.lobehub.chat');
104
+
105
+ vi.mocked(windows).mockReturnValue(false);
106
+ });
107
+
108
+ it('should handle macOS platform', async () => {
109
+ const { macOS } = await import('electron-is');
110
+ const { Notification } = await import('electron');
111
+ vi.mocked(macOS).mockReturnValue(true);
112
+ vi.mocked(Notification.isSupported).mockReturnValue(true);
113
+
114
+ // Should not throw
115
+ expect(() => controller.afterAppReady()).not.toThrow();
116
+
117
+ vi.mocked(macOS).mockReturnValue(false);
118
+ });
119
+ });
120
+
121
+ describe('showDesktopNotification', () => {
122
+ const params: ShowDesktopNotificationParams = {
123
+ body: 'Test body',
124
+ title: 'Test title',
125
+ };
126
+
127
+ it('should return error when notifications are not supported', async () => {
128
+ const { Notification } = await import('electron');
129
+ vi.mocked(Notification.isSupported).mockReturnValue(false);
130
+
131
+ const result = await controller.showDesktopNotification(params);
132
+
133
+ expect(result).toEqual({
134
+ error: 'Desktop notifications not supported',
135
+ success: false,
136
+ });
137
+ });
138
+
139
+ it('should skip notification when window is visible and focused', async () => {
140
+ const { Notification } = await import('electron');
141
+ vi.mocked(Notification.isSupported).mockReturnValue(true);
142
+ mockBrowserWindow.isVisible.mockReturnValue(true);
143
+ mockBrowserWindow.isFocused.mockReturnValue(true);
144
+ mockBrowserWindow.isMinimized.mockReturnValue(false);
145
+
146
+ const result = await controller.showDesktopNotification(params);
147
+
148
+ expect(result).toEqual({
149
+ reason: 'Window is visible',
150
+ skipped: true,
151
+ success: true,
152
+ });
153
+ });
154
+
155
+ it('should show notification when window is hidden', async () => {
156
+ const { Notification } = await import('electron');
157
+ vi.mocked(Notification.isSupported).mockReturnValue(true);
158
+ mockBrowserWindow.isVisible.mockReturnValue(false);
159
+
160
+ const promise = controller.showDesktopNotification(params);
161
+ vi.advanceTimersByTime(100);
162
+ const result = await promise;
163
+
164
+ expect(Notification).toHaveBeenCalledWith({
165
+ body: 'Test body',
166
+ hasReply: false,
167
+ silent: false,
168
+ timeoutType: 'default',
169
+ title: 'Test title',
170
+ urgency: 'normal',
171
+ });
172
+ expect(result).toEqual({ success: true });
173
+ });
174
+
175
+ it('should show notification when window is minimized', async () => {
176
+ const { Notification } = await import('electron');
177
+ vi.mocked(Notification.isSupported).mockReturnValue(true);
178
+ mockBrowserWindow.isVisible.mockReturnValue(true);
179
+ mockBrowserWindow.isFocused.mockReturnValue(true);
180
+ mockBrowserWindow.isMinimized.mockReturnValue(true);
181
+
182
+ const promise = controller.showDesktopNotification(params);
183
+ vi.advanceTimersByTime(100);
184
+ const result = await promise;
185
+
186
+ expect(Notification).toHaveBeenCalled();
187
+ expect(result).toEqual({ success: true });
188
+ });
189
+
190
+ it('should show notification when window is not focused', async () => {
191
+ const { Notification } = await import('electron');
192
+ vi.mocked(Notification.isSupported).mockReturnValue(true);
193
+ mockBrowserWindow.isVisible.mockReturnValue(true);
194
+ mockBrowserWindow.isFocused.mockReturnValue(false);
195
+ mockBrowserWindow.isMinimized.mockReturnValue(false);
196
+
197
+ const promise = controller.showDesktopNotification(params);
198
+ vi.advanceTimersByTime(100);
199
+ const result = await promise;
200
+
201
+ expect(Notification).toHaveBeenCalled();
202
+ expect(result).toEqual({ success: true });
203
+ });
204
+
205
+ it('should pass silent option to notification', async () => {
206
+ const { Notification } = await import('electron');
207
+ vi.mocked(Notification.isSupported).mockReturnValue(true);
208
+ mockBrowserWindow.isVisible.mockReturnValue(false);
209
+
210
+ const paramsWithSilent: ShowDesktopNotificationParams = {
211
+ ...params,
212
+ silent: true,
213
+ };
214
+
215
+ const promise = controller.showDesktopNotification(paramsWithSilent);
216
+ vi.advanceTimersByTime(100);
217
+ await promise;
218
+
219
+ expect(Notification).toHaveBeenCalledWith(
220
+ expect.objectContaining({
221
+ silent: true,
222
+ }),
223
+ );
224
+ });
225
+
226
+ it('should register click handler to show main window', async () => {
227
+ const { Notification } = await import('electron');
228
+ vi.mocked(Notification.isSupported).mockReturnValue(true);
229
+ mockBrowserWindow.isVisible.mockReturnValue(false);
230
+
231
+ // Get the mock instance that will be created
232
+ const mockInstance = { on: vi.fn(), show: vi.fn() };
233
+ vi.mocked(Notification).mockReturnValue(mockInstance as any);
234
+
235
+ const promise = controller.showDesktopNotification(params);
236
+ vi.advanceTimersByTime(100);
237
+ await promise;
238
+
239
+ // Find the click handler
240
+ const clickHandler = mockInstance.on.mock.calls.find((call) => call[0] === 'click')?.[1];
241
+
242
+ expect(clickHandler).toBeDefined();
243
+
244
+ // Simulate click
245
+ clickHandler();
246
+
247
+ expect(mockMainWindow.show).toHaveBeenCalled();
248
+ expect(mockBrowserWindow.focus).toHaveBeenCalled();
249
+ });
250
+
251
+ it('should handle notification error', async () => {
252
+ const { Notification } = await import('electron');
253
+ vi.mocked(Notification.isSupported).mockReturnValue(true);
254
+ mockBrowserWindow.isVisible.mockReturnValue(false);
255
+ vi.mocked(Notification).mockImplementationOnce(() => {
256
+ throw new Error('Notification error');
257
+ });
258
+
259
+ const result = await controller.showDesktopNotification(params);
260
+
261
+ expect(result).toEqual({
262
+ error: 'Notification error',
263
+ success: false,
264
+ });
265
+ });
266
+
267
+ it('should handle unknown error type', async () => {
268
+ const { Notification } = await import('electron');
269
+ vi.mocked(Notification.isSupported).mockReturnValue(true);
270
+ mockBrowserWindow.isVisible.mockReturnValue(false);
271
+ vi.mocked(Notification).mockImplementationOnce(() => {
272
+ throw 'string error';
273
+ });
274
+
275
+ const result = await controller.showDesktopNotification(params);
276
+
277
+ expect(result).toEqual({
278
+ error: 'Unknown error',
279
+ success: false,
280
+ });
281
+ });
282
+ });
283
+
284
+ describe('isMainWindowHidden', () => {
285
+ it('should return false when window is visible and focused', () => {
286
+ mockBrowserWindow.isVisible.mockReturnValue(true);
287
+ mockBrowserWindow.isFocused.mockReturnValue(true);
288
+ mockBrowserWindow.isMinimized.mockReturnValue(false);
289
+ mockBrowserWindow.isDestroyed.mockReturnValue(false);
290
+
291
+ const result = controller.isMainWindowHidden();
292
+
293
+ expect(result).toBe(false);
294
+ });
295
+
296
+ it('should return true when window is not visible', () => {
297
+ mockBrowserWindow.isVisible.mockReturnValue(false);
298
+ mockBrowserWindow.isFocused.mockReturnValue(true);
299
+ mockBrowserWindow.isMinimized.mockReturnValue(false);
300
+ mockBrowserWindow.isDestroyed.mockReturnValue(false);
301
+
302
+ const result = controller.isMainWindowHidden();
303
+
304
+ expect(result).toBe(true);
305
+ });
306
+
307
+ it('should return true when window is minimized', () => {
308
+ mockBrowserWindow.isVisible.mockReturnValue(true);
309
+ mockBrowserWindow.isFocused.mockReturnValue(true);
310
+ mockBrowserWindow.isMinimized.mockReturnValue(true);
311
+ mockBrowserWindow.isDestroyed.mockReturnValue(false);
312
+
313
+ const result = controller.isMainWindowHidden();
314
+
315
+ expect(result).toBe(true);
316
+ });
317
+
318
+ it('should return true when window is not focused', () => {
319
+ mockBrowserWindow.isVisible.mockReturnValue(true);
320
+ mockBrowserWindow.isFocused.mockReturnValue(false);
321
+ mockBrowserWindow.isMinimized.mockReturnValue(false);
322
+ mockBrowserWindow.isDestroyed.mockReturnValue(false);
323
+
324
+ const result = controller.isMainWindowHidden();
325
+
326
+ expect(result).toBe(true);
327
+ });
328
+
329
+ it('should return true when window is destroyed', () => {
330
+ mockBrowserWindow.isDestroyed.mockReturnValue(true);
331
+
332
+ const result = controller.isMainWindowHidden();
333
+
334
+ expect(result).toBe(true);
335
+ });
336
+
337
+ it('should return true on error', () => {
338
+ mockBrowserManager.getMainWindow.mockImplementationOnce(() => {
339
+ throw new Error('Window not available');
340
+ });
341
+
342
+ const result = controller.isMainWindowHidden();
343
+
344
+ expect(result).toBe(true);
345
+ });
346
+ });
347
+ });