@qlover/create-app 0.6.2 → 0.7.0
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 +53 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/react-app/README.en.md +257 -0
- package/dist/templates/react-app/README.md +29 -231
- 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/bootstrap.md +562 -0
- package/dist/templates/react-app/docs/en/development-guide.md +523 -0
- package/dist/templates/react-app/docs/en/env.md +482 -0
- package/dist/templates/react-app/docs/en/global.md +509 -0
- package/dist/templates/react-app/docs/en/i18n.md +268 -0
- package/dist/templates/react-app/docs/en/index.md +173 -0
- package/dist/templates/react-app/docs/en/ioc.md +424 -0
- package/dist/templates/react-app/docs/en/project-structure.md +434 -0
- package/dist/templates/react-app/docs/en/request.md +425 -0
- package/dist/templates/react-app/docs/en/router.md +404 -0
- package/dist/templates/react-app/docs/en/store.md +321 -0
- package/dist/templates/react-app/docs/en/test-guide.md +782 -0
- package/dist/templates/react-app/docs/en/theme.md +424 -0
- package/dist/templates/react-app/docs/en/typescript-guide.md +473 -0
- package/dist/templates/react-app/docs/zh/bootstrap.md +7 -0
- package/dist/templates/react-app/docs/zh/development-guide.md +523 -0
- package/dist/templates/react-app/docs/zh/env.md +24 -25
- package/dist/templates/react-app/docs/zh/global.md +28 -27
- package/dist/templates/react-app/docs/zh/i18n.md +268 -0
- package/dist/templates/react-app/docs/zh/index.md +173 -0
- package/dist/templates/react-app/docs/zh/ioc.md +44 -32
- package/dist/templates/react-app/docs/zh/project-structure.md +434 -0
- package/dist/templates/react-app/docs/zh/request.md +429 -0
- package/dist/templates/react-app/docs/zh/router.md +408 -0
- package/dist/templates/react-app/docs/zh/store.md +321 -0
- package/dist/templates/react-app/docs/zh/test-guide.md +782 -0
- package/dist/templates/react-app/docs/zh/theme.md +424 -0
- package/dist/templates/react-app/docs/zh/typescript-guide.md +473 -0
- package/dist/templates/react-app/package.json +9 -20
- 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/styles/css/antd-themes/dark.css +3 -1
- package/dist/templates/react-app/src/styles/css/antd-themes/index.css +1 -1
- package/dist/templates/react-app/src/styles/css/antd-themes/pink.css +6 -1
- package/dist/templates/react-app/src/styles/css/page.css +1 -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 +1 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/**
|
|
3
|
+
* RequestStatusCatcher test suite
|
|
4
|
+
*
|
|
5
|
+
* Coverage:
|
|
6
|
+
* 1. constructor - Logger injection
|
|
7
|
+
* 2. default - Default handler behavior
|
|
8
|
+
* 3. handler - Dynamic status code handling
|
|
9
|
+
* 4. case200 - Success case handling
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
13
|
+
import { RequestStatusCatcher } from '@/base/cases/RequestStatusCatcher';
|
|
14
|
+
import { MockLogger } from '../../../__mocks__/MockLogger';
|
|
15
|
+
import type { RequestAdapterResponse } from '@qlover/fe-corekit';
|
|
16
|
+
|
|
17
|
+
describe('RequestStatusCatcher', () => {
|
|
18
|
+
let logger: MockLogger;
|
|
19
|
+
let statusCatcher: RequestStatusCatcher;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
logger = new MockLogger();
|
|
23
|
+
statusCatcher = new RequestStatusCatcher(logger);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('constructor', () => {
|
|
27
|
+
it('should be properly initialized with logger', () => {
|
|
28
|
+
expect(statusCatcher).toBeInstanceOf(RequestStatusCatcher);
|
|
29
|
+
expect(logger).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('default handler', () => {
|
|
34
|
+
it('should log warning for unhandled status codes', () => {
|
|
35
|
+
const context: RequestAdapterResponse = {
|
|
36
|
+
data: null,
|
|
37
|
+
status: 418,
|
|
38
|
+
statusText: "I'm a teapot",
|
|
39
|
+
headers: {},
|
|
40
|
+
config: {
|
|
41
|
+
method: 'GET',
|
|
42
|
+
url: 'https://api.example.com/coffee',
|
|
43
|
+
headers: {}
|
|
44
|
+
},
|
|
45
|
+
response: new Response()
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
statusCatcher.default(context);
|
|
49
|
+
|
|
50
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
51
|
+
'RequestStatusCatcher default handler',
|
|
52
|
+
context
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('handler', () => {
|
|
58
|
+
it('should call case200 for 200 status code', () => {
|
|
59
|
+
const context: RequestAdapterResponse = {
|
|
60
|
+
data: { success: true },
|
|
61
|
+
status: 200,
|
|
62
|
+
statusText: 'OK',
|
|
63
|
+
headers: {},
|
|
64
|
+
config: {
|
|
65
|
+
method: 'GET',
|
|
66
|
+
url: 'https://api.example.com/data',
|
|
67
|
+
headers: {}
|
|
68
|
+
},
|
|
69
|
+
response: new Response()
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
statusCatcher.handler(context);
|
|
73
|
+
// case200 is currently empty, so we just verify it doesn't throw
|
|
74
|
+
expect(true).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should call default handler for unknown status codes', () => {
|
|
78
|
+
const context: RequestAdapterResponse = {
|
|
79
|
+
data: null,
|
|
80
|
+
status: 499,
|
|
81
|
+
statusText: 'Unknown Status',
|
|
82
|
+
headers: {},
|
|
83
|
+
config: {
|
|
84
|
+
method: 'GET',
|
|
85
|
+
url: 'https://api.example.com/unknown',
|
|
86
|
+
headers: {}
|
|
87
|
+
},
|
|
88
|
+
response: new Response()
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
statusCatcher.handler(context);
|
|
92
|
+
|
|
93
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
94
|
+
'RequestStatusCatcher default handler',
|
|
95
|
+
context
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle null or undefined status', () => {
|
|
100
|
+
const context = {
|
|
101
|
+
data: null,
|
|
102
|
+
status: undefined,
|
|
103
|
+
statusText: 'No Status',
|
|
104
|
+
headers: {},
|
|
105
|
+
config: {
|
|
106
|
+
method: 'GET',
|
|
107
|
+
url: 'https://api.example.com/nostatus',
|
|
108
|
+
headers: {}
|
|
109
|
+
},
|
|
110
|
+
response: new Response()
|
|
111
|
+
} as unknown as RequestAdapterResponse;
|
|
112
|
+
|
|
113
|
+
statusCatcher.handler(context);
|
|
114
|
+
|
|
115
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
116
|
+
'RequestStatusCatcher default handler',
|
|
117
|
+
context
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('case200', () => {
|
|
123
|
+
it('should handle successful responses', () => {
|
|
124
|
+
const context: RequestAdapterResponse = {
|
|
125
|
+
data: { success: true },
|
|
126
|
+
status: 200,
|
|
127
|
+
statusText: 'OK',
|
|
128
|
+
headers: {},
|
|
129
|
+
config: {
|
|
130
|
+
method: 'GET',
|
|
131
|
+
url: 'https://api.example.com/success',
|
|
132
|
+
headers: {}
|
|
133
|
+
},
|
|
134
|
+
response: new Response()
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
statusCatcher.case200(context);
|
|
138
|
+
// Currently case200 is empty, so we just verify it doesn't throw
|
|
139
|
+
expect(true).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('dynamic handler resolution', () => {
|
|
144
|
+
it('should dynamically resolve and call status handlers', () => {
|
|
145
|
+
// Add a custom handler for testing
|
|
146
|
+
const customHandler = vi.fn();
|
|
147
|
+
(statusCatcher as any).case418 = customHandler;
|
|
148
|
+
|
|
149
|
+
const context: RequestAdapterResponse = {
|
|
150
|
+
data: null,
|
|
151
|
+
status: 418,
|
|
152
|
+
statusText: "I'm a teapot",
|
|
153
|
+
headers: {},
|
|
154
|
+
config: {
|
|
155
|
+
method: 'GET',
|
|
156
|
+
url: 'https://api.example.com/teapot',
|
|
157
|
+
headers: {}
|
|
158
|
+
},
|
|
159
|
+
response: new Response()
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
statusCatcher.handler(context);
|
|
163
|
+
|
|
164
|
+
expect(customHandler).toHaveBeenCalledWith(context);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle non-function properties gracefully', () => {
|
|
168
|
+
// Add a non-function property
|
|
169
|
+
(statusCatcher as any).case500 = 'not a function';
|
|
170
|
+
|
|
171
|
+
const context: RequestAdapterResponse = {
|
|
172
|
+
data: null,
|
|
173
|
+
status: 500,
|
|
174
|
+
statusText: 'Internal Server Error',
|
|
175
|
+
headers: {},
|
|
176
|
+
config: {
|
|
177
|
+
method: 'GET',
|
|
178
|
+
url: 'https://api.example.com/error',
|
|
179
|
+
headers: {}
|
|
180
|
+
},
|
|
181
|
+
response: new Response()
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
statusCatcher.handler(context);
|
|
185
|
+
|
|
186
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
187
|
+
'RequestStatusCatcher default handler',
|
|
188
|
+
context
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/**
|
|
3
|
+
* RouterLoader test suite
|
|
4
|
+
*
|
|
5
|
+
* Coverage:
|
|
6
|
+
* 1. constructor - Initialization and validation
|
|
7
|
+
* 2. getComponentMaps - Component mapping management
|
|
8
|
+
* 3. getComponent - Component resolution
|
|
9
|
+
* 4. toRoute - Route transformation
|
|
10
|
+
* 5. error handling - Invalid configurations
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
14
|
+
import { RouterLoader } from '@/base/cases/RouterLoader';
|
|
15
|
+
import type {
|
|
16
|
+
RouteConfigValue,
|
|
17
|
+
RouterLoaderOptions
|
|
18
|
+
} from '@/base/cases/RouterLoader';
|
|
19
|
+
import { createElement } from 'react';
|
|
20
|
+
|
|
21
|
+
describe('RouterLoader', () => {
|
|
22
|
+
// Mock components and render function
|
|
23
|
+
const mockHomeComponent = vi.fn(() => createElement('div', { id: 'home' }));
|
|
24
|
+
const mockAboutComponent = vi.fn(() => createElement('div', { id: 'about' }));
|
|
25
|
+
const mockNotFoundComponent = vi.fn(() =>
|
|
26
|
+
createElement('div', { id: '404' })
|
|
27
|
+
);
|
|
28
|
+
const mockRender = vi.fn((route) =>
|
|
29
|
+
createElement('div', null, route.element())
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
let defaultOptions: RouterLoaderOptions;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
defaultOptions = {
|
|
36
|
+
routes: [
|
|
37
|
+
{
|
|
38
|
+
path: '/',
|
|
39
|
+
element: 'Home',
|
|
40
|
+
children: [
|
|
41
|
+
{
|
|
42
|
+
path: 'about',
|
|
43
|
+
element: 'About'
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
componentMaps: {
|
|
49
|
+
Home: mockHomeComponent,
|
|
50
|
+
About: mockAboutComponent,
|
|
51
|
+
'404': mockNotFoundComponent
|
|
52
|
+
},
|
|
53
|
+
render: mockRender
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
vi.clearAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('constructor', () => {
|
|
60
|
+
it('should create instance with valid options', () => {
|
|
61
|
+
const loader = new RouterLoader(defaultOptions);
|
|
62
|
+
expect(loader).toBeInstanceOf(RouterLoader);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should throw error when render is not provided', () => {
|
|
66
|
+
const invalidOptions = { ...defaultOptions, render: undefined };
|
|
67
|
+
expect(() => new RouterLoader(invalidOptions as any)).toThrow(
|
|
68
|
+
'RouterLoader render is required'
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('getComponentMaps', () => {
|
|
74
|
+
it('should return component maps', () => {
|
|
75
|
+
const loader = new RouterLoader(defaultOptions);
|
|
76
|
+
const maps = loader.getComponentMaps();
|
|
77
|
+
expect(maps).toEqual(defaultOptions.componentMaps);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return empty object when no component maps provided', () => {
|
|
81
|
+
const options = { ...defaultOptions, componentMaps: undefined };
|
|
82
|
+
const loader = new RouterLoader(options);
|
|
83
|
+
expect(loader.getComponentMaps()).toEqual({});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('getComponent', () => {
|
|
88
|
+
let loader: RouterLoader;
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
loader = new RouterLoader(defaultOptions);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return component for valid element', () => {
|
|
95
|
+
const component = loader.getComponent('Home');
|
|
96
|
+
expect(component).toBe(mockHomeComponent);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should throw error for non-existent component', () => {
|
|
100
|
+
expect(() => loader.getComponent('NonExistent')).toThrow(
|
|
101
|
+
'Component not found: NonExistent'
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should return 404 component for empty element', () => {
|
|
106
|
+
const component = loader.getComponent('404');
|
|
107
|
+
expect(component).toBe(mockNotFoundComponent);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('toRoute', () => {
|
|
112
|
+
let loader: RouterLoader;
|
|
113
|
+
let consoleWarnSpy: any;
|
|
114
|
+
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
loader = new RouterLoader(defaultOptions);
|
|
117
|
+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
consoleWarnSpy.mockRestore();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should transform route configuration correctly', () => {
|
|
125
|
+
const route: RouteConfigValue = {
|
|
126
|
+
path: '/test',
|
|
127
|
+
element: 'Home'
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const result = loader.toRoute(route);
|
|
131
|
+
|
|
132
|
+
expect(result.path).toBe('/test');
|
|
133
|
+
expect(result.element).toBeTruthy();
|
|
134
|
+
expect(mockRender).toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle nested routes', () => {
|
|
138
|
+
const route: RouteConfigValue = {
|
|
139
|
+
path: '/parent',
|
|
140
|
+
element: 'Home',
|
|
141
|
+
children: [
|
|
142
|
+
{
|
|
143
|
+
path: 'child',
|
|
144
|
+
element: 'About'
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const result = loader.toRoute(route);
|
|
150
|
+
|
|
151
|
+
expect(result.path).toBe('/parent');
|
|
152
|
+
expect(result.children).toHaveLength(1);
|
|
153
|
+
expect(result.children![0].path).toBe('child');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should warn and use 404 component for invalid element', () => {
|
|
157
|
+
const route: RouteConfigValue = {
|
|
158
|
+
path: '/invalid',
|
|
159
|
+
element: undefined
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const result = loader.toRoute(route);
|
|
163
|
+
|
|
164
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
165
|
+
'Invalid route, path is: /invalid, element is: undefined'
|
|
166
|
+
);
|
|
167
|
+
expect(result.element).toBeTruthy();
|
|
168
|
+
expect(mockRender).toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should preserve route metadata', () => {
|
|
172
|
+
const route: RouteConfigValue = {
|
|
173
|
+
path: '/meta',
|
|
174
|
+
element: 'Home',
|
|
175
|
+
meta: {
|
|
176
|
+
title: 'Test Page',
|
|
177
|
+
description: 'A test page',
|
|
178
|
+
category: 'main'
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const result = loader.toRoute(route);
|
|
183
|
+
|
|
184
|
+
expect(result.path).toBe('/meta');
|
|
185
|
+
expect((result as any).meta).toEqual({
|
|
186
|
+
title: 'Test Page',
|
|
187
|
+
description: 'A test page',
|
|
188
|
+
category: 'main'
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should handle empty children array', () => {
|
|
193
|
+
const route: RouteConfigValue = {
|
|
194
|
+
path: '/empty',
|
|
195
|
+
element: 'Home',
|
|
196
|
+
children: []
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const result = loader.toRoute(route);
|
|
200
|
+
|
|
201
|
+
expect(result.children).toEqual([]);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('error handling', () => {
|
|
206
|
+
it('should handle missing component maps gracefully', () => {
|
|
207
|
+
const options = {
|
|
208
|
+
routes: defaultOptions.routes,
|
|
209
|
+
render: mockRender
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const loader = new RouterLoader(options);
|
|
213
|
+
expect(() => loader.getComponent('Home')).toThrow(
|
|
214
|
+
'Component not found: Home'
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should handle invalid route configurations', () => {
|
|
219
|
+
const loader = new RouterLoader(defaultOptions);
|
|
220
|
+
const invalidRoute = {
|
|
221
|
+
path: '/invalid'
|
|
222
|
+
} as RouteConfigValue;
|
|
223
|
+
|
|
224
|
+
const consoleWarnSpy = vi
|
|
225
|
+
.spyOn(console, 'warn')
|
|
226
|
+
.mockImplementation(() => {});
|
|
227
|
+
loader.toRoute(invalidRoute);
|
|
228
|
+
|
|
229
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
230
|
+
'Invalid route, path is: /invalid, element is: undefined'
|
|
231
|
+
);
|
|
232
|
+
consoleWarnSpy.mockRestore();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* I18nService test suite
|
|
3
|
+
*
|
|
4
|
+
* Coverage:
|
|
5
|
+
* 1. constructor - State initialization
|
|
6
|
+
* 2. onBefore - Plugin initialization
|
|
7
|
+
* 3. changeLanguage - Language switching
|
|
8
|
+
* 4. changeLoading - Loading state management
|
|
9
|
+
* 5. static methods - Language validation and retrieval
|
|
10
|
+
* 6. translation - Key translation with parameters
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
14
|
+
import { I18nService, I18nServiceState } from '@/base/services/I18nService';
|
|
15
|
+
import i18n from 'i18next';
|
|
16
|
+
import i18nConfig from '@config/i18n';
|
|
17
|
+
|
|
18
|
+
const { supportedLngs, fallbackLng } = i18nConfig;
|
|
19
|
+
|
|
20
|
+
// Mock i18next
|
|
21
|
+
vi.mock('i18next', () => ({
|
|
22
|
+
default: {
|
|
23
|
+
use: vi.fn().mockReturnThis(),
|
|
24
|
+
init: vi.fn(),
|
|
25
|
+
t: vi.fn(),
|
|
26
|
+
changeLanguage: vi.fn(),
|
|
27
|
+
language: 'en',
|
|
28
|
+
services: {
|
|
29
|
+
languageDetector: {
|
|
30
|
+
addDetector: vi.fn()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// Mock react-i18next
|
|
37
|
+
vi.mock('react-i18next', () => ({
|
|
38
|
+
initReactI18next: {}
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Mock i18next-browser-languagedetector
|
|
42
|
+
vi.mock('i18next-browser-languagedetector', () => ({
|
|
43
|
+
default: {}
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Mock i18next-http-backend
|
|
47
|
+
vi.mock('i18next-http-backend', () => ({
|
|
48
|
+
default: {}
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
describe('I18nService', () => {
|
|
52
|
+
let service: I18nService;
|
|
53
|
+
const testPath = '/en/test/path';
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
service = new I18nService(testPath);
|
|
57
|
+
vi.clearAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
vi.clearAllMocks();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('constructor', () => {
|
|
65
|
+
it('should initialize with correct state', () => {
|
|
66
|
+
expect(service.state).toBeInstanceOf(I18nServiceState);
|
|
67
|
+
expect(service.state.language).toBe('en');
|
|
68
|
+
expect(service.state.loading).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should have correct plugin name', () => {
|
|
72
|
+
expect(service.pluginName).toBe('I18nService');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should initialize with correct selector', () => {
|
|
76
|
+
const state = new I18nServiceState('en');
|
|
77
|
+
expect(service.selector.loading(state)).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('onBefore', () => {
|
|
82
|
+
it('should initialize i18n with correct configuration', () => {
|
|
83
|
+
service.onBefore();
|
|
84
|
+
|
|
85
|
+
expect(i18n.use).toHaveBeenCalledTimes(3);
|
|
86
|
+
expect(i18n.init).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({
|
|
88
|
+
debug: false,
|
|
89
|
+
detection: {
|
|
90
|
+
order: ['pathLanguageDetector', 'navigator'],
|
|
91
|
+
caches: []
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should add custom language detector', () => {
|
|
98
|
+
service.onBefore();
|
|
99
|
+
|
|
100
|
+
expect(i18n.services.languageDetector.addDetector).toHaveBeenCalledWith(
|
|
101
|
+
expect.objectContaining({
|
|
102
|
+
name: 'pathLanguageDetector',
|
|
103
|
+
lookup: expect.any(Function),
|
|
104
|
+
cacheUserLanguage: expect.any(Function)
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should detect language from path correctly', () => {
|
|
110
|
+
service.onBefore();
|
|
111
|
+
const detector = vi.mocked(i18n.services.languageDetector.addDetector)
|
|
112
|
+
.mock.calls[0][0];
|
|
113
|
+
const language = detector.lookup();
|
|
114
|
+
expect(language).toBe('en');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return fallback language for invalid path', () => {
|
|
118
|
+
const invalidService = new I18nService('/invalid/path');
|
|
119
|
+
invalidService.onBefore();
|
|
120
|
+
const detector = vi.mocked(i18n.services.languageDetector.addDetector)
|
|
121
|
+
.mock.calls[0][0];
|
|
122
|
+
const language = detector.lookup();
|
|
123
|
+
expect(language).toBe(fallbackLng);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('changeLanguage', () => {
|
|
128
|
+
it('should change language using i18n', async () => {
|
|
129
|
+
await service.changeLanguage('en');
|
|
130
|
+
expect(i18n.changeLanguage).toHaveBeenCalledWith('en');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should handle language change error', async () => {
|
|
134
|
+
vi.mocked(i18n.changeLanguage).mockRejectedValueOnce(
|
|
135
|
+
new Error('Change failed')
|
|
136
|
+
);
|
|
137
|
+
await expect(service.changeLanguage('en')).rejects.toThrow(
|
|
138
|
+
'Change failed'
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('changeLoading', () => {
|
|
144
|
+
it('should update loading state', () => {
|
|
145
|
+
const emitSpy = vi.spyOn(service, 'emit');
|
|
146
|
+
service.changeLoading(true);
|
|
147
|
+
expect(emitSpy).toHaveBeenCalledWith({
|
|
148
|
+
language: 'en',
|
|
149
|
+
loading: true
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should preserve current state when changing loading', () => {
|
|
154
|
+
const currentState = service.state;
|
|
155
|
+
service.changeLoading(true);
|
|
156
|
+
expect(service.state).toEqual({
|
|
157
|
+
...currentState,
|
|
158
|
+
loading: true
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('static methods', () => {
|
|
164
|
+
describe('getCurrentLanguage', () => {
|
|
165
|
+
it('should return current i18n language', () => {
|
|
166
|
+
expect(I18nService.getCurrentLanguage()).toBe('en');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('isValidLanguage', () => {
|
|
171
|
+
it('should validate supported languages', () => {
|
|
172
|
+
for (const lang of supportedLngs) {
|
|
173
|
+
expect(I18nService.isValidLanguage(lang)).toBe(true);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should reject unsupported languages', () => {
|
|
178
|
+
expect(I18nService.isValidLanguage('invalid')).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('translation', () => {
|
|
184
|
+
beforeEach(() => {
|
|
185
|
+
// @ts-expect-error
|
|
186
|
+
vi.mocked(i18n.t).mockImplementation((key) => `translated_${key}`);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should translate key without parameters', () => {
|
|
190
|
+
const key = 'test.key';
|
|
191
|
+
const result = service.t(key);
|
|
192
|
+
expect(result).toBe('translated_test.key');
|
|
193
|
+
expect(i18n.t).toHaveBeenCalledWith(key, {
|
|
194
|
+
lng: 'en'
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should translate key with parameters', () => {
|
|
199
|
+
const key = 'test.key';
|
|
200
|
+
const params = { value: 'test' };
|
|
201
|
+
const result = service.t(key, params);
|
|
202
|
+
expect(result).toBe('translated_test.key');
|
|
203
|
+
expect(i18n.t).toHaveBeenCalledWith(key, {
|
|
204
|
+
lng: 'en',
|
|
205
|
+
...params
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should return key when translation is empty', () => {
|
|
210
|
+
vi.mocked(i18n.t).mockReturnValueOnce('');
|
|
211
|
+
const key = 'test.key';
|
|
212
|
+
const result = service.t(key);
|
|
213
|
+
expect(result).toBe(key);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should return key when translation equals key', () => {
|
|
217
|
+
// @ts-expect-error
|
|
218
|
+
vi.mocked(i18n.t).mockImplementation((key) => key);
|
|
219
|
+
const key = 'test.key';
|
|
220
|
+
const result = service.t(key);
|
|
221
|
+
expect(result).toBe(key);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|