@lobehub/lobehub 2.0.0-next.164 → 2.0.0-next.166
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/.cursor/rules/desktop-feature-implementation.mdc +31 -34
- package/.cursor/rules/desktop-local-tools-implement.mdc +3 -3
- package/.cursor/rules/desktop-window-management.mdc +56 -66
- package/CHANGELOG.md +50 -0
- package/Dockerfile +44 -52
- package/README.md +6 -6
- package/README.zh-CN.md +6 -6
- package/apps/desktop/Development.md +42 -46
- package/apps/desktop/README.md +37 -1
- package/apps/desktop/README.zh-CN.md +26 -1
- package/apps/desktop/electron.vite.config.ts +1 -0
- package/apps/desktop/src/main/controllers/AuthCtr.ts +4 -3
- package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +33 -20
- package/apps/desktop/src/main/controllers/DevtoolsCtr.ts +4 -2
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +14 -13
- package/apps/desktop/src/main/controllers/MenuCtr.ts +5 -4
- package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +18 -19
- package/apps/desktop/src/main/controllers/NotificationCtr.ts +4 -3
- package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +5 -4
- package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +3 -2
- package/apps/desktop/src/main/controllers/ShellCommandCtr.ts +5 -4
- package/apps/desktop/src/main/controllers/ShortcutCtr.ts +4 -3
- package/apps/desktop/src/main/controllers/SystemCtr.ts +7 -37
- package/apps/desktop/src/main/controllers/SystemServerCtr.ts +38 -0
- package/apps/desktop/src/main/controllers/TrayMenuCtr.ts +5 -4
- package/apps/desktop/src/main/controllers/UpdaterCtr.ts +6 -5
- package/apps/desktop/src/main/controllers/UploadFileCtr.ts +3 -25
- package/apps/desktop/src/main/controllers/UploadFileServerCtr.ts +33 -0
- package/apps/desktop/src/main/controllers/__tests__/AuthCtr.test.ts +9 -1
- package/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts +29 -9
- package/apps/desktop/src/main/controllers/__tests__/DevtoolsCtr.test.ts +12 -3
- package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +7 -0
- package/apps/desktop/src/main/controllers/__tests__/MenuCtr.test.ts +10 -0
- package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +10 -0
- package/apps/desktop/src/main/controllers/__tests__/NotificationCtr.test.ts +8 -0
- package/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts +8 -0
- package/apps/desktop/src/main/controllers/__tests__/RemoteServerSyncCtr.test.ts +1 -0
- package/apps/desktop/src/main/controllers/__tests__/ShellCommandCtr.test.ts +10 -0
- package/apps/desktop/src/main/controllers/__tests__/ShortcutCtr.test.ts +11 -0
- package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +43 -73
- package/apps/desktop/src/main/controllers/__tests__/SystemServerCtr.test.ts +75 -0
- package/apps/desktop/src/main/controllers/__tests__/TrayMenuCtr.test.ts +24 -13
- package/apps/desktop/src/main/controllers/__tests__/UpdaterCtr.test.ts +13 -2
- package/apps/desktop/src/main/controllers/__tests__/UploadFileCtr.test.ts +29 -108
- package/apps/desktop/src/main/controllers/__tests__/UploadFileServerCtr.test.ts +55 -0
- package/apps/desktop/src/main/controllers/_template.ts +2 -2
- package/apps/desktop/src/main/controllers/index.ts +5 -29
- package/apps/desktop/src/main/controllers/registry.ts +52 -0
- package/apps/desktop/src/main/core/App.ts +15 -47
- package/apps/desktop/src/main/core/__tests__/App.test.ts +5 -4
- package/apps/desktop/src/main/core/infrastructure/IoCContainer.ts +0 -5
- package/apps/desktop/src/main/core/infrastructure/__tests__/IoCContainer.test.ts +0 -50
- package/apps/desktop/src/main/exports.d.ts +8 -0
- package/apps/desktop/src/main/exports.ts +2 -0
- package/apps/desktop/src/main/global.d.ts +3 -0
- package/apps/desktop/src/main/modules/fileSearch/__tests__/macOS.integration.test.ts +17 -8
- package/apps/desktop/src/main/package.json +10 -0
- package/apps/desktop/src/main/services/fileSrv.ts +1 -1
- package/apps/desktop/src/main/utils/ipc/__tests__/base.test.ts +91 -0
- package/apps/desktop/src/main/utils/ipc/base.ts +170 -0
- package/apps/desktop/src/main/utils/ipc/index.ts +11 -0
- package/apps/desktop/src/main/utils/ipc/utility.ts +20 -0
- package/apps/desktop/src/preload/electronApi.ts +4 -1
- package/apps/desktop/src/preload/invoke.test.ts +13 -16
- package/apps/desktop/src/preload/invoke.ts +2 -5
- package/apps/desktop/src/preload/routeInterceptor.test.ts +13 -13
- package/apps/desktop/src/preload/routeInterceptor.ts +4 -4
- package/apps/desktop/tsconfig.json +15 -5
- package/changelog/v1.json +10 -0
- package/package.json +4 -3
- package/packages/electron-client-ipc/src/index.ts +1 -1
- package/packages/electron-client-ipc/src/ipc.test.ts +62 -0
- package/packages/electron-client-ipc/src/ipc.ts +63 -0
- package/packages/electron-client-ipc/src/streamInvoke.ts +7 -1
- package/packages/electron-client-ipc/src/types/dispatch.ts +1 -10
- package/packages/electron-client-ipc/vitest.config.mts +10 -0
- package/packages/electron-server-ipc/src/ipcClient.ts +1 -2
- package/packages/electron-server-ipc/src/ipcServer.ts +1 -2
- package/packages/electron-server-ipc/src/types/index.ts +1 -5
- package/pnpm-workspace.yaml +1 -1
- package/scripts/i18nWorkflow/const.ts +2 -2
- package/scripts/i18nWorkflow/i18nConfig.ts +7 -0
- package/scripts/i18nWorkflow/utils.ts +1 -1
- package/src/app/[variants]/(main)/discover/(detail)/provider/features/Sidebar/ActionButton/ProviderConfig.tsx +2 -2
- package/src/locales/default/setting.ts +1 -0
- package/src/server/modules/ElectronIPCClient/index.ts +59 -13
- package/src/services/electron/__tests__/devtools.test.ts +10 -6
- package/src/services/electron/autoUpdate.ts +5 -5
- package/src/services/electron/desktopNotification.ts +4 -7
- package/src/services/electron/devtools.ts +2 -2
- package/src/services/electron/file.ts +3 -2
- package/src/services/electron/localFileService.ts +17 -16
- package/src/services/electron/remoteServer.ts +7 -6
- package/src/services/electron/settings.ts +9 -11
- package/src/services/electron/system.ts +8 -6
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +1 -1
- package/src/store/global/actions/general.ts +8 -10
- package/src/utils/electron/desktopRemoteRPCFetch.ts +3 -2
- package/src/utils/electron/ipc.ts +12 -0
- package/tsconfig.json +5 -0
- package/apps/desktop/src/main/types/ipcClientEvent.ts +0 -3
- package/packages/electron-client-ipc/src/dispatch.ts +0 -41
|
@@ -2,9 +2,38 @@ import { ThemeMode } from '@lobechat/electron-client-ipc';
|
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
|
|
4
4
|
import type { App } from '@/core/App';
|
|
5
|
+
import type { IpcContext } from '@/utils/ipc';
|
|
6
|
+
import { IpcHandler } from '@/utils/ipc/base';
|
|
5
7
|
|
|
6
8
|
import SystemController from '../SystemCtr';
|
|
7
9
|
|
|
10
|
+
const { ipcHandlers, ipcMainHandleMock } = vi.hoisted(() => {
|
|
11
|
+
const handlers = new Map<string, (event: any, ...args: any[]) => any>();
|
|
12
|
+
const handle = vi.fn((channel: string, handler: any) => {
|
|
13
|
+
handlers.set(channel, handler);
|
|
14
|
+
});
|
|
15
|
+
return { ipcHandlers: handlers, ipcMainHandleMock: handle };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const invokeIpc = async <T = any>(
|
|
19
|
+
channel: string,
|
|
20
|
+
payload?: any,
|
|
21
|
+
context?: Partial<IpcContext>,
|
|
22
|
+
): Promise<T> => {
|
|
23
|
+
const handler = ipcHandlers.get(channel);
|
|
24
|
+
if (!handler) throw new Error(`IPC handler for ${channel} not found`);
|
|
25
|
+
|
|
26
|
+
const fakeEvent = {
|
|
27
|
+
sender: context?.sender ?? ({ id: 'test' } as any),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (payload === undefined) {
|
|
31
|
+
return handler(fakeEvent);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return handler(fakeEvent, payload);
|
|
35
|
+
};
|
|
36
|
+
|
|
8
37
|
// Mock logger
|
|
9
38
|
vi.mock('@/utils/logger', () => ({
|
|
10
39
|
createLogger: () => ({
|
|
@@ -21,6 +50,9 @@ vi.mock('electron', () => ({
|
|
|
21
50
|
getLocale: vi.fn(() => 'en-US'),
|
|
22
51
|
getPath: vi.fn((name: string) => `/mock/path/${name}`),
|
|
23
52
|
},
|
|
53
|
+
ipcMain: {
|
|
54
|
+
handle: ipcMainHandleMock,
|
|
55
|
+
},
|
|
24
56
|
nativeTheme: {
|
|
25
57
|
on: vi.fn(),
|
|
26
58
|
shouldUseDarkColors: false,
|
|
@@ -38,19 +70,6 @@ vi.mock('electron-is', () => ({
|
|
|
38
70
|
macOS: vi.fn(() => true),
|
|
39
71
|
}));
|
|
40
72
|
|
|
41
|
-
// Mock node:fs
|
|
42
|
-
vi.mock('node:fs', () => ({
|
|
43
|
-
readFileSync: vi.fn(),
|
|
44
|
-
writeFileSync: vi.fn(),
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
// Mock @/const/dir
|
|
48
|
-
vi.mock('@/const/dir', () => ({
|
|
49
|
-
DB_SCHEMA_HASH_FILENAME: 'db-schema-hash.txt',
|
|
50
|
-
LOCAL_DATABASE_DIR: 'database',
|
|
51
|
-
userDataDir: '/mock/user/data',
|
|
52
|
-
}));
|
|
53
|
-
|
|
54
73
|
// Mock browserManager
|
|
55
74
|
const mockBrowserManager = {
|
|
56
75
|
broadcastToAllWindows: vi.fn(),
|
|
@@ -80,12 +99,15 @@ describe('SystemController', () => {
|
|
|
80
99
|
|
|
81
100
|
beforeEach(() => {
|
|
82
101
|
vi.clearAllMocks();
|
|
102
|
+
ipcHandlers.clear();
|
|
103
|
+
ipcMainHandleMock.mockClear();
|
|
104
|
+
(IpcHandler.getInstance() as any).registeredChannels?.clear();
|
|
83
105
|
controller = new SystemController(mockApp);
|
|
84
106
|
});
|
|
85
107
|
|
|
86
108
|
describe('getAppState', () => {
|
|
87
109
|
it('should return app state with system info', async () => {
|
|
88
|
-
const result = await
|
|
110
|
+
const result = await invokeIpc('system.getAppState');
|
|
89
111
|
|
|
90
112
|
expect(result).toMatchObject({
|
|
91
113
|
arch: expect.any(String),
|
|
@@ -108,7 +130,7 @@ describe('SystemController', () => {
|
|
|
108
130
|
const { nativeTheme } = await import('electron');
|
|
109
131
|
Object.defineProperty(nativeTheme, 'shouldUseDarkColors', { value: true });
|
|
110
132
|
|
|
111
|
-
const result = await
|
|
133
|
+
const result = await invokeIpc('system.getAppState');
|
|
112
134
|
|
|
113
135
|
expect(result.systemAppearance).toBe('dark');
|
|
114
136
|
|
|
@@ -121,7 +143,7 @@ describe('SystemController', () => {
|
|
|
121
143
|
it('should check accessibility on macOS', async () => {
|
|
122
144
|
const { systemPreferences } = await import('electron');
|
|
123
145
|
|
|
124
|
-
|
|
146
|
+
await invokeIpc('system.checkAccessibilityForMacOS');
|
|
125
147
|
|
|
126
148
|
expect(systemPreferences.isTrustedAccessibilityClient).toHaveBeenCalledWith(true);
|
|
127
149
|
});
|
|
@@ -130,7 +152,7 @@ describe('SystemController', () => {
|
|
|
130
152
|
const { macOS } = await import('electron-is');
|
|
131
153
|
vi.mocked(macOS).mockReturnValue(false);
|
|
132
154
|
|
|
133
|
-
const result =
|
|
155
|
+
const result = await invokeIpc('system.checkAccessibilityForMacOS');
|
|
134
156
|
|
|
135
157
|
expect(result).toBeUndefined();
|
|
136
158
|
|
|
@@ -143,7 +165,7 @@ describe('SystemController', () => {
|
|
|
143
165
|
it('should open external link', async () => {
|
|
144
166
|
const { shell } = await import('electron');
|
|
145
167
|
|
|
146
|
-
await
|
|
168
|
+
await invokeIpc('system.openExternalLink', 'https://example.com');
|
|
147
169
|
|
|
148
170
|
expect(shell.openExternal).toHaveBeenCalledWith('https://example.com');
|
|
149
171
|
});
|
|
@@ -151,7 +173,7 @@ describe('SystemController', () => {
|
|
|
151
173
|
|
|
152
174
|
describe('updateLocale', () => {
|
|
153
175
|
it('should update locale and broadcast change', async () => {
|
|
154
|
-
const result = await
|
|
176
|
+
const result = await invokeIpc('system.updateLocale', 'zh-CN');
|
|
155
177
|
|
|
156
178
|
expect(mockStoreManager.set).toHaveBeenCalledWith('locale', 'zh-CN');
|
|
157
179
|
expect(mockI18n.changeLanguage).toHaveBeenCalledWith('zh-CN');
|
|
@@ -162,7 +184,7 @@ describe('SystemController', () => {
|
|
|
162
184
|
});
|
|
163
185
|
|
|
164
186
|
it('should use system locale when set to auto', async () => {
|
|
165
|
-
await
|
|
187
|
+
await invokeIpc('system.updateLocale', 'auto');
|
|
166
188
|
|
|
167
189
|
expect(mockI18n.changeLanguage).toHaveBeenCalledWith('en-US');
|
|
168
190
|
});
|
|
@@ -172,7 +194,7 @@ describe('SystemController', () => {
|
|
|
172
194
|
it('should update theme mode and broadcast change', async () => {
|
|
173
195
|
const themeMode: ThemeMode = 'dark';
|
|
174
196
|
|
|
175
|
-
await
|
|
197
|
+
await invokeIpc('system.updateThemeModeHandler', themeMode);
|
|
176
198
|
|
|
177
199
|
expect(mockStoreManager.set).toHaveBeenCalledWith('themeMode', 'dark');
|
|
178
200
|
expect(mockBrowserManager.broadcastToAllWindows).toHaveBeenCalledWith('themeChanged', {
|
|
@@ -182,58 +204,6 @@ describe('SystemController', () => {
|
|
|
182
204
|
});
|
|
183
205
|
});
|
|
184
206
|
|
|
185
|
-
describe('getDatabasePath', () => {
|
|
186
|
-
it('should return database path', async () => {
|
|
187
|
-
const result = await controller.getDatabasePath();
|
|
188
|
-
|
|
189
|
-
expect(result).toBe('/mock/storage/database');
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
describe('getDatabaseSchemaHash', () => {
|
|
194
|
-
it('should return schema hash when file exists', async () => {
|
|
195
|
-
const { readFileSync } = await import('node:fs');
|
|
196
|
-
vi.mocked(readFileSync).mockReturnValue('abc123');
|
|
197
|
-
|
|
198
|
-
const result = await controller.getDatabaseSchemaHash();
|
|
199
|
-
|
|
200
|
-
expect(result).toBe('abc123');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('should return undefined when file does not exist', async () => {
|
|
204
|
-
const { readFileSync } = await import('node:fs');
|
|
205
|
-
vi.mocked(readFileSync).mockImplementation(() => {
|
|
206
|
-
throw new Error('File not found');
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const result = await controller.getDatabaseSchemaHash();
|
|
210
|
-
|
|
211
|
-
expect(result).toBeUndefined();
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
describe('getUserDataPath', () => {
|
|
216
|
-
it('should return user data path', async () => {
|
|
217
|
-
const result = await controller.getUserDataPath();
|
|
218
|
-
|
|
219
|
-
expect(result).toBe('/mock/user/data');
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
describe('setDatabaseSchemaHash', () => {
|
|
224
|
-
it('should write schema hash to file', async () => {
|
|
225
|
-
const { writeFileSync } = await import('node:fs');
|
|
226
|
-
|
|
227
|
-
await controller.setDatabaseSchemaHash('newhash123');
|
|
228
|
-
|
|
229
|
-
expect(writeFileSync).toHaveBeenCalledWith(
|
|
230
|
-
'/mock/storage/db-schema-hash.txt',
|
|
231
|
-
'newhash123',
|
|
232
|
-
'utf8',
|
|
233
|
-
);
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
207
|
describe('afterAppReady', () => {
|
|
238
208
|
it('should initialize system theme listener', async () => {
|
|
239
209
|
const { nativeTheme } = await import('electron');
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { App } from '@/core/App';
|
|
4
|
+
|
|
5
|
+
import SystemServerCtr from '../SystemServerCtr';
|
|
6
|
+
|
|
7
|
+
vi.mock('@/utils/logger', () => ({
|
|
8
|
+
createLogger: () => ({
|
|
9
|
+
debug: vi.fn(),
|
|
10
|
+
error: vi.fn(),
|
|
11
|
+
info: vi.fn(),
|
|
12
|
+
warn: vi.fn(),
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('node:fs', () => ({
|
|
17
|
+
readFileSync: vi.fn(),
|
|
18
|
+
writeFileSync: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('@/const/dir', () => ({
|
|
22
|
+
DB_SCHEMA_HASH_FILENAME: 'db-schema-hash.txt',
|
|
23
|
+
LOCAL_DATABASE_DIR: 'database',
|
|
24
|
+
userDataDir: '/mock/user/data',
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const mockApp = {
|
|
28
|
+
appStoragePath: '/mock/storage',
|
|
29
|
+
} as unknown as App;
|
|
30
|
+
|
|
31
|
+
describe('SystemServerCtr', () => {
|
|
32
|
+
let controller: SystemServerCtr;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
controller = new SystemServerCtr(mockApp);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns database path', async () => {
|
|
40
|
+
await expect(controller.getDatabasePath()).resolves.toBe('/mock/storage/database');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('reads schema hash when file exists', async () => {
|
|
44
|
+
const { readFileSync } = await import('node:fs');
|
|
45
|
+
vi.mocked(readFileSync).mockReturnValue('hash123');
|
|
46
|
+
|
|
47
|
+
await expect(controller.getDatabaseSchemaHash()).resolves.toBe('hash123');
|
|
48
|
+
expect(readFileSync).toHaveBeenCalledWith('/mock/storage/db-schema-hash.txt', 'utf8');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('returns undefined when schema hash file missing', async () => {
|
|
52
|
+
const { readFileSync } = await import('node:fs');
|
|
53
|
+
vi.mocked(readFileSync).mockImplementation(() => {
|
|
54
|
+
throw new Error('missing');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await expect(controller.getDatabaseSchemaHash()).resolves.toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns user data path', async () => {
|
|
61
|
+
await expect(controller.getUserDataPath()).resolves.toBe('/mock/user/data');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('writes schema hash to disk', async () => {
|
|
65
|
+
const { writeFileSync } = await import('node:fs');
|
|
66
|
+
|
|
67
|
+
await controller.setDatabaseSchemaHash('newhash');
|
|
68
|
+
|
|
69
|
+
expect(writeFileSync).toHaveBeenCalledWith(
|
|
70
|
+
'/mock/storage/db-schema-hash.txt',
|
|
71
|
+
'newhash',
|
|
72
|
+
'utf8',
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {
|
|
3
2
|
ShowTrayNotificationParams,
|
|
4
3
|
UpdateTrayIconParams,
|
|
5
|
-
UpdateTrayTooltipParams
|
|
4
|
+
UpdateTrayTooltipParams,
|
|
6
5
|
} from '@lobechat/electron-client-ipc';
|
|
6
|
+
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
7
|
|
|
8
8
|
import type { App } from '@/core/App';
|
|
9
9
|
|
|
10
|
+
import TrayMenuCtr from '../TrayMenuCtr';
|
|
11
|
+
|
|
12
|
+
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
|
13
|
+
ipcMainHandleMock: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('electron', () => ({
|
|
17
|
+
ipcMain: {
|
|
18
|
+
handle: ipcMainHandleMock,
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
|
|
10
22
|
// 模拟 logger
|
|
11
23
|
vi.mock('@/utils/logger', () => ({
|
|
12
24
|
createLogger: () => ({
|
|
@@ -15,8 +27,6 @@ vi.mock('@/utils/logger', () => ({
|
|
|
15
27
|
}),
|
|
16
28
|
}));
|
|
17
29
|
|
|
18
|
-
import TrayMenuCtr from '../TrayMenuCtr';
|
|
19
|
-
|
|
20
30
|
// 保存原始平台,确保测试结束后能恢复
|
|
21
31
|
const originalPlatform = process.platform;
|
|
22
32
|
|
|
@@ -45,6 +55,7 @@ describe('TrayMenuCtr', () => {
|
|
|
45
55
|
|
|
46
56
|
beforeEach(() => {
|
|
47
57
|
vi.clearAllMocks();
|
|
58
|
+
ipcMainHandleMock.mockClear();
|
|
48
59
|
// 为每个测试重置 mockedTray
|
|
49
60
|
mockGetMainTray.mockReset();
|
|
50
61
|
trayMenuCtr = new TrayMenuCtr(mockApp);
|
|
@@ -69,7 +80,7 @@ describe('TrayMenuCtr', () => {
|
|
|
69
80
|
it('should display balloon notification on Windows platform', async () => {
|
|
70
81
|
// 模拟 Windows 平台
|
|
71
82
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
72
|
-
|
|
83
|
+
|
|
73
84
|
const mockedTray = {
|
|
74
85
|
displayBalloon: mockDisplayBalloon,
|
|
75
86
|
};
|
|
@@ -125,9 +136,9 @@ describe('TrayMenuCtr', () => {
|
|
|
125
136
|
|
|
126
137
|
expect(mockGetMainTray).toHaveBeenCalled();
|
|
127
138
|
expect(mockDisplayBalloon).not.toHaveBeenCalled();
|
|
128
|
-
expect(result).toEqual({
|
|
139
|
+
expect(result).toEqual({
|
|
129
140
|
error: 'Tray notifications are only supported on Windows platform',
|
|
130
|
-
success: false
|
|
141
|
+
success: false,
|
|
131
142
|
});
|
|
132
143
|
});
|
|
133
144
|
});
|
|
@@ -136,7 +147,7 @@ describe('TrayMenuCtr', () => {
|
|
|
136
147
|
it('should update tray icon on Windows platform', async () => {
|
|
137
148
|
// 模拟 Windows 平台
|
|
138
149
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
139
|
-
|
|
150
|
+
|
|
140
151
|
const mockedTray = {
|
|
141
152
|
updateIcon: mockUpdateIcon,
|
|
142
153
|
};
|
|
@@ -156,7 +167,7 @@ describe('TrayMenuCtr', () => {
|
|
|
156
167
|
it('should handle errors when updating icon', async () => {
|
|
157
168
|
// 模拟 Windows 平台
|
|
158
169
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
159
|
-
|
|
170
|
+
|
|
160
171
|
const error = new Error('Failed to update icon');
|
|
161
172
|
const mockedTray = {
|
|
162
173
|
updateIcon: vi.fn().mockImplementation(() => {
|
|
@@ -198,7 +209,7 @@ describe('TrayMenuCtr', () => {
|
|
|
198
209
|
it('should update tray tooltip on Windows platform', async () => {
|
|
199
210
|
// 模拟 Windows 平台
|
|
200
211
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
201
|
-
|
|
212
|
+
|
|
202
213
|
const mockedTray = {
|
|
203
214
|
updateTooltip: mockUpdateTooltip,
|
|
204
215
|
};
|
|
@@ -234,7 +245,7 @@ describe('TrayMenuCtr', () => {
|
|
|
234
245
|
it('should return error when tooltip is not provided', async () => {
|
|
235
246
|
// 模拟 Windows 平台
|
|
236
247
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
237
|
-
|
|
248
|
+
|
|
238
249
|
const mockedTray = {
|
|
239
250
|
updateTooltip: mockUpdateTooltip,
|
|
240
251
|
};
|
|
@@ -253,4 +264,4 @@ describe('TrayMenuCtr', () => {
|
|
|
253
264
|
});
|
|
254
265
|
});
|
|
255
266
|
});
|
|
256
|
-
});
|
|
267
|
+
});
|
|
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
import type { App } from '@/core/App';
|
|
4
4
|
|
|
5
|
+
import UpdaterCtr from '../UpdaterCtr';
|
|
6
|
+
|
|
5
7
|
// 模拟 logger
|
|
6
8
|
vi.mock('@/utils/logger', () => ({
|
|
7
9
|
createLogger: () => ({
|
|
@@ -9,7 +11,15 @@ vi.mock('@/utils/logger', () => ({
|
|
|
9
11
|
}),
|
|
10
12
|
}));
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
|
15
|
+
ipcMainHandleMock: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('electron', () => ({
|
|
19
|
+
ipcMain: {
|
|
20
|
+
handle: ipcMainHandleMock,
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
13
23
|
|
|
14
24
|
// 模拟 App 及其依赖项
|
|
15
25
|
const mockCheckForUpdates = vi.fn();
|
|
@@ -31,6 +41,7 @@ describe('UpdaterCtr', () => {
|
|
|
31
41
|
|
|
32
42
|
beforeEach(() => {
|
|
33
43
|
vi.clearAllMocks();
|
|
44
|
+
ipcMainHandleMock.mockClear();
|
|
34
45
|
updaterCtr = new UpdaterCtr(mockApp);
|
|
35
46
|
});
|
|
36
47
|
|
|
@@ -79,4 +90,4 @@ describe('UpdaterCtr', () => {
|
|
|
79
90
|
await expect(updaterCtr.downloadUpdate()).rejects.toThrow(error);
|
|
80
91
|
});
|
|
81
92
|
});
|
|
82
|
-
});
|
|
93
|
+
});
|
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import type { App } from '@/core/App';
|
|
4
|
+
import { IpcHandler } from '@/utils/ipc/base';
|
|
4
5
|
|
|
5
6
|
import UploadFileCtr from '../UploadFileCtr';
|
|
6
7
|
|
|
8
|
+
const { ipcHandlers, ipcMainHandleMock } = vi.hoisted(() => {
|
|
9
|
+
const handlers = new Map<string, (event: any, ...args: any[]) => any>();
|
|
10
|
+
const handle = vi.fn((channel: string, handler: any) => {
|
|
11
|
+
handlers.set(channel, handler);
|
|
12
|
+
});
|
|
13
|
+
return { ipcHandlers: handlers, ipcMainHandleMock: handle };
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const invokeIpc = async <T = any>(channel: string, payload?: any): Promise<T> => {
|
|
17
|
+
const handler = ipcHandlers.get(channel);
|
|
18
|
+
if (!handler) throw new Error(`IPC handler for ${channel} not found`);
|
|
19
|
+
|
|
20
|
+
const fakeEvent = { sender: { id: 'test' } as any };
|
|
21
|
+
if (payload === undefined) return handler(fakeEvent);
|
|
22
|
+
return handler(fakeEvent, payload);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
vi.mock('electron', () => ({
|
|
26
|
+
ipcMain: {
|
|
27
|
+
handle: ipcMainHandleMock,
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
7
31
|
// Mock FileService module to prevent electron dependency issues
|
|
8
32
|
vi.mock('@/services/fileSrv', () => ({
|
|
9
33
|
default: class MockFileService {},
|
|
@@ -12,9 +36,6 @@ vi.mock('@/services/fileSrv', () => ({
|
|
|
12
36
|
// Mock FileService instance methods
|
|
13
37
|
const mockFileService = {
|
|
14
38
|
uploadFile: vi.fn(),
|
|
15
|
-
getFilePath: vi.fn(),
|
|
16
|
-
getFileHTTPURL: vi.fn(),
|
|
17
|
-
deleteFiles: vi.fn(),
|
|
18
39
|
};
|
|
19
40
|
|
|
20
41
|
const mockApp = {
|
|
@@ -26,6 +47,9 @@ describe('UploadFileCtr', () => {
|
|
|
26
47
|
|
|
27
48
|
beforeEach(() => {
|
|
28
49
|
vi.clearAllMocks();
|
|
50
|
+
ipcHandlers.clear();
|
|
51
|
+
ipcMainHandleMock.mockClear();
|
|
52
|
+
(IpcHandler.getInstance() as any).registeredChannels?.clear();
|
|
29
53
|
controller = new UploadFileCtr(mockApp);
|
|
30
54
|
});
|
|
31
55
|
|
|
@@ -41,7 +65,7 @@ describe('UploadFileCtr', () => {
|
|
|
41
65
|
const expectedResult = { id: 'file-id-123', url: '/files/file-id-123' };
|
|
42
66
|
mockFileService.uploadFile.mockResolvedValue(expectedResult);
|
|
43
67
|
|
|
44
|
-
const result = await
|
|
68
|
+
const result = await invokeIpc('upload.uploadFile', params);
|
|
45
69
|
|
|
46
70
|
expect(result).toEqual(expectedResult);
|
|
47
71
|
expect(mockFileService.uploadFile).toHaveBeenCalledWith(params);
|
|
@@ -58,110 +82,7 @@ describe('UploadFileCtr', () => {
|
|
|
58
82
|
const error = new Error('Upload failed');
|
|
59
83
|
mockFileService.uploadFile.mockRejectedValue(error);
|
|
60
84
|
|
|
61
|
-
await expect(
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe('getFileUrlById', () => {
|
|
66
|
-
it('should get file path by id successfully', async () => {
|
|
67
|
-
const fileId = 'file-id-123';
|
|
68
|
-
const expectedPath = '/files/abc123.txt';
|
|
69
|
-
mockFileService.getFilePath.mockResolvedValue(expectedPath);
|
|
70
|
-
|
|
71
|
-
const result = await controller.getFileUrlById(fileId);
|
|
72
|
-
|
|
73
|
-
expect(result).toBe(expectedPath);
|
|
74
|
-
expect(mockFileService.getFilePath).toHaveBeenCalledWith(fileId);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should handle get file path error', async () => {
|
|
78
|
-
const fileId = 'non-existent-id';
|
|
79
|
-
const error = new Error('File not found');
|
|
80
|
-
mockFileService.getFilePath.mockRejectedValue(error);
|
|
81
|
-
|
|
82
|
-
await expect(controller.getFileUrlById(fileId)).rejects.toThrow('File not found');
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('getFileHTTPURL', () => {
|
|
87
|
-
it('should get file HTTP URL successfully', async () => {
|
|
88
|
-
const filePath = '/files/abc123.txt';
|
|
89
|
-
const expectedUrl = 'http://localhost:3000/files/abc123.txt';
|
|
90
|
-
mockFileService.getFileHTTPURL.mockResolvedValue(expectedUrl);
|
|
91
|
-
|
|
92
|
-
const result = await controller.getFileHTTPURL(filePath);
|
|
93
|
-
|
|
94
|
-
expect(result).toBe(expectedUrl);
|
|
95
|
-
expect(mockFileService.getFileHTTPURL).toHaveBeenCalledWith(filePath);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should handle get HTTP URL error', async () => {
|
|
99
|
-
const filePath = '/files/abc123.txt';
|
|
100
|
-
const error = new Error('Failed to generate URL');
|
|
101
|
-
mockFileService.getFileHTTPURL.mockRejectedValue(error);
|
|
102
|
-
|
|
103
|
-
await expect(controller.getFileHTTPURL(filePath)).rejects.toThrow('Failed to generate URL');
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('deleteFiles', () => {
|
|
108
|
-
it('should delete files successfully', async () => {
|
|
109
|
-
const paths = ['/files/file1.txt', '/files/file2.txt'];
|
|
110
|
-
mockFileService.deleteFiles.mockResolvedValue(undefined);
|
|
111
|
-
|
|
112
|
-
await controller.deleteFiles(paths);
|
|
113
|
-
|
|
114
|
-
expect(mockFileService.deleteFiles).toHaveBeenCalledWith(paths);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should handle delete files error', async () => {
|
|
118
|
-
const paths = ['/files/file1.txt'];
|
|
119
|
-
const error = new Error('Delete failed');
|
|
120
|
-
mockFileService.deleteFiles.mockRejectedValue(error);
|
|
121
|
-
|
|
122
|
-
await expect(controller.deleteFiles(paths)).rejects.toThrow('Delete failed');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should handle empty paths array', async () => {
|
|
126
|
-
const paths: string[] = [];
|
|
127
|
-
mockFileService.deleteFiles.mockResolvedValue(undefined);
|
|
128
|
-
|
|
129
|
-
await controller.deleteFiles(paths);
|
|
130
|
-
|
|
131
|
-
expect(mockFileService.deleteFiles).toHaveBeenCalledWith([]);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe('createFile', () => {
|
|
136
|
-
it('should create file successfully', async () => {
|
|
137
|
-
const params = {
|
|
138
|
-
hash: 'xyz789',
|
|
139
|
-
path: '/test/newfile.txt',
|
|
140
|
-
content: 'bmV3IGZpbGUgY29udGVudA==',
|
|
141
|
-
filename: 'newfile.txt',
|
|
142
|
-
type: 'text/plain',
|
|
143
|
-
};
|
|
144
|
-
const expectedResult = { id: 'new-file-id', url: '/files/new-file-id' };
|
|
145
|
-
mockFileService.uploadFile.mockResolvedValue(expectedResult);
|
|
146
|
-
|
|
147
|
-
const result = await controller.createFile(params);
|
|
148
|
-
|
|
149
|
-
expect(result).toEqual(expectedResult);
|
|
150
|
-
expect(mockFileService.uploadFile).toHaveBeenCalledWith(params);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should handle create file error', async () => {
|
|
154
|
-
const params = {
|
|
155
|
-
hash: 'xyz789',
|
|
156
|
-
path: '/test/newfile.txt',
|
|
157
|
-
content: 'bmV3IGZpbGUgY29udGVudA==',
|
|
158
|
-
filename: 'newfile.txt',
|
|
159
|
-
type: 'text/plain',
|
|
160
|
-
};
|
|
161
|
-
const error = new Error('Create failed');
|
|
162
|
-
mockFileService.uploadFile.mockRejectedValue(error);
|
|
163
|
-
|
|
164
|
-
await expect(controller.createFile(params)).rejects.toThrow('Create failed');
|
|
85
|
+
await expect(invokeIpc('upload.uploadFile', params)).rejects.toThrow('Upload failed');
|
|
165
86
|
});
|
|
166
87
|
});
|
|
167
88
|
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { App } from '@/core/App';
|
|
4
|
+
|
|
5
|
+
import UploadFileServerCtr from '../UploadFileServerCtr';
|
|
6
|
+
|
|
7
|
+
vi.mock('@/services/fileSrv', () => ({
|
|
8
|
+
default: class MockFileService {},
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const mockFileService = {
|
|
12
|
+
getFileHTTPURL: vi.fn(),
|
|
13
|
+
getFilePath: vi.fn(),
|
|
14
|
+
deleteFiles: vi.fn(),
|
|
15
|
+
uploadFile: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const mockApp = {
|
|
19
|
+
getService: vi.fn(() => mockFileService),
|
|
20
|
+
} as unknown as App;
|
|
21
|
+
|
|
22
|
+
describe('UploadFileServerCtr', () => {
|
|
23
|
+
let controller: UploadFileServerCtr;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
controller = new UploadFileServerCtr(mockApp);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('gets file path by id', async () => {
|
|
31
|
+
mockFileService.getFilePath.mockResolvedValue('path');
|
|
32
|
+
await expect(controller.getFileUrlById('id')).resolves.toBe('path');
|
|
33
|
+
expect(mockFileService.getFilePath).toHaveBeenCalledWith('id');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('gets HTTP URL', async () => {
|
|
37
|
+
mockFileService.getFileHTTPURL.mockResolvedValue('url');
|
|
38
|
+
await expect(controller.getFileHTTPURL('/path')).resolves.toBe('url');
|
|
39
|
+
expect(mockFileService.getFileHTTPURL).toHaveBeenCalledWith('/path');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('deletes files', async () => {
|
|
43
|
+
mockFileService.deleteFiles.mockResolvedValue(undefined);
|
|
44
|
+
await controller.deleteFiles(['a']);
|
|
45
|
+
expect(mockFileService.deleteFiles).toHaveBeenCalledWith(['a']);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('creates files via upload service', async () => {
|
|
49
|
+
const params = { filename: 'file' } as any;
|
|
50
|
+
mockFileService.uploadFile.mockResolvedValue({ success: true });
|
|
51
|
+
|
|
52
|
+
await expect(controller.createFile(params)).resolves.toEqual({ success: true });
|
|
53
|
+
expect(mockFileService.uploadFile).toHaveBeenCalledWith(params);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ControllerModule,
|
|
1
|
+
import { ControllerModule, IpcMethod } from './index';
|
|
2
2
|
|
|
3
3
|
export default class DevtoolsCtr extends ControllerModule {
|
|
4
|
-
@
|
|
4
|
+
@IpcMethod()
|
|
5
5
|
async openDevtools() {
|
|
6
6
|
const devtoolsBrowser = this.app.browserManager.retrieveByIdentifier('devtools');
|
|
7
7
|
devtoolsBrowser.show();
|