@h-ai/cli 0.1.0-alpha.53 → 0.1.0-alpha.55
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/README.md +18 -0
- package/dist/{chunk-U4PA632G.js → chunk-FVDXJYMO.js} +34 -67
- package/dist/chunk-FVDXJYMO.js.map +1 -0
- package/dist/cli-config-templates-WGN2I23J.js +5 -0
- package/dist/{cli-config-templates-GSXKLGU5.js.map → cli-config-templates-WGN2I23J.js.map} +1 -1
- package/dist/index.js +174 -105
- package/dist/index.js.map +1 -1
- package/dist/{src-QNEJJ3BV.js → src-6ZBWDBUB.js} +127 -87
- package/dist/{src-QNEJJ3BV.js.map → src-6ZBWDBUB.js.map} +1 -1
- package/package.json +13 -4
- package/templates/apps/api/apps/{{projectName}}-contract/package.json.hbs +1 -0
- package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -0
- package/templates/apps/api/package.json.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +1 -0
- package/templates/apps/fullstack/package.json.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +1 -0
- package/templates/base/eslint.config.js.hbs +1 -0
- package/templates/base/package.json.hbs +3 -1
- package/templates/skills/AGENTS.md +8 -22
- package/templates/skills/CLAUDE.md +3 -16
- package/templates/skills/bridges/admin/AGENTS.md +3 -16
- package/templates/skills/bridges/admin/CLAUDE.md +3 -11
- package/templates/skills/bridges/admin/copilot-instructions.md +2 -39
- package/templates/skills/bridges/api/AGENTS.md +5 -18
- package/templates/skills/bridges/api/CLAUDE.md +3 -11
- package/templates/skills/bridges/api/copilot-instructions.md +3 -38
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +24 -59
- package/templates/skills/bridges/fullstack/CLAUDE.md.hbs +1 -15
- package/templates/skills/bridges/fullstack/copilot-instructions.md.hbs +5 -45
- package/templates/skills/bridges/h5/AGENTS.md +4 -17
- package/templates/skills/bridges/h5/CLAUDE.md +3 -11
- package/templates/skills/bridges/h5/copilot-instructions.md +2 -37
- package/templates/skills/bridges/mobile-app/AGENTS.md +5 -18
- package/templates/skills/bridges/mobile-app/CLAUDE.md +3 -11
- package/templates/skills/bridges/mobile-app/copilot-instructions.md +2 -38
- package/templates/skills/bridges/website/AGENTS.md +4 -17
- package/templates/skills/bridges/website/CLAUDE.md +3 -11
- package/templates/skills/bridges/website/copilot-instructions.md +2 -37
- package/templates/skills/copilot-instructions.md +2 -36
- package/templates/skills/hai-ai/SKILL.md +35 -389
- package/templates/skills/hai-ai/guide.md +383 -0
- package/templates/skills/hai-ai/reference.md +6 -2
- package/templates/skills/hai-api-client/SKILL.md +6 -6
- package/templates/skills/hai-api-contract/SKILL.md +10 -25
- package/templates/skills/hai-app-create/SKILL.md +29 -388
- package/templates/skills/hai-app-create/reference.md +350 -0
- package/templates/skills/hai-app-review/SKILL.md +15 -23
- package/templates/skills/hai-app-tests/SKILL.md +29 -494
- package/templates/skills/hai-app-tests/reference.md +442 -0
- package/templates/skills/hai-audit/SKILL.md +6 -6
- package/templates/skills/hai-build/SKILL.md +19 -330
- package/templates/skills/hai-build/reference.md +326 -0
- package/templates/skills/hai-cache/SKILL.md +28 -31
- package/templates/skills/hai-capacitor/SKILL.md +19 -364
- package/templates/skills/hai-capacitor/reference.md +352 -0
- package/templates/skills/hai-ci/SKILL.md +17 -20
- package/templates/skills/hai-core/SKILL.md +25 -564
- package/templates/skills/hai-core/reference.md +533 -0
- package/templates/skills/hai-crypto/SKILL.md +8 -20
- package/templates/skills/hai-datapipe/SKILL.md +9 -23
- package/templates/skills/hai-deploy/SKILL.md +18 -24
- package/templates/skills/hai-framework-sync/SKILL.md +8 -20
- package/templates/skills/hai-iam/SKILL.md +27 -365
- package/templates/skills/hai-iam/reference.md +343 -0
- package/templates/skills/hai-kit/SKILL.md +6 -6
- package/templates/skills/hai-payment/SKILL.md +6 -26
- package/templates/skills/hai-pr-review/SKILL.md +6 -22
- package/templates/skills/hai-reach/SKILL.md +19 -317
- package/templates/skills/hai-reach/reference.md +303 -0
- package/templates/skills/hai-reldb/SKILL.md +19 -472
- package/templates/skills/hai-reldb/reference.md +452 -0
- package/templates/skills/hai-scheduler/SKILL.md +18 -6
- package/templates/skills/hai-serv/SKILL.md +27 -759
- package/templates/skills/hai-serv/reference.md +736 -0
- package/templates/skills/hai-storage/SKILL.md +7 -25
- package/templates/skills/hai-ui/SKILL.md +29 -621
- package/templates/skills/hai-ui/reference.md +598 -0
- package/templates/skills/hai-vecdb/SKILL.md +18 -303
- package/templates/skills/hai-vecdb/reference.md +291 -0
- package/dist/chunk-U4PA632G.js.map +0 -1
- package/dist/cli-config-templates-GSXKLGU5.js +0 -5
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# hai-app-tests — 详细用法
|
|
2
|
+
|
|
3
|
+
从 [SKILL.md](SKILL.md) 选择主题后读取对应小节。代码片段展示 API 用法;省略的业务变量、schema、依赖与 return 上下文需由应用补齐。
|
|
4
|
+
|
|
5
|
+
> 面向 AI 助手的 TDD 测试指南。**测试先行**:先写测试定义行为,再写实现让测试通过。涵盖 Vitest 单元测试与 Playwright E2E 测试。
|
|
6
|
+
|
|
7
|
+
## 适用场景
|
|
8
|
+
|
|
9
|
+
- 以 TDD 方式开发新功能(先写测试 → 确认失败 → 再实现 → 确认通过)
|
|
10
|
+
- 为 `$lib/server/services/` 编写服务层单元测试
|
|
11
|
+
- 为 API 端点编写 E2E 测试(Playwright)
|
|
12
|
+
- 为页面交互编写 UI E2E 测试(Playwright)
|
|
13
|
+
- 为工具函数、Schema 校验编写单元测试
|
|
14
|
+
- 需要提升测试覆盖率
|
|
15
|
+
|
|
16
|
+
## TDD 核心工作流(必须严格遵循)
|
|
17
|
+
|
|
18
|
+
### 阶段一:需求分析(Red 前准备)
|
|
19
|
+
|
|
20
|
+
1. **理解需求**:明确功能的输入、输出、边界条件、错误场景
|
|
21
|
+
2. **拆分测试点**:将需求转化为可验证的测试用例清单
|
|
22
|
+
3. **确定测试类型**:
|
|
23
|
+
- **单元测试(Vitest)**:服务层、工具函数、Schema 校验、HaiResult 处理
|
|
24
|
+
- **E2E 测试(Playwright)**:API 端点、页面交互、认证流程、跨模块集成
|
|
25
|
+
|
|
26
|
+
### 阶段二:Red — 编写失败的测试
|
|
27
|
+
|
|
28
|
+
1. **编写单元测试**:覆盖正常路径、边界路径、权限路径、错误路径
|
|
29
|
+
2. **编写 E2E 测试**:覆盖 API 端点和关键用户交互流程
|
|
30
|
+
3. **运行测试确认新增目标用例因预期行为缺失而失败(既有回归保持通过)**:
|
|
31
|
+
```bash
|
|
32
|
+
pnpm --filter <app-name> test # 单元测试 — 新增目标用例应 FAIL,既有回归应 PASS
|
|
33
|
+
pnpm --filter <app-name> test:e2e # E2E 测试 — 新增目标用例应 FAIL,既有回归应 PASS
|
|
34
|
+
```
|
|
35
|
+
4. 新增用例若已通过,先确认现有实现是否已满足需求;仅在用例没有覆盖目标行为时修正
|
|
36
|
+
|
|
37
|
+
### 阶段三:Green — 最小实现
|
|
38
|
+
|
|
39
|
+
1. 参照 `hai-app-create` 技能编写实现代码
|
|
40
|
+
2. 每实现一个功能点,立即运行对应测试确认通过
|
|
41
|
+
3. 只写让测试通过的最小代码,不做过度设计
|
|
42
|
+
4. 运行全部测试确认通过:
|
|
43
|
+
```bash
|
|
44
|
+
pnpm --filter <app-name> test # 单元测试 — 应全部 PASS
|
|
45
|
+
pnpm --filter <app-name> test:e2e # E2E 测试 — 应全部 PASS
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 阶段四:Refactor — 重构优化
|
|
49
|
+
|
|
50
|
+
1. 参照 `hai-app-review` 技能审查代码规范
|
|
51
|
+
2. 重构后再次运行所有测试确认仍然通过
|
|
52
|
+
3. 运行质量门禁:`pnpm typecheck && pnpm lint && pnpm test`
|
|
53
|
+
|
|
54
|
+
## 测试目录约定
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
tests/ # 单元测试(Vitest)
|
|
58
|
+
services/
|
|
59
|
+
user.test.ts # 服务层测试
|
|
60
|
+
article.test.ts
|
|
61
|
+
schemas/
|
|
62
|
+
user-schema.test.ts # Schema 校验测试
|
|
63
|
+
utils/
|
|
64
|
+
helper.test.ts # 工具函数测试
|
|
65
|
+
e2e/ # E2E 测试(Playwright)
|
|
66
|
+
helpers.ts # E2E 共用工具函数
|
|
67
|
+
health.spec.ts # 健康检查 API
|
|
68
|
+
auth-api.spec.ts # 认证 API 测试
|
|
69
|
+
auth-ui.spec.ts # 认证页面 UI 测试
|
|
70
|
+
users-api.spec.ts # 用户管理 API 测试
|
|
71
|
+
users-ui.spec.ts # 用户管理页面 UI 测试
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 核心规则
|
|
75
|
+
|
|
76
|
+
1. **测试先行**:先写测试定义预期行为,再写实现代码。
|
|
77
|
+
2. **统一入口**:通过框架模块公共 API 测试(`reldb.sql`、`iam.auth`),不直接调用内部实现。
|
|
78
|
+
3. **从实际场景出发**:测试用例验证真实业务行为,不做形式覆盖。
|
|
79
|
+
4. **先审查后修复**:测试不通过时先审查业务逻辑是否有问题,不直接修改测试迎合通过。
|
|
80
|
+
5. **Mock 外部依赖**:单元测试中,数据库、缓存、网络请求使用 mock,不直连真实服务。
|
|
81
|
+
6. **E2E 真实环境**:E2E 测试运行在完整应用环境中,不使用 mock。
|
|
82
|
+
|
|
83
|
+
## 覆盖范围
|
|
84
|
+
|
|
85
|
+
每个功能至少覆盖以下路径(单元测试 + E2E 按需):
|
|
86
|
+
|
|
87
|
+
| 路径 | 说明 | 单元测试示例 | E2E 测试示例 |
|
|
88
|
+
| -------- | ------------------------ | ------------------ | ----------------------- |
|
|
89
|
+
| 正常路径 | 核心功能可用 | 创建用户成功 | POST /api/users → 201 |
|
|
90
|
+
| 边界路径 | 非法输入、空值、格式错误 | 空名称、超长字符串 | 空表单提交 → 校验提示 |
|
|
91
|
+
| 权限路径 | 未认证、无权限 | 未登录调用服务 | 未登录访问 → 401/重定向 |
|
|
92
|
+
| 错误路径 | 服务异常、数据库错误 | DB 失败时的 HaiResult | 服务不可用 → 错误提示页 |
|
|
93
|
+
|
|
94
|
+
## 单元测试(Vitest)
|
|
95
|
+
|
|
96
|
+
### 服务层测试模板
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { reldb } from '@h-ai/reldb'
|
|
100
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
101
|
+
|
|
102
|
+
// 内存 SQLite 隔离数据;应用服务测试同样从服务的公共入口验证结果。
|
|
103
|
+
beforeAll(async () => {
|
|
104
|
+
const init = await reldb.init({ type: 'sqlite', database: ':memory:' })
|
|
105
|
+
expect(init.success).toBe(true)
|
|
106
|
+
const table = await reldb.ddl.createTable('articles', {
|
|
107
|
+
id: { type: 'TEXT', primaryKey: true },
|
|
108
|
+
title: { type: 'TEXT', notNull: true },
|
|
109
|
+
})
|
|
110
|
+
expect(table.success).toBe(true)
|
|
111
|
+
})
|
|
112
|
+
afterAll(async () => { await reldb.close() })
|
|
113
|
+
|
|
114
|
+
describe('article persistence', () => {
|
|
115
|
+
it('creates and reads the stored article', async () => {
|
|
116
|
+
const articles = reldb.crud.table<{ id: string, title: string }>({ table: 'articles', dbType: 'sqlite' })
|
|
117
|
+
const saved = await articles.create({ id: 'a1', title: 'Test' })
|
|
118
|
+
expect(saved.success).toBe(true)
|
|
119
|
+
const found = await articles.findById('a1')
|
|
120
|
+
expect(found.success).toBe(true)
|
|
121
|
+
if (found.success) expect(found.data).toEqual({ id: 'a1', title: 'Test' })
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
服务层另测权限、校验和 DB 失败透传;mock 必须沿真实注入边界或 CRUD 仓库实例配置,create 返回 ExecuteResult,不能伪造为实体。
|
|
127
|
+
|
|
128
|
+
### Schema 校验测试模板
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { CreateUserSchema } from '$lib/server/schemas/user'
|
|
132
|
+
// tests/schemas/user-schema.test.ts
|
|
133
|
+
import { describe, expect, it } from 'vitest'
|
|
134
|
+
|
|
135
|
+
describe('CreateUserSchema', () => {
|
|
136
|
+
it('should accept valid input', () => {
|
|
137
|
+
const result = CreateUserSchema.safeParse({
|
|
138
|
+
username: 'testuser',
|
|
139
|
+
email: 'test@example.com',
|
|
140
|
+
password: '12345678',
|
|
141
|
+
})
|
|
142
|
+
expect(result.success).toBe(true)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should reject short username', () => {
|
|
146
|
+
const result = CreateUserSchema.safeParse({
|
|
147
|
+
username: 'ab',
|
|
148
|
+
email: 'test@example.com',
|
|
149
|
+
password: '12345678',
|
|
150
|
+
})
|
|
151
|
+
expect(result.success).toBe(false)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should reject invalid email', () => {
|
|
155
|
+
const result = CreateUserSchema.safeParse({
|
|
156
|
+
username: 'testuser',
|
|
157
|
+
email: 'not-email',
|
|
158
|
+
password: '12345678',
|
|
159
|
+
})
|
|
160
|
+
expect(result.success).toBe(false)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## E2E 测试(Playwright)
|
|
166
|
+
|
|
167
|
+
### Playwright 配置
|
|
168
|
+
|
|
169
|
+
项目中应有 `playwright.config.ts`(由 CLI 生成):
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// playwright.config.ts
|
|
173
|
+
import process from 'node:process'
|
|
174
|
+
import { defineConfig } from '@playwright/test'
|
|
175
|
+
|
|
176
|
+
const DEFAULT_BASE_URL = 'http://localhost:4173'
|
|
177
|
+
const baseURL = process.env.BASE_URL || DEFAULT_BASE_URL
|
|
178
|
+
const previewHost = resolveHostFromBaseUrl(baseURL, 'localhost')
|
|
179
|
+
const previewPort = resolvePortFromBaseUrl(baseURL, '4173')
|
|
180
|
+
|
|
181
|
+
function resolveHostFromBaseUrl(urlString: string, fallbackHost: string): string {
|
|
182
|
+
try {
|
|
183
|
+
const url = new URL(urlString)
|
|
184
|
+
return url.hostname || fallbackHost
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return fallbackHost
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resolvePortFromBaseUrl(urlString: string, fallbackPort: string): string {
|
|
192
|
+
try {
|
|
193
|
+
const url = new URL(urlString)
|
|
194
|
+
if (url.port)
|
|
195
|
+
return url.port
|
|
196
|
+
return url.protocol === 'https:' ? '443' : '80'
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return fallbackPort
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export default defineConfig({
|
|
204
|
+
testDir: './e2e',
|
|
205
|
+
fullyParallel: false,
|
|
206
|
+
forbidOnly: !!process.env.CI,
|
|
207
|
+
retries: process.env.CI ? 2 : 0,
|
|
208
|
+
workers: 2,
|
|
209
|
+
reporter: 'list',
|
|
210
|
+
timeout: 30_000,
|
|
211
|
+
use: {
|
|
212
|
+
baseURL,
|
|
213
|
+
channel: 'chrome',
|
|
214
|
+
trace: 'on-first-retry',
|
|
215
|
+
screenshot: 'only-on-failure',
|
|
216
|
+
},
|
|
217
|
+
webServer: {
|
|
218
|
+
command: `pnpm build && pnpm preview --host ${previewHost} --port ${previewPort} --strictPort`,
|
|
219
|
+
env: { HAI_E2E: '1', BASE_URL: baseURL },
|
|
220
|
+
url: baseURL,
|
|
221
|
+
reuseExistingServer: false,
|
|
222
|
+
timeout: 180_000,
|
|
223
|
+
},
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### E2E 工具函数模板
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// e2e/helpers.ts
|
|
231
|
+
import type { APIRequestContext, Page } from '@playwright/test'
|
|
232
|
+
|
|
233
|
+
/** 生成唯一测试用户 */
|
|
234
|
+
export function uniqueUser(prefix = 'e2e') {
|
|
235
|
+
const safePrefix = (prefix.replace(/\W/g, '') || 'e2e').slice(0, 8)
|
|
236
|
+
const entropy = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`
|
|
237
|
+
const id = entropy.slice(-10)
|
|
238
|
+
const username = `${safePrefix}_${id}`.slice(0, 20)
|
|
239
|
+
return {
|
|
240
|
+
username,
|
|
241
|
+
email: `${safePrefix}_${id}@test.local`,
|
|
242
|
+
password: 'Test1234!@',
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** 通过 API 注册用户 */
|
|
247
|
+
export async function registerViaApi(
|
|
248
|
+
request: APIRequestContext,
|
|
249
|
+
user: ReturnType<typeof uniqueUser>,
|
|
250
|
+
) {
|
|
251
|
+
return request.post('/api/auth/register', {
|
|
252
|
+
data: {
|
|
253
|
+
username: user.username,
|
|
254
|
+
email: user.email,
|
|
255
|
+
password: user.password,
|
|
256
|
+
confirmPassword: user.password,
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** 注册并登录,返回已认证的 Page */
|
|
262
|
+
export async function registerAndLogin(
|
|
263
|
+
page: Page,
|
|
264
|
+
request: APIRequestContext,
|
|
265
|
+
prefix = 'e2e',
|
|
266
|
+
) {
|
|
267
|
+
const user = uniqueUser(prefix)
|
|
268
|
+
await registerViaApi(request, user)
|
|
269
|
+
await page.goto('/login')
|
|
270
|
+
await page.fill('[name="username"]', user.username)
|
|
271
|
+
await page.fill('[name="password"]', user.password)
|
|
272
|
+
await page.click('button[type="submit"]')
|
|
273
|
+
await page.waitForURL('/')
|
|
274
|
+
return user
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### API 端点 E2E 测试模板
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// e2e/articles-api.spec.ts
|
|
282
|
+
import { expect, test } from '@playwright/test'
|
|
283
|
+
import { registerAndLogin } from './helpers'
|
|
284
|
+
|
|
285
|
+
test.describe('Articles API', () => {
|
|
286
|
+
test('GET /api/articles 未认证返回 401', async ({ request }) => {
|
|
287
|
+
const response = await request.get('/api/articles')
|
|
288
|
+
expect(response.status()).toBe(401)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test('POST /api/articles 创建文章成功', async ({ page, request }) => {
|
|
292
|
+
await registerAndLogin(page, request, 'article')
|
|
293
|
+
|
|
294
|
+
const response = await page.request.post('/api/articles', {
|
|
295
|
+
data: { title: 'E2E Test Article', content: 'Test content' },
|
|
296
|
+
})
|
|
297
|
+
expect(response.status()).toBe(201)
|
|
298
|
+
|
|
299
|
+
const body = await response.json()
|
|
300
|
+
expect(body.success).toBe(true)
|
|
301
|
+
expect(body.data.title).toBe('E2E Test Article')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('POST /api/articles 空标题返回 400', async ({ page, request }) => {
|
|
305
|
+
await registerAndLogin(page, request, 'article')
|
|
306
|
+
|
|
307
|
+
const response = await page.request.post('/api/articles', {
|
|
308
|
+
data: { title: '', content: '' },
|
|
309
|
+
})
|
|
310
|
+
expect(response.status()).toBe(400)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
test('GET /api/articles 认证后获取列表', async ({ page, request }) => {
|
|
314
|
+
await registerAndLogin(page, request, 'article')
|
|
315
|
+
|
|
316
|
+
const response = await page.request.get('/api/articles')
|
|
317
|
+
expect(response.ok()).toBeTruthy()
|
|
318
|
+
|
|
319
|
+
const body = await response.json()
|
|
320
|
+
expect(body.success).toBe(true)
|
|
321
|
+
expect(Array.isArray(body.data)).toBe(true)
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### 页面 UI E2E 测试模板
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// e2e/articles-ui.spec.ts
|
|
330
|
+
import { expect, test } from '@playwright/test'
|
|
331
|
+
import { registerAndLogin } from './helpers'
|
|
332
|
+
|
|
333
|
+
test.describe('Articles Page', () => {
|
|
334
|
+
test('未登录跳转登录页', async ({ page }) => {
|
|
335
|
+
await page.goto('/admin/articles')
|
|
336
|
+
await expect(page).toHaveURL(/login/)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test('已登录显示文章列表', async ({ page, request }) => {
|
|
340
|
+
await registerAndLogin(page, request, 'artui')
|
|
341
|
+
await page.goto('/admin/articles')
|
|
342
|
+
|
|
343
|
+
await expect(page.locator('h1, h2').first()).toContainText(/文章|article/i)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test('创建文章表单提交', async ({ page, request }) => {
|
|
347
|
+
await registerAndLogin(page, request, 'artui')
|
|
348
|
+
await page.goto('/admin/articles/new')
|
|
349
|
+
|
|
350
|
+
await page.fill('[name="title"]', 'E2E Article')
|
|
351
|
+
await page.fill('[name="content"]', 'E2E content')
|
|
352
|
+
await page.click('button[type="submit"]')
|
|
353
|
+
|
|
354
|
+
// 验证跳转或成功提示
|
|
355
|
+
await expect(page.locator('.alert-success, .toast')).toBeVisible({ timeout: 5000 })
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## TDD 用例设计策略
|
|
361
|
+
|
|
362
|
+
### 从需求到测试用例的转换
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
需求:用户可以创建文章,标题必填且不超过 100 字符,需要登录后才能操作
|
|
366
|
+
|
|
367
|
+
↓ 拆分测试点
|
|
368
|
+
|
|
369
|
+
单元测试(Vitest):
|
|
370
|
+
✅ 创建文章成功返回 HaiResult.ok
|
|
371
|
+
✅ 空标题返回 HaiResult.err + VALIDATION_FAILED
|
|
372
|
+
✅ 超长标题(>100 字符)返回 HaiResult.err
|
|
373
|
+
✅ DB 异常时透传错误
|
|
374
|
+
|
|
375
|
+
Schema 测试(Vitest):
|
|
376
|
+
✅ 合法输入通过校验
|
|
377
|
+
✅ 空标题被拒绝
|
|
378
|
+
✅ 超长标题被拒绝
|
|
379
|
+
✅ 缺少必填字段被拒绝
|
|
380
|
+
|
|
381
|
+
E2E 测试(Playwright):
|
|
382
|
+
✅ POST /api/articles 未认证 → 401
|
|
383
|
+
✅ POST /api/articles 合法数据 → 201
|
|
384
|
+
✅ POST /api/articles 空标题 → 400
|
|
385
|
+
✅ 页面表单提交成功 → 跳转/提示
|
|
386
|
+
✅ 未登录访问创建页 → 重定向登录
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## 断言规范
|
|
390
|
+
|
|
391
|
+
- 始终校验 `result.success`
|
|
392
|
+
- 失败时校验 `error.code`(不只检查 `success === false`)
|
|
393
|
+
- 使用类型缩窄后访问 data 或 error
|
|
394
|
+
- E2E 断言使用 Playwright 内置 expect 匹配器
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
// ✅ 单元测试断言
|
|
398
|
+
const result = await service.create(input)
|
|
399
|
+
expect(result.success).toBe(true)
|
|
400
|
+
if (result.success) {
|
|
401
|
+
expect(result.data.id).toBeDefined()
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ✅ 单元测试错误断言
|
|
405
|
+
const result = await service.create(invalidInput)
|
|
406
|
+
expect(result.success).toBe(false)
|
|
407
|
+
if (!result.success) {
|
|
408
|
+
expect(result.error.code).toBe('VALIDATION_FAILED')
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ✅ E2E API 断言
|
|
412
|
+
const response = await request.post('/api/articles', { data: input })
|
|
413
|
+
expect(response.status()).toBe(201)
|
|
414
|
+
const body = await response.json()
|
|
415
|
+
expect(body.success).toBe(true)
|
|
416
|
+
|
|
417
|
+
// ✅ E2E UI 断言
|
|
418
|
+
await expect(page.locator('.alert-success')).toBeVisible()
|
|
419
|
+
await expect(page).toHaveURL('/admin/articles')
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## 质量门禁
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
# 运行单元测试
|
|
426
|
+
pnpm --filter <app-name> test
|
|
427
|
+
|
|
428
|
+
# 运行 E2E 测试
|
|
429
|
+
pnpm --filter <app-name> test:e2e
|
|
430
|
+
|
|
431
|
+
# 覆盖率
|
|
432
|
+
pnpm --filter <app-name> test:coverage
|
|
433
|
+
|
|
434
|
+
# 全量门禁
|
|
435
|
+
pnpm typecheck && pnpm lint && pnpm test
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## 相关 Skills
|
|
439
|
+
|
|
440
|
+
- `hai-app-create`:应用功能创建规范(TDD Green 阶段参照)
|
|
441
|
+
- `hai-app-review`:应用代码审查规范(TDD Refactor 阶段参照)
|
|
442
|
+
- `hai-build`:项目架构总览与 TDD 工作流导航
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hai-audit
|
|
3
|
-
description: 使用 @h-ai/audit
|
|
3
|
+
description: "使用 @h-ai/audit 记录、查询、统计和清理操作审计。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# @h-ai/audit — 审计日志模块
|
|
@@ -9,11 +9,11 @@ description: 使用 @h-ai/audit 进行审计日志的记录、查询、清理与
|
|
|
9
9
|
|
|
10
10
|
| 项目 | 契约 |
|
|
11
11
|
| --- | --- |
|
|
12
|
-
| 能力 | 使用 @h-ai/audit
|
|
13
|
-
| 适用场景 |
|
|
14
|
-
| 输入 |
|
|
15
|
-
| 输出 |
|
|
16
|
-
| 限制 |
|
|
12
|
+
| 能力 | 使用 @h-ai/audit 记录、查询、统计和清理操作审计 |
|
|
13
|
+
| 适用场景 | 业务操作留痕与审计记录检索 |
|
|
14
|
+
| 输入 | AuditConfigInput、已初始化 reldb、审计事件与查询条件 |
|
|
15
|
+
| 输出 | 记录/查询/统计的 HaiResult |
|
|
16
|
+
| 限制 | 仅服务端;审计内容需脱敏,保留期和清理范围由应用策略决定。 |
|
|
17
17
|
|
|
18
18
|
统一审计日志模块,基于 `@h-ai/reldb` 实现持久化,提供审计日志的记录、分页查询、清理与统计。
|
|
19
19
|
|