@lobehub/chat 1.84.24 → 1.84.25
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 +25 -0
- package/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts +195 -0
- package/apps/desktop/src/main/controllers/__tests__/DevtoolsCtr.test.ts +44 -0
- package/apps/desktop/src/main/controllers/__tests__/MenuCtr.test.ts +82 -0
- package/apps/desktop/src/main/controllers/__tests__/ShortcutCtr.test.ts +64 -0
- package/apps/desktop/src/main/controllers/__tests__/TrayMenuCtr.test.ts +256 -0
- package/apps/desktop/src/main/controllers/__tests__/UpdaterCtr.test.ts +82 -0
- package/apps/desktop/src/main/services/fileSrv.ts +49 -10
- package/apps/desktop/vitest.config.ts +17 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/electron-server-ipc/package.json +3 -0
- package/packages/electron-server-ipc/src/ipcClient.ts +58 -21
- package/packages/electron-server-ipc/src/ipcServer.test.ts +417 -0
- package/packages/electron-server-ipc/src/ipcServer.ts +21 -16
- package/vitest.config.ts +3 -2
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,31 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.84.25](https://github.com/lobehub/lobe-chat/compare/v1.84.24...v1.84.25)
|
6
|
+
|
7
|
+
<sup>Released on **2025-05-08**</sup>
|
8
|
+
|
9
|
+
#### 🐛 Bug Fixes
|
10
|
+
|
11
|
+
- **misc**: Fix desktop upload image on macOS.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### What's fixed
|
19
|
+
|
20
|
+
- **misc**: Fix desktop upload image on macOS, closes [#7741](https://github.com/lobehub/lobe-chat/issues/7741) ([07d5374](https://github.com/lobehub/lobe-chat/commit/07d5374))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
5
30
|
### [Version 1.84.24](https://github.com/lobehub/lobe-chat/compare/v1.84.23...v1.84.24)
|
6
31
|
|
7
32
|
<sup>Released on **2025-05-08**</sup>
|
@@ -0,0 +1,195 @@
|
|
1
|
+
import { beforeEach, describe, expect, it, vi, Mock } from 'vitest';
|
2
|
+
import { InterceptRouteParams } from '@lobechat/electron-client-ipc';
|
3
|
+
|
4
|
+
import type { App } from '@/core/App';
|
5
|
+
import type { IpcClientEventSender } from '@/types/ipcClientEvent';
|
6
|
+
import { BrowsersIdentifiers, AppBrowsersIdentifiers } from '@/appBrowsers';
|
7
|
+
|
8
|
+
import BrowserWindowsCtr from '../BrowserWindowsCtr';
|
9
|
+
|
10
|
+
// 模拟 App 及其依赖项
|
11
|
+
const mockToggleVisible = vi.fn();
|
12
|
+
const mockShowSettingsWindowWithTab = vi.fn();
|
13
|
+
const mockCloseWindow = vi.fn();
|
14
|
+
const mockMinimizeWindow = vi.fn();
|
15
|
+
const mockMaximizeWindow = vi.fn();
|
16
|
+
const mockRetrieveByIdentifier = vi.fn();
|
17
|
+
const mockGetMainWindow = vi.fn(() => ({
|
18
|
+
toggleVisible: mockToggleVisible,
|
19
|
+
}));
|
20
|
+
const mockShow = vi.fn();
|
21
|
+
|
22
|
+
// mock findMatchingRoute and extractSubPath
|
23
|
+
vi.mock('~common/routes', async () => ({
|
24
|
+
findMatchingRoute: vi.fn(),
|
25
|
+
extractSubPath: vi.fn(),
|
26
|
+
}));
|
27
|
+
const { findMatchingRoute, extractSubPath } = await import('~common/routes');
|
28
|
+
|
29
|
+
const mockApp = {
|
30
|
+
browserManager: {
|
31
|
+
getMainWindow: mockGetMainWindow,
|
32
|
+
showSettingsWindowWithTab: mockShowSettingsWindowWithTab,
|
33
|
+
closeWindow: mockCloseWindow,
|
34
|
+
minimizeWindow: mockMinimizeWindow,
|
35
|
+
maximizeWindow: mockMaximizeWindow,
|
36
|
+
retrieveByIdentifier: mockRetrieveByIdentifier.mockImplementation((identifier: AppBrowsersIdentifiers | string) => {
|
37
|
+
if (identifier === BrowsersIdentifiers.settings || identifier === 'some-other-window') {
|
38
|
+
return { show: mockShow };
|
39
|
+
}
|
40
|
+
return { show: mockShow }; // Default mock for other identifiers
|
41
|
+
}),
|
42
|
+
},
|
43
|
+
} as unknown as App;
|
44
|
+
|
45
|
+
describe('BrowserWindowsCtr', () => {
|
46
|
+
let browserWindowsCtr: BrowserWindowsCtr;
|
47
|
+
|
48
|
+
beforeEach(() => {
|
49
|
+
vi.clearAllMocks();
|
50
|
+
browserWindowsCtr = new BrowserWindowsCtr(mockApp);
|
51
|
+
});
|
52
|
+
|
53
|
+
describe('toggleMainWindow', () => {
|
54
|
+
it('should get the main window and toggle its visibility', async () => {
|
55
|
+
await browserWindowsCtr.toggleMainWindow();
|
56
|
+
expect(mockGetMainWindow).toHaveBeenCalled();
|
57
|
+
expect(mockToggleVisible).toHaveBeenCalled();
|
58
|
+
});
|
59
|
+
});
|
60
|
+
|
61
|
+
describe('openSettingsWindow', () => {
|
62
|
+
it('should show the settings window with the specified tab', async () => {
|
63
|
+
const tab = 'appearance';
|
64
|
+
const result = await browserWindowsCtr.openSettingsWindow(tab);
|
65
|
+
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith(tab);
|
66
|
+
expect(result).toEqual({ success: true });
|
67
|
+
});
|
68
|
+
|
69
|
+
it('should return error if showing settings window fails', async () => {
|
70
|
+
const errorMessage = 'Failed to show';
|
71
|
+
mockShowSettingsWindowWithTab.mockRejectedValueOnce(new Error(errorMessage));
|
72
|
+
const result = await browserWindowsCtr.openSettingsWindow('display');
|
73
|
+
expect(result).toEqual({ error: errorMessage, success: false });
|
74
|
+
});
|
75
|
+
});
|
76
|
+
|
77
|
+
const testSenderIdentifierString: string = 'test-window-event-id';
|
78
|
+
const sender: IpcClientEventSender = {
|
79
|
+
identifier: testSenderIdentifierString,
|
80
|
+
};
|
81
|
+
|
82
|
+
describe('closeWindow', () => {
|
83
|
+
it('should close the window with the given sender identifier', () => {
|
84
|
+
browserWindowsCtr.closeWindow(undefined, sender);
|
85
|
+
expect(mockCloseWindow).toHaveBeenCalledWith(testSenderIdentifierString);
|
86
|
+
});
|
87
|
+
});
|
88
|
+
|
89
|
+
describe('minimizeWindow', () => {
|
90
|
+
it('should minimize the window with the given sender identifier', () => {
|
91
|
+
browserWindowsCtr.minimizeWindow(undefined, sender);
|
92
|
+
expect(mockMinimizeWindow).toHaveBeenCalledWith(testSenderIdentifierString);
|
93
|
+
});
|
94
|
+
});
|
95
|
+
|
96
|
+
describe('maximizeWindow', () => {
|
97
|
+
it('should maximize the window with the given sender identifier', () => {
|
98
|
+
browserWindowsCtr.maximizeWindow(undefined, sender);
|
99
|
+
expect(mockMaximizeWindow).toHaveBeenCalledWith(testSenderIdentifierString);
|
100
|
+
});
|
101
|
+
});
|
102
|
+
|
103
|
+
describe('interceptRoute', () => {
|
104
|
+
const baseParams = { source: 'link-click' as const };
|
105
|
+
|
106
|
+
it('should not intercept if no matching route is found', async () => {
|
107
|
+
const params: InterceptRouteParams = { ...baseParams, path: '/unknown/route', url: 'app://host/unknown/route' };
|
108
|
+
(findMatchingRoute as Mock).mockReturnValue(undefined);
|
109
|
+
const result = await browserWindowsCtr.interceptRoute(params);
|
110
|
+
expect(findMatchingRoute).toHaveBeenCalledWith(params.path);
|
111
|
+
expect(result).toEqual({ intercepted: false, path: params.path, source: params.source });
|
112
|
+
});
|
113
|
+
|
114
|
+
it('should show settings window if matched route target is settings', async () => {
|
115
|
+
const params: InterceptRouteParams = { ...baseParams, path: '/settings/common', url: 'app://host/settings/common' };
|
116
|
+
const matchedRoute = { targetWindow: BrowsersIdentifiers.settings, pathPrefix: '/settings' };
|
117
|
+
const subPath = 'common';
|
118
|
+
(findMatchingRoute as Mock).mockReturnValue(matchedRoute);
|
119
|
+
(extractSubPath as Mock).mockReturnValue(subPath);
|
120
|
+
|
121
|
+
const result = await browserWindowsCtr.interceptRoute(params);
|
122
|
+
|
123
|
+
expect(findMatchingRoute).toHaveBeenCalledWith(params.path);
|
124
|
+
expect(extractSubPath).toHaveBeenCalledWith(params.path, matchedRoute.pathPrefix);
|
125
|
+
expect(mockShowSettingsWindowWithTab).toHaveBeenCalledWith(subPath);
|
126
|
+
expect(result).toEqual({
|
127
|
+
intercepted: true,
|
128
|
+
path: params.path,
|
129
|
+
source: params.source,
|
130
|
+
subPath,
|
131
|
+
targetWindow: matchedRoute.targetWindow,
|
132
|
+
});
|
133
|
+
expect(mockShow).not.toHaveBeenCalled();
|
134
|
+
});
|
135
|
+
|
136
|
+
it('should open target window if matched route target is not settings', async () => {
|
137
|
+
const params: InterceptRouteParams = { ...baseParams, path: '/other/page', url: 'app://host/other/page' };
|
138
|
+
const targetWindowIdentifier = 'some-other-window' as AppBrowsersIdentifiers;
|
139
|
+
const matchedRoute = { targetWindow: targetWindowIdentifier, pathPrefix: '/other' };
|
140
|
+
(findMatchingRoute as Mock).mockReturnValue(matchedRoute);
|
141
|
+
|
142
|
+
const result = await browserWindowsCtr.interceptRoute(params);
|
143
|
+
|
144
|
+
expect(findMatchingRoute).toHaveBeenCalledWith(params.path);
|
145
|
+
expect(mockRetrieveByIdentifier).toHaveBeenCalledWith(targetWindowIdentifier);
|
146
|
+
expect(mockShow).toHaveBeenCalled();
|
147
|
+
expect(result).toEqual({
|
148
|
+
intercepted: true,
|
149
|
+
path: params.path,
|
150
|
+
source: params.source,
|
151
|
+
targetWindow: matchedRoute.targetWindow,
|
152
|
+
});
|
153
|
+
expect(mockShowSettingsWindowWithTab).not.toHaveBeenCalled();
|
154
|
+
});
|
155
|
+
|
156
|
+
it('should return error if processing route interception fails for settings', async () => {
|
157
|
+
const params: InterceptRouteParams = { ...baseParams, path: '/settings/general', url: 'app://host/settings/general' };
|
158
|
+
const matchedRoute = { targetWindow: BrowsersIdentifiers.settings, pathPrefix: '/settings' };
|
159
|
+
const subPath = 'general';
|
160
|
+
const errorMessage = 'Processing error for settings';
|
161
|
+
(findMatchingRoute as Mock).mockReturnValue(matchedRoute);
|
162
|
+
(extractSubPath as Mock).mockReturnValue(subPath);
|
163
|
+
mockShowSettingsWindowWithTab.mockRejectedValueOnce(new Error(errorMessage));
|
164
|
+
|
165
|
+
const result = await browserWindowsCtr.interceptRoute(params);
|
166
|
+
|
167
|
+
expect(result).toEqual({
|
168
|
+
error: errorMessage,
|
169
|
+
intercepted: false,
|
170
|
+
path: params.path,
|
171
|
+
source: params.source,
|
172
|
+
});
|
173
|
+
});
|
174
|
+
|
175
|
+
it('should return error if processing route interception fails for other window', async () => {
|
176
|
+
const params: InterceptRouteParams = { ...baseParams, path: '/another/custom', url: 'app://host/another/custom' };
|
177
|
+
const targetWindowIdentifier = 'another-custom-window' as AppBrowsersIdentifiers;
|
178
|
+
const matchedRoute = { targetWindow: targetWindowIdentifier, pathPrefix: '/another' };
|
179
|
+
const errorMessage = 'Processing error for other window';
|
180
|
+
(findMatchingRoute as Mock).mockReturnValue(matchedRoute);
|
181
|
+
mockRetrieveByIdentifier.mockImplementationOnce(() => {
|
182
|
+
throw new Error(errorMessage);
|
183
|
+
});
|
184
|
+
|
185
|
+
const result = await browserWindowsCtr.interceptRoute(params);
|
186
|
+
|
187
|
+
expect(result).toEqual({
|
188
|
+
error: errorMessage,
|
189
|
+
intercepted: false,
|
190
|
+
path: params.path,
|
191
|
+
source: params.source,
|
192
|
+
});
|
193
|
+
});
|
194
|
+
});
|
195
|
+
});
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import type { App } from '@/core/App';
|
4
|
+
|
5
|
+
import DevtoolsCtr from '../DevtoolsCtr';
|
6
|
+
|
7
|
+
// 模拟 App 及其依赖项
|
8
|
+
const mockShow = vi.fn();
|
9
|
+
const mockRetrieveByIdentifier = vi.fn(() => ({
|
10
|
+
show: mockShow,
|
11
|
+
}));
|
12
|
+
|
13
|
+
// 创建一个足够模拟 App 行为的对象,以满足 DevtoolsCtr 的需求
|
14
|
+
const mockApp = {
|
15
|
+
browserManager: {
|
16
|
+
retrieveByIdentifier: mockRetrieveByIdentifier,
|
17
|
+
},
|
18
|
+
// 如果 DevtoolsCtr 或其基类在构造或方法调用中使用了 app 的其他属性/方法,
|
19
|
+
// 也需要在这里添加相应的模拟
|
20
|
+
} as unknown as App; // 使用类型断言,因为我们只模拟了部分 App 结构
|
21
|
+
|
22
|
+
describe('DevtoolsCtr', () => {
|
23
|
+
let devtoolsCtr: DevtoolsCtr;
|
24
|
+
|
25
|
+
beforeEach(() => {
|
26
|
+
vi.clearAllMocks(); // 只清除 vi.fn() 创建的模拟函数的记录,不影响 IoCContainer 状态
|
27
|
+
|
28
|
+
// 实例化 DevtoolsCtr。
|
29
|
+
// 它将继承自真实的 ControllerModule。
|
30
|
+
// 其 @ipcClientEvent 装饰器会执行并与真实的 IoCContainer 交互。
|
31
|
+
devtoolsCtr = new DevtoolsCtr(mockApp);
|
32
|
+
});
|
33
|
+
|
34
|
+
describe('openDevtools', () => {
|
35
|
+
it('should retrieve the devtools browser window using app.browserManager and show it', async () => {
|
36
|
+
await devtoolsCtr.openDevtools();
|
37
|
+
|
38
|
+
// 验证 browserManager.retrieveByIdentifier 是否以 'devtools' 参数被调用
|
39
|
+
expect(mockRetrieveByIdentifier).toHaveBeenCalledWith('devtools');
|
40
|
+
// 验证返回对象的 show 方法是否被调用
|
41
|
+
expect(mockShow).toHaveBeenCalled();
|
42
|
+
});
|
43
|
+
});
|
44
|
+
});
|
@@ -0,0 +1,82 @@
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import type { App } from '@/core/App';
|
4
|
+
|
5
|
+
import MenuController from '../MenuCtr';
|
6
|
+
|
7
|
+
// 模拟 App 及其依赖项
|
8
|
+
const mockRefreshMenus = vi.fn();
|
9
|
+
const mockShowContextMenu = vi.fn();
|
10
|
+
const mockRebuildAppMenu = vi.fn();
|
11
|
+
|
12
|
+
const mockApp = {
|
13
|
+
menuManager: {
|
14
|
+
refreshMenus: mockRefreshMenus,
|
15
|
+
showContextMenu: mockShowContextMenu,
|
16
|
+
rebuildAppMenu: mockRebuildAppMenu,
|
17
|
+
},
|
18
|
+
} as unknown as App;
|
19
|
+
|
20
|
+
describe('MenuController', () => {
|
21
|
+
let menuController: MenuController;
|
22
|
+
|
23
|
+
beforeEach(() => {
|
24
|
+
vi.clearAllMocks();
|
25
|
+
menuController = new MenuController(mockApp);
|
26
|
+
});
|
27
|
+
|
28
|
+
describe('refreshAppMenu', () => {
|
29
|
+
it('should call menuManager.refreshMenus', () => {
|
30
|
+
// 模拟返回值
|
31
|
+
mockRefreshMenus.mockReturnValueOnce(true);
|
32
|
+
|
33
|
+
const result = menuController.refreshAppMenu();
|
34
|
+
|
35
|
+
expect(mockRefreshMenus).toHaveBeenCalled();
|
36
|
+
expect(result).toBe(true);
|
37
|
+
});
|
38
|
+
});
|
39
|
+
|
40
|
+
describe('showContextMenu', () => {
|
41
|
+
it('should call menuManager.showContextMenu with type only', () => {
|
42
|
+
const menuType = 'chat';
|
43
|
+
mockShowContextMenu.mockReturnValueOnce({ shown: true });
|
44
|
+
|
45
|
+
const result = menuController.showContextMenu(menuType);
|
46
|
+
|
47
|
+
expect(mockShowContextMenu).toHaveBeenCalledWith(menuType, undefined);
|
48
|
+
expect(result).toEqual({ shown: true });
|
49
|
+
});
|
50
|
+
|
51
|
+
it('should call menuManager.showContextMenu with type and data', () => {
|
52
|
+
const menuType = 'file';
|
53
|
+
const menuData = { fileId: '123', filePath: '/path/to/file.txt' };
|
54
|
+
mockShowContextMenu.mockReturnValueOnce({ shown: true });
|
55
|
+
|
56
|
+
const result = menuController.showContextMenu(menuType, menuData);
|
57
|
+
|
58
|
+
expect(mockShowContextMenu).toHaveBeenCalledWith(menuType, menuData);
|
59
|
+
expect(result).toEqual({ shown: true });
|
60
|
+
});
|
61
|
+
});
|
62
|
+
|
63
|
+
describe('setDevMenuVisibility', () => {
|
64
|
+
it('should call menuManager.rebuildAppMenu with showDevItems true', () => {
|
65
|
+
mockRebuildAppMenu.mockReturnValueOnce(true);
|
66
|
+
|
67
|
+
const result = menuController.setDevMenuVisibility(true);
|
68
|
+
|
69
|
+
expect(mockRebuildAppMenu).toHaveBeenCalledWith({ showDevItems: true });
|
70
|
+
expect(result).toBe(true);
|
71
|
+
});
|
72
|
+
|
73
|
+
it('should call menuManager.rebuildAppMenu with showDevItems false', () => {
|
74
|
+
mockRebuildAppMenu.mockReturnValueOnce(true);
|
75
|
+
|
76
|
+
const result = menuController.setDevMenuVisibility(false);
|
77
|
+
|
78
|
+
expect(mockRebuildAppMenu).toHaveBeenCalledWith({ showDevItems: false });
|
79
|
+
expect(result).toBe(true);
|
80
|
+
});
|
81
|
+
});
|
82
|
+
});
|
@@ -0,0 +1,64 @@
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import type { App } from '@/core/App';
|
4
|
+
|
5
|
+
import ShortcutController from '../ShortcutCtr';
|
6
|
+
|
7
|
+
// 模拟 App 及其依赖项
|
8
|
+
const mockGetShortcutsConfig = vi.fn().mockReturnValue({
|
9
|
+
toggleMainWindow: 'CommandOrControl+Shift+L',
|
10
|
+
openSettings: 'CommandOrControl+,'
|
11
|
+
});
|
12
|
+
const mockUpdateShortcutConfig = vi.fn().mockImplementation((id, accelerator) => {
|
13
|
+
// 简单模拟更新成功
|
14
|
+
return true;
|
15
|
+
});
|
16
|
+
|
17
|
+
const mockApp = {
|
18
|
+
shortcutManager: {
|
19
|
+
getShortcutsConfig: mockGetShortcutsConfig,
|
20
|
+
updateShortcutConfig: mockUpdateShortcutConfig,
|
21
|
+
},
|
22
|
+
} as unknown as App;
|
23
|
+
|
24
|
+
describe('ShortcutController', () => {
|
25
|
+
let shortcutController: ShortcutController;
|
26
|
+
|
27
|
+
beforeEach(() => {
|
28
|
+
vi.clearAllMocks();
|
29
|
+
shortcutController = new ShortcutController(mockApp);
|
30
|
+
});
|
31
|
+
|
32
|
+
describe('getShortcutsConfig', () => {
|
33
|
+
it('should return shortcuts config from shortcutManager', () => {
|
34
|
+
const result = shortcutController.getShortcutsConfig();
|
35
|
+
|
36
|
+
expect(mockGetShortcutsConfig).toHaveBeenCalled();
|
37
|
+
expect(result).toEqual({
|
38
|
+
toggleMainWindow: 'CommandOrControl+Shift+L',
|
39
|
+
openSettings: 'CommandOrControl+,'
|
40
|
+
});
|
41
|
+
});
|
42
|
+
});
|
43
|
+
|
44
|
+
describe('updateShortcutConfig', () => {
|
45
|
+
it('should call shortcutManager.updateShortcutConfig with correct parameters', () => {
|
46
|
+
const id = 'toggleMainWindow';
|
47
|
+
const accelerator = 'CommandOrControl+Alt+L';
|
48
|
+
|
49
|
+
const result = shortcutController.updateShortcutConfig(id, accelerator);
|
50
|
+
|
51
|
+
expect(mockUpdateShortcutConfig).toHaveBeenCalledWith(id, accelerator);
|
52
|
+
expect(result).toBe(true);
|
53
|
+
});
|
54
|
+
|
55
|
+
it('should return the result from shortcutManager.updateShortcutConfig', () => {
|
56
|
+
// 模拟更新失败的情况
|
57
|
+
mockUpdateShortcutConfig.mockReturnValueOnce(false);
|
58
|
+
|
59
|
+
const result = shortcutController.updateShortcutConfig('invalidKey', 'invalid+combo');
|
60
|
+
|
61
|
+
expect(result).toBe(false);
|
62
|
+
});
|
63
|
+
});
|
64
|
+
});
|
@@ -0,0 +1,256 @@
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
import {
|
3
|
+
ShowTrayNotificationParams,
|
4
|
+
UpdateTrayIconParams,
|
5
|
+
UpdateTrayTooltipParams
|
6
|
+
} from '@lobechat/electron-client-ipc';
|
7
|
+
|
8
|
+
import type { App } from '@/core/App';
|
9
|
+
|
10
|
+
// 模拟 logger
|
11
|
+
vi.mock('@/utils/logger', () => ({
|
12
|
+
createLogger: () => ({
|
13
|
+
debug: vi.fn(),
|
14
|
+
error: vi.fn(),
|
15
|
+
}),
|
16
|
+
}));
|
17
|
+
|
18
|
+
import TrayMenuCtr from '../TrayMenuCtr';
|
19
|
+
|
20
|
+
// 保存原始平台,确保测试结束后能恢复
|
21
|
+
const originalPlatform = process.platform;
|
22
|
+
|
23
|
+
// 模拟 App 及其依赖项
|
24
|
+
const mockToggleVisible = vi.fn();
|
25
|
+
const mockGetMainWindow = vi.fn(() => ({
|
26
|
+
toggleVisible: mockToggleVisible,
|
27
|
+
}));
|
28
|
+
|
29
|
+
const mockDisplayBalloon = vi.fn();
|
30
|
+
const mockUpdateIcon = vi.fn();
|
31
|
+
const mockUpdateTooltip = vi.fn();
|
32
|
+
const mockGetMainTray = vi.fn();
|
33
|
+
|
34
|
+
const mockApp = {
|
35
|
+
browserManager: {
|
36
|
+
getMainWindow: mockGetMainWindow,
|
37
|
+
},
|
38
|
+
trayManager: {
|
39
|
+
getMainTray: mockGetMainTray,
|
40
|
+
},
|
41
|
+
} as unknown as App;
|
42
|
+
|
43
|
+
describe('TrayMenuCtr', () => {
|
44
|
+
let trayMenuCtr: TrayMenuCtr;
|
45
|
+
|
46
|
+
beforeEach(() => {
|
47
|
+
vi.clearAllMocks();
|
48
|
+
// 为每个测试重置 mockedTray
|
49
|
+
mockGetMainTray.mockReset();
|
50
|
+
trayMenuCtr = new TrayMenuCtr(mockApp);
|
51
|
+
});
|
52
|
+
|
53
|
+
// 在所有测试完成后恢复平台设置
|
54
|
+
afterAll(() => {
|
55
|
+
// 恢复原始平台
|
56
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
57
|
+
});
|
58
|
+
|
59
|
+
describe('toggleMainWindow', () => {
|
60
|
+
it('should get the main window and toggle its visibility', async () => {
|
61
|
+
await trayMenuCtr.toggleMainWindow();
|
62
|
+
|
63
|
+
expect(mockGetMainWindow).toHaveBeenCalled();
|
64
|
+
expect(mockToggleVisible).toHaveBeenCalled();
|
65
|
+
});
|
66
|
+
});
|
67
|
+
|
68
|
+
describe('showNotification', () => {
|
69
|
+
it('should display balloon notification on Windows platform', async () => {
|
70
|
+
// 模拟 Windows 平台
|
71
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
72
|
+
|
73
|
+
const mockedTray = {
|
74
|
+
displayBalloon: mockDisplayBalloon,
|
75
|
+
};
|
76
|
+
mockGetMainTray.mockReturnValue(mockedTray);
|
77
|
+
|
78
|
+
const options: ShowTrayNotificationParams = {
|
79
|
+
title: 'Test Notification',
|
80
|
+
content: 'This is a test notification',
|
81
|
+
iconType: 'info',
|
82
|
+
};
|
83
|
+
|
84
|
+
const result = await trayMenuCtr.showNotification(options);
|
85
|
+
|
86
|
+
expect(mockGetMainTray).toHaveBeenCalled();
|
87
|
+
expect(mockDisplayBalloon).toHaveBeenCalledWith({
|
88
|
+
title: options.title,
|
89
|
+
content: options.content,
|
90
|
+
iconType: options.iconType,
|
91
|
+
});
|
92
|
+
expect(result).toEqual({ success: true });
|
93
|
+
});
|
94
|
+
|
95
|
+
it('should return error when not on Windows platform', async () => {
|
96
|
+
// 模拟非 Windows 平台
|
97
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
98
|
+
|
99
|
+
const options: ShowTrayNotificationParams = {
|
100
|
+
title: 'Test Notification',
|
101
|
+
content: 'This is a test notification',
|
102
|
+
};
|
103
|
+
|
104
|
+
const result = await trayMenuCtr.showNotification(options);
|
105
|
+
|
106
|
+
expect(mockGetMainTray).not.toHaveBeenCalled();
|
107
|
+
expect(mockDisplayBalloon).not.toHaveBeenCalled();
|
108
|
+
expect(result).toEqual({
|
109
|
+
error: '托盘通知仅在 Windows 平台支持',
|
110
|
+
success: false,
|
111
|
+
});
|
112
|
+
});
|
113
|
+
|
114
|
+
it('should return error when tray is not available on Windows', async () => {
|
115
|
+
// 模拟 Windows 平台但没有托盘
|
116
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
117
|
+
mockGetMainTray.mockReturnValue(null);
|
118
|
+
|
119
|
+
const options: ShowTrayNotificationParams = {
|
120
|
+
title: 'Test Notification',
|
121
|
+
content: 'This is a test notification',
|
122
|
+
};
|
123
|
+
|
124
|
+
const result = await trayMenuCtr.showNotification(options);
|
125
|
+
|
126
|
+
expect(mockGetMainTray).toHaveBeenCalled();
|
127
|
+
expect(mockDisplayBalloon).not.toHaveBeenCalled();
|
128
|
+
expect(result).toEqual({
|
129
|
+
error: '托盘通知仅在 Windows 平台支持',
|
130
|
+
success: false
|
131
|
+
});
|
132
|
+
});
|
133
|
+
});
|
134
|
+
|
135
|
+
describe('updateTrayIcon', () => {
|
136
|
+
it('should update tray icon on Windows platform', async () => {
|
137
|
+
// 模拟 Windows 平台
|
138
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
139
|
+
|
140
|
+
const mockedTray = {
|
141
|
+
updateIcon: mockUpdateIcon,
|
142
|
+
};
|
143
|
+
mockGetMainTray.mockReturnValue(mockedTray);
|
144
|
+
|
145
|
+
const options: UpdateTrayIconParams = {
|
146
|
+
iconPath: '/path/to/icon.png',
|
147
|
+
};
|
148
|
+
|
149
|
+
const result = await trayMenuCtr.updateTrayIcon(options);
|
150
|
+
|
151
|
+
expect(mockGetMainTray).toHaveBeenCalled();
|
152
|
+
expect(mockUpdateIcon).toHaveBeenCalledWith(options.iconPath);
|
153
|
+
expect(result).toEqual({ success: true });
|
154
|
+
});
|
155
|
+
|
156
|
+
it('should handle errors when updating icon', async () => {
|
157
|
+
// 模拟 Windows 平台
|
158
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
159
|
+
|
160
|
+
const error = new Error('Failed to update icon');
|
161
|
+
const mockedTray = {
|
162
|
+
updateIcon: vi.fn().mockImplementation(() => {
|
163
|
+
throw error;
|
164
|
+
}),
|
165
|
+
};
|
166
|
+
mockGetMainTray.mockReturnValue(mockedTray);
|
167
|
+
|
168
|
+
const options: UpdateTrayIconParams = {
|
169
|
+
iconPath: '/path/to/icon.png',
|
170
|
+
};
|
171
|
+
|
172
|
+
const result = await trayMenuCtr.updateTrayIcon(options);
|
173
|
+
|
174
|
+
expect(result).toEqual({
|
175
|
+
error: String(error),
|
176
|
+
success: false,
|
177
|
+
});
|
178
|
+
});
|
179
|
+
|
180
|
+
it('should return error when not on Windows platform', async () => {
|
181
|
+
// 模拟非 Windows 平台
|
182
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
183
|
+
|
184
|
+
const options: UpdateTrayIconParams = {
|
185
|
+
iconPath: '/path/to/icon.png',
|
186
|
+
};
|
187
|
+
|
188
|
+
const result = await trayMenuCtr.updateTrayIcon(options);
|
189
|
+
|
190
|
+
expect(result).toEqual({
|
191
|
+
error: '托盘功能仅在 Windows 平台支持',
|
192
|
+
success: false,
|
193
|
+
});
|
194
|
+
});
|
195
|
+
});
|
196
|
+
|
197
|
+
describe('updateTrayTooltip', () => {
|
198
|
+
it('should update tray tooltip on Windows platform', async () => {
|
199
|
+
// 模拟 Windows 平台
|
200
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
201
|
+
|
202
|
+
const mockedTray = {
|
203
|
+
updateTooltip: mockUpdateTooltip,
|
204
|
+
};
|
205
|
+
mockGetMainTray.mockReturnValue(mockedTray);
|
206
|
+
|
207
|
+
const options: UpdateTrayTooltipParams = {
|
208
|
+
tooltip: 'New tooltip text',
|
209
|
+
};
|
210
|
+
|
211
|
+
const result = await trayMenuCtr.updateTrayTooltip(options);
|
212
|
+
|
213
|
+
expect(mockGetMainTray).toHaveBeenCalled();
|
214
|
+
expect(mockUpdateTooltip).toHaveBeenCalledWith(options.tooltip);
|
215
|
+
expect(result).toEqual({ success: true });
|
216
|
+
});
|
217
|
+
|
218
|
+
it('should return error when not on Windows platform', async () => {
|
219
|
+
// 模拟非 Windows 平台
|
220
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
221
|
+
|
222
|
+
const options: UpdateTrayTooltipParams = {
|
223
|
+
tooltip: 'New tooltip text',
|
224
|
+
};
|
225
|
+
|
226
|
+
const result = await trayMenuCtr.updateTrayTooltip(options);
|
227
|
+
|
228
|
+
expect(result).toEqual({
|
229
|
+
error: '托盘功能仅在 Windows 平台支持',
|
230
|
+
success: false,
|
231
|
+
});
|
232
|
+
});
|
233
|
+
|
234
|
+
it('should return error when tooltip is not provided', async () => {
|
235
|
+
// 模拟 Windows 平台
|
236
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
237
|
+
|
238
|
+
const mockedTray = {
|
239
|
+
updateTooltip: mockUpdateTooltip,
|
240
|
+
};
|
241
|
+
mockGetMainTray.mockReturnValue(mockedTray);
|
242
|
+
|
243
|
+
const options: UpdateTrayTooltipParams = {
|
244
|
+
tooltip: undefined as any,
|
245
|
+
};
|
246
|
+
|
247
|
+
const result = await trayMenuCtr.updateTrayTooltip(options);
|
248
|
+
|
249
|
+
expect(mockUpdateTooltip).not.toHaveBeenCalled();
|
250
|
+
expect(result).toEqual({
|
251
|
+
error: '托盘功能仅在 Windows 平台支持',
|
252
|
+
success: false,
|
253
|
+
});
|
254
|
+
});
|
255
|
+
});
|
256
|
+
});
|