@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.
- package/CHANGELOG.md +50 -0
- package/apps/desktop/package.json +1 -0
- package/apps/desktop/src/main/core/ui/__tests__/MenuManager.test.ts +320 -0
- package/apps/desktop/src/main/core/ui/__tests__/Tray.test.ts +518 -0
- package/apps/desktop/src/main/core/ui/__tests__/TrayManager.test.ts +360 -0
- package/apps/desktop/src/main/menus/impls/BaseMenuPlatform.test.ts +49 -0
- package/apps/desktop/src/main/menus/impls/linux.test.ts +552 -0
- package/apps/desktop/src/main/menus/impls/macOS.test.ts +464 -0
- package/apps/desktop/src/main/menus/impls/windows.test.ts +429 -0
- package/apps/desktop/src/main/modules/fileSearch/__tests__/macOS.integration.test.ts +2 -2
- package/apps/desktop/src/main/services/__tests__/fileSearchSrv.test.ts +402 -0
- package/apps/desktop/src/main/utils/__tests__/file-system.test.ts +91 -0
- package/apps/desktop/src/main/utils/__tests__/logger.test.ts +229 -0
- package/apps/desktop/src/preload/electronApi.test.ts +142 -0
- package/apps/desktop/src/preload/invoke.test.ts +145 -0
- package/apps/desktop/src/preload/routeInterceptor.test.ts +374 -0
- package/apps/desktop/src/preload/streamer.test.ts +365 -0
- package/apps/desktop/vitest.config.mts +1 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/marketAuth.json +13 -0
- package/locales/bg-BG/marketAuth.json +13 -0
- package/locales/de-DE/marketAuth.json +13 -0
- package/locales/en-US/marketAuth.json +13 -0
- package/locales/es-ES/marketAuth.json +13 -0
- package/locales/fa-IR/marketAuth.json +13 -0
- package/locales/fr-FR/marketAuth.json +13 -0
- package/locales/it-IT/marketAuth.json +13 -0
- package/locales/ja-JP/marketAuth.json +13 -0
- package/locales/ko-KR/marketAuth.json +13 -0
- package/locales/nl-NL/marketAuth.json +13 -0
- package/locales/pl-PL/marketAuth.json +13 -0
- package/locales/pt-BR/marketAuth.json +13 -0
- package/locales/ru-RU/marketAuth.json +13 -0
- package/locales/tr-TR/marketAuth.json +13 -0
- package/locales/vi-VN/marketAuth.json +13 -0
- package/locales/zh-CN/marketAuth.json +13 -0
- package/locales/zh-TW/marketAuth.json +13 -0
- package/package.json +1 -1
- package/packages/database/migrations/0054_better_auth_two_factor.sql +2 -0
- package/packages/database/src/core/migrations.json +1 -1
- package/packages/database/src/models/user.ts +27 -5
- package/packages/types/src/discover/mcp.ts +2 -1
- package/packages/types/src/tool/plugin.ts +2 -1
- package/scripts/migrateServerDB/errorHint.js +26 -0
- package/scripts/migrateServerDB/index.ts +5 -1
- package/src/app/[variants]/(main)/chat/settings/features/SmartAgentActionButton/MarketPublishButton.tsx +0 -2
- package/src/app/[variants]/(main)/discover/(detail)/mcp/features/Sidebar/ActionButton/index.tsx +33 -7
- package/src/features/PluginStore/McpList/List/Action.tsx +20 -1
- package/src/layout/AuthProvider/MarketAuth/MarketAuthConfirmModal.tsx +158 -0
- package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +130 -14
- package/src/libs/mcp/types.ts +8 -0
- package/src/locales/default/marketAuth.ts +13 -0
- package/src/server/routers/lambda/market/index.ts +85 -2
- package/src/server/services/discover/index.ts +45 -4
- package/src/services/discover.ts +1 -1
- package/src/services/mcp.ts +18 -3
- package/src/store/tool/slices/mcpStore/action.test.ts +141 -0
- package/src/store/tool/slices/mcpStore/action.ts +153 -11
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { Menu, app, dialog, shell } from 'electron';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import type { App } from '@/core/App';
|
|
5
|
+
|
|
6
|
+
import { LinuxMenu } from './linux';
|
|
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
|
+
getVersion: vi.fn(() => '1.0.0'),
|
|
17
|
+
},
|
|
18
|
+
shell: {
|
|
19
|
+
openExternal: vi.fn(),
|
|
20
|
+
},
|
|
21
|
+
dialog: {
|
|
22
|
+
showMessageBox: vi.fn(),
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// Mock isDev
|
|
27
|
+
vi.mock('@/const/env', () => ({
|
|
28
|
+
isDev: false,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
// Mock App instance
|
|
32
|
+
const createMockApp = () => {
|
|
33
|
+
const mockT = vi.fn((key: string, params?: any) => {
|
|
34
|
+
const translations: Record<string, string> = {
|
|
35
|
+
'file.title': 'File',
|
|
36
|
+
'file.preferences': 'Preferences',
|
|
37
|
+
'file.quit': 'Quit',
|
|
38
|
+
'common.checkUpdates': 'Check for Updates',
|
|
39
|
+
'window.close': 'Close',
|
|
40
|
+
'window.minimize': 'Minimize',
|
|
41
|
+
'window.title': 'Window',
|
|
42
|
+
'edit.title': 'Edit',
|
|
43
|
+
'edit.undo': 'Undo',
|
|
44
|
+
'edit.redo': 'Redo',
|
|
45
|
+
'edit.cut': 'Cut',
|
|
46
|
+
'edit.copy': 'Copy',
|
|
47
|
+
'edit.paste': 'Paste',
|
|
48
|
+
'edit.selectAll': 'Select All',
|
|
49
|
+
'edit.delete': 'Delete',
|
|
50
|
+
'view.title': 'View',
|
|
51
|
+
'view.resetZoom': 'Reset Zoom',
|
|
52
|
+
'view.zoomIn': 'Zoom In',
|
|
53
|
+
'view.zoomOut': 'Zoom Out',
|
|
54
|
+
'view.toggleFullscreen': 'Toggle Full Screen',
|
|
55
|
+
'help.title': 'Help',
|
|
56
|
+
'help.visitWebsite': 'Visit Website',
|
|
57
|
+
'help.githubRepo': 'GitHub Repository',
|
|
58
|
+
'help.about': 'About',
|
|
59
|
+
'dev.title': 'Developer',
|
|
60
|
+
'dev.reload': 'Reload',
|
|
61
|
+
'dev.forceReload': 'Force Reload',
|
|
62
|
+
'dev.devTools': 'Developer Tools',
|
|
63
|
+
'dev.devPanel': 'Dev Panel',
|
|
64
|
+
'tray.open': `Open ${params?.appName || 'App'}`,
|
|
65
|
+
'tray.quit': 'Quit',
|
|
66
|
+
};
|
|
67
|
+
return translations[key] || key;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const mockCommonT = vi.fn((key: string) => {
|
|
71
|
+
const translations: Record<string, string> = {
|
|
72
|
+
'actions.ok': 'OK',
|
|
73
|
+
};
|
|
74
|
+
return translations[key] || key;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const mockDialogT = vi.fn((key: string, params?: any) => {
|
|
78
|
+
const translations: Record<string, string> = {
|
|
79
|
+
'about.title': 'About',
|
|
80
|
+
'about.message': `${params?.appName || 'App'} ${params?.appVersion || '1.0.0'}`,
|
|
81
|
+
'about.detail': 'LobeChat Desktop Application',
|
|
82
|
+
};
|
|
83
|
+
return translations[key] || key;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
i18n: {
|
|
88
|
+
ns: vi.fn((namespace: string) => {
|
|
89
|
+
if (namespace === 'common') return mockCommonT;
|
|
90
|
+
if (namespace === 'dialog') return mockDialogT;
|
|
91
|
+
return mockT;
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
browserManager: {
|
|
95
|
+
showMainWindow: vi.fn(),
|
|
96
|
+
retrieveByIdentifier: vi.fn(() => ({
|
|
97
|
+
show: vi.fn(),
|
|
98
|
+
})),
|
|
99
|
+
},
|
|
100
|
+
updaterManager: {
|
|
101
|
+
checkForUpdates: vi.fn(),
|
|
102
|
+
},
|
|
103
|
+
} as unknown as App;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
describe('LinuxMenu', () => {
|
|
107
|
+
let linuxMenu: LinuxMenu;
|
|
108
|
+
let mockApp: App;
|
|
109
|
+
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
vi.clearAllMocks();
|
|
112
|
+
mockApp = createMockApp();
|
|
113
|
+
linuxMenu = new LinuxMenu(mockApp);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('buildAndSetAppMenu', () => {
|
|
117
|
+
it('should build and set application menu', () => {
|
|
118
|
+
const menu = linuxMenu.buildAndSetAppMenu();
|
|
119
|
+
|
|
120
|
+
expect(Menu.buildFromTemplate).toHaveBeenCalled();
|
|
121
|
+
expect(Menu.setApplicationMenu).toHaveBeenCalled();
|
|
122
|
+
expect(menu).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should include developer menu when showDevItems is true', () => {
|
|
126
|
+
linuxMenu.buildAndSetAppMenu({ showDevItems: true });
|
|
127
|
+
|
|
128
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
129
|
+
const devMenu = template.find((item: any) => item.label === 'Developer');
|
|
130
|
+
expect(devMenu).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should not include developer menu when showDevItems is false', () => {
|
|
134
|
+
linuxMenu.buildAndSetAppMenu({ showDevItems: false });
|
|
135
|
+
|
|
136
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
137
|
+
const devMenu = template.find((item: any) => item.label === 'Developer');
|
|
138
|
+
expect(devMenu).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should create menu with File, Edit, View, Window, Help', () => {
|
|
142
|
+
linuxMenu.buildAndSetAppMenu();
|
|
143
|
+
|
|
144
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
145
|
+
const menuLabels = template.map((item: any) => item.label);
|
|
146
|
+
|
|
147
|
+
expect(menuLabels).toContain('File');
|
|
148
|
+
expect(menuLabels).toContain('Edit');
|
|
149
|
+
expect(menuLabels).toContain('View');
|
|
150
|
+
expect(menuLabels).toContain('Window');
|
|
151
|
+
expect(menuLabels).toContain('Help');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('buildContextMenu', () => {
|
|
156
|
+
it('should build chat context menu', () => {
|
|
157
|
+
const menu = linuxMenu.buildContextMenu('chat');
|
|
158
|
+
|
|
159
|
+
expect(Menu.buildFromTemplate).toHaveBeenCalled();
|
|
160
|
+
expect(menu).toBeDefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should build editor context menu', () => {
|
|
164
|
+
const menu = linuxMenu.buildContextMenu('editor');
|
|
165
|
+
|
|
166
|
+
expect(Menu.buildFromTemplate).toHaveBeenCalled();
|
|
167
|
+
expect(menu).toBeDefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should build default context menu for unknown type', () => {
|
|
171
|
+
const menu = linuxMenu.buildContextMenu('unknown');
|
|
172
|
+
|
|
173
|
+
expect(Menu.buildFromTemplate).toHaveBeenCalled();
|
|
174
|
+
expect(menu).toBeDefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should pass data to context menu', () => {
|
|
178
|
+
const data = { selection: 'text' };
|
|
179
|
+
linuxMenu.buildContextMenu('chat', data);
|
|
180
|
+
|
|
181
|
+
expect(Menu.buildFromTemplate).toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('buildTrayMenu', () => {
|
|
186
|
+
it('should build tray menu', () => {
|
|
187
|
+
const menu = linuxMenu.buildTrayMenu();
|
|
188
|
+
|
|
189
|
+
expect(Menu.buildFromTemplate).toHaveBeenCalled();
|
|
190
|
+
expect(menu).toBeDefined();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should include open and quit items in tray menu', () => {
|
|
194
|
+
linuxMenu.buildTrayMenu();
|
|
195
|
+
|
|
196
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
197
|
+
expect(template.length).toBeGreaterThan(0);
|
|
198
|
+
expect(template.some((item: any) => item.label?.includes('Open'))).toBe(true);
|
|
199
|
+
expect(template.some((item: any) => item.label === 'Quit')).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('refresh', () => {
|
|
204
|
+
it('should rebuild application menu', () => {
|
|
205
|
+
linuxMenu.refresh();
|
|
206
|
+
|
|
207
|
+
expect(Menu.buildFromTemplate).toHaveBeenCalled();
|
|
208
|
+
expect(Menu.setApplicationMenu).toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should pass options to rebuild', () => {
|
|
212
|
+
linuxMenu.refresh({ showDevItems: true });
|
|
213
|
+
|
|
214
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
215
|
+
const devMenu = template.find((item: any) => item.label === 'Developer');
|
|
216
|
+
expect(devMenu).toBeDefined();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('menu item click handlers', () => {
|
|
221
|
+
it('should handle preferences click', () => {
|
|
222
|
+
linuxMenu.buildAndSetAppMenu();
|
|
223
|
+
|
|
224
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
225
|
+
const fileMenu = template.find((item: any) => item.label === 'File');
|
|
226
|
+
const preferencesItem = fileMenu.submenu.find((item: any) => item.label === 'Preferences');
|
|
227
|
+
|
|
228
|
+
expect(preferencesItem).toBeDefined();
|
|
229
|
+
preferencesItem.click();
|
|
230
|
+
expect(mockApp.browserManager.retrieveByIdentifier).toHaveBeenCalledWith('settings');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should handle check for updates click', () => {
|
|
234
|
+
linuxMenu.buildAndSetAppMenu();
|
|
235
|
+
|
|
236
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
237
|
+
const fileMenu = template.find((item: any) => item.label === 'File');
|
|
238
|
+
const checkUpdatesItem = fileMenu.submenu.find(
|
|
239
|
+
(item: any) => item.label === 'Check for Updates',
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect(checkUpdatesItem).toBeDefined();
|
|
243
|
+
checkUpdatesItem.click();
|
|
244
|
+
expect(mockApp.updaterManager.checkForUpdates).toHaveBeenCalledWith({ manual: true });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should handle visit website click', async () => {
|
|
248
|
+
linuxMenu.buildAndSetAppMenu();
|
|
249
|
+
|
|
250
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
251
|
+
const helpMenu = template.find((item: any) => item.label === 'Help');
|
|
252
|
+
const visitWebsiteItem = helpMenu.submenu.find((item: any) => item.label === 'Visit Website');
|
|
253
|
+
|
|
254
|
+
expect(visitWebsiteItem).toBeDefined();
|
|
255
|
+
await visitWebsiteItem.click();
|
|
256
|
+
expect(shell.openExternal).toHaveBeenCalledWith('https://lobehub.com');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should handle github repo click', async () => {
|
|
260
|
+
linuxMenu.buildAndSetAppMenu();
|
|
261
|
+
|
|
262
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
263
|
+
const helpMenu = template.find((item: any) => item.label === 'Help');
|
|
264
|
+
const githubItem = helpMenu.submenu.find((item: any) => item.label === 'GitHub Repository');
|
|
265
|
+
|
|
266
|
+
expect(githubItem).toBeDefined();
|
|
267
|
+
await githubItem.click();
|
|
268
|
+
expect(shell.openExternal).toHaveBeenCalledWith('https://github.com/lobehub/lobe-chat');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should handle about dialog click', () => {
|
|
272
|
+
linuxMenu.buildAndSetAppMenu();
|
|
273
|
+
|
|
274
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
275
|
+
const helpMenu = template.find((item: any) => item.label === 'Help');
|
|
276
|
+
const aboutItem = helpMenu.submenu.find((item: any) => item.label === 'About');
|
|
277
|
+
|
|
278
|
+
expect(aboutItem).toBeDefined();
|
|
279
|
+
aboutItem.click();
|
|
280
|
+
expect(dialog.showMessageBox).toHaveBeenCalledWith(
|
|
281
|
+
expect.objectContaining({
|
|
282
|
+
type: 'info',
|
|
283
|
+
title: 'About',
|
|
284
|
+
buttons: ['OK'],
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should handle tray open click', () => {
|
|
290
|
+
linuxMenu.buildTrayMenu();
|
|
291
|
+
|
|
292
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
293
|
+
const openItem = template.find((item: any) => item.label?.includes('Open'));
|
|
294
|
+
|
|
295
|
+
expect(openItem).toBeDefined();
|
|
296
|
+
openItem.click();
|
|
297
|
+
expect(mockApp.browserManager.showMainWindow).toHaveBeenCalled();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('menu accelerators', () => {
|
|
302
|
+
it('should use Ctrl prefix for Linux shortcuts', () => {
|
|
303
|
+
linuxMenu.buildAndSetAppMenu();
|
|
304
|
+
|
|
305
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
306
|
+
const editMenu = template.find((item: any) => item.label === 'Edit');
|
|
307
|
+
const copyItem = editMenu.submenu.find((item: any) => item.label === 'Copy');
|
|
308
|
+
|
|
309
|
+
expect(copyItem.accelerator).toBe('Ctrl+C');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should set correct accelerator for close', () => {
|
|
313
|
+
linuxMenu.buildAndSetAppMenu();
|
|
314
|
+
|
|
315
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
316
|
+
const fileMenu = template.find((item: any) => item.label === 'File');
|
|
317
|
+
const closeItem = fileMenu.submenu.find((item: any) => item.label === 'Close');
|
|
318
|
+
|
|
319
|
+
expect(closeItem.accelerator).toBe('Ctrl+W');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should set correct accelerator for minimize', () => {
|
|
323
|
+
linuxMenu.buildAndSetAppMenu();
|
|
324
|
+
|
|
325
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
326
|
+
const fileMenu = template.find((item: any) => item.label === 'File');
|
|
327
|
+
const minimizeItem = fileMenu.submenu.find((item: any) => item.label === 'Minimize');
|
|
328
|
+
|
|
329
|
+
expect(minimizeItem.accelerator).toBe('Ctrl+M');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should set Ctrl+Shift+Z for redo', () => {
|
|
333
|
+
linuxMenu.buildAndSetAppMenu();
|
|
334
|
+
|
|
335
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
336
|
+
const editMenu = template.find((item: any) => item.label === 'Edit');
|
|
337
|
+
const redoItem = editMenu.submenu.find((item: any) => item.label === 'Redo');
|
|
338
|
+
|
|
339
|
+
expect(redoItem.accelerator).toBe('Ctrl+Shift+Z');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should set F11 for fullscreen', () => {
|
|
343
|
+
linuxMenu.buildAndSetAppMenu();
|
|
344
|
+
|
|
345
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
346
|
+
const viewMenu = template.find((item: any) => item.label === 'View');
|
|
347
|
+
const fullscreenItem = viewMenu.submenu.find(
|
|
348
|
+
(item: any) => item.label === 'Toggle Full Screen',
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
expect(fullscreenItem.accelerator).toBe('F11');
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe('developer menu items', () => {
|
|
356
|
+
it('should include dev tools shortcuts in developer menu', () => {
|
|
357
|
+
linuxMenu.buildAndSetAppMenu({ showDevItems: true });
|
|
358
|
+
|
|
359
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
360
|
+
const devMenu = template.find((item: any) => item.label === 'Developer');
|
|
361
|
+
|
|
362
|
+
expect(devMenu).toBeDefined();
|
|
363
|
+
expect(devMenu.submenu.length).toBeGreaterThan(0);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should handle dev panel click', () => {
|
|
367
|
+
linuxMenu.buildAndSetAppMenu({ showDevItems: true });
|
|
368
|
+
|
|
369
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
370
|
+
const devMenu = template.find((item: any) => item.label === 'Developer');
|
|
371
|
+
const devPanelItem = devMenu.submenu.find((item: any) => item.label === 'Dev Panel');
|
|
372
|
+
|
|
373
|
+
expect(devPanelItem).toBeDefined();
|
|
374
|
+
devPanelItem.click();
|
|
375
|
+
expect(mockApp.browserManager.retrieveByIdentifier).toHaveBeenCalledWith('devtools');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should set Ctrl+Shift+I for developer tools', () => {
|
|
379
|
+
linuxMenu.buildAndSetAppMenu({ showDevItems: true });
|
|
380
|
+
|
|
381
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
382
|
+
const devMenu = template.find((item: any) => item.label === 'Developer');
|
|
383
|
+
const devToolsItem = devMenu.submenu.find((item: any) => item.label === 'Developer Tools');
|
|
384
|
+
|
|
385
|
+
expect(devToolsItem.accelerator).toBe('Ctrl+Shift+I');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should include reload options in developer menu', () => {
|
|
389
|
+
linuxMenu.buildAndSetAppMenu({ showDevItems: true });
|
|
390
|
+
|
|
391
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
392
|
+
const devMenu = template.find((item: any) => item.label === 'Developer');
|
|
393
|
+
|
|
394
|
+
const reloadItem = devMenu.submenu.find((item: any) => item.label === 'Reload');
|
|
395
|
+
const forceReloadItem = devMenu.submenu.find((item: any) => item.label === 'Force Reload');
|
|
396
|
+
|
|
397
|
+
expect(reloadItem).toBeDefined();
|
|
398
|
+
expect(forceReloadItem).toBeDefined();
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe('context menu templates', () => {
|
|
403
|
+
it('should include copy and paste in chat context menu', () => {
|
|
404
|
+
linuxMenu.buildContextMenu('chat');
|
|
405
|
+
|
|
406
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
407
|
+
const copyItem = template.find((item: any) => item.role === 'copy');
|
|
408
|
+
const pasteItem = template.find((item: any) => item.role === 'paste');
|
|
409
|
+
|
|
410
|
+
expect(copyItem).toBeDefined();
|
|
411
|
+
expect(pasteItem).toBeDefined();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should use Ctrl accelerators in context menus', () => {
|
|
415
|
+
linuxMenu.buildContextMenu('editor');
|
|
416
|
+
|
|
417
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
418
|
+
const copyItem = template.find((item: any) => item.role === 'copy');
|
|
419
|
+
|
|
420
|
+
expect(copyItem.accelerator).toBe('Ctrl+C');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should include cut in editor context menu', () => {
|
|
424
|
+
linuxMenu.buildContextMenu('editor');
|
|
425
|
+
|
|
426
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
427
|
+
const cutItem = template.find((item: any) => item.role === 'cut');
|
|
428
|
+
|
|
429
|
+
expect(cutItem).toBeDefined();
|
|
430
|
+
expect(cutItem.accelerator).toBe('Ctrl+X');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('should include delete in editor context menu', () => {
|
|
434
|
+
linuxMenu.buildContextMenu('editor');
|
|
435
|
+
|
|
436
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
437
|
+
const deleteItem = template.find((item: any) => item.role === 'delete');
|
|
438
|
+
|
|
439
|
+
expect(deleteItem).toBeDefined();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('should not include cut in chat context menu', () => {
|
|
443
|
+
linuxMenu.buildContextMenu('chat');
|
|
444
|
+
|
|
445
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
446
|
+
const cutItem = template.find((item: any) => item.role === 'cut');
|
|
447
|
+
|
|
448
|
+
expect(cutItem).toBeUndefined();
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe('menu structure', () => {
|
|
453
|
+
it('should have separators in menus', () => {
|
|
454
|
+
linuxMenu.buildAndSetAppMenu();
|
|
455
|
+
|
|
456
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
457
|
+
const fileMenu = template.find((item: any) => item.label === 'File');
|
|
458
|
+
const hasSeparator = fileMenu.submenu.some((item: any) => item.type === 'separator');
|
|
459
|
+
|
|
460
|
+
expect(hasSeparator).toBe(true);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('should have minimize and close in window menu', () => {
|
|
464
|
+
linuxMenu.buildAndSetAppMenu();
|
|
465
|
+
|
|
466
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
467
|
+
const windowMenu = template.find((item: any) => item.label === 'Window');
|
|
468
|
+
|
|
469
|
+
const minimizeItem = windowMenu.submenu.find((item: any) => item.role === 'minimize');
|
|
470
|
+
const closeItem = windowMenu.submenu.find((item: any) => item.role === 'close');
|
|
471
|
+
|
|
472
|
+
expect(minimizeItem).toBeDefined();
|
|
473
|
+
expect(closeItem).toBeDefined();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('should have zoom controls in view menu', () => {
|
|
477
|
+
linuxMenu.buildAndSetAppMenu();
|
|
478
|
+
|
|
479
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
480
|
+
const viewMenu = template.find((item: any) => item.label === 'View');
|
|
481
|
+
|
|
482
|
+
const resetZoomItem = viewMenu.submenu.find((item: any) => item.role === 'resetZoom');
|
|
483
|
+
const zoomInItem = viewMenu.submenu.find((item: any) => item.role === 'zoomIn');
|
|
484
|
+
const zoomOutItem = viewMenu.submenu.find((item: any) => item.role === 'zoomOut');
|
|
485
|
+
|
|
486
|
+
expect(resetZoomItem).toBeDefined();
|
|
487
|
+
expect(zoomInItem).toBeDefined();
|
|
488
|
+
expect(zoomOutItem).toBeDefined();
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
describe('about dialog', () => {
|
|
493
|
+
it('should show about dialog with app info', () => {
|
|
494
|
+
linuxMenu.buildAndSetAppMenu();
|
|
495
|
+
|
|
496
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
497
|
+
const helpMenu = template.find((item: any) => item.label === 'Help');
|
|
498
|
+
const aboutItem = helpMenu.submenu.find((item: any) => item.label === 'About');
|
|
499
|
+
|
|
500
|
+
aboutItem.click();
|
|
501
|
+
|
|
502
|
+
expect(mockApp.i18n.ns).toHaveBeenCalledWith('common');
|
|
503
|
+
expect(mockApp.i18n.ns).toHaveBeenCalledWith('dialog');
|
|
504
|
+
expect(app.getName).toHaveBeenCalled();
|
|
505
|
+
expect(app.getVersion).toHaveBeenCalled();
|
|
506
|
+
expect(dialog.showMessageBox).toHaveBeenCalled();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('should display app name and version in about dialog', () => {
|
|
510
|
+
linuxMenu.buildAndSetAppMenu();
|
|
511
|
+
|
|
512
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
513
|
+
const helpMenu = template.find((item: any) => item.label === 'Help');
|
|
514
|
+
const aboutItem = helpMenu.submenu.find((item: any) => item.label === 'About');
|
|
515
|
+
|
|
516
|
+
aboutItem.click();
|
|
517
|
+
|
|
518
|
+
const callArgs = (dialog.showMessageBox as any).mock.calls[0][0];
|
|
519
|
+
expect(callArgs.message).toContain('LobeChat');
|
|
520
|
+
expect(callArgs.message).toContain('1.0.0');
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
describe('i18n integration', () => {
|
|
525
|
+
it('should use i18n for all menu labels', () => {
|
|
526
|
+
linuxMenu.buildAndSetAppMenu();
|
|
527
|
+
|
|
528
|
+
expect(mockApp.i18n.ns).toHaveBeenCalledWith('menu');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should request translations for tray menu', () => {
|
|
532
|
+
linuxMenu.buildTrayMenu();
|
|
533
|
+
|
|
534
|
+
expect(mockApp.i18n.ns).toHaveBeenCalled();
|
|
535
|
+
expect(app.getName).toHaveBeenCalled();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('should use multiple i18n namespaces for about dialog', () => {
|
|
539
|
+
linuxMenu.buildAndSetAppMenu();
|
|
540
|
+
|
|
541
|
+
const template = (Menu.buildFromTemplate as any).mock.calls[0][0];
|
|
542
|
+
const helpMenu = template.find((item: any) => item.label === 'Help');
|
|
543
|
+
const aboutItem = helpMenu.submenu.find((item: any) => item.label === 'About');
|
|
544
|
+
|
|
545
|
+
vi.clearAllMocks();
|
|
546
|
+
aboutItem.click();
|
|
547
|
+
|
|
548
|
+
expect(mockApp.i18n.ns).toHaveBeenCalledWith('common');
|
|
549
|
+
expect(mockApp.i18n.ns).toHaveBeenCalledWith('dialog');
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
});
|