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