@lobehub/lobehub 2.0.0-next.142 → 2.0.0-next.144

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 (58) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/package.json +1 -0
  3. package/apps/desktop/src/main/core/ui/__tests__/MenuManager.test.ts +320 -0
  4. package/apps/desktop/src/main/core/ui/__tests__/Tray.test.ts +518 -0
  5. package/apps/desktop/src/main/core/ui/__tests__/TrayManager.test.ts +360 -0
  6. package/apps/desktop/src/main/menus/impls/BaseMenuPlatform.test.ts +49 -0
  7. package/apps/desktop/src/main/menus/impls/linux.test.ts +552 -0
  8. package/apps/desktop/src/main/menus/impls/macOS.test.ts +464 -0
  9. package/apps/desktop/src/main/menus/impls/windows.test.ts +429 -0
  10. package/apps/desktop/src/main/modules/fileSearch/__tests__/macOS.integration.test.ts +2 -2
  11. package/apps/desktop/src/main/services/__tests__/fileSearchSrv.test.ts +402 -0
  12. package/apps/desktop/src/main/utils/__tests__/file-system.test.ts +91 -0
  13. package/apps/desktop/src/main/utils/__tests__/logger.test.ts +229 -0
  14. package/apps/desktop/src/preload/electronApi.test.ts +142 -0
  15. package/apps/desktop/src/preload/invoke.test.ts +145 -0
  16. package/apps/desktop/src/preload/routeInterceptor.test.ts +374 -0
  17. package/apps/desktop/src/preload/streamer.test.ts +365 -0
  18. package/apps/desktop/vitest.config.mts +1 -0
  19. package/changelog/v1.json +18 -0
  20. package/locales/ar/marketAuth.json +13 -0
  21. package/locales/bg-BG/marketAuth.json +13 -0
  22. package/locales/de-DE/marketAuth.json +13 -0
  23. package/locales/en-US/marketAuth.json +13 -0
  24. package/locales/es-ES/marketAuth.json +13 -0
  25. package/locales/fa-IR/marketAuth.json +13 -0
  26. package/locales/fr-FR/marketAuth.json +13 -0
  27. package/locales/it-IT/marketAuth.json +13 -0
  28. package/locales/ja-JP/marketAuth.json +13 -0
  29. package/locales/ko-KR/marketAuth.json +13 -0
  30. package/locales/nl-NL/marketAuth.json +13 -0
  31. package/locales/pl-PL/marketAuth.json +13 -0
  32. package/locales/pt-BR/marketAuth.json +13 -0
  33. package/locales/ru-RU/marketAuth.json +13 -0
  34. package/locales/tr-TR/marketAuth.json +13 -0
  35. package/locales/vi-VN/marketAuth.json +13 -0
  36. package/locales/zh-CN/marketAuth.json +13 -0
  37. package/locales/zh-TW/marketAuth.json +13 -0
  38. package/package.json +1 -1
  39. package/packages/database/migrations/0054_better_auth_two_factor.sql +2 -0
  40. package/packages/database/src/core/migrations.json +1 -1
  41. package/packages/database/src/models/user.ts +27 -5
  42. package/packages/types/src/discover/mcp.ts +2 -1
  43. package/packages/types/src/tool/plugin.ts +2 -1
  44. package/scripts/migrateServerDB/errorHint.js +26 -0
  45. package/scripts/migrateServerDB/index.ts +5 -1
  46. package/src/app/[variants]/(main)/chat/settings/features/SmartAgentActionButton/MarketPublishButton.tsx +0 -2
  47. package/src/app/[variants]/(main)/discover/(detail)/mcp/features/Sidebar/ActionButton/index.tsx +33 -7
  48. package/src/features/PluginStore/McpList/List/Action.tsx +20 -1
  49. package/src/layout/AuthProvider/MarketAuth/MarketAuthConfirmModal.tsx +158 -0
  50. package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +130 -14
  51. package/src/libs/mcp/types.ts +8 -0
  52. package/src/locales/default/marketAuth.ts +13 -0
  53. package/src/server/routers/lambda/market/index.ts +85 -2
  54. package/src/server/services/discover/index.ts +45 -4
  55. package/src/services/discover.ts +1 -1
  56. package/src/services/mcp.ts +18 -3
  57. package/src/store/tool/slices/mcpStore/action.test.ts +141 -0
  58. package/src/store/tool/slices/mcpStore/action.ts +153 -11
@@ -0,0 +1,518 @@
1
+ import { Tray as ElectronTray, Menu, app, nativeImage } from 'electron';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import type { App } from '../../App';
5
+ import { Tray } from '../Tray';
6
+
7
+ // Mock electron modules
8
+ vi.mock('electron', () => ({
9
+ Tray: vi.fn(),
10
+ Menu: {
11
+ buildFromTemplate: vi.fn(),
12
+ },
13
+ nativeImage: {
14
+ createFromPath: vi.fn(),
15
+ },
16
+ app: {
17
+ quit: vi.fn(),
18
+ },
19
+ }));
20
+
21
+ // Mock logger
22
+ vi.mock('@/utils/logger', () => ({
23
+ createLogger: () => ({
24
+ debug: vi.fn(),
25
+ info: vi.fn(),
26
+ warn: vi.fn(),
27
+ error: vi.fn(),
28
+ }),
29
+ }));
30
+
31
+ // Mock dir constants
32
+ vi.mock('@/const/dir', () => ({
33
+ resourcesDir: '/mock/resources',
34
+ }));
35
+
36
+ describe('Tray', () => {
37
+ let tray: Tray;
38
+ let mockApp: App;
39
+ let mockElectronTray: any;
40
+ let mockBrowserWindow: any;
41
+ let mockMainWindow: any;
42
+
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+
46
+ // Mock Electron Tray instance
47
+ mockElectronTray = {
48
+ setToolTip: vi.fn(),
49
+ setContextMenu: vi.fn(),
50
+ setImage: vi.fn(),
51
+ on: vi.fn(),
52
+ destroy: vi.fn(),
53
+ displayBalloon: vi.fn(),
54
+ };
55
+
56
+ // Mock BrowserWindow
57
+ mockBrowserWindow = {
58
+ isVisible: vi.fn(),
59
+ isFocused: vi.fn(),
60
+ focus: vi.fn(),
61
+ };
62
+
63
+ // Mock MainWindow
64
+ mockMainWindow = {
65
+ browserWindow: mockBrowserWindow,
66
+ hide: vi.fn(),
67
+ show: vi.fn(),
68
+ broadcast: vi.fn(),
69
+ };
70
+
71
+ // Mock App
72
+ mockApp = {
73
+ browserManager: {
74
+ showMainWindow: vi.fn(),
75
+ getMainWindow: vi.fn(() => mockMainWindow),
76
+ },
77
+ } as unknown as App;
78
+
79
+ // Mock electron constructors
80
+ vi.mocked(ElectronTray).mockImplementation(() => mockElectronTray);
81
+ vi.mocked(nativeImage.createFromPath).mockReturnValue({} as any);
82
+ vi.mocked(Menu.buildFromTemplate).mockReturnValue({} as any);
83
+ });
84
+
85
+ describe('constructor', () => {
86
+ it('should initialize tray with provided options', () => {
87
+ tray = new Tray(
88
+ {
89
+ iconPath: 'tray.png',
90
+ identifier: 'test-tray',
91
+ tooltip: 'Test Tray',
92
+ },
93
+ mockApp,
94
+ );
95
+
96
+ expect(tray.identifier).toBe('test-tray');
97
+ expect(tray.options.iconPath).toBe('tray.png');
98
+ expect(tray.options.tooltip).toBe('Test Tray');
99
+ });
100
+
101
+ it('should call retrieveOrInitialize during construction', () => {
102
+ tray = new Tray(
103
+ {
104
+ iconPath: 'tray.png',
105
+ identifier: 'test-tray',
106
+ },
107
+ mockApp,
108
+ );
109
+
110
+ expect(nativeImage.createFromPath).toHaveBeenCalledWith('/mock/resources/tray.png');
111
+ expect(ElectronTray).toHaveBeenCalled();
112
+ });
113
+ });
114
+
115
+ describe('retrieveOrInitialize', () => {
116
+ it('should create new tray instance with icon and tooltip', () => {
117
+ tray = new Tray(
118
+ {
119
+ iconPath: 'tray.png',
120
+ identifier: 'test-tray',
121
+ tooltip: 'Test Tray',
122
+ },
123
+ mockApp,
124
+ );
125
+
126
+ expect(nativeImage.createFromPath).toHaveBeenCalledWith('/mock/resources/tray.png');
127
+ expect(ElectronTray).toHaveBeenCalled();
128
+ expect(mockElectronTray.setToolTip).toHaveBeenCalledWith('Test Tray');
129
+ });
130
+
131
+ it('should not set tooltip if not provided', () => {
132
+ tray = new Tray(
133
+ {
134
+ iconPath: 'tray.png',
135
+ identifier: 'test-tray',
136
+ },
137
+ mockApp,
138
+ );
139
+
140
+ expect(mockElectronTray.setToolTip).not.toHaveBeenCalled();
141
+ });
142
+
143
+ it('should return existing tray instance if already created', () => {
144
+ tray = new Tray(
145
+ {
146
+ iconPath: 'tray.png',
147
+ identifier: 'test-tray',
148
+ },
149
+ mockApp,
150
+ );
151
+
152
+ const firstTray = tray.tray;
153
+ const secondTray = tray.tray;
154
+
155
+ expect(firstTray).toBe(secondTray);
156
+ expect(ElectronTray).toHaveBeenCalledTimes(1);
157
+ });
158
+
159
+ it('should register click event handler', () => {
160
+ tray = new Tray(
161
+ {
162
+ iconPath: 'tray.png',
163
+ identifier: 'test-tray',
164
+ },
165
+ mockApp,
166
+ );
167
+
168
+ expect(mockElectronTray.on).toHaveBeenCalledWith('click', expect.any(Function));
169
+ });
170
+
171
+ it('should set default context menu', () => {
172
+ tray = new Tray(
173
+ {
174
+ iconPath: 'tray.png',
175
+ identifier: 'test-tray',
176
+ },
177
+ mockApp,
178
+ );
179
+
180
+ expect(Menu.buildFromTemplate).toHaveBeenCalled();
181
+ expect(mockElectronTray.setContextMenu).toHaveBeenCalled();
182
+ });
183
+
184
+ it('should handle errors when creating tray', () => {
185
+ const error = new Error('Failed to create tray');
186
+ vi.mocked(ElectronTray).mockImplementation(() => {
187
+ throw error;
188
+ });
189
+
190
+ expect(() => {
191
+ tray = new Tray(
192
+ {
193
+ iconPath: 'tray.png',
194
+ identifier: 'test-tray',
195
+ },
196
+ mockApp,
197
+ );
198
+ }).toThrow(error);
199
+ });
200
+ });
201
+
202
+ describe('setContextMenu', () => {
203
+ beforeEach(() => {
204
+ tray = new Tray(
205
+ {
206
+ iconPath: 'tray.png',
207
+ identifier: 'test-tray',
208
+ },
209
+ mockApp,
210
+ );
211
+ vi.clearAllMocks();
212
+ });
213
+
214
+ it('should set default context menu when no template provided', () => {
215
+ tray.setContextMenu();
216
+
217
+ expect(Menu.buildFromTemplate).toHaveBeenCalledWith(
218
+ expect.arrayContaining([
219
+ expect.objectContaining({ label: 'Show Main Window' }),
220
+ expect.objectContaining({ type: 'separator' }),
221
+ expect.objectContaining({ label: 'Quit' }),
222
+ ]),
223
+ );
224
+ expect(mockElectronTray.setContextMenu).toHaveBeenCalled();
225
+ });
226
+
227
+ it('should set custom context menu when template provided', () => {
228
+ const customTemplate = [
229
+ { label: 'Custom Item 1', click: vi.fn() },
230
+ { label: 'Custom Item 2', click: vi.fn() },
231
+ ];
232
+
233
+ tray.setContextMenu(customTemplate);
234
+
235
+ expect(Menu.buildFromTemplate).toHaveBeenCalledWith(customTemplate);
236
+ expect(mockElectronTray.setContextMenu).toHaveBeenCalled();
237
+ });
238
+
239
+ it('should call showMainWindow when Show Main Window is clicked', () => {
240
+ tray.setContextMenu();
241
+
242
+ const templateArg = vi.mocked(Menu.buildFromTemplate).mock.calls[0][0];
243
+ const showMainWindowItem = templateArg.find((item: any) => item.label === 'Show Main Window');
244
+
245
+ showMainWindowItem?.click?.();
246
+
247
+ expect(mockApp.browserManager.showMainWindow).toHaveBeenCalled();
248
+ });
249
+
250
+ it('should call app.quit when Quit is clicked', () => {
251
+ tray.setContextMenu();
252
+
253
+ const templateArg = vi.mocked(Menu.buildFromTemplate).mock.calls[0][0];
254
+ const quitItem = templateArg.find((item: any) => item.label === 'Quit');
255
+
256
+ quitItem?.click?.();
257
+
258
+ expect(app.quit).toHaveBeenCalled();
259
+ });
260
+ });
261
+
262
+ describe('onClick', () => {
263
+ beforeEach(() => {
264
+ tray = new Tray(
265
+ {
266
+ iconPath: 'tray.png',
267
+ identifier: 'test-tray',
268
+ },
269
+ mockApp,
270
+ );
271
+ });
272
+
273
+ it('should hide window when it is visible and focused', () => {
274
+ mockBrowserWindow.isVisible.mockReturnValue(true);
275
+ mockBrowserWindow.isFocused.mockReturnValue(true);
276
+
277
+ tray.onClick();
278
+
279
+ expect(mockMainWindow.hide).toHaveBeenCalled();
280
+ expect(mockMainWindow.show).not.toHaveBeenCalled();
281
+ });
282
+
283
+ it('should show and focus window when it is not visible', () => {
284
+ mockBrowserWindow.isVisible.mockReturnValue(false);
285
+ mockBrowserWindow.isFocused.mockReturnValue(false);
286
+
287
+ tray.onClick();
288
+
289
+ expect(mockMainWindow.show).toHaveBeenCalled();
290
+ expect(mockBrowserWindow.focus).toHaveBeenCalled();
291
+ expect(mockMainWindow.hide).not.toHaveBeenCalled();
292
+ });
293
+
294
+ it('should show and focus window when it is visible but not focused', () => {
295
+ mockBrowserWindow.isVisible.mockReturnValue(true);
296
+ mockBrowserWindow.isFocused.mockReturnValue(false);
297
+
298
+ tray.onClick();
299
+
300
+ expect(mockMainWindow.show).toHaveBeenCalled();
301
+ expect(mockBrowserWindow.focus).toHaveBeenCalled();
302
+ expect(mockMainWindow.hide).not.toHaveBeenCalled();
303
+ });
304
+
305
+ it('should handle case when main window is null', () => {
306
+ vi.mocked(mockApp.browserManager.getMainWindow).mockReturnValue(null);
307
+
308
+ expect(() => tray.onClick()).not.toThrow();
309
+ });
310
+ });
311
+
312
+ describe('updateIcon', () => {
313
+ beforeEach(() => {
314
+ tray = new Tray(
315
+ {
316
+ iconPath: 'tray.png',
317
+ identifier: 'test-tray',
318
+ },
319
+ mockApp,
320
+ );
321
+ vi.clearAllMocks();
322
+ });
323
+
324
+ it('should update tray icon successfully', () => {
325
+ const newIcon = {};
326
+ vi.mocked(nativeImage.createFromPath).mockReturnValue(newIcon as any);
327
+
328
+ tray.updateIcon('new-icon.png');
329
+
330
+ expect(nativeImage.createFromPath).toHaveBeenCalledWith('/mock/resources/new-icon.png');
331
+ expect(mockElectronTray.setImage).toHaveBeenCalledWith(newIcon);
332
+ expect(tray.options.iconPath).toBe('new-icon.png');
333
+ });
334
+
335
+ it('should handle errors when updating icon', () => {
336
+ const error = new Error('Failed to load icon');
337
+ vi.mocked(nativeImage.createFromPath).mockImplementation(() => {
338
+ throw error;
339
+ });
340
+
341
+ expect(() => tray.updateIcon('bad-icon.png')).not.toThrow();
342
+ });
343
+ });
344
+
345
+ describe('updateTooltip', () => {
346
+ beforeEach(() => {
347
+ tray = new Tray(
348
+ {
349
+ iconPath: 'tray.png',
350
+ identifier: 'test-tray',
351
+ },
352
+ mockApp,
353
+ );
354
+ });
355
+
356
+ it('should update tray tooltip successfully', () => {
357
+ tray.updateTooltip('New Tooltip');
358
+
359
+ expect(mockElectronTray.setToolTip).toHaveBeenCalledWith('New Tooltip');
360
+ expect(tray.options.tooltip).toBe('New Tooltip');
361
+ });
362
+ });
363
+
364
+ describe('displayBalloon', () => {
365
+ const originalPlatform = process.platform;
366
+
367
+ afterEach(() => {
368
+ Object.defineProperty(process, 'platform', { value: originalPlatform });
369
+ });
370
+
371
+ beforeEach(() => {
372
+ tray = new Tray(
373
+ {
374
+ iconPath: 'tray.png',
375
+ identifier: 'test-tray',
376
+ },
377
+ mockApp,
378
+ );
379
+ });
380
+
381
+ it('should display balloon notification on Windows', () => {
382
+ Object.defineProperty(process, 'platform', { value: 'win32' });
383
+
384
+ const options = {
385
+ title: 'Test',
386
+ content: 'Test content',
387
+ };
388
+
389
+ tray.displayBalloon(options);
390
+
391
+ expect(mockElectronTray.displayBalloon).toHaveBeenCalledWith(options);
392
+ });
393
+
394
+ it('should not display balloon notification on macOS', () => {
395
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
396
+
397
+ const options = {
398
+ title: 'Test',
399
+ content: 'Test content',
400
+ };
401
+
402
+ tray.displayBalloon(options);
403
+
404
+ expect(mockElectronTray.displayBalloon).not.toHaveBeenCalled();
405
+ });
406
+
407
+ it('should not display balloon notification on Linux', () => {
408
+ Object.defineProperty(process, 'platform', { value: 'linux' });
409
+
410
+ const options = {
411
+ title: 'Test',
412
+ content: 'Test content',
413
+ };
414
+
415
+ tray.displayBalloon(options);
416
+
417
+ expect(mockElectronTray.displayBalloon).not.toHaveBeenCalled();
418
+ });
419
+ });
420
+
421
+ describe('broadcast', () => {
422
+ beforeEach(() => {
423
+ tray = new Tray(
424
+ {
425
+ iconPath: 'tray.png',
426
+ identifier: 'test-tray',
427
+ },
428
+ mockApp,
429
+ );
430
+ });
431
+
432
+ it('should broadcast message to main window', () => {
433
+ const channel = 'test-channel' as any;
434
+ const data = { test: 'data' };
435
+
436
+ tray.broadcast(channel, data);
437
+
438
+ expect(mockApp.browserManager.getMainWindow).toHaveBeenCalled();
439
+ expect(mockMainWindow.broadcast).toHaveBeenCalledWith(channel, data);
440
+ });
441
+
442
+ it('should handle case when main window is null', () => {
443
+ vi.mocked(mockApp.browserManager.getMainWindow).mockReturnValue(null);
444
+
445
+ expect(() => tray.broadcast('test-channel' as any)).not.toThrow();
446
+ });
447
+ });
448
+
449
+ describe('destroy', () => {
450
+ beforeEach(() => {
451
+ tray = new Tray(
452
+ {
453
+ iconPath: 'tray.png',
454
+ identifier: 'test-tray',
455
+ },
456
+ mockApp,
457
+ );
458
+ });
459
+
460
+ it('should destroy tray instance', () => {
461
+ tray.destroy();
462
+
463
+ expect(mockElectronTray.destroy).toHaveBeenCalled();
464
+ });
465
+
466
+ it('should handle multiple destroy calls', () => {
467
+ tray.destroy();
468
+ tray.destroy();
469
+
470
+ expect(mockElectronTray.destroy).toHaveBeenCalledTimes(1);
471
+ });
472
+
473
+ it('should allow creating new tray after destroy', () => {
474
+ tray.destroy();
475
+ vi.clearAllMocks();
476
+
477
+ const newTray = tray.tray;
478
+
479
+ expect(newTray).toBeDefined();
480
+ expect(ElectronTray).toHaveBeenCalled();
481
+ });
482
+ });
483
+
484
+ describe('integration tests', () => {
485
+ it('should handle complete tray lifecycle', () => {
486
+ tray = new Tray(
487
+ {
488
+ iconPath: 'tray.png',
489
+ identifier: 'test-tray',
490
+ tooltip: 'Test Tray',
491
+ },
492
+ mockApp,
493
+ );
494
+
495
+ // Verify creation
496
+ expect(tray.tray).toBeDefined();
497
+ expect(mockElectronTray.setToolTip).toHaveBeenCalledWith('Test Tray');
498
+
499
+ // Update icon
500
+ tray.updateIcon('new-icon.png');
501
+ expect(mockElectronTray.setImage).toHaveBeenCalled();
502
+
503
+ // Update tooltip
504
+ tray.updateTooltip('New Tooltip');
505
+ expect(mockElectronTray.setToolTip).toHaveBeenCalledWith('New Tooltip');
506
+
507
+ // Test click behavior
508
+ mockBrowserWindow.isVisible.mockReturnValue(true);
509
+ mockBrowserWindow.isFocused.mockReturnValue(true);
510
+ tray.onClick();
511
+ expect(mockMainWindow.hide).toHaveBeenCalled();
512
+
513
+ // Destroy
514
+ tray.destroy();
515
+ expect(mockElectronTray.destroy).toHaveBeenCalled();
516
+ });
517
+ });
518
+ });