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

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 (54) hide show
  1. package/CHANGELOG.md +25 -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 +9 -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/src/models/user.ts +2 -0
  40. package/packages/types/src/discover/mcp.ts +2 -1
  41. package/packages/types/src/tool/plugin.ts +2 -1
  42. package/src/app/[variants]/(main)/chat/settings/features/SmartAgentActionButton/MarketPublishButton.tsx +0 -2
  43. package/src/app/[variants]/(main)/discover/(detail)/mcp/features/Sidebar/ActionButton/index.tsx +33 -7
  44. package/src/features/PluginStore/McpList/List/Action.tsx +20 -1
  45. package/src/layout/AuthProvider/MarketAuth/MarketAuthConfirmModal.tsx +158 -0
  46. package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +130 -14
  47. package/src/libs/mcp/types.ts +8 -0
  48. package/src/locales/default/marketAuth.ts +13 -0
  49. package/src/server/routers/lambda/market/index.ts +85 -2
  50. package/src/server/services/discover/index.ts +45 -4
  51. package/src/services/discover.ts +1 -1
  52. package/src/services/mcp.ts +18 -3
  53. package/src/store/tool/slices/mcpStore/action.test.ts +141 -0
  54. package/src/store/tool/slices/mcpStore/action.ts +153 -11
@@ -0,0 +1,429 @@
1
+ import { Menu, app, shell } from 'electron';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import type { App } from '@/core/App';
5
+
6
+ import { WindowsMenu } from './windows';
7
+
8
+ // Mock Electron modules
9
+ vi.mock('electron', () => ({
10
+ Menu: {
11
+ buildFromTemplate: vi.fn((template) => ({ template })),
12
+ setApplicationMenu: vi.fn(),
13
+ },
14
+ app: {
15
+ getName: vi.fn(() => 'LobeChat'),
16
+ },
17
+ shell: {
18
+ openExternal: vi.fn(),
19
+ },
20
+ }));
21
+
22
+ // Mock isDev
23
+ vi.mock('@/const/env', () => ({
24
+ isDev: false,
25
+ }));
26
+
27
+ // Mock App instance
28
+ const createMockApp = () => {
29
+ const mockT = vi.fn((key: string, params?: any) => {
30
+ const translations: Record<string, string> = {
31
+ 'file.title': 'File',
32
+ 'file.preferences': 'Settings',
33
+ 'file.quit': 'Exit',
34
+ 'common.checkUpdates': 'Check for Updates',
35
+ 'window.close': 'Close',
36
+ 'window.minimize': 'Minimize',
37
+ 'window.title': 'Window',
38
+ 'edit.title': 'Edit',
39
+ 'edit.undo': 'Undo',
40
+ 'edit.redo': 'Redo',
41
+ 'edit.cut': 'Cut',
42
+ 'edit.copy': 'Copy',
43
+ 'edit.paste': 'Paste',
44
+ 'edit.selectAll': 'Select All',
45
+ 'edit.delete': 'Delete',
46
+ 'view.title': 'View',
47
+ 'view.resetZoom': 'Reset Zoom',
48
+ 'view.zoomIn': 'Zoom In',
49
+ 'view.zoomOut': 'Zoom Out',
50
+ 'view.toggleFullscreen': 'Full Screen',
51
+ 'help.title': 'Help',
52
+ 'help.visitWebsite': 'Visit Website',
53
+ 'help.githubRepo': 'GitHub Repository',
54
+ 'dev.title': 'Developer',
55
+ 'dev.reload': 'Reload',
56
+ 'dev.forceReload': 'Force Reload',
57
+ 'dev.devTools': 'Developer Tools',
58
+ 'dev.devPanel': 'Dev Panel',
59
+ 'tray.open': `Open ${params?.appName || 'App'}`,
60
+ 'tray.quit': 'Quit',
61
+ };
62
+ return translations[key] || key;
63
+ });
64
+
65
+ return {
66
+ i18n: {
67
+ ns: vi.fn(() => mockT),
68
+ },
69
+ browserManager: {
70
+ showMainWindow: vi.fn(),
71
+ retrieveByIdentifier: vi.fn(() => ({
72
+ show: vi.fn(),
73
+ })),
74
+ },
75
+ updaterManager: {
76
+ checkForUpdates: vi.fn(),
77
+ },
78
+ } as unknown as App;
79
+ };
80
+
81
+ describe('WindowsMenu', () => {
82
+ let windowsMenu: WindowsMenu;
83
+ let mockApp: App;
84
+
85
+ beforeEach(() => {
86
+ vi.clearAllMocks();
87
+ mockApp = createMockApp();
88
+ windowsMenu = new WindowsMenu(mockApp);
89
+ });
90
+
91
+ describe('buildAndSetAppMenu', () => {
92
+ it('should build and set application menu', () => {
93
+ const menu = windowsMenu.buildAndSetAppMenu();
94
+
95
+ expect(Menu.buildFromTemplate).toHaveBeenCalled();
96
+ expect(Menu.setApplicationMenu).toHaveBeenCalled();
97
+ expect(menu).toBeDefined();
98
+ });
99
+
100
+ it('should include developer menu when showDevItems is true', () => {
101
+ windowsMenu.buildAndSetAppMenu({ showDevItems: true });
102
+
103
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
104
+ const devMenu = template.find((item: any) => item.label === 'Developer');
105
+ expect(devMenu).toBeDefined();
106
+ });
107
+
108
+ it('should not include developer menu when showDevItems is false', () => {
109
+ windowsMenu.buildAndSetAppMenu({ showDevItems: false });
110
+
111
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
112
+ const devMenu = template.find((item: any) => item.label === 'Developer');
113
+ expect(devMenu).toBeUndefined();
114
+ });
115
+
116
+ it('should create menu with File, Edit, View, Window, Help', () => {
117
+ windowsMenu.buildAndSetAppMenu();
118
+
119
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
120
+ const menuLabels = template.map((item: any) => item.label);
121
+
122
+ expect(menuLabels).toContain('File');
123
+ expect(menuLabels).toContain('Edit');
124
+ expect(menuLabels).toContain('View');
125
+ expect(menuLabels).toContain('Window');
126
+ expect(menuLabels).toContain('Help');
127
+ });
128
+ });
129
+
130
+ describe('buildContextMenu', () => {
131
+ it('should build chat context menu', () => {
132
+ const menu = windowsMenu.buildContextMenu('chat');
133
+
134
+ expect(Menu.buildFromTemplate).toHaveBeenCalled();
135
+ expect(menu).toBeDefined();
136
+ });
137
+
138
+ it('should build editor context menu', () => {
139
+ const menu = windowsMenu.buildContextMenu('editor');
140
+
141
+ expect(Menu.buildFromTemplate).toHaveBeenCalled();
142
+ expect(menu).toBeDefined();
143
+ });
144
+
145
+ it('should build default context menu for unknown type', () => {
146
+ const menu = windowsMenu.buildContextMenu('unknown');
147
+
148
+ expect(Menu.buildFromTemplate).toHaveBeenCalled();
149
+ expect(menu).toBeDefined();
150
+ });
151
+
152
+ it('should pass data to context menu', () => {
153
+ const data = { text: 'selected text' };
154
+ windowsMenu.buildContextMenu('editor', data);
155
+
156
+ expect(Menu.buildFromTemplate).toHaveBeenCalled();
157
+ });
158
+ });
159
+
160
+ describe('buildTrayMenu', () => {
161
+ it('should build tray menu', () => {
162
+ const menu = windowsMenu.buildTrayMenu();
163
+
164
+ expect(Menu.buildFromTemplate).toHaveBeenCalled();
165
+ expect(menu).toBeDefined();
166
+ });
167
+
168
+ it('should include open and quit items in tray menu', () => {
169
+ windowsMenu.buildTrayMenu();
170
+
171
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
172
+ expect(template.length).toBeGreaterThan(0);
173
+ expect(template.some((item: any) => item.label?.includes('Open'))).toBe(true);
174
+ expect(template.some((item: any) => item.label === 'Quit')).toBe(true);
175
+ });
176
+ });
177
+
178
+ describe('refresh', () => {
179
+ it('should rebuild application menu', () => {
180
+ windowsMenu.refresh();
181
+
182
+ expect(Menu.buildFromTemplate).toHaveBeenCalled();
183
+ expect(Menu.setApplicationMenu).toHaveBeenCalled();
184
+ });
185
+
186
+ it('should pass options to rebuild', () => {
187
+ windowsMenu.refresh({ showDevItems: true });
188
+
189
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
190
+ const devMenu = template.find((item: any) => item.label === 'Developer');
191
+ expect(devMenu).toBeDefined();
192
+ });
193
+ });
194
+
195
+ describe('menu item click handlers', () => {
196
+ it('should handle preferences click', () => {
197
+ windowsMenu.buildAndSetAppMenu();
198
+
199
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
200
+ const fileMenu = template.find((item: any) => item.label === 'File');
201
+ const preferencesItem = fileMenu.submenu.find((item: any) => item.label === 'Settings');
202
+
203
+ expect(preferencesItem).toBeDefined();
204
+ preferencesItem.click();
205
+ expect(mockApp.browserManager.retrieveByIdentifier).toHaveBeenCalledWith('settings');
206
+ });
207
+
208
+ it('should handle check for updates click', () => {
209
+ windowsMenu.buildAndSetAppMenu();
210
+
211
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
212
+ const fileMenu = template.find((item: any) => item.label === 'File');
213
+ const checkUpdatesItem = fileMenu.submenu.find(
214
+ (item: any) => item.label === 'Check for Updates',
215
+ );
216
+
217
+ expect(checkUpdatesItem).toBeDefined();
218
+ checkUpdatesItem.click();
219
+ expect(mockApp.updaterManager.checkForUpdates).toHaveBeenCalledWith({ manual: true });
220
+ });
221
+
222
+ it('should handle visit website click', async () => {
223
+ windowsMenu.buildAndSetAppMenu();
224
+
225
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
226
+ const helpMenu = template.find((item: any) => item.label === 'Help');
227
+ const visitWebsiteItem = helpMenu.submenu.find((item: any) => item.label === 'Visit Website');
228
+
229
+ expect(visitWebsiteItem).toBeDefined();
230
+ await visitWebsiteItem.click();
231
+ expect(shell.openExternal).toHaveBeenCalledWith('https://lobehub.com');
232
+ });
233
+
234
+ it('should handle github repo click', async () => {
235
+ windowsMenu.buildAndSetAppMenu();
236
+
237
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
238
+ const helpMenu = template.find((item: any) => item.label === 'Help');
239
+ const githubItem = helpMenu.submenu.find((item: any) => item.label === 'GitHub Repository');
240
+
241
+ expect(githubItem).toBeDefined();
242
+ await githubItem.click();
243
+ expect(shell.openExternal).toHaveBeenCalledWith('https://github.com/lobehub/lobe-chat');
244
+ });
245
+
246
+ it('should handle tray open click', () => {
247
+ windowsMenu.buildTrayMenu();
248
+
249
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
250
+ const openItem = template.find((item: any) => item.label?.includes('Open'));
251
+
252
+ expect(openItem).toBeDefined();
253
+ openItem.click();
254
+ expect(mockApp.browserManager.showMainWindow).toHaveBeenCalled();
255
+ });
256
+ });
257
+
258
+ describe('menu accelerators', () => {
259
+ it('should use Ctrl prefix for Windows shortcuts', () => {
260
+ windowsMenu.buildAndSetAppMenu();
261
+
262
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
263
+ const editMenu = template.find((item: any) => item.label === 'Edit');
264
+ const copyItem = editMenu.submenu.find((item: any) => item.label === 'Copy');
265
+
266
+ expect(copyItem.accelerator).toBe('Ctrl+C');
267
+ });
268
+
269
+ it('should set correct accelerator for close', () => {
270
+ windowsMenu.buildAndSetAppMenu();
271
+
272
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
273
+ const fileMenu = template.find((item: any) => item.label === 'File');
274
+ const closeItem = fileMenu.submenu.find((item: any) => item.label === 'Close');
275
+
276
+ expect(closeItem.accelerator).toBe('Alt+F4');
277
+ });
278
+
279
+ it('should set correct accelerator for minimize', () => {
280
+ windowsMenu.buildAndSetAppMenu();
281
+
282
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
283
+ const fileMenu = template.find((item: any) => item.label === 'File');
284
+ const minimizeItem = fileMenu.submenu.find((item: any) => item.label === 'Minimize');
285
+
286
+ expect(minimizeItem.accelerator).toBe('Ctrl+M');
287
+ });
288
+
289
+ it('should set F11 for fullscreen', () => {
290
+ windowsMenu.buildAndSetAppMenu();
291
+
292
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
293
+ const viewMenu = template.find((item: any) => item.label === 'View');
294
+ const fullscreenItem = viewMenu.submenu.find((item: any) => item.label === 'Full Screen');
295
+
296
+ expect(fullscreenItem.accelerator).toBe('F11');
297
+ });
298
+ });
299
+
300
+ describe('developer menu items', () => {
301
+ it('should include dev tools shortcuts in developer menu', () => {
302
+ windowsMenu.buildAndSetAppMenu({ showDevItems: true });
303
+
304
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
305
+ const devMenu = template.find((item: any) => item.label === 'Developer');
306
+
307
+ expect(devMenu).toBeDefined();
308
+ expect(devMenu.submenu.length).toBeGreaterThan(0);
309
+ });
310
+
311
+ it('should handle dev panel click', () => {
312
+ windowsMenu.buildAndSetAppMenu({ showDevItems: true });
313
+
314
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
315
+ const devMenu = template.find((item: any) => item.label === 'Developer');
316
+ const devPanelItem = devMenu.submenu.find((item: any) => item.label === 'Dev Panel');
317
+
318
+ expect(devPanelItem).toBeDefined();
319
+ devPanelItem.click();
320
+ expect(mockApp.browserManager.retrieveByIdentifier).toHaveBeenCalledWith('devtools');
321
+ });
322
+
323
+ it('should set Ctrl+Shift+I for developer tools', () => {
324
+ windowsMenu.buildAndSetAppMenu({ showDevItems: true });
325
+
326
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
327
+ const devMenu = template.find((item: any) => item.label === 'Developer');
328
+ const devToolsItem = devMenu.submenu.find((item: any) => item.label === 'Developer Tools');
329
+
330
+ expect(devToolsItem.accelerator).toBe('Ctrl+Shift+I');
331
+ });
332
+ });
333
+
334
+ describe('context menu templates', () => {
335
+ it('should include copy and paste in chat context menu', () => {
336
+ windowsMenu.buildContextMenu('chat');
337
+
338
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
339
+ const copyItem = template.find((item: any) => item.role === 'copy');
340
+ const pasteItem = template.find((item: any) => item.role === 'paste');
341
+
342
+ expect(copyItem).toBeDefined();
343
+ expect(pasteItem).toBeDefined();
344
+ });
345
+
346
+ it('should use Ctrl accelerators in context menus', () => {
347
+ windowsMenu.buildContextMenu('editor');
348
+
349
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
350
+ const copyItem = template.find((item: any) => item.role === 'copy');
351
+
352
+ expect(copyItem.accelerator).toBe('Ctrl+C');
353
+ });
354
+
355
+ it('should include cut in editor context menu', () => {
356
+ windowsMenu.buildContextMenu('editor');
357
+
358
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
359
+ const cutItem = template.find((item: any) => item.role === 'cut');
360
+
361
+ expect(cutItem).toBeDefined();
362
+ expect(cutItem.accelerator).toBe('Ctrl+X');
363
+ });
364
+
365
+ it('should include delete in editor context menu', () => {
366
+ windowsMenu.buildContextMenu('editor');
367
+
368
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
369
+ const deleteItem = template.find((item: any) => item.role === 'delete');
370
+
371
+ expect(deleteItem).toBeDefined();
372
+ });
373
+ });
374
+
375
+ describe('menu structure', () => {
376
+ it('should have separators in menus', () => {
377
+ windowsMenu.buildAndSetAppMenu();
378
+
379
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
380
+ const fileMenu = template.find((item: any) => item.label === 'File');
381
+ const hasSeparator = fileMenu.submenu.some((item: any) => item.type === 'separator');
382
+
383
+ expect(hasSeparator).toBe(true);
384
+ });
385
+
386
+ it('should have minimize and close in window menu', () => {
387
+ windowsMenu.buildAndSetAppMenu();
388
+
389
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
390
+ const windowMenu = template.find((item: any) => item.label === 'Window');
391
+
392
+ const minimizeItem = windowMenu.submenu.find((item: any) => item.role === 'minimize');
393
+ const closeItem = windowMenu.submenu.find((item: any) => item.role === 'close');
394
+
395
+ expect(minimizeItem).toBeDefined();
396
+ expect(closeItem).toBeDefined();
397
+ });
398
+
399
+ it('should have zoom controls in view menu', () => {
400
+ windowsMenu.buildAndSetAppMenu();
401
+
402
+ const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
403
+ const viewMenu = template.find((item: any) => item.label === 'View');
404
+
405
+ const resetZoomItem = viewMenu.submenu.find((item: any) => item.role === 'resetZoom');
406
+ const zoomInItem = viewMenu.submenu.find((item: any) => item.role === 'zoomIn');
407
+ const zoomOutItem = viewMenu.submenu.find((item: any) => item.role === 'zoomOut');
408
+
409
+ expect(resetZoomItem).toBeDefined();
410
+ expect(zoomInItem).toBeDefined();
411
+ expect(zoomOutItem).toBeDefined();
412
+ });
413
+ });
414
+
415
+ describe('i18n integration', () => {
416
+ it('should use i18n for all menu labels', () => {
417
+ windowsMenu.buildAndSetAppMenu();
418
+
419
+ expect(mockApp.i18n.ns).toHaveBeenCalledWith('menu');
420
+ });
421
+
422
+ it('should request translations multiple times for tray menu', () => {
423
+ windowsMenu.buildTrayMenu();
424
+
425
+ expect(mockApp.i18n.ns).toHaveBeenCalled();
426
+ expect(app.getName).toHaveBeenCalled();
427
+ });
428
+ });
429
+ });
@@ -108,10 +108,10 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
108
108
 
109
109
  expect(results.length).toBeGreaterThan(0);
110
110
 
111
- // Should find test files
111
+ // Should find test files (can be in __tests__ directory or co-located with source files)
112
112
  const testFile = results.find((r) => r.name.endsWith('.test.ts'));
113
113
  expect(testFile).toBeDefined();
114
- expect(testFile!.path).toContain('__tests__');
114
+ expect(testFile!.path).toMatch(/(__tests__|\.test\.ts$)/);
115
115
  });
116
116
  });
117
117