@qlover/create-app 0.6.3 → 0.7.1
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 +29 -0
- package/dist/configs/node-lib/eslint.config.js +3 -3
- package/dist/configs/react-app/eslint.config.js +3 -3
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/pack-app/eslint.config.js +3 -3
- package/dist/templates/pack-app/package.json +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/I18nService.ts +13 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockAppConfit.ts +48 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockDialogHandler.ts +16 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockLogger.ts +14 -0
- package/dist/templates/react-app/__tests__/__mocks__/createMockGlobals.ts +92 -0
- package/dist/templates/react-app/__tests__/setup/index.ts +51 -0
- package/dist/templates/react-app/__tests__/src/App.test.tsx +139 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppConfig.test.ts +288 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppError.test.ts +102 -0
- package/dist/templates/react-app/__tests__/src/base/cases/DialogHandler.test.ts +228 -0
- package/dist/templates/react-app/__tests__/src/base/cases/I18nKeyErrorPlugin.test.ts +207 -0
- package/dist/templates/react-app/__tests__/src/base/cases/InversifyContainer.test.ts +181 -0
- package/dist/templates/react-app/__tests__/src/base/cases/PublicAssetsPath.test.ts +61 -0
- package/dist/templates/react-app/__tests__/src/base/cases/RequestLogger.test.ts +199 -0
- package/dist/templates/react-app/__tests__/src/base/cases/RequestStatusCatcher.test.ts +192 -0
- package/dist/templates/react-app/__tests__/src/base/cases/RouterLoader.test.ts +235 -0
- package/dist/templates/react-app/__tests__/src/base/services/I18nService.test.ts +224 -0
- package/dist/templates/react-app/__tests__/src/core/IOC.test.ts +257 -0
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapsApp.test.ts +72 -0
- package/dist/templates/react-app/__tests__/src/main.integration.test.tsx +62 -0
- package/dist/templates/react-app/__tests__/src/main.test.tsx +46 -0
- package/dist/templates/react-app/__tests__/src/uikit/components/BaseHeader.test.tsx +88 -0
- package/dist/templates/react-app/config/app.router.ts +155 -0
- package/dist/templates/react-app/config/common.ts +9 -1
- package/dist/templates/react-app/docs/en/test-guide.md +782 -0
- package/dist/templates/react-app/docs/zh/test-guide.md +782 -0
- package/dist/templates/react-app/package.json +9 -20
- package/dist/templates/react-app/public/locales/en/common.json +1 -1
- package/dist/templates/react-app/public/locales/zh/common.json +1 -1
- package/dist/templates/react-app/src/base/cases/AppConfig.ts +16 -9
- package/dist/templates/react-app/src/base/cases/PublicAssetsPath.ts +7 -1
- package/dist/templates/react-app/src/base/services/I18nService.ts +15 -4
- package/dist/templates/react-app/src/base/services/RouteService.ts +43 -7
- package/dist/templates/react-app/src/core/bootstraps/BootstrapApp.ts +31 -10
- package/dist/templates/react-app/src/core/bootstraps/BootstrapsRegistry.ts +1 -1
- package/dist/templates/react-app/src/core/globals.ts +1 -3
- package/dist/templates/react-app/src/core/registers/RegisterCommon.ts +5 -3
- package/dist/templates/react-app/src/main.tsx +6 -1
- package/dist/templates/react-app/src/pages/404.tsx +0 -1
- package/dist/templates/react-app/src/pages/500.tsx +1 -1
- package/dist/templates/react-app/src/pages/base/RedirectPathname.tsx +3 -1
- package/dist/templates/react-app/src/uikit/components/BaseHeader.tsx +9 -2
- package/dist/templates/react-app/src/uikit/components/LocaleLink.tsx +5 -3
- package/dist/templates/react-app/src/uikit/hooks/useI18nGuard.ts +4 -6
- package/dist/templates/react-app/tsconfig.json +2 -1
- package/dist/templates/react-app/tsconfig.test.json +13 -0
- package/dist/templates/react-app/vite.config.ts +3 -2
- package/package.json +3 -3
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* I18nKeyErrorPlugin test suite
|
|
3
|
+
*
|
|
4
|
+
* Coverage:
|
|
5
|
+
* 1. constructor - Constructor initialization
|
|
6
|
+
* 2. plugin name - Plugin name verification
|
|
7
|
+
* 3. onError handling - Error translation behavior
|
|
8
|
+
* 4. edge cases - Various error scenarios
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
12
|
+
import { I18nKeyErrorPlugin } from '@/base/cases/I18nKeyErrorPlugin';
|
|
13
|
+
import { MockLogger } from '../../../__mocks__/MockLogger';
|
|
14
|
+
import { MockI18nService } from '../../../__mocks__/I18nService';
|
|
15
|
+
import type { ExecutorContext } from '@qlover/fe-corekit';
|
|
16
|
+
|
|
17
|
+
describe('I18nKeyErrorPlugin', () => {
|
|
18
|
+
let plugin: I18nKeyErrorPlugin;
|
|
19
|
+
let mockLogger: MockLogger;
|
|
20
|
+
let mockI18nService: MockI18nService;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
mockLogger = new MockLogger();
|
|
24
|
+
mockI18nService = new MockI18nService();
|
|
25
|
+
plugin = new I18nKeyErrorPlugin(mockLogger, mockI18nService);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('plugin properties', () => {
|
|
29
|
+
it('should have correct plugin name', () => {
|
|
30
|
+
expect(plugin.pluginName).toBe('I18nKeyErrorPlugin');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('onError handling', () => {
|
|
35
|
+
it('should handle non-Error objects', () => {
|
|
36
|
+
const context: ExecutorContext<unknown> = {
|
|
37
|
+
error: new Error('not an error'),
|
|
38
|
+
parameters: {},
|
|
39
|
+
returnValue: undefined,
|
|
40
|
+
hooksRuntimes: {}
|
|
41
|
+
};
|
|
42
|
+
const result = plugin.onError(context);
|
|
43
|
+
expect(result).toBeUndefined();
|
|
44
|
+
expect(mockLogger.debug).not.toHaveBeenCalled();
|
|
45
|
+
expect(mockI18nService.t).toHaveBeenCalledWith('not an error');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should handle Error objects without i18n key', () => {
|
|
49
|
+
const error = new Error('regular error');
|
|
50
|
+
const context: ExecutorContext<unknown> = {
|
|
51
|
+
error,
|
|
52
|
+
parameters: {},
|
|
53
|
+
returnValue: undefined,
|
|
54
|
+
hooksRuntimes: {}
|
|
55
|
+
};
|
|
56
|
+
const result = plugin.onError(context);
|
|
57
|
+
expect(result).toBeUndefined();
|
|
58
|
+
expect(mockLogger.debug).not.toHaveBeenCalled();
|
|
59
|
+
expect(mockI18nService.t).toHaveBeenCalledWith('regular error');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should translate i18n key errors', () => {
|
|
63
|
+
const error = new Error('error.key');
|
|
64
|
+
mockI18nService.t.mockReturnValueOnce('Translated error message');
|
|
65
|
+
|
|
66
|
+
const context: ExecutorContext<unknown> = {
|
|
67
|
+
error,
|
|
68
|
+
parameters: {},
|
|
69
|
+
returnValue: undefined,
|
|
70
|
+
hooksRuntimes: {}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const result = plugin.onError(context);
|
|
74
|
+
|
|
75
|
+
expect(result).toBeInstanceOf(Error);
|
|
76
|
+
expect(result?.message).toBe('Translated error message');
|
|
77
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
78
|
+
'I18nKeyErrorPlugin Error:',
|
|
79
|
+
'Translated error message'
|
|
80
|
+
);
|
|
81
|
+
expect(mockI18nService.t).toHaveBeenCalledWith('error.key');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should not translate when i18n returns same key', () => {
|
|
85
|
+
const error = new Error('error.key');
|
|
86
|
+
mockI18nService.t.mockReturnValueOnce('error.key');
|
|
87
|
+
|
|
88
|
+
const context: ExecutorContext<unknown> = {
|
|
89
|
+
error,
|
|
90
|
+
parameters: {},
|
|
91
|
+
returnValue: undefined,
|
|
92
|
+
hooksRuntimes: {}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const result = plugin.onError(context);
|
|
96
|
+
|
|
97
|
+
expect(result).toBeUndefined();
|
|
98
|
+
expect(mockLogger.debug).not.toHaveBeenCalled();
|
|
99
|
+
expect(mockI18nService.t).toHaveBeenCalledWith('error.key');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('error translation', () => {
|
|
104
|
+
it('should handle complex i18n keys with parameters', () => {
|
|
105
|
+
const error = new Error('error.with.params');
|
|
106
|
+
mockI18nService.t.mockReturnValueOnce(
|
|
107
|
+
'Error with param1: {0} and param2: {1}'
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const context: ExecutorContext<unknown> = {
|
|
111
|
+
error,
|
|
112
|
+
parameters: { param1: 'value1', param2: 'value2' },
|
|
113
|
+
returnValue: undefined,
|
|
114
|
+
hooksRuntimes: {}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const result = plugin.onError(context);
|
|
118
|
+
|
|
119
|
+
expect(result).toBeInstanceOf(Error);
|
|
120
|
+
expect(mockI18nService.t).toHaveBeenCalledWith('error.with.params');
|
|
121
|
+
expect(mockLogger.debug).toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle nested error objects', () => {
|
|
125
|
+
const originalError = new Error('original.error');
|
|
126
|
+
const wrappedError = new Error('wrapped.error');
|
|
127
|
+
// @ts-expect-error
|
|
128
|
+
wrappedError.cause = originalError;
|
|
129
|
+
|
|
130
|
+
mockI18nService.t
|
|
131
|
+
.mockReturnValueOnce('Wrapped Error')
|
|
132
|
+
.mockReturnValueOnce('Original Error');
|
|
133
|
+
|
|
134
|
+
const context: ExecutorContext<unknown> = {
|
|
135
|
+
error: wrappedError,
|
|
136
|
+
parameters: {},
|
|
137
|
+
returnValue: undefined,
|
|
138
|
+
hooksRuntimes: {}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const result = plugin.onError(context);
|
|
142
|
+
|
|
143
|
+
expect(result).toBeInstanceOf(Error);
|
|
144
|
+
expect(mockI18nService.t).toHaveBeenCalledWith('wrapped.error');
|
|
145
|
+
expect(mockLogger.debug).toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('edge cases', () => {
|
|
150
|
+
it('should handle empty error message', () => {
|
|
151
|
+
const error = new Error('');
|
|
152
|
+
const context: ExecutorContext<unknown> = {
|
|
153
|
+
error,
|
|
154
|
+
parameters: {},
|
|
155
|
+
returnValue: undefined,
|
|
156
|
+
hooksRuntimes: {}
|
|
157
|
+
};
|
|
158
|
+
const result = plugin.onError(context);
|
|
159
|
+
expect(result).toBeUndefined();
|
|
160
|
+
expect(mockLogger.debug).not.toHaveBeenCalled();
|
|
161
|
+
// Empty string is not a valid i18n key, so t() should not be called
|
|
162
|
+
expect(mockI18nService.t).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should handle null error message', () => {
|
|
166
|
+
const error = new Error();
|
|
167
|
+
error.message = ''; // Force empty message
|
|
168
|
+
const context: ExecutorContext<unknown> = {
|
|
169
|
+
error,
|
|
170
|
+
parameters: {},
|
|
171
|
+
returnValue: undefined,
|
|
172
|
+
hooksRuntimes: {}
|
|
173
|
+
};
|
|
174
|
+
const result = plugin.onError(context);
|
|
175
|
+
expect(result).toBeUndefined();
|
|
176
|
+
expect(mockLogger.debug).not.toHaveBeenCalled();
|
|
177
|
+
// Empty string is not a valid i18n key, so t() should not be called
|
|
178
|
+
expect(mockI18nService.t).not.toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle undefined context error', () => {
|
|
182
|
+
const context: ExecutorContext<unknown> = {
|
|
183
|
+
error: undefined,
|
|
184
|
+
parameters: {},
|
|
185
|
+
returnValue: undefined,
|
|
186
|
+
hooksRuntimes: {}
|
|
187
|
+
};
|
|
188
|
+
const result = plugin.onError(context);
|
|
189
|
+
expect(result).toBeUndefined();
|
|
190
|
+
expect(mockLogger.debug).not.toHaveBeenCalled();
|
|
191
|
+
expect(mockI18nService.t).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should handle null context error', () => {
|
|
195
|
+
const context: ExecutorContext<unknown> = {
|
|
196
|
+
error: undefined,
|
|
197
|
+
parameters: {},
|
|
198
|
+
returnValue: undefined,
|
|
199
|
+
hooksRuntimes: {}
|
|
200
|
+
};
|
|
201
|
+
const result = plugin.onError(context);
|
|
202
|
+
expect(result).toBeUndefined();
|
|
203
|
+
expect(mockLogger.debug).not.toHaveBeenCalled();
|
|
204
|
+
expect(mockI18nService.t).not.toHaveBeenCalled();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InversifyContainer test-suite
|
|
3
|
+
*
|
|
4
|
+
* Coverage:
|
|
5
|
+
* 1. constructor - Container initialization tests
|
|
6
|
+
* 2. bind - Value binding tests
|
|
7
|
+
* 3. get - Value retrieval tests
|
|
8
|
+
* 4. auto-binding - Injectable class auto-binding tests
|
|
9
|
+
* 5. singleton scope - Singleton pattern verification tests
|
|
10
|
+
* 6. error handling - Error cases and boundary tests
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { InversifyContainer } from '@/base/cases/InversifyContainer';
|
|
14
|
+
import { injectable } from 'inversify';
|
|
15
|
+
|
|
16
|
+
describe('InversifyContainer', () => {
|
|
17
|
+
let container: InversifyContainer;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
container = new InversifyContainer();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('constructor', () => {
|
|
24
|
+
it('should create container instance', () => {
|
|
25
|
+
expect(container).toBeInstanceOf(InversifyContainer);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('bind', () => {
|
|
30
|
+
it('should bind and get constant value', () => {
|
|
31
|
+
const key = Symbol('test');
|
|
32
|
+
const value = { test: 'value' };
|
|
33
|
+
|
|
34
|
+
container.bind(key, value);
|
|
35
|
+
const result = container.get(key);
|
|
36
|
+
|
|
37
|
+
expect(result).toBe(value);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should handle multiple bindings', () => {
|
|
41
|
+
const key1 = Symbol('test1');
|
|
42
|
+
const key2 = Symbol('test2');
|
|
43
|
+
const value1 = { test: 'value1' };
|
|
44
|
+
const value2 = { test: 'value2' };
|
|
45
|
+
|
|
46
|
+
container.bind(key1, value1);
|
|
47
|
+
container.bind(key2, value2);
|
|
48
|
+
|
|
49
|
+
expect(container.get(key1)).toBe(value1);
|
|
50
|
+
expect(container.get(key2)).toBe(value2);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should handle binding of null and undefined values', () => {
|
|
54
|
+
const key1 = Symbol('null');
|
|
55
|
+
const key2 = Symbol('undefined');
|
|
56
|
+
container.bind(key1, null);
|
|
57
|
+
container.bind(key2, undefined);
|
|
58
|
+
|
|
59
|
+
expect(container.get(key1)).toBeNull();
|
|
60
|
+
expect(container.get(key2)).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('get', () => {
|
|
65
|
+
it('should throw error when getting unbound non-injectable value', () => {
|
|
66
|
+
const key = Symbol('nonexistent');
|
|
67
|
+
expect(() => container.get(key)).toThrow();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should handle retrieval of primitive values', () => {
|
|
71
|
+
const numberKey = Symbol('number');
|
|
72
|
+
const stringKey = Symbol('string');
|
|
73
|
+
const booleanKey = Symbol('boolean');
|
|
74
|
+
|
|
75
|
+
container.bind(numberKey, 42);
|
|
76
|
+
container.bind(stringKey, 'test');
|
|
77
|
+
container.bind(booleanKey, true);
|
|
78
|
+
|
|
79
|
+
expect(container.get(numberKey)).toBe(42);
|
|
80
|
+
expect(container.get(stringKey)).toBe('test');
|
|
81
|
+
expect(container.get(booleanKey)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('auto-binding', () => {
|
|
86
|
+
it('should auto bind injectable class', () => {
|
|
87
|
+
@injectable()
|
|
88
|
+
class TestService {
|
|
89
|
+
getValue(): string {
|
|
90
|
+
return 'test';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const result = container.get(TestService);
|
|
95
|
+
|
|
96
|
+
expect(result).toBeInstanceOf(TestService);
|
|
97
|
+
expect(result.getValue()).toBe('test');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should handle manual dependency injection', () => {
|
|
101
|
+
@injectable()
|
|
102
|
+
class ServiceA {
|
|
103
|
+
getValue(): string {
|
|
104
|
+
return 'A';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@injectable()
|
|
109
|
+
class ServiceB {
|
|
110
|
+
constructor(private serviceA: ServiceA) {}
|
|
111
|
+
|
|
112
|
+
getValueWithA(): string {
|
|
113
|
+
return `B with ${this.serviceA.getValue()}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const serviceA = container.get(ServiceA);
|
|
118
|
+
container.bind(ServiceA, serviceA);
|
|
119
|
+
container.bind(ServiceB, new ServiceB(serviceA));
|
|
120
|
+
|
|
121
|
+
const serviceB = container.get(ServiceB);
|
|
122
|
+
expect(serviceB).toBeInstanceOf(ServiceB);
|
|
123
|
+
expect(serviceB.getValueWithA()).toBe('B with A');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('singleton scope', () => {
|
|
128
|
+
it('should maintain singleton scope for simple services', () => {
|
|
129
|
+
@injectable()
|
|
130
|
+
class TestService {
|
|
131
|
+
private count = 0;
|
|
132
|
+
increment(): number {
|
|
133
|
+
return ++this.count;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const instance1 = container.get(TestService);
|
|
138
|
+
const instance2 = container.get(TestService);
|
|
139
|
+
|
|
140
|
+
instance1.increment();
|
|
141
|
+
expect(instance2.increment()).toBe(2);
|
|
142
|
+
expect(instance1).toBe(instance2);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should maintain singleton scope for complex services', () => {
|
|
146
|
+
@injectable()
|
|
147
|
+
class ComplexService {
|
|
148
|
+
private state = { count: 0 };
|
|
149
|
+
|
|
150
|
+
updateState(): { count: number } {
|
|
151
|
+
this.state.count++;
|
|
152
|
+
return this.state;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const instance1 = container.get(ComplexService);
|
|
157
|
+
instance1.updateState();
|
|
158
|
+
instance1.updateState();
|
|
159
|
+
|
|
160
|
+
const instance2 = container.get(ComplexService);
|
|
161
|
+
expect(instance2.updateState().count).toBe(3);
|
|
162
|
+
expect(instance1).toBe(instance2);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('error handling', () => {
|
|
167
|
+
it('should throw error when getting unbound non-injectable value', () => {
|
|
168
|
+
const key = Symbol('nonexistent');
|
|
169
|
+
expect(() => container.get(key)).toThrow();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should throw error when getting non-existent binding', () => {
|
|
173
|
+
const nonExistentKey = Symbol('non-existent');
|
|
174
|
+
expect(() => container.get(nonExistentKey)).toThrow();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should handle invalid service identifier', () => {
|
|
178
|
+
expect(() => container.get({} as unknown as symbol)).toThrow();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PublicAssetsPath test suite
|
|
3
|
+
*
|
|
4
|
+
* Coverage:
|
|
5
|
+
* 1. constructor - Default and custom prefix initialization
|
|
6
|
+
* 2. getPath - Simple path concatenation with prefix
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from 'vitest';
|
|
10
|
+
import { PublicAssetsPath } from '@/base/cases/PublicAssetsPath';
|
|
11
|
+
import { routerPrefix } from '@config/common';
|
|
12
|
+
|
|
13
|
+
describe('PublicAssetsPath', () => {
|
|
14
|
+
describe('constructor', () => {
|
|
15
|
+
it('should initialize with default prefix from config', () => {
|
|
16
|
+
const publicPath = new PublicAssetsPath();
|
|
17
|
+
expect(publicPath.getPath('test')).toBe(`${routerPrefix}/test`);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should initialize with custom prefix', () => {
|
|
21
|
+
const customPrefix = '/custom';
|
|
22
|
+
const publicPath = new PublicAssetsPath(customPrefix);
|
|
23
|
+
expect(publicPath.getPath('test')).toBe('/custom/test');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should initialize with empty prefix', () => {
|
|
27
|
+
const publicPath = new PublicAssetsPath('');
|
|
28
|
+
expect(publicPath.getPath('test')).toBe('/test');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('getPath', () => {
|
|
33
|
+
it('should concatenate prefix with path', () => {
|
|
34
|
+
const publicPath = new PublicAssetsPath('/prefix');
|
|
35
|
+
expect(publicPath.getPath('test')).toBe('/prefix/test');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should handle empty path', () => {
|
|
39
|
+
const publicPath = new PublicAssetsPath('/prefix');
|
|
40
|
+
expect(publicPath.getPath('')).toBe('/prefix/');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should preserve the input path as is', () => {
|
|
44
|
+
const publicPath = new PublicAssetsPath('/prefix');
|
|
45
|
+
const paths = [
|
|
46
|
+
'simple/path',
|
|
47
|
+
'/path/with/leading/slash',
|
|
48
|
+
'path/with/trailing/slash/',
|
|
49
|
+
'path with spaces',
|
|
50
|
+
'path/with/unicode/字符',
|
|
51
|
+
'path?with=query#and-hash'
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
paths.forEach((path) => {
|
|
55
|
+
expect(publicPath.getPath(path)).toBe(
|
|
56
|
+
`/prefix/${path.replace(/^\//, '')}`
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RequestLogger test suite
|
|
3
|
+
*
|
|
4
|
+
* Coverage:
|
|
5
|
+
* 1. constructor - Logger injection
|
|
6
|
+
* 2. onBefore - Request initialization logging
|
|
7
|
+
* 3. onSuccess - Successful request logging
|
|
8
|
+
* 4. onError - Error handling and logging
|
|
9
|
+
* 5. loggerError - Error formatting and logging
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
13
|
+
import { RequestLogger } from '@/base/cases/RequestLogger';
|
|
14
|
+
import { MockLogger } from '../../../__mocks__/MockLogger';
|
|
15
|
+
import type {
|
|
16
|
+
ExecutorContext,
|
|
17
|
+
RequestAdapterFetchConfig,
|
|
18
|
+
RequestAdapterResponse
|
|
19
|
+
} from '@qlover/fe-corekit';
|
|
20
|
+
import type {
|
|
21
|
+
ApiCatchPluginConfig,
|
|
22
|
+
ApiCatchPluginResponse
|
|
23
|
+
} from '@qlover/corekit-bridge';
|
|
24
|
+
|
|
25
|
+
describe('RequestLogger', () => {
|
|
26
|
+
let logger: MockLogger;
|
|
27
|
+
let requestLogger: RequestLogger;
|
|
28
|
+
let mockDate: Date;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
logger = new MockLogger();
|
|
32
|
+
requestLogger = new RequestLogger(logger);
|
|
33
|
+
mockDate = new Date('2024-01-01T12:00:00');
|
|
34
|
+
vi.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('plugin properties', () => {
|
|
38
|
+
it('should have correct plugin name', () => {
|
|
39
|
+
expect(requestLogger.pluginName).toBe('RequestLogger');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should have logger instance', () => {
|
|
43
|
+
expect(requestLogger.logger).toBe(logger);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('onBefore', () => {
|
|
48
|
+
it('should log request details before execution', () => {
|
|
49
|
+
const context: ExecutorContext<RequestAdapterFetchConfig<unknown>> = {
|
|
50
|
+
parameters: {
|
|
51
|
+
method: 'GET',
|
|
52
|
+
url: 'https://api.example.com/data',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' }
|
|
54
|
+
},
|
|
55
|
+
returnValue: undefined,
|
|
56
|
+
hooksRuntimes: {}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
requestLogger.onBefore(context);
|
|
60
|
+
|
|
61
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
62
|
+
`%c[Request before]%c [${mockDate.toLocaleString()}] GET https://api.example.com/data`,
|
|
63
|
+
'color: #0ff;',
|
|
64
|
+
'color: inherit;',
|
|
65
|
+
context.parameters
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('onSuccess', () => {
|
|
71
|
+
it('should log successful response', async () => {
|
|
72
|
+
const response = { data: { id: 1 }, status: 200 };
|
|
73
|
+
const context: ExecutorContext<
|
|
74
|
+
RequestAdapterFetchConfig & ApiCatchPluginConfig
|
|
75
|
+
> = {
|
|
76
|
+
parameters: {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
url: 'https://api.example.com/create',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' }
|
|
80
|
+
},
|
|
81
|
+
returnValue: response,
|
|
82
|
+
hooksRuntimes: {}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
await requestLogger.onSuccess(context);
|
|
86
|
+
|
|
87
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
88
|
+
`%c[Request success]%c [${mockDate.toLocaleString()}] POST https://api.example.com/create`,
|
|
89
|
+
'color: #0f0;',
|
|
90
|
+
'color: inherit;',
|
|
91
|
+
response
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle API catch plugin error', async () => {
|
|
96
|
+
const apiError = {
|
|
97
|
+
id: 'API_ERROR',
|
|
98
|
+
message: 'API Error',
|
|
99
|
+
name: 'ExecutorError'
|
|
100
|
+
};
|
|
101
|
+
const response: RequestAdapterResponse & ApiCatchPluginResponse = {
|
|
102
|
+
data: null,
|
|
103
|
+
status: 400,
|
|
104
|
+
statusText: 'Bad Request',
|
|
105
|
+
headers: {},
|
|
106
|
+
config: {
|
|
107
|
+
method: 'GET',
|
|
108
|
+
url: 'https://api.example.com/error',
|
|
109
|
+
headers: {}
|
|
110
|
+
},
|
|
111
|
+
response: new Response(),
|
|
112
|
+
apiCatchResult: apiError
|
|
113
|
+
};
|
|
114
|
+
const context: ExecutorContext<
|
|
115
|
+
RequestAdapterFetchConfig & ApiCatchPluginConfig
|
|
116
|
+
> = {
|
|
117
|
+
parameters: {
|
|
118
|
+
method: 'GET',
|
|
119
|
+
url: 'https://api.example.com/error',
|
|
120
|
+
headers: {}
|
|
121
|
+
},
|
|
122
|
+
returnValue: response,
|
|
123
|
+
hooksRuntimes: {}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
await requestLogger.onSuccess(context);
|
|
127
|
+
|
|
128
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
129
|
+
`%c[Request error]%c [${mockDate.toLocaleString()}] GET https://api.example.com/error`,
|
|
130
|
+
'color: #f00;',
|
|
131
|
+
'color: inherit;',
|
|
132
|
+
apiError
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('onError', () => {
|
|
138
|
+
it('should log request error', () => {
|
|
139
|
+
const error = new Error('Network error');
|
|
140
|
+
const context: ExecutorContext<RequestAdapterFetchConfig> = {
|
|
141
|
+
parameters: {
|
|
142
|
+
method: 'PUT',
|
|
143
|
+
url: 'https://api.example.com/update',
|
|
144
|
+
headers: {}
|
|
145
|
+
},
|
|
146
|
+
error,
|
|
147
|
+
returnValue: undefined,
|
|
148
|
+
hooksRuntimes: {}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
requestLogger.onError(context);
|
|
152
|
+
|
|
153
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
154
|
+
`%c[Request error]%c [${mockDate.toLocaleString()}] PUT https://api.example.com/update`,
|
|
155
|
+
'color: #f00;',
|
|
156
|
+
'color: inherit;',
|
|
157
|
+
error
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('loggerError', () => {
|
|
163
|
+
it('should format and log error details', () => {
|
|
164
|
+
const config: RequestAdapterFetchConfig = {
|
|
165
|
+
method: 'DELETE',
|
|
166
|
+
url: 'https://api.example.com/delete',
|
|
167
|
+
headers: {}
|
|
168
|
+
};
|
|
169
|
+
const error = new Error('Operation failed');
|
|
170
|
+
|
|
171
|
+
requestLogger.loggerError(config, error);
|
|
172
|
+
|
|
173
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
174
|
+
`%c[Request error]%c [${mockDate.toLocaleString()}] DELETE https://api.example.com/delete`,
|
|
175
|
+
'color: #f00;',
|
|
176
|
+
'color: inherit;',
|
|
177
|
+
error
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should handle non-Error objects', () => {
|
|
182
|
+
const config: RequestAdapterFetchConfig = {
|
|
183
|
+
method: 'GET',
|
|
184
|
+
url: 'https://api.example.com/data',
|
|
185
|
+
headers: {}
|
|
186
|
+
};
|
|
187
|
+
const error = { code: 404, message: 'Not found' };
|
|
188
|
+
|
|
189
|
+
requestLogger.loggerError(config, error);
|
|
190
|
+
|
|
191
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
192
|
+
`%c[Request error]%c [${mockDate.toLocaleString()}] GET https://api.example.com/data`,
|
|
193
|
+
'color: #f00;',
|
|
194
|
+
'color: inherit;',
|
|
195
|
+
error
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|