@lobehub/lobehub 2.0.0-next.163 → 2.0.0-next.165

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 (124) hide show
  1. package/.cursor/rules/desktop-feature-implementation.mdc +31 -34
  2. package/.cursor/rules/desktop-local-tools-implement.mdc +3 -3
  3. package/.cursor/rules/desktop-window-management.mdc +56 -66
  4. package/CHANGELOG.md +52 -0
  5. package/README.md +6 -6
  6. package/README.zh-CN.md +6 -6
  7. package/apps/desktop/Development.md +42 -46
  8. package/apps/desktop/README.md +37 -1
  9. package/apps/desktop/README.zh-CN.md +26 -1
  10. package/apps/desktop/electron.vite.config.ts +1 -0
  11. package/apps/desktop/src/main/controllers/AuthCtr.ts +4 -3
  12. package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +33 -20
  13. package/apps/desktop/src/main/controllers/DevtoolsCtr.ts +4 -2
  14. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +14 -13
  15. package/apps/desktop/src/main/controllers/MenuCtr.ts +5 -4
  16. package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +18 -19
  17. package/apps/desktop/src/main/controllers/NotificationCtr.ts +4 -3
  18. package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +5 -4
  19. package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +3 -2
  20. package/apps/desktop/src/main/controllers/ShellCommandCtr.ts +5 -4
  21. package/apps/desktop/src/main/controllers/ShortcutCtr.ts +4 -3
  22. package/apps/desktop/src/main/controllers/SystemCtr.ts +7 -37
  23. package/apps/desktop/src/main/controllers/SystemServerCtr.ts +38 -0
  24. package/apps/desktop/src/main/controllers/TrayMenuCtr.ts +5 -4
  25. package/apps/desktop/src/main/controllers/UpdaterCtr.ts +6 -5
  26. package/apps/desktop/src/main/controllers/UploadFileCtr.ts +3 -25
  27. package/apps/desktop/src/main/controllers/UploadFileServerCtr.ts +33 -0
  28. package/apps/desktop/src/main/controllers/__tests__/AuthCtr.test.ts +9 -1
  29. package/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts +29 -9
  30. package/apps/desktop/src/main/controllers/__tests__/DevtoolsCtr.test.ts +12 -3
  31. package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +7 -0
  32. package/apps/desktop/src/main/controllers/__tests__/MenuCtr.test.ts +10 -0
  33. package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +10 -0
  34. package/apps/desktop/src/main/controllers/__tests__/NotificationCtr.test.ts +8 -0
  35. package/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts +8 -0
  36. package/apps/desktop/src/main/controllers/__tests__/RemoteServerSyncCtr.test.ts +1 -0
  37. package/apps/desktop/src/main/controllers/__tests__/ShellCommandCtr.test.ts +10 -0
  38. package/apps/desktop/src/main/controllers/__tests__/ShortcutCtr.test.ts +11 -0
  39. package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +43 -73
  40. package/apps/desktop/src/main/controllers/__tests__/SystemServerCtr.test.ts +75 -0
  41. package/apps/desktop/src/main/controllers/__tests__/TrayMenuCtr.test.ts +24 -13
  42. package/apps/desktop/src/main/controllers/__tests__/UpdaterCtr.test.ts +13 -2
  43. package/apps/desktop/src/main/controllers/__tests__/UploadFileCtr.test.ts +29 -108
  44. package/apps/desktop/src/main/controllers/__tests__/UploadFileServerCtr.test.ts +55 -0
  45. package/apps/desktop/src/main/controllers/_template.ts +2 -2
  46. package/apps/desktop/src/main/controllers/index.ts +5 -29
  47. package/apps/desktop/src/main/controllers/registry.ts +52 -0
  48. package/apps/desktop/src/main/core/App.ts +15 -47
  49. package/apps/desktop/src/main/core/__tests__/App.test.ts +5 -4
  50. package/apps/desktop/src/main/core/infrastructure/IoCContainer.ts +0 -5
  51. package/apps/desktop/src/main/core/infrastructure/__tests__/IoCContainer.test.ts +0 -50
  52. package/apps/desktop/src/main/exports.d.ts +8 -0
  53. package/apps/desktop/src/main/exports.ts +2 -0
  54. package/apps/desktop/src/main/global.d.ts +3 -0
  55. package/apps/desktop/src/main/modules/fileSearch/__tests__/macOS.integration.test.ts +17 -8
  56. package/apps/desktop/src/main/package.json +10 -0
  57. package/apps/desktop/src/main/services/fileSrv.ts +1 -1
  58. package/apps/desktop/src/main/utils/ipc/__tests__/base.test.ts +91 -0
  59. package/apps/desktop/src/main/utils/ipc/base.ts +170 -0
  60. package/apps/desktop/src/main/utils/ipc/index.ts +11 -0
  61. package/apps/desktop/src/main/utils/ipc/utility.ts +20 -0
  62. package/apps/desktop/src/preload/electronApi.ts +4 -1
  63. package/apps/desktop/src/preload/invoke.test.ts +13 -16
  64. package/apps/desktop/src/preload/invoke.ts +2 -5
  65. package/apps/desktop/src/preload/routeInterceptor.test.ts +13 -13
  66. package/apps/desktop/src/preload/routeInterceptor.ts +4 -4
  67. package/apps/desktop/tsconfig.json +15 -5
  68. package/changelog/v1.json +14 -0
  69. package/locales/ar/auth.json +3 -0
  70. package/locales/bg-BG/auth.json +3 -0
  71. package/locales/de-DE/auth.json +3 -0
  72. package/locales/en-US/auth.json +4 -1
  73. package/locales/es-ES/auth.json +3 -0
  74. package/locales/fa-IR/auth.json +3 -0
  75. package/locales/fr-FR/auth.json +3 -0
  76. package/locales/it-IT/auth.json +3 -0
  77. package/locales/ja-JP/auth.json +3 -0
  78. package/locales/ko-KR/auth.json +3 -0
  79. package/locales/nl-NL/auth.json +3 -0
  80. package/locales/pl-PL/auth.json +3 -0
  81. package/locales/pt-BR/auth.json +3 -0
  82. package/locales/ru-RU/auth.json +3 -0
  83. package/locales/tr-TR/auth.json +3 -0
  84. package/locales/vi-VN/auth.json +3 -0
  85. package/locales/zh-CN/auth.json +3 -0
  86. package/locales/zh-TW/auth.json +3 -0
  87. package/package.json +4 -3
  88. package/packages/electron-client-ipc/src/index.ts +1 -1
  89. package/packages/electron-client-ipc/src/ipc.test.ts +62 -0
  90. package/packages/electron-client-ipc/src/ipc.ts +63 -0
  91. package/packages/electron-client-ipc/src/streamInvoke.ts +7 -1
  92. package/packages/electron-client-ipc/src/types/dispatch.ts +1 -10
  93. package/packages/electron-client-ipc/vitest.config.mts +10 -0
  94. package/packages/electron-server-ipc/src/ipcClient.ts +1 -2
  95. package/packages/electron-server-ipc/src/ipcServer.ts +1 -2
  96. package/packages/electron-server-ipc/src/types/index.ts +1 -5
  97. package/pnpm-workspace.yaml +1 -1
  98. package/scripts/i18nWorkflow/const.ts +2 -2
  99. package/scripts/i18nWorkflow/i18nConfig.ts +7 -0
  100. package/scripts/i18nWorkflow/utils.ts +1 -1
  101. package/src/app/[variants]/(auth)/signup/[[...signup]]/BetterAuthSignUpForm.tsx +23 -0
  102. package/src/app/[variants]/(main)/(mobile)/me/(home)/features/UserBanner.tsx +3 -9
  103. package/src/app/[variants]/(main)/discover/(detail)/provider/features/Sidebar/ActionButton/ProviderConfig.tsx +2 -2
  104. package/src/app/[variants]/(main)/profile/(home)/Client.tsx +206 -138
  105. package/src/features/User/PlanTag.tsx +4 -4
  106. package/src/locales/default/auth.ts +3 -0
  107. package/src/locales/default/setting.ts +1 -0
  108. package/src/server/modules/ElectronIPCClient/index.ts +59 -13
  109. package/src/services/electron/__tests__/devtools.test.ts +10 -6
  110. package/src/services/electron/autoUpdate.ts +5 -5
  111. package/src/services/electron/desktopNotification.ts +4 -7
  112. package/src/services/electron/devtools.ts +2 -2
  113. package/src/services/electron/file.ts +3 -2
  114. package/src/services/electron/localFileService.ts +17 -16
  115. package/src/services/electron/remoteServer.ts +7 -6
  116. package/src/services/electron/settings.ts +9 -11
  117. package/src/services/electron/system.ts +8 -6
  118. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +1 -1
  119. package/src/store/global/actions/general.ts +8 -10
  120. package/src/utils/electron/desktopRemoteRPCFetch.ts +3 -2
  121. package/src/utils/electron/ipc.ts +12 -0
  122. package/tsconfig.json +5 -0
  123. package/apps/desktop/src/main/types/ipcClientEvent.ts +0 -3
  124. 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 controller.getAppState();
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 controller.getAppState();
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
- controller.checkAccessibilityForMacOS();
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 = controller.checkAccessibilityForMacOS();
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 controller.openExternalLink('https://example.com');
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 controller.updateLocale('zh-CN');
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 controller.updateLocale('auto');
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 controller.updateThemeModeHandler(themeMode);
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 { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
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
- import UpdaterCtr from '../UpdaterCtr';
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 controller.uploadFile(params);
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(controller.uploadFile(params)).rejects.toThrow('Upload failed');
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, ipcClientEvent } from './index';
1
+ import { ControllerModule, IpcMethod } from './index';
2
2
 
3
3
  export default class DevtoolsCtr extends ControllerModule {
4
- @ipcClientEvent('openDevtools')
4
+ @IpcMethod()
5
5
  async openDevtools() {
6
6
  const devtoolsBrowser = this.app.browserManager.retrieveByIdentifier('devtools');
7
7
  devtoolsBrowser.show();