@lobehub/lobehub 2.0.0-next.242 → 2.0.0-next.243

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 (27) hide show
  1. package/.cursor/rules/desktop-feature-implementation.mdc +2 -2
  2. package/.cursor/rules/desktop-local-tools-implement.mdc +2 -2
  3. package/CHANGELOG.md +27 -0
  4. package/apps/desktop/Development.md +1 -6
  5. package/apps/desktop/README.md +2 -17
  6. package/apps/desktop/README.zh-CN.md +1 -15
  7. package/apps/desktop/src/main/controllers/index.ts +1 -1
  8. package/apps/desktop/src/main/controllers/registry.ts +0 -9
  9. package/apps/desktop/src/main/core/App.ts +1 -11
  10. package/apps/desktop/src/main/core/browser/Browser.ts +278 -457
  11. package/apps/desktop/src/main/core/browser/WindowStateManager.ts +180 -0
  12. package/apps/desktop/src/main/core/browser/WindowThemeManager.ts +167 -0
  13. package/apps/desktop/src/main/core/browser/__tests__/WindowStateManager.test.ts +237 -0
  14. package/apps/desktop/src/main/core/browser/__tests__/WindowThemeManager.test.ts +240 -0
  15. package/apps/desktop/src/main/exports.d.ts +1 -1
  16. package/apps/desktop/src/main/exports.ts +1 -1
  17. package/apps/desktop/src/main/utils/__tests__/http-headers.test.ts +131 -0
  18. package/apps/desktop/src/main/utils/http-headers.ts +61 -0
  19. package/apps/desktop/src/main/utils/ipc/__tests__/base.test.ts +1 -22
  20. package/apps/desktop/src/main/utils/ipc/base.ts +0 -20
  21. package/apps/desktop/src/main/utils/ipc/index.ts +1 -9
  22. package/changelog/v1.json +5 -0
  23. package/package.json +1 -1
  24. package/src/features/ChatInput/InputEditor/Placeholder.tsx +4 -1
  25. package/apps/desktop/src/main/controllers/UploadFileServerCtr.ts +0 -33
  26. package/apps/desktop/src/main/controllers/__tests__/UploadFileServerCtr.test.ts +0 -55
  27. package/src/server/modules/ElectronIPCClient/index.ts +0 -92
@@ -0,0 +1,240 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { WindowThemeManager } from '../WindowThemeManager';
4
+
5
+ // Use vi.hoisted to define mocks before hoisting
6
+ const { mockNativeTheme, mockBrowserWindow } = vi.hoisted(() => ({
7
+ mockBrowserWindow: {
8
+ isDestroyed: vi.fn().mockReturnValue(false),
9
+ setBackgroundColor: vi.fn(),
10
+ setTitleBarOverlay: vi.fn(),
11
+ },
12
+ mockNativeTheme: {
13
+ off: vi.fn(),
14
+ on: vi.fn(),
15
+ shouldUseDarkColors: false,
16
+ },
17
+ }));
18
+
19
+ vi.mock('electron', () => ({
20
+ nativeTheme: mockNativeTheme,
21
+ }));
22
+
23
+ vi.mock('@/utils/logger', () => ({
24
+ createLogger: () => ({
25
+ debug: vi.fn(),
26
+ error: vi.fn(),
27
+ info: vi.fn(),
28
+ warn: vi.fn(),
29
+ }),
30
+ }));
31
+
32
+ vi.mock('@/const/dir', () => ({
33
+ buildDir: '/mock/build',
34
+ }));
35
+
36
+ vi.mock('@/const/env', () => ({
37
+ isDev: false,
38
+ isWindows: true,
39
+ }));
40
+
41
+ vi.mock('@/const/theme', () => ({
42
+ BACKGROUND_DARK: '#1a1a1a',
43
+ BACKGROUND_LIGHT: '#ffffff',
44
+ SYMBOL_COLOR_DARK: '#ffffff',
45
+ SYMBOL_COLOR_LIGHT: '#000000',
46
+ THEME_CHANGE_DELAY: 0,
47
+ TITLE_BAR_HEIGHT: 32,
48
+ }));
49
+
50
+ describe('WindowThemeManager', () => {
51
+ let manager: WindowThemeManager;
52
+
53
+ beforeEach(() => {
54
+ vi.clearAllMocks();
55
+ vi.useFakeTimers();
56
+
57
+ mockNativeTheme.shouldUseDarkColors = false;
58
+ mockBrowserWindow.isDestroyed.mockReturnValue(false);
59
+
60
+ manager = new WindowThemeManager('test-window');
61
+ });
62
+
63
+ afterEach(() => {
64
+ vi.useRealTimers();
65
+ });
66
+
67
+ describe('isDarkMode', () => {
68
+ it('should return true when shouldUseDarkColors is true', () => {
69
+ mockNativeTheme.shouldUseDarkColors = true;
70
+
71
+ expect(manager.isDarkMode).toBe(true);
72
+ });
73
+
74
+ it('should return false when shouldUseDarkColors is false', () => {
75
+ mockNativeTheme.shouldUseDarkColors = false;
76
+
77
+ expect(manager.isDarkMode).toBe(false);
78
+ });
79
+ });
80
+
81
+ describe('getPlatformConfig', () => {
82
+ it('should return Windows dark theme config when in dark mode', () => {
83
+ mockNativeTheme.shouldUseDarkColors = true;
84
+
85
+ const config = manager.getPlatformConfig();
86
+
87
+ expect(config).toEqual({
88
+ backgroundColor: '#1a1a1a',
89
+ icon: undefined,
90
+ titleBarOverlay: {
91
+ color: '#1a1a1a',
92
+ height: 32,
93
+ symbolColor: '#ffffff',
94
+ },
95
+ titleBarStyle: 'hidden',
96
+ });
97
+ });
98
+
99
+ it('should return Windows light theme config when in light mode', () => {
100
+ mockNativeTheme.shouldUseDarkColors = false;
101
+
102
+ const config = manager.getPlatformConfig();
103
+
104
+ expect(config).toEqual({
105
+ backgroundColor: '#ffffff',
106
+ icon: undefined,
107
+ titleBarOverlay: {
108
+ color: '#ffffff',
109
+ height: 32,
110
+ symbolColor: '#000000',
111
+ },
112
+ titleBarStyle: 'hidden',
113
+ });
114
+ });
115
+ });
116
+
117
+ describe('attach', () => {
118
+ it('should setup theme listener', () => {
119
+ manager.attach(mockBrowserWindow as any);
120
+
121
+ expect(mockNativeTheme.on).toHaveBeenCalledWith('updated', expect.any(Function));
122
+ });
123
+
124
+ it('should apply initial visual effects', () => {
125
+ manager.attach(mockBrowserWindow as any);
126
+
127
+ expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalled();
128
+ expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalled();
129
+ });
130
+
131
+ it('should not setup duplicate listeners', () => {
132
+ manager.attach(mockBrowserWindow as any);
133
+ manager.attach(mockBrowserWindow as any);
134
+
135
+ expect(mockNativeTheme.on).toHaveBeenCalledTimes(1);
136
+ });
137
+ });
138
+
139
+ describe('cleanup', () => {
140
+ it('should remove theme listener', () => {
141
+ manager.attach(mockBrowserWindow as any);
142
+ manager.cleanup();
143
+
144
+ expect(mockNativeTheme.off).toHaveBeenCalledWith('updated', expect.any(Function));
145
+ });
146
+
147
+ it('should not throw if cleanup called without attach', () => {
148
+ expect(() => manager.cleanup()).not.toThrow();
149
+ expect(mockNativeTheme.off).not.toHaveBeenCalled();
150
+ });
151
+ });
152
+
153
+ describe('handleAppThemeChange', () => {
154
+ it('should reapply visual effects after delay', () => {
155
+ manager.attach(mockBrowserWindow as any);
156
+ mockBrowserWindow.setBackgroundColor.mockClear();
157
+ mockBrowserWindow.setTitleBarOverlay.mockClear();
158
+
159
+ manager.handleAppThemeChange();
160
+ vi.advanceTimersByTime(0);
161
+
162
+ expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalled();
163
+ expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalled();
164
+ });
165
+ });
166
+
167
+ describe('reapplyVisualEffects', () => {
168
+ it('should apply visual effects', () => {
169
+ manager.attach(mockBrowserWindow as any);
170
+ mockBrowserWindow.setBackgroundColor.mockClear();
171
+
172
+ manager.reapplyVisualEffects();
173
+
174
+ expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalled();
175
+ });
176
+ });
177
+
178
+ describe('applyVisualEffects', () => {
179
+ it('should apply dark theme when in dark mode', () => {
180
+ mockNativeTheme.shouldUseDarkColors = true;
181
+ manager.attach(mockBrowserWindow as any);
182
+
183
+ expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalledWith('#1a1a1a');
184
+ expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalledWith({
185
+ color: '#1a1a1a',
186
+ height: 32,
187
+ symbolColor: '#ffffff',
188
+ });
189
+ });
190
+
191
+ it('should apply light theme when in light mode', () => {
192
+ mockNativeTheme.shouldUseDarkColors = false;
193
+ manager.attach(mockBrowserWindow as any);
194
+
195
+ expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalledWith('#ffffff');
196
+ expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalledWith({
197
+ color: '#ffffff',
198
+ height: 32,
199
+ symbolColor: '#000000',
200
+ });
201
+ });
202
+
203
+ it('should not apply effects when window is destroyed', () => {
204
+ manager.attach(mockBrowserWindow as any);
205
+ mockBrowserWindow.setBackgroundColor.mockClear();
206
+ mockBrowserWindow.isDestroyed.mockReturnValue(true);
207
+
208
+ manager.reapplyVisualEffects();
209
+
210
+ expect(mockBrowserWindow.setBackgroundColor).not.toHaveBeenCalled();
211
+ });
212
+
213
+ it('should not apply effects when no window attached', () => {
214
+ // Manager without attached window
215
+ const freshManager = new WindowThemeManager('fresh-window');
216
+
217
+ // Should not throw
218
+ expect(() => freshManager.reapplyVisualEffects()).not.toThrow();
219
+ });
220
+ });
221
+
222
+ describe('theme change listener', () => {
223
+ it('should reapply visual effects on system theme change', () => {
224
+ manager.attach(mockBrowserWindow as any);
225
+
226
+ // Get the theme change handler
227
+ const themeHandler = mockNativeTheme.on.mock.calls.find((call) => call[0] === 'updated')?.[1];
228
+
229
+ expect(themeHandler).toBeDefined();
230
+
231
+ mockBrowserWindow.setBackgroundColor.mockClear();
232
+
233
+ // Simulate theme change
234
+ themeHandler();
235
+ vi.advanceTimersByTime(0);
236
+
237
+ expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalled();
238
+ });
239
+ });
240
+ });
@@ -5,4 +5,4 @@ declare module '@lobechat/electron-client-ipc' {
5
5
  interface DesktopIpcServicesMap extends DesktopIpcServices {}
6
6
  }
7
7
 
8
- export { type DesktopIpcServices, type DesktopServerIpcServices } from './controllers/registry';
8
+ export { type DesktopIpcServices } from './controllers/registry';
@@ -1,2 +1,2 @@
1
1
  // Export types for renderer/server to use
2
- export type { DesktopIpcServices, DesktopServerIpcServices } from './controllers/registry';
2
+ export type { DesktopIpcServices } from './controllers/registry';
@@ -0,0 +1,131 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ deleteResponseHeader,
5
+ getResponseHeader,
6
+ hasResponseHeader,
7
+ setResponseHeader,
8
+ } from '../http-headers';
9
+
10
+ describe('http-headers utilities', () => {
11
+ describe('setResponseHeader', () => {
12
+ it('should set a new header', () => {
13
+ const headers: Record<string, string[]> = {};
14
+
15
+ setResponseHeader(headers, 'Content-Type', 'application/json');
16
+
17
+ expect(headers['Content-Type']).toEqual(['application/json']);
18
+ });
19
+
20
+ it('should replace existing header with same case', () => {
21
+ const headers: Record<string, string[]> = {
22
+ 'Content-Type': ['text/html'],
23
+ };
24
+
25
+ setResponseHeader(headers, 'Content-Type', 'application/json');
26
+
27
+ expect(headers['Content-Type']).toEqual(['application/json']);
28
+ expect(Object.keys(headers)).toHaveLength(1);
29
+ });
30
+
31
+ it('should replace existing header with different case', () => {
32
+ const headers: Record<string, string[]> = {
33
+ 'content-type': ['text/html'],
34
+ };
35
+
36
+ setResponseHeader(headers, 'Content-Type', 'application/json');
37
+
38
+ expect(headers['Content-Type']).toEqual(['application/json']);
39
+ expect(headers['content-type']).toBeUndefined();
40
+ expect(Object.keys(headers)).toHaveLength(1);
41
+ });
42
+
43
+ it('should handle array values', () => {
44
+ const headers: Record<string, string[]> = {};
45
+
46
+ setResponseHeader(headers, 'Set-Cookie', ['a=1', 'b=2']);
47
+
48
+ expect(headers['Set-Cookie']).toEqual(['a=1', 'b=2']);
49
+ });
50
+
51
+ it('should replace multiple headers with different cases', () => {
52
+ const headers: Record<string, string[]> = {
53
+ 'ACCESS-CONTROL-ALLOW-ORIGIN': ['*'],
54
+ 'access-control-allow-origin': ['http://localhost'],
55
+ };
56
+
57
+ setResponseHeader(headers, 'Access-Control-Allow-Origin', 'http://example.com');
58
+
59
+ expect(headers['Access-Control-Allow-Origin']).toEqual(['http://example.com']);
60
+ expect(Object.keys(headers)).toHaveLength(1);
61
+ });
62
+ });
63
+
64
+ describe('hasResponseHeader', () => {
65
+ it('should return true for existing header', () => {
66
+ const headers = { 'Content-Type': ['application/json'] };
67
+
68
+ expect(hasResponseHeader(headers, 'Content-Type')).toBe(true);
69
+ });
70
+
71
+ it('should return true for existing header with different case', () => {
72
+ const headers = { 'content-type': ['application/json'] };
73
+
74
+ expect(hasResponseHeader(headers, 'Content-Type')).toBe(true);
75
+ });
76
+
77
+ it('should return false for non-existing header', () => {
78
+ const headers = { 'Content-Type': ['application/json'] };
79
+
80
+ expect(hasResponseHeader(headers, 'Authorization')).toBe(false);
81
+ });
82
+ });
83
+
84
+ describe('getResponseHeader', () => {
85
+ it('should get header value', () => {
86
+ const headers = { 'Content-Type': ['application/json'] };
87
+
88
+ expect(getResponseHeader(headers, 'Content-Type')).toEqual(['application/json']);
89
+ });
90
+
91
+ it('should get header value with different case', () => {
92
+ const headers = { 'content-type': ['application/json'] };
93
+
94
+ expect(getResponseHeader(headers, 'Content-Type')).toEqual(['application/json']);
95
+ });
96
+
97
+ it('should return undefined for non-existing header', () => {
98
+ const headers = { 'Content-Type': ['application/json'] };
99
+
100
+ expect(getResponseHeader(headers, 'Authorization')).toBeUndefined();
101
+ });
102
+ });
103
+
104
+ describe('deleteResponseHeader', () => {
105
+ it('should delete existing header', () => {
106
+ const headers: Record<string, string[]> = { 'Content-Type': ['application/json'] };
107
+
108
+ const result = deleteResponseHeader(headers, 'Content-Type');
109
+
110
+ expect(result).toBe(true);
111
+ expect(headers['Content-Type']).toBeUndefined();
112
+ });
113
+
114
+ it('should delete header with different case', () => {
115
+ const headers: Record<string, string[]> = { 'content-type': ['application/json'] };
116
+
117
+ const result = deleteResponseHeader(headers, 'Content-Type');
118
+
119
+ expect(result).toBe(true);
120
+ expect(headers['content-type']).toBeUndefined();
121
+ });
122
+
123
+ it('should return false for non-existing header', () => {
124
+ const headers: Record<string, string[]> = { 'Content-Type': ['application/json'] };
125
+
126
+ const result = deleteResponseHeader(headers, 'Authorization');
127
+
128
+ expect(result).toBe(false);
129
+ });
130
+ });
131
+ });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * HTTP headers utilities for Electron webRequest
3
+ *
4
+ * Electron's webRequest responseHeaders is a plain JS object where keys are case-sensitive,
5
+ * but HTTP headers are case-insensitive per spec. These utilities handle this mismatch.
6
+ */
7
+
8
+ type ElectronResponseHeaders = Record<string, string[]>;
9
+
10
+ /**
11
+ * Set a header value, replacing any existing header with the same name (case-insensitive)
12
+ */
13
+ export function setResponseHeader(
14
+ headers: ElectronResponseHeaders,
15
+ name: string,
16
+ value: string | string[],
17
+ ): void {
18
+ // Delete any existing header with same name (case-insensitive)
19
+ for (const key of Object.keys(headers)) {
20
+ if (key.toLowerCase() === name.toLowerCase()) {
21
+ delete headers[key];
22
+ }
23
+ }
24
+ headers[name] = Array.isArray(value) ? value : [value];
25
+ }
26
+
27
+ /**
28
+ * Check if a header exists (case-insensitive)
29
+ */
30
+ export function hasResponseHeader(headers: ElectronResponseHeaders, name: string): boolean {
31
+ return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
32
+ }
33
+
34
+ /**
35
+ * Get a header value (case-insensitive)
36
+ */
37
+ export function getResponseHeader(
38
+ headers: ElectronResponseHeaders,
39
+ name: string,
40
+ ): string[] | undefined {
41
+ for (const key of Object.keys(headers)) {
42
+ if (key.toLowerCase() === name.toLowerCase()) {
43
+ return headers[key];
44
+ }
45
+ }
46
+ return undefined;
47
+ }
48
+
49
+ /**
50
+ * Delete a header (case-insensitive)
51
+ */
52
+ export function deleteResponseHeader(headers: ElectronResponseHeaders, name: string): boolean {
53
+ let deleted = false;
54
+ for (const key of Object.keys(headers)) {
55
+ if (key.toLowerCase() === name.toLowerCase()) {
56
+ delete headers[key];
57
+ deleted = true;
58
+ }
59
+ }
60
+ return deleted;
61
+ }
@@ -1,13 +1,7 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  import type { IpcContext } from '../base';
4
- import {
5
- IpcMethod,
6
- IpcServerMethod,
7
- IpcService,
8
- getIpcContext,
9
- getServerMethodMetadata,
10
- } from '../base';
4
+ import { IpcMethod, IpcService, getIpcContext } from '../base';
11
5
 
12
6
  const { ipcMainHandleMock } = vi.hoisted(() => ({
13
7
  ipcMainHandleMock: vi.fn(),
@@ -73,19 +67,4 @@ describe('ipc service base', () => {
73
67
  expect(service.invokedWith).toBe('test');
74
68
  expect(ipcMainHandleMock).toHaveBeenCalledWith('direct.run', expect.any(Function));
75
69
  });
76
-
77
- it('collects server method metadata for decorators', () => {
78
- class ServerService extends IpcService {
79
- static readonly groupName = 'server';
80
-
81
- @IpcServerMethod()
82
- fetch(_: string) {
83
- return 'ok';
84
- }
85
- }
86
-
87
- const metadata = getServerMethodMetadata(ServerService);
88
- expect(metadata).toBeDefined();
89
- expect(metadata?.get('fetch')).toBe('fetch');
90
- });
91
70
  });
@@ -10,7 +10,6 @@ export interface IpcContext {
10
10
 
11
11
  // Metadata storage for decorated methods
12
12
  const methodMetadata = new WeakMap<any, Map<string, string>>();
13
- const serverMethodMetadata = new WeakMap<any, Map<string, string>>();
14
13
  const ipcContextStorage = new AsyncLocalStorage<IpcContext>();
15
14
 
16
15
  // Decorator for IPC methods
@@ -29,21 +28,6 @@ export function IpcMethod() {
29
28
  };
30
29
  }
31
30
 
32
- export function IpcServerMethod(channelName?: string) {
33
- return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
34
- const { constructor } = target;
35
-
36
- if (!serverMethodMetadata.has(constructor)) {
37
- serverMethodMetadata.set(constructor, new Map());
38
- }
39
-
40
- const methods = serverMethodMetadata.get(constructor)!;
41
- methods.set(propertyKey, channelName || propertyKey);
42
-
43
- return descriptor;
44
- };
45
- }
46
-
47
31
  // Handler registry for IPC methods
48
32
  export class IpcHandler {
49
33
  private static instance: IpcHandler;
@@ -157,10 +141,6 @@ export type CreateServicesResult<T extends readonly IpcServiceConstructor[]> = {
157
141
  [K in T[number] as K['groupName']]: InstanceType<K>;
158
142
  };
159
143
 
160
- export function getServerMethodMetadata(target: IpcServiceConstructor) {
161
- return serverMethodMetadata.get(target);
162
- }
163
-
164
144
  export function getIpcContext() {
165
145
  return ipcContextStorage.getStore();
166
146
  }
@@ -1,11 +1,3 @@
1
1
  export type { CreateServicesResult, IpcContext, IpcServiceConstructor } from './base';
2
- export {
3
- createServices,
4
- getIpcContext,
5
- getServerMethodMetadata,
6
- IpcMethod,
7
- IpcServerMethod,
8
- IpcService,
9
- runWithIpcContext,
10
- } from './base';
2
+ export { createServices, getIpcContext, IpcMethod, IpcService, runWithIpcContext } from './base';
11
3
  export type { ExtractServiceMethods, MergeIpcService } from './utility';
package/changelog/v1.json CHANGED
@@ -1,4 +1,9 @@
1
1
  [
2
+ {
3
+ "children": {},
4
+ "date": "2026-01-08",
5
+ "version": "2.0.0-next.243"
6
+ },
2
7
  {
3
8
  "children": {},
4
9
  "date": "2026-01-08",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.242",
3
+ "version": "2.0.0-next.243",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -1,7 +1,7 @@
1
1
  import { KeyEnum } from '@lobechat/types';
2
2
  import { Flexbox, Hotkey, combineKeys } from '@lobehub/ui';
3
3
  import { memo } from 'react';
4
- import { Trans } from 'react-i18next';
4
+ import { Trans, useTranslation } from 'react-i18next';
5
5
 
6
6
  import { useUserStore } from '@/store/user';
7
7
  import { preferenceSelectors } from '@/store/user/selectors';
@@ -12,6 +12,9 @@ const Placeholder = memo(() => {
12
12
  ? KeyEnum.Enter
13
13
  : combineKeys([KeyEnum.Mod, KeyEnum.Enter]);
14
14
 
15
+ // Don't remove this line for i18n reactivity
16
+ void useTranslation('chat');
17
+
15
18
  return (
16
19
  <Flexbox align={'center'} as={'span'} gap={4} horizontal wrap={'wrap'}>
17
20
  <Trans
@@ -1,33 +0,0 @@
1
- import { CreateFileParams } from '@lobechat/electron-server-ipc';
2
-
3
- import FileService from '@/services/fileSrv';
4
-
5
- import { ControllerModule, IpcServerMethod } from './index';
6
-
7
- export default class UploadFileServerCtr extends ControllerModule {
8
- static override readonly groupName = 'upload';
9
-
10
- private get fileService() {
11
- return this.app.getService(FileService);
12
- }
13
-
14
- @IpcServerMethod()
15
- async getFileUrlById(id: string) {
16
- return this.fileService.getFilePath(id);
17
- }
18
-
19
- @IpcServerMethod()
20
- async getFileHTTPURL(path: string) {
21
- return this.fileService.getFileHTTPURL(path);
22
- }
23
-
24
- @IpcServerMethod()
25
- async deleteFiles(paths: string[]) {
26
- return this.fileService.deleteFiles(paths);
27
- }
28
-
29
- @IpcServerMethod()
30
- async createFile(params: CreateFileParams) {
31
- return this.fileService.uploadFile(params);
32
- }
33
- }
@@ -1,55 +0,0 @@
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
- });