@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
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
一个现代化的 React 前端项目模板,集成了多项实用功能和最佳实践。
|
|
4
4
|
|
|
5
|
+
[English](./README.en.md)
|
|
6
|
+
|
|
5
7
|
## 🌟 特性亮点
|
|
6
8
|
|
|
7
9
|
- 🚀 基于 Vite 的快速开发体验
|
|
@@ -64,6 +66,29 @@ pnpm build
|
|
|
64
66
|
pnpm test
|
|
65
67
|
```
|
|
66
68
|
|
|
69
|
+
## 📚 文档指南
|
|
70
|
+
|
|
71
|
+
项目提供了详细的开发文档,涵盖了所有主要功能和最佳实践:
|
|
72
|
+
|
|
73
|
+
### 基础文档
|
|
74
|
+
- [项目概述](./docs/zh/index.md) - 项目整体介绍和快速开始指南
|
|
75
|
+
- [项目结构](./docs/zh/project-structure.md) - 详细的项目目录结构说明
|
|
76
|
+
- [开发指南](./docs/zh/development-guide.md) - 项目开发规范和最佳实践
|
|
77
|
+
- [环境配置](./docs/zh/env.md) - 环境变量和配置管理说明
|
|
78
|
+
- [全局配置](./docs/zh/global.md) - 应用全局配置和设置说明
|
|
79
|
+
|
|
80
|
+
### 核心功能
|
|
81
|
+
- [启动流程](./docs/zh/bootstrap.md) - 应用启动流程和生命周期管理
|
|
82
|
+
- [IOC容器](./docs/zh/ioc.md) - 依赖注入系统的使用说明
|
|
83
|
+
- [路由管理](./docs/zh/router.md) - 路由配置和页面导航说明
|
|
84
|
+
- [状态管理](./docs/zh/store.md) - 应用状态管理方案说明
|
|
85
|
+
- [请求处理](./docs/zh/request.md) - API 请求处理机制说明
|
|
86
|
+
|
|
87
|
+
### 功能扩展
|
|
88
|
+
- [国际化](./docs/zh/i18n.md) - 多语言支持和翻译管理
|
|
89
|
+
- [主题系统](./docs/zh/theme.md) - 主题配置和暗色模式支持
|
|
90
|
+
- [TypeScript指南](./docs/zh/typescript-guide.md) - TypeScript 使用规范和最佳实践
|
|
91
|
+
|
|
67
92
|
## 🔨 核心功能
|
|
68
93
|
|
|
69
94
|
### IOC 容器
|
|
@@ -72,15 +97,15 @@ pnpm test
|
|
|
72
97
|
- 支持服务自动注册和依赖管理
|
|
73
98
|
- 提供完整的类型推导
|
|
74
99
|
|
|
75
|
-
###
|
|
100
|
+
### 环境配置
|
|
76
101
|
|
|
77
102
|
[vite 环境变量和模式](https://cn.vite.dev/guide/env-and-mode#env-variables-and-modes)
|
|
78
103
|
|
|
79
104
|
`vite dev` 默认 NODE_ENV 表示为 development, 他会加载可能的 `.env[mode]` 文件, 比如 .env.local -> .env
|
|
80
105
|
|
|
81
|
-
`vite build
|
|
106
|
+
`vite build` 默认 NODE_ENV 表示为 production, 他会加载可能的 `.env[mode]` 文件, 比如 .env.production -> .env
|
|
82
107
|
|
|
83
|
-
|
|
108
|
+
Node.js NODE_ENV 只支持 development,production,test
|
|
84
109
|
|
|
85
110
|
这个和 vite 中的 mode 是完全不一样的, mode 可以根据 `--mode` 指定不同模式,来加载不同的 env 配置
|
|
86
111
|
|
|
@@ -99,127 +124,6 @@ vite dev --mode local # 加载 .env.local
|
|
|
99
124
|
- URL 路径语言检测和切换
|
|
100
125
|
- 内置语言切换组件
|
|
101
126
|
|
|
102
|
-
#### 国际化配置
|
|
103
|
-
|
|
104
|
-
项目使用 `@brain-toolkit/ts2locales` 插件从 TypeScript 注释自动生成国际化资源:
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
// config/Identifier/I18n.ts
|
|
108
|
-
export const Messages = {
|
|
109
|
-
/**
|
|
110
|
-
* @description Home page welcome message
|
|
111
|
-
* @localZh 欢迎来到主页
|
|
112
|
-
* @localEn Welcome to the home page
|
|
113
|
-
*/
|
|
114
|
-
HOME_WELCOME: 'home.welcome',
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* @description Get started button text
|
|
118
|
-
* @localZh 开始使用
|
|
119
|
-
* @localEn Get Started
|
|
120
|
-
*/
|
|
121
|
-
HOME_GET_STARTED: 'home.get_started'
|
|
122
|
-
};
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
Vite 插件配置:
|
|
126
|
-
|
|
127
|
-
```typescript
|
|
128
|
-
// vite.config.ts
|
|
129
|
-
import ts2Locales from '@brain-toolkit/ts2locales/vite';
|
|
130
|
-
import i18nConfig from './config/i18n';
|
|
131
|
-
|
|
132
|
-
export default defineConfig({
|
|
133
|
-
plugins: [
|
|
134
|
-
ts2Locales({
|
|
135
|
-
locales: i18nConfig.supportedLngs,
|
|
136
|
-
options: [
|
|
137
|
-
{
|
|
138
|
-
source: './config/Identifier/error.ts',
|
|
139
|
-
target: './public/locales/{{lng}}/common.json'
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
source: './config/Identifier/index.ts',
|
|
143
|
-
target: './public/locales/{{lng}}/common.json'
|
|
144
|
-
}
|
|
145
|
-
]
|
|
146
|
-
})
|
|
147
|
-
]
|
|
148
|
-
});
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
#### 国际化服务
|
|
152
|
-
|
|
153
|
-
项目提供了 `I18nService` 用于管理国际化状态和语言切换:
|
|
154
|
-
|
|
155
|
-
```typescript
|
|
156
|
-
import { I18nService } from '@/base/services/I18nService';
|
|
157
|
-
|
|
158
|
-
// 获取当前语言
|
|
159
|
-
const currentLang = I18nService.getCurrentLanguage();
|
|
160
|
-
|
|
161
|
-
// 切换语言
|
|
162
|
-
await i18nService.changeLanguage('zh');
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
#### 在组件中使用
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
import { useBaseRoutePage } from '@/uikit/contexts/BaseRouteContext';
|
|
169
|
-
import * as i18nKeys from '@config/Identifier';
|
|
170
|
-
|
|
171
|
-
function MyComponent() {
|
|
172
|
-
const { t } = useBaseRoutePage();
|
|
173
|
-
|
|
174
|
-
return (
|
|
175
|
-
<div>
|
|
176
|
-
<h1>{t(i18nKeys.HOME_WELCOME)}</h1>
|
|
177
|
-
<button>{t(i18nKeys.HOME_GET_STARTED)}</button>
|
|
178
|
-
</div>
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
#### 语言切换组件
|
|
184
|
-
|
|
185
|
-
项目提供了开箱即用的语言切换组件:
|
|
186
|
-
|
|
187
|
-
```typescript
|
|
188
|
-
import LanguageSwitcher from '@/uikit/components/LanguageSwitcher';
|
|
189
|
-
|
|
190
|
-
function Header() {
|
|
191
|
-
return (
|
|
192
|
-
<header>
|
|
193
|
-
<LanguageSwitcher />
|
|
194
|
-
</header>
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
#### 最佳实践
|
|
200
|
-
|
|
201
|
-
1. 国际化标识符管理:
|
|
202
|
-
|
|
203
|
-
- 在 `config/Identifier/index.ts` 中集中管理UI文本
|
|
204
|
-
- 在 `config/Identifier/error.ts` 中集中管理错误信息
|
|
205
|
-
- 使用有意义的 key 命名(如:'page.home.title')
|
|
206
|
-
|
|
207
|
-
2. TypeScript 注释规范:
|
|
208
|
-
|
|
209
|
-
- 使用 `@description` 描述文本用途
|
|
210
|
-
- 使用 `@localZh` 定义中文文本
|
|
211
|
-
- 使用 `@localEn` 定义英文文本
|
|
212
|
-
|
|
213
|
-
3. 路由配置:
|
|
214
|
-
|
|
215
|
-
- 使用 `LocaleLink` 组件进行页面跳转
|
|
216
|
-
- URL 格式:`/{lang}/path`(如:'/zh/about')
|
|
217
|
-
|
|
218
|
-
4. 组件开发:
|
|
219
|
-
- 使用 `useBaseRoutePage` hook 获取翻译函数
|
|
220
|
-
- 从 `@config/Identifier` 引入国际化 key
|
|
221
|
-
- 避免硬编码文本,始终使用国际化 key
|
|
222
|
-
|
|
223
127
|
### 主题系统
|
|
224
128
|
|
|
225
129
|
- 基于 Tailwind CSS 的主题配置
|
|
@@ -266,106 +170,6 @@ export class RequestController extends StoreInterface<RequestControllerState> {
|
|
|
266
170
|
}
|
|
267
171
|
```
|
|
268
172
|
|
|
269
|
-
#### 请求插件系统
|
|
270
|
-
|
|
271
|
-
项目内置多个实用的请求插件:
|
|
272
|
-
|
|
273
|
-
1. `FetchURLPlugin`: URL 处理和构建
|
|
274
|
-
2. `RequestCommonPlugin`: 通用请求配置
|
|
275
|
-
3. `ApiMockPlugin`: Mock 数据支持
|
|
276
|
-
4. `FetchAbortPlugin`: 请求中断控制
|
|
277
|
-
5. `RequestLogger`: 请求日志记录
|
|
278
|
-
6. `ApiCatchPlugin`: 统一错误处理
|
|
279
|
-
|
|
280
|
-
使用示例:
|
|
281
|
-
|
|
282
|
-
```typescript
|
|
283
|
-
// 配置请求适配器
|
|
284
|
-
const adapter = new RequestAdapter({
|
|
285
|
-
plugins: [
|
|
286
|
-
new FetchURLPlugin(),
|
|
287
|
-
new RequestCommonPlugin(),
|
|
288
|
-
new ApiMockPlugin(),
|
|
289
|
-
new FetchAbortPlugin(),
|
|
290
|
-
new RequestLogger()
|
|
291
|
-
]
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// 发起请求
|
|
295
|
-
const response = await adapter.request({
|
|
296
|
-
url: '/api/users',
|
|
297
|
-
method: 'GET',
|
|
298
|
-
requestId: 'uniqueId', // 用于中断请求
|
|
299
|
-
mock: true // 启用 mock 数据
|
|
300
|
-
});
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
#### 请求调度器
|
|
304
|
-
|
|
305
|
-
使用 `RequestScheduler` 管理复杂的请求流程:
|
|
306
|
-
|
|
307
|
-
```typescript
|
|
308
|
-
export class FeApi extends RequestScheduler<RequestConfig> {
|
|
309
|
-
async getIpInfo() {
|
|
310
|
-
return this.request<void, IpInfo>({
|
|
311
|
-
url: '/api/ip',
|
|
312
|
-
method: 'GET'
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
#### Mock 数据支持
|
|
319
|
-
|
|
320
|
-
项目支持灵活的 Mock 数据配置:
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
// 配置 Mock 数据
|
|
324
|
-
const mockConfig = {
|
|
325
|
-
'/api/users': {
|
|
326
|
-
GET: () => ({
|
|
327
|
-
code: 200,
|
|
328
|
-
data: {
|
|
329
|
-
id: 1,
|
|
330
|
-
name: 'John Doe'
|
|
331
|
-
}
|
|
332
|
-
})
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
// 在请求中使用
|
|
337
|
-
const response = await api.request({
|
|
338
|
-
url: '/api/users',
|
|
339
|
-
method: 'GET',
|
|
340
|
-
mock: true // 启用 mock
|
|
341
|
-
});
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
#### 最佳实践
|
|
345
|
-
|
|
346
|
-
1. API 定义:
|
|
347
|
-
|
|
348
|
-
- 在 `src/base/apis` 中集中管理 API 定义
|
|
349
|
-
- 使用 TypeScript 接口定义请求和响应类型
|
|
350
|
-
- 遵循 RESTful API 设计规范
|
|
351
|
-
|
|
352
|
-
2. 错误处理:
|
|
353
|
-
|
|
354
|
-
- 使用 `ApiCatchPlugin` 统一处理错误
|
|
355
|
-
- 定义清晰的错误类型和错误码
|
|
356
|
-
- 提供友好的错误提示
|
|
357
|
-
|
|
358
|
-
3. 请求状态管理:
|
|
359
|
-
|
|
360
|
-
- 使用 `SliceStore` 管理请求状态
|
|
361
|
-
- 处理加载、成功、错误等状态
|
|
362
|
-
- 实现请求防抖和节流
|
|
363
|
-
|
|
364
|
-
4. Mock 数据:
|
|
365
|
-
- 提供合理的 Mock 数据结构
|
|
366
|
-
- 支持动态 Mock 数据生成
|
|
367
|
-
- 便于本地开发和测试
|
|
368
|
-
|
|
369
173
|
### 控制器模式
|
|
370
174
|
|
|
371
175
|
提供多个开箱即用的控制器:
|
|
@@ -376,7 +180,7 @@ const response = await api.request({
|
|
|
376
180
|
- UserController
|
|
377
181
|
- ThemeController
|
|
378
182
|
|
|
379
|
-
##
|
|
183
|
+
## 🛠️ 开发指南
|
|
380
184
|
|
|
381
185
|
### API 开发规范
|
|
382
186
|
|
|
@@ -389,12 +193,6 @@ const response = await api.request({
|
|
|
389
193
|
2. 更新 `config/app.router.json`
|
|
390
194
|
3. 添加对应的控制器(如需要)
|
|
391
195
|
|
|
392
|
-
### 国际化开发
|
|
393
|
-
|
|
394
|
-
1. 在 `public/locales` 添加翻译文件
|
|
395
|
-
2. 使用 `useTranslation` hook 进行调用
|
|
396
|
-
3. 支持按需加载语言包
|
|
397
|
-
|
|
398
196
|
### 构建优化
|
|
399
197
|
|
|
400
198
|
项目使用 Vite 进行构建,并进行了以下优化:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import { I18nService } from '@/base/services/I18nService';
|
|
3
|
+
|
|
4
|
+
export class MockI18nService extends I18nService {
|
|
5
|
+
constructor() {
|
|
6
|
+
super('/');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
t = vi.fn((key: string) => key);
|
|
10
|
+
changeLanguage = vi.fn();
|
|
11
|
+
changeLoading = vi.fn();
|
|
12
|
+
onBefore = vi.fn();
|
|
13
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { EnvConfigInterface } from '@qlover/corekit-bridge';
|
|
2
|
+
import { name, version } from '../../package.json';
|
|
3
|
+
|
|
4
|
+
export class MockAppConfig implements EnvConfigInterface {
|
|
5
|
+
appName = name;
|
|
6
|
+
|
|
7
|
+
appVersion = version;
|
|
8
|
+
|
|
9
|
+
env: string = import.meta.env.MODE;
|
|
10
|
+
|
|
11
|
+
userTokenStorageKey = '__fe_user_token__';
|
|
12
|
+
|
|
13
|
+
userInfoStorageKey = '__fe_user_info__';
|
|
14
|
+
|
|
15
|
+
openAiModels = [
|
|
16
|
+
'gpt-4o-mini',
|
|
17
|
+
'gpt-3.5-turbo',
|
|
18
|
+
'gpt-3.5-turbo-2',
|
|
19
|
+
'gpt-4',
|
|
20
|
+
'gpt-4-32k'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
openAiBaseUrl = '';
|
|
24
|
+
|
|
25
|
+
openAiToken = '';
|
|
26
|
+
|
|
27
|
+
openAiTokenPrefix = '';
|
|
28
|
+
|
|
29
|
+
openAiRequireToken = true;
|
|
30
|
+
|
|
31
|
+
loginUser = '';
|
|
32
|
+
|
|
33
|
+
loginPassword = '';
|
|
34
|
+
|
|
35
|
+
feApiBaseUrl = '';
|
|
36
|
+
|
|
37
|
+
userApiBaseUrl = '';
|
|
38
|
+
|
|
39
|
+
aiApiBaseUrl = 'https://api.openai.com/v1';
|
|
40
|
+
|
|
41
|
+
aiApiToken = '';
|
|
42
|
+
|
|
43
|
+
aiApiTokenPrefix = 'Bearer';
|
|
44
|
+
|
|
45
|
+
aiApiRequireToken = true;
|
|
46
|
+
|
|
47
|
+
bootHref = '';
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { InteractionHubInterface } from '@/base/port/InteractionHubInterface';
|
|
2
|
+
import { AntdStaticApiInterface } from '@brain-toolkit/antd-theme-override/react';
|
|
3
|
+
import { vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
export class MockDialogHandler
|
|
6
|
+
implements InteractionHubInterface, AntdStaticApiInterface
|
|
7
|
+
{
|
|
8
|
+
setMessage = vi.fn();
|
|
9
|
+
setModal = vi.fn();
|
|
10
|
+
setNotification = vi.fn();
|
|
11
|
+
success = vi.fn();
|
|
12
|
+
error = vi.fn();
|
|
13
|
+
info = vi.fn();
|
|
14
|
+
warn = vi.fn();
|
|
15
|
+
confirm = vi.fn();
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LoggerInterface } from '@qlover/logger';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
export class MockLogger implements LoggerInterface {
|
|
5
|
+
info = vi.fn();
|
|
6
|
+
error = vi.fn();
|
|
7
|
+
debug = vi.fn();
|
|
8
|
+
warn = vi.fn();
|
|
9
|
+
trace = vi.fn();
|
|
10
|
+
log = vi.fn();
|
|
11
|
+
fatal = vi.fn();
|
|
12
|
+
addAppender = vi.fn();
|
|
13
|
+
context = vi.fn();
|
|
14
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import { MockLogger } from './MockLogger';
|
|
3
|
+
import { MockAppConfig } from './MockAppConfit';
|
|
4
|
+
import { MockDialogHandler } from './MockDialogHandler';
|
|
5
|
+
|
|
6
|
+
export function createMockGlobals() {
|
|
7
|
+
const mockLogger = new MockLogger();
|
|
8
|
+
const mockAppConfig = new MockAppConfig();
|
|
9
|
+
const mockDialogHandler = new MockDialogHandler();
|
|
10
|
+
|
|
11
|
+
// Mock JSON serializer
|
|
12
|
+
const mockJSON = {
|
|
13
|
+
serialize: vi.fn((data) => JSON.stringify(data)),
|
|
14
|
+
deserialize: vi.fn((data) => JSON.parse(data)),
|
|
15
|
+
stringify: vi.fn((data) => JSON.stringify(data)),
|
|
16
|
+
parse: vi.fn((data) => JSON.parse(data))
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
const storageOptions: Record<string, any> = {
|
|
21
|
+
localStorage: {},
|
|
22
|
+
cookieStorage: {},
|
|
23
|
+
localStorageEncrypt: {}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Mock localStorage with actual storage simulation
|
|
27
|
+
const mockLocalStorageData = storageOptions.localStorage ?? {};
|
|
28
|
+
const mockLocalStorage = {
|
|
29
|
+
get: vi.fn((key: string) => mockLocalStorageData[key]),
|
|
30
|
+
set: vi.fn((key: string, value: unknown) => {
|
|
31
|
+
mockLocalStorageData[key] = value;
|
|
32
|
+
}),
|
|
33
|
+
getItem: vi.fn((key: string) => mockLocalStorageData[key]),
|
|
34
|
+
setItem: vi.fn((key: string, value: unknown) => {
|
|
35
|
+
mockLocalStorageData[key] = value;
|
|
36
|
+
}),
|
|
37
|
+
remove: vi.fn((key: string) => {
|
|
38
|
+
delete mockLocalStorageData[key];
|
|
39
|
+
}),
|
|
40
|
+
removeItem: vi.fn((key: string) => {
|
|
41
|
+
delete mockLocalStorageData[key];
|
|
42
|
+
}),
|
|
43
|
+
clear: vi.fn(() => {
|
|
44
|
+
Object.keys(mockLocalStorageData).forEach((key) => {
|
|
45
|
+
delete mockLocalStorageData[key];
|
|
46
|
+
});
|
|
47
|
+
}),
|
|
48
|
+
has: vi.fn((key: string) => key in mockLocalStorageData),
|
|
49
|
+
keys: vi.fn(() => Object.keys(mockLocalStorageData)),
|
|
50
|
+
size: vi.fn(() => Object.keys(mockLocalStorageData).length)
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Mock localStorageEncrypt (same as localStorage for now)
|
|
54
|
+
const mockLocalStorageEncrypt = { ...mockLocalStorage };
|
|
55
|
+
|
|
56
|
+
// Mock cookieStorage
|
|
57
|
+
const mockCookieStorageData = storageOptions.cookieStorage ?? {};
|
|
58
|
+
const mockCookieStorage = {
|
|
59
|
+
get: vi.fn((key: string) => mockCookieStorageData[key]),
|
|
60
|
+
set: vi.fn((key: string, value: unknown, _options?: unknown) => {
|
|
61
|
+
mockCookieStorageData[key] = value;
|
|
62
|
+
}),
|
|
63
|
+
getItem: vi.fn((key: string) => mockCookieStorageData[key]),
|
|
64
|
+
setItem: vi.fn((key: string, value: unknown, _options?: unknown) => {
|
|
65
|
+
mockCookieStorageData[key] = value;
|
|
66
|
+
}),
|
|
67
|
+
remove: vi.fn((key: string) => {
|
|
68
|
+
delete mockCookieStorageData[key];
|
|
69
|
+
}),
|
|
70
|
+
removeItem: vi.fn((key: string) => {
|
|
71
|
+
delete mockCookieStorageData[key];
|
|
72
|
+
}),
|
|
73
|
+
clear: vi.fn(() => {
|
|
74
|
+
Object.keys(mockCookieStorageData).forEach((key) => {
|
|
75
|
+
delete mockCookieStorageData[key];
|
|
76
|
+
});
|
|
77
|
+
}),
|
|
78
|
+
has: vi.fn((key: string) => key in mockCookieStorageData),
|
|
79
|
+
keys: vi.fn(() => Object.keys(mockCookieStorageData)),
|
|
80
|
+
size: vi.fn(() => Object.keys(mockCookieStorageData).length)
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
logger: mockLogger,
|
|
85
|
+
appConfig: mockAppConfig,
|
|
86
|
+
dialogHandler: mockDialogHandler,
|
|
87
|
+
JSON: mockJSON,
|
|
88
|
+
localStorage: mockLocalStorage,
|
|
89
|
+
localStorageEncrypt: mockLocalStorageEncrypt,
|
|
90
|
+
cookieStorage: mockCookieStorage
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createMockGlobals } from '../__mocks__/createMockGlobals';
|
|
2
|
+
|
|
3
|
+
// 设置测试环境
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
// 清理DOM
|
|
6
|
+
document.body.innerHTML = '';
|
|
7
|
+
|
|
8
|
+
// 创建root元素(如果不存在)
|
|
9
|
+
if (!document.getElementById('root')) {
|
|
10
|
+
const rootElement = document.createElement('div');
|
|
11
|
+
rootElement.id = 'root';
|
|
12
|
+
document.body.appendChild(rootElement);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
// 清理所有mock
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// 全局测试配置
|
|
22
|
+
global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
|
23
|
+
observe: vi.fn(),
|
|
24
|
+
unobserve: vi.fn(),
|
|
25
|
+
disconnect: vi.fn()
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Mock window.matchMedia
|
|
29
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
30
|
+
writable: true,
|
|
31
|
+
value: vi.fn().mockImplementation((query) => ({
|
|
32
|
+
matches: false,
|
|
33
|
+
media: query,
|
|
34
|
+
onchange: null,
|
|
35
|
+
addListener: vi.fn(), // deprecated
|
|
36
|
+
removeListener: vi.fn(), // deprecated
|
|
37
|
+
addEventListener: vi.fn(),
|
|
38
|
+
removeEventListener: vi.fn(),
|
|
39
|
+
dispatchEvent: vi.fn()
|
|
40
|
+
}))
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Mock IntersectionObserver
|
|
44
|
+
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
|
|
45
|
+
observe: vi.fn(),
|
|
46
|
+
unobserve: vi.fn(),
|
|
47
|
+
disconnect: vi.fn()
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
// Mock globals
|
|
51
|
+
vi.mock('@/core/globals', () => createMockGlobals());
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import App from '@/App';
|
|
3
|
+
import { routerPrefix } from '@config/common';
|
|
4
|
+
|
|
5
|
+
// Mock the dependencies
|
|
6
|
+
vi.mock('react-router-dom', async () => {
|
|
7
|
+
const actual = await vi.importActual('react-router-dom');
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
createBrowserRouter: vi.fn(() => ({
|
|
11
|
+
routes: [],
|
|
12
|
+
basename: routerPrefix
|
|
13
|
+
})),
|
|
14
|
+
RouterProvider: vi.fn(({ router }) => (
|
|
15
|
+
<div data-testid="router-provider">
|
|
16
|
+
<div data-testid="router-basename">{router.basename}</div>
|
|
17
|
+
<div data-testid="router-routes-count">
|
|
18
|
+
{router.routes?.length || 0}
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
))
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
vi.mock('@brain-toolkit/antd-theme-override/react', () => ({
|
|
26
|
+
AntdThemeProvider: vi.fn(({ children, theme }) => (
|
|
27
|
+
<div data-testid="antd-theme-provider">
|
|
28
|
+
<div data-testid="theme-key">{theme?.cssVar?.key}</div>
|
|
29
|
+
<div data-testid="theme-prefix">{theme?.cssVar?.prefix}</div>
|
|
30
|
+
{children}
|
|
31
|
+
</div>
|
|
32
|
+
))
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock('@/core/IOC', () => ({
|
|
36
|
+
IOC: vi.fn((service) => {
|
|
37
|
+
if (service === 'RouteService') {
|
|
38
|
+
return {
|
|
39
|
+
routes: [
|
|
40
|
+
{ path: '/', component: 'HomePage' },
|
|
41
|
+
{ path: '/about', component: 'AboutPage' }
|
|
42
|
+
]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (service === 'DialogHandler') {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
return {};
|
|
49
|
+
})
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
vi.mock('@/uikit/hooks/useStore', () => ({
|
|
53
|
+
useStore: vi.fn((service, selector) => {
|
|
54
|
+
if (service && selector) {
|
|
55
|
+
return [
|
|
56
|
+
{ path: '/', component: 'HomePage' },
|
|
57
|
+
{ path: '/about', component: 'AboutPage' }
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
})
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
vi.mock('@/base/cases/RouterLoader', () => ({
|
|
65
|
+
RouterLoader: vi.fn().mockImplementation(() => ({
|
|
66
|
+
toRoute: vi.fn((route) => ({
|
|
67
|
+
path: route.path,
|
|
68
|
+
element: <div data-testid={`route-${route.path}`}>{route.component}</div>
|
|
69
|
+
}))
|
|
70
|
+
}))
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
// Mock the CSS import
|
|
74
|
+
vi.mock('@/styles/css/index.css', () => ({}));
|
|
75
|
+
|
|
76
|
+
describe('App Component', () => {
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
vi.clearAllMocks();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should render without crashing', () => {
|
|
82
|
+
render(<App />);
|
|
83
|
+
|
|
84
|
+
// Check if the main app structure is rendered
|
|
85
|
+
expect(screen.getByTestId('antd-theme-provider')).toBeDefined();
|
|
86
|
+
expect(screen.getByTestId('router-provider')).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should configure theme provider correctly', () => {
|
|
90
|
+
render(<App />);
|
|
91
|
+
|
|
92
|
+
// Check theme configuration
|
|
93
|
+
expect(screen.getByTestId('theme-key').textContent).toBe('fe-theme');
|
|
94
|
+
expect(screen.getByTestId('theme-prefix').textContent).toBe('fe');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should configure router with correct basename', () => {
|
|
98
|
+
render(<App />);
|
|
99
|
+
|
|
100
|
+
// Check router configuration
|
|
101
|
+
expect(screen.getByTestId('router-basename')).toBeDefined();
|
|
102
|
+
expect(screen.getByTestId('router-basename').textContent).toBe(
|
|
103
|
+
routerPrefix
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should render router provider with routes', () => {
|
|
108
|
+
render(<App />);
|
|
109
|
+
|
|
110
|
+
// Check that router provider is rendered
|
|
111
|
+
expect(screen.getByTestId('router-provider')).toBeDefined();
|
|
112
|
+
expect(screen.getByTestId('router-routes-count')).toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should have proper component structure', () => {
|
|
116
|
+
const { container } = render(<App />);
|
|
117
|
+
|
|
118
|
+
// Check the overall structure
|
|
119
|
+
expect(container.firstChild).toBeDefined();
|
|
120
|
+
|
|
121
|
+
// Check that theme provider wraps router provider
|
|
122
|
+
const themeProvider = screen.getByTestId('antd-theme-provider');
|
|
123
|
+
const routerProvider = screen.getByTestId('router-provider');
|
|
124
|
+
|
|
125
|
+
expect(themeProvider.contains(routerProvider)).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle empty routes gracefully', async () => {
|
|
129
|
+
// Mock useStore to return empty routes
|
|
130
|
+
const { useStore } = await import('@/uikit/hooks/useStore');
|
|
131
|
+
vi.mocked(useStore).mockReturnValueOnce([]);
|
|
132
|
+
|
|
133
|
+
render(<App />);
|
|
134
|
+
|
|
135
|
+
// Should still render without crashing
|
|
136
|
+
expect(screen.getByTestId('antd-theme-provider')).toBeDefined();
|
|
137
|
+
expect(screen.getByTestId('router-provider')).toBeDefined();
|
|
138
|
+
});
|
|
139
|
+
});
|