code-abyss 1.6.16 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/bin/install.js +59 -163
- package/bin/lib/ccline.js +82 -0
- package/bin/lib/utils.js +61 -0
- package/package.json +5 -2
- package/skills/SKILL.md +24 -16
- package/skills/domains/ai/SKILL.md +2 -2
- package/skills/domains/ai/prompt-and-eval.md +279 -0
- package/skills/domains/architecture/SKILL.md +2 -3
- package/skills/domains/architecture/security-arch.md +87 -0
- package/skills/domains/data-engineering/SKILL.md +188 -26
- package/skills/domains/development/SKILL.md +1 -4
- package/skills/domains/devops/SKILL.md +3 -5
- package/skills/domains/devops/performance.md +63 -0
- package/skills/domains/devops/testing.md +97 -0
- package/skills/domains/frontend-design/SKILL.md +12 -3
- package/skills/domains/frontend-design/claymorphism/SKILL.md +117 -0
- package/skills/domains/frontend-design/claymorphism/references/tokens.css +52 -0
- package/skills/domains/frontend-design/engineering.md +287 -0
- package/skills/domains/frontend-design/glassmorphism/SKILL.md +138 -0
- package/skills/domains/frontend-design/glassmorphism/references/tokens.css +32 -0
- package/skills/domains/frontend-design/liquid-glass/SKILL.md +135 -0
- package/skills/domains/frontend-design/liquid-glass/references/tokens.css +81 -0
- package/skills/domains/frontend-design/neubrutalism/SKILL.md +141 -0
- package/skills/domains/frontend-design/neubrutalism/references/tokens.css +44 -0
- package/skills/domains/infrastructure/SKILL.md +174 -34
- package/skills/domains/mobile/SKILL.md +211 -21
- package/skills/domains/orchestration/SKILL.md +1 -0
- package/skills/domains/security/SKILL.md +4 -6
- package/skills/domains/security/blue-team.md +57 -0
- package/skills/domains/security/red-team.md +54 -0
- package/skills/domains/security/threat-intel.md +50 -0
- package/skills/orchestration/multi-agent/SKILL.md +195 -46
- package/skills/run_skill.js +139 -0
- package/skills/tools/gen-docs/SKILL.md +6 -4
- package/skills/tools/gen-docs/scripts/doc_generator.js +363 -0
- package/skills/tools/lib/shared.js +98 -0
- package/skills/tools/verify-change/SKILL.md +8 -6
- package/skills/tools/verify-change/scripts/change_analyzer.js +289 -0
- package/skills/tools/verify-module/SKILL.md +6 -4
- package/skills/tools/verify-module/scripts/module_scanner.js +171 -0
- package/skills/tools/verify-quality/SKILL.md +5 -3
- package/skills/tools/verify-quality/scripts/quality_checker.js +337 -0
- package/skills/tools/verify-security/SKILL.md +7 -5
- package/skills/tools/verify-security/scripts/security_scanner.js +283 -0
- package/skills/__pycache__/run_skill.cpython-312.pyc +0 -0
- package/skills/domains/COVERAGE_PLAN.md +0 -232
- package/skills/domains/ai/model-evaluation.md +0 -790
- package/skills/domains/ai/prompt-engineering.md +0 -703
- package/skills/domains/architecture/compliance.md +0 -299
- package/skills/domains/architecture/data-security.md +0 -184
- package/skills/domains/data-engineering/data-pipeline.md +0 -762
- package/skills/domains/data-engineering/data-quality.md +0 -894
- package/skills/domains/data-engineering/stream-processing.md +0 -791
- package/skills/domains/development/dart.md +0 -963
- package/skills/domains/development/kotlin.md +0 -834
- package/skills/domains/development/php.md +0 -659
- package/skills/domains/development/swift.md +0 -755
- package/skills/domains/devops/e2e-testing.md +0 -914
- package/skills/domains/devops/performance-testing.md +0 -734
- package/skills/domains/devops/testing-strategy.md +0 -667
- package/skills/domains/frontend-design/build-tools.md +0 -743
- package/skills/domains/frontend-design/performance.md +0 -734
- package/skills/domains/frontend-design/testing.md +0 -699
- package/skills/domains/infrastructure/gitops.md +0 -735
- package/skills/domains/infrastructure/iac.md +0 -855
- package/skills/domains/infrastructure/kubernetes.md +0 -1018
- package/skills/domains/mobile/android-dev.md +0 -979
- package/skills/domains/mobile/cross-platform.md +0 -795
- package/skills/domains/mobile/ios-dev.md +0 -931
- package/skills/domains/security/secrets-management.md +0 -834
- package/skills/domains/security/supply-chain.md +0 -931
- package/skills/domains/security/threat-modeling.md +0 -828
- package/skills/run_skill.py +0 -153
- package/skills/tests/README.md +0 -225
- package/skills/tests/SUMMARY.md +0 -362
- package/skills/tests/__init__.py +0 -3
- package/skills/tests/__pycache__/test_change_analyzer.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_doc_generator.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_module_scanner.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_quality_checker.cpython-312.pyc +0 -0
- package/skills/tests/__pycache__/test_security_scanner.cpython-312.pyc +0 -0
- package/skills/tests/test_change_analyzer.py +0 -558
- package/skills/tests/test_doc_generator.py +0 -538
- package/skills/tests/test_module_scanner.py +0 -376
- package/skills/tests/test_quality_checker.py +0 -516
- package/skills/tests/test_security_scanner.py +0 -426
- package/skills/tools/gen-docs/scripts/__pycache__/doc_generator.cpython-312.pyc +0 -0
- package/skills/tools/gen-docs/scripts/doc_generator.py +0 -520
- package/skills/tools/verify-change/scripts/__pycache__/change_analyzer.cpython-312.pyc +0 -0
- package/skills/tools/verify-change/scripts/change_analyzer.py +0 -529
- package/skills/tools/verify-module/scripts/__pycache__/module_scanner.cpython-312.pyc +0 -0
- package/skills/tools/verify-module/scripts/module_scanner.py +0 -321
- package/skills/tools/verify-quality/scripts/__pycache__/quality_checker.cpython-312.pyc +0 -0
- package/skills/tools/verify-quality/scripts/quality_checker.py +0 -481
- package/skills/tools/verify-security/scripts/__pycache__/security_scanner.cpython-312.pyc +0 -0
- package/skills/tools/verify-security/scripts/security_scanner.py +0 -374
|
@@ -1,699 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: testing
|
|
3
|
-
description: 前端测试技术。Vitest、Playwright、Jest、Cypress、测试金字塔、E2E测试、单元测试、集成测试。当用户提到前端测试、Vitest、Playwright、E2E测试、单元测试、测试覆盖率时使用。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# 🎨 🧪 前端测试 · Frontend Testing
|
|
7
|
-
|
|
8
|
-
## 测试金字塔
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
/\
|
|
12
|
-
/ \ E2E Tests (10%)
|
|
13
|
-
/----\
|
|
14
|
-
/ \ Integration Tests (20%)
|
|
15
|
-
/--------\
|
|
16
|
-
/ \ Unit Tests (70%)
|
|
17
|
-
/____________\
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
| 层级 | 数量 | 速度 | 成本 | 信心 |
|
|
21
|
-
|------|------|------|------|------|
|
|
22
|
-
| E2E | 少 | 慢 | 高 | 高 |
|
|
23
|
-
| 集成 | 中 | 中 | 中 | 中 |
|
|
24
|
-
| 单元 | 多 | 快 | 低 | 低 |
|
|
25
|
-
|
|
26
|
-
## 测试策略决策树
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
需要测试什么?
|
|
30
|
-
│
|
|
31
|
-
├─ 纯函数/工具 → 单元测试 (Vitest/Jest)
|
|
32
|
-
│
|
|
33
|
-
├─ React 组件
|
|
34
|
-
│ ├─ UI 渲染 → 组件测试 (Testing Library)
|
|
35
|
-
│ ├─ 交互逻辑 → 集成测试
|
|
36
|
-
│ └─ 视觉回归 → Chromatic/Percy
|
|
37
|
-
│
|
|
38
|
-
├─ API 集成 → MSW Mock + 集成测试
|
|
39
|
-
│
|
|
40
|
-
└─ 用户流程 → E2E 测试 (Playwright/Cypress)
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Vitest (推荐)
|
|
44
|
-
|
|
45
|
-
### 基础配置
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
// vitest.config.ts
|
|
49
|
-
import { defineConfig } from 'vitest/config'
|
|
50
|
-
import react from '@vitejs/plugin-react'
|
|
51
|
-
|
|
52
|
-
export default defineConfig({
|
|
53
|
-
plugins: [react()],
|
|
54
|
-
test: {
|
|
55
|
-
globals: true,
|
|
56
|
-
environment: 'jsdom',
|
|
57
|
-
setupFiles: './src/test/setup.ts',
|
|
58
|
-
coverage: {
|
|
59
|
-
provider: 'v8',
|
|
60
|
-
reporter: ['text', 'json', 'html'],
|
|
61
|
-
exclude: ['node_modules/', 'src/test/'],
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
})
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### 单元测试
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
// utils.test.ts
|
|
71
|
-
import { describe, it, expect } from 'vitest'
|
|
72
|
-
import { formatCurrency, debounce } from './utils'
|
|
73
|
-
|
|
74
|
-
describe('formatCurrency', () => {
|
|
75
|
-
it('formats number to currency', () => {
|
|
76
|
-
expect(formatCurrency(1234.56)).toBe('$1,234.56')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('handles zero', () => {
|
|
80
|
-
expect(formatCurrency(0)).toBe('$0.00')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('handles negative numbers', () => {
|
|
84
|
-
expect(formatCurrency(-100)).toBe('-$100.00')
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
describe('debounce', () => {
|
|
89
|
-
it('delays function execution', async () => {
|
|
90
|
-
let count = 0
|
|
91
|
-
const fn = debounce(() => count++, 100)
|
|
92
|
-
|
|
93
|
-
fn()
|
|
94
|
-
fn()
|
|
95
|
-
fn()
|
|
96
|
-
|
|
97
|
-
expect(count).toBe(0)
|
|
98
|
-
|
|
99
|
-
await new Promise((resolve) => setTimeout(resolve, 150))
|
|
100
|
-
expect(count).toBe(1)
|
|
101
|
-
})
|
|
102
|
-
})
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### React 组件测试
|
|
106
|
-
|
|
107
|
-
```typescript
|
|
108
|
-
// Button.test.tsx
|
|
109
|
-
import { render, screen, fireEvent } from '@testing-library/react'
|
|
110
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
111
|
-
import { Button } from './Button'
|
|
112
|
-
|
|
113
|
-
describe('Button', () => {
|
|
114
|
-
it('renders with text', () => {
|
|
115
|
-
render(<Button>Click me</Button>)
|
|
116
|
-
expect(screen.getByText('Click me')).toBeInTheDocument()
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('calls onClick when clicked', () => {
|
|
120
|
-
const handleClick = vi.fn()
|
|
121
|
-
render(<Button onClick={handleClick}>Click</Button>)
|
|
122
|
-
|
|
123
|
-
fireEvent.click(screen.getByText('Click'))
|
|
124
|
-
expect(handleClick).toHaveBeenCalledTimes(1)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('is disabled when disabled prop is true', () => {
|
|
128
|
-
render(<Button disabled>Click</Button>)
|
|
129
|
-
expect(screen.getByRole('button')).toBeDisabled()
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('applies variant styles', () => {
|
|
133
|
-
render(<Button variant="primary">Click</Button>)
|
|
134
|
-
expect(screen.getByRole('button')).toHaveClass('btn-primary')
|
|
135
|
-
})
|
|
136
|
-
})
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### Hooks 测试
|
|
140
|
-
|
|
141
|
-
```typescript
|
|
142
|
-
// useCounter.test.ts
|
|
143
|
-
import { renderHook, act } from '@testing-library/react'
|
|
144
|
-
import { describe, it, expect } from 'vitest'
|
|
145
|
-
import { useCounter } from './useCounter'
|
|
146
|
-
|
|
147
|
-
describe('useCounter', () => {
|
|
148
|
-
it('initializes with default value', () => {
|
|
149
|
-
const { result } = renderHook(() => useCounter())
|
|
150
|
-
expect(result.current.count).toBe(0)
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
it('increments counter', () => {
|
|
154
|
-
const { result } = renderHook(() => useCounter())
|
|
155
|
-
|
|
156
|
-
act(() => {
|
|
157
|
-
result.current.increment()
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
expect(result.current.count).toBe(1)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('decrements counter', () => {
|
|
164
|
-
const { result } = renderHook(() => useCounter(5))
|
|
165
|
-
|
|
166
|
-
act(() => {
|
|
167
|
-
result.current.decrement()
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
expect(result.current.count).toBe(4)
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('resets counter', () => {
|
|
174
|
-
const { result } = renderHook(() => useCounter(10))
|
|
175
|
-
|
|
176
|
-
act(() => {
|
|
177
|
-
result.current.increment()
|
|
178
|
-
result.current.reset()
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
expect(result.current.count).toBe(10)
|
|
182
|
-
})
|
|
183
|
-
})
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### 异步测试
|
|
187
|
-
|
|
188
|
-
```typescript
|
|
189
|
-
// api.test.ts
|
|
190
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
191
|
-
import { fetchUser, createUser } from './api'
|
|
192
|
-
|
|
193
|
-
// Mock fetch
|
|
194
|
-
global.fetch = vi.fn()
|
|
195
|
-
|
|
196
|
-
describe('API', () => {
|
|
197
|
-
beforeEach(() => {
|
|
198
|
-
vi.clearAllMocks()
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('fetches user successfully', async () => {
|
|
202
|
-
const mockUser = { id: '1', name: 'John' }
|
|
203
|
-
;(fetch as any).mockResolvedValueOnce({
|
|
204
|
-
ok: true,
|
|
205
|
-
json: async () => mockUser,
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
const user = await fetchUser('1')
|
|
209
|
-
expect(user).toEqual(mockUser)
|
|
210
|
-
expect(fetch).toHaveBeenCalledWith('/api/users/1')
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
it('handles fetch error', async () => {
|
|
214
|
-
;(fetch as any).mockResolvedValueOnce({
|
|
215
|
-
ok: false,
|
|
216
|
-
status: 404,
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
await expect(fetchUser('999')).rejects.toThrow('User not found')
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
it('creates user', async () => {
|
|
223
|
-
const newUser = { name: 'Jane', email: 'jane@example.com' }
|
|
224
|
-
const createdUser = { id: '2', ...newUser }
|
|
225
|
-
|
|
226
|
-
;(fetch as any).mockResolvedValueOnce({
|
|
227
|
-
ok: true,
|
|
228
|
-
json: async () => createdUser,
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
const result = await createUser(newUser)
|
|
232
|
-
expect(result).toEqual(createdUser)
|
|
233
|
-
})
|
|
234
|
-
})
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
## MSW (Mock Service Worker)
|
|
238
|
-
|
|
239
|
-
### 配置 MSW
|
|
240
|
-
|
|
241
|
-
```typescript
|
|
242
|
-
// src/mocks/handlers.ts
|
|
243
|
-
import { http, HttpResponse } from 'msw'
|
|
244
|
-
|
|
245
|
-
export const handlers = [
|
|
246
|
-
http.get('/api/users/:id', ({ params }) => {
|
|
247
|
-
const { id } = params
|
|
248
|
-
return HttpResponse.json({
|
|
249
|
-
id,
|
|
250
|
-
name: 'John Doe',
|
|
251
|
-
email: 'john@example.com',
|
|
252
|
-
})
|
|
253
|
-
}),
|
|
254
|
-
|
|
255
|
-
http.post('/api/users', async ({ request }) => {
|
|
256
|
-
const body = await request.json()
|
|
257
|
-
return HttpResponse.json(
|
|
258
|
-
{ id: '123', ...body },
|
|
259
|
-
{ status: 201 }
|
|
260
|
-
)
|
|
261
|
-
}),
|
|
262
|
-
|
|
263
|
-
http.delete('/api/users/:id', () => {
|
|
264
|
-
return new HttpResponse(null, { status: 204 })
|
|
265
|
-
}),
|
|
266
|
-
]
|
|
267
|
-
|
|
268
|
-
// src/mocks/server.ts
|
|
269
|
-
import { setupServer } from 'msw/node'
|
|
270
|
-
import { handlers } from './handlers'
|
|
271
|
-
|
|
272
|
-
export const server = setupServer(...handlers)
|
|
273
|
-
|
|
274
|
-
// src/test/setup.ts
|
|
275
|
-
import { beforeAll, afterEach, afterAll } from 'vitest'
|
|
276
|
-
import { server } from '../mocks/server'
|
|
277
|
-
|
|
278
|
-
beforeAll(() => server.listen())
|
|
279
|
-
afterEach(() => server.resetHandlers())
|
|
280
|
-
afterAll(() => server.close())
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
### 使用 MSW 测试
|
|
284
|
-
|
|
285
|
-
```typescript
|
|
286
|
-
// UserProfile.test.tsx
|
|
287
|
-
import { render, screen, waitFor } from '@testing-library/react'
|
|
288
|
-
import { describe, it, expect } from 'vitest'
|
|
289
|
-
import { server } from '../mocks/server'
|
|
290
|
-
import { http, HttpResponse } from 'msw'
|
|
291
|
-
import { UserProfile } from './UserProfile'
|
|
292
|
-
|
|
293
|
-
describe('UserProfile', () => {
|
|
294
|
-
it('displays user data', async () => {
|
|
295
|
-
render(<UserProfile userId="1" />)
|
|
296
|
-
|
|
297
|
-
await waitFor(() => {
|
|
298
|
-
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
|
299
|
-
expect(screen.getByText('john@example.com')).toBeInTheDocument()
|
|
300
|
-
})
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
it('handles loading state', () => {
|
|
304
|
-
render(<UserProfile userId="1" />)
|
|
305
|
-
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
it('handles error state', async () => {
|
|
309
|
-
server.use(
|
|
310
|
-
http.get('/api/users/:id', () => {
|
|
311
|
-
return new HttpResponse(null, { status: 500 })
|
|
312
|
-
})
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
render(<UserProfile userId="1" />)
|
|
316
|
-
|
|
317
|
-
await waitFor(() => {
|
|
318
|
-
expect(screen.getByText('Error loading user')).toBeInTheDocument()
|
|
319
|
-
})
|
|
320
|
-
})
|
|
321
|
-
})
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
## Playwright (E2E 推荐)
|
|
325
|
-
|
|
326
|
-
### 配置 Playwright
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
// playwright.config.ts
|
|
330
|
-
import { defineConfig, devices } from '@playwright/test'
|
|
331
|
-
|
|
332
|
-
export default defineConfig({
|
|
333
|
-
testDir: './e2e',
|
|
334
|
-
fullyParallel: true,
|
|
335
|
-
forbidOnly: !!process.env.CI,
|
|
336
|
-
retries: process.env.CI ? 2 : 0,
|
|
337
|
-
workers: process.env.CI ? 1 : undefined,
|
|
338
|
-
reporter: 'html',
|
|
339
|
-
use: {
|
|
340
|
-
baseURL: 'http://localhost:3000',
|
|
341
|
-
trace: 'on-first-retry',
|
|
342
|
-
screenshot: 'only-on-failure',
|
|
343
|
-
},
|
|
344
|
-
projects: [
|
|
345
|
-
{
|
|
346
|
-
name: 'chromium',
|
|
347
|
-
use: { ...devices['Desktop Chrome'] },
|
|
348
|
-
},
|
|
349
|
-
{
|
|
350
|
-
name: 'firefox',
|
|
351
|
-
use: { ...devices['Desktop Firefox'] },
|
|
352
|
-
},
|
|
353
|
-
{
|
|
354
|
-
name: 'webkit',
|
|
355
|
-
use: { ...devices['Desktop Safari'] },
|
|
356
|
-
},
|
|
357
|
-
{
|
|
358
|
-
name: 'Mobile Chrome',
|
|
359
|
-
use: { ...devices['Pixel 5'] },
|
|
360
|
-
},
|
|
361
|
-
],
|
|
362
|
-
webServer: {
|
|
363
|
-
command: 'npm run dev',
|
|
364
|
-
url: 'http://localhost:3000',
|
|
365
|
-
reuseExistingServer: !process.env.CI,
|
|
366
|
-
},
|
|
367
|
-
})
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
### 基础 E2E 测试
|
|
371
|
-
|
|
372
|
-
```typescript
|
|
373
|
-
// e2e/login.spec.ts
|
|
374
|
-
import { test, expect } from '@playwright/test'
|
|
375
|
-
|
|
376
|
-
test.describe('Login', () => {
|
|
377
|
-
test('successful login', async ({ page }) => {
|
|
378
|
-
await page.goto('/login')
|
|
379
|
-
|
|
380
|
-
await page.fill('input[name="email"]', 'user@example.com')
|
|
381
|
-
await page.fill('input[name="password"]', 'password123')
|
|
382
|
-
await page.click('button[type="submit"]')
|
|
383
|
-
|
|
384
|
-
await expect(page).toHaveURL('/dashboard')
|
|
385
|
-
await expect(page.locator('h1')).toContainText('Dashboard')
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
test('shows error for invalid credentials', async ({ page }) => {
|
|
389
|
-
await page.goto('/login')
|
|
390
|
-
|
|
391
|
-
await page.fill('input[name="email"]', 'wrong@example.com')
|
|
392
|
-
await page.fill('input[name="password"]', 'wrongpass')
|
|
393
|
-
await page.click('button[type="submit"]')
|
|
394
|
-
|
|
395
|
-
await expect(page.locator('.error')).toContainText('Invalid credentials')
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
test('validates required fields', async ({ page }) => {
|
|
399
|
-
await page.goto('/login')
|
|
400
|
-
await page.click('button[type="submit"]')
|
|
401
|
-
|
|
402
|
-
await expect(page.locator('input[name="email"]:invalid')).toBeVisible()
|
|
403
|
-
})
|
|
404
|
-
})
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
### Page Object Model
|
|
408
|
-
|
|
409
|
-
```typescript
|
|
410
|
-
// e2e/pages/LoginPage.ts
|
|
411
|
-
import { Page, Locator } from '@playwright/test'
|
|
412
|
-
|
|
413
|
-
export class LoginPage {
|
|
414
|
-
readonly page: Page
|
|
415
|
-
readonly emailInput: Locator
|
|
416
|
-
readonly passwordInput: Locator
|
|
417
|
-
readonly submitButton: Locator
|
|
418
|
-
readonly errorMessage: Locator
|
|
419
|
-
|
|
420
|
-
constructor(page: Page) {
|
|
421
|
-
this.page = page
|
|
422
|
-
this.emailInput = page.locator('input[name="email"]')
|
|
423
|
-
this.passwordInput = page.locator('input[name="password"]')
|
|
424
|
-
this.submitButton = page.locator('button[type="submit"]')
|
|
425
|
-
this.errorMessage = page.locator('.error')
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async goto() {
|
|
429
|
-
await this.page.goto('/login')
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
async login(email: string, password: string) {
|
|
433
|
-
await this.emailInput.fill(email)
|
|
434
|
-
await this.passwordInput.fill(password)
|
|
435
|
-
await this.submitButton.click()
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// 使用 Page Object
|
|
440
|
-
test('login with page object', async ({ page }) => {
|
|
441
|
-
const loginPage = new LoginPage(page)
|
|
442
|
-
await loginPage.goto()
|
|
443
|
-
await loginPage.login('user@example.com', 'password123')
|
|
444
|
-
|
|
445
|
-
await expect(page).toHaveURL('/dashboard')
|
|
446
|
-
})
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
### API 测试
|
|
450
|
-
|
|
451
|
-
```typescript
|
|
452
|
-
// e2e/api.spec.ts
|
|
453
|
-
import { test, expect } from '@playwright/test'
|
|
454
|
-
|
|
455
|
-
test.describe('API', () => {
|
|
456
|
-
test('GET /api/users', async ({ request }) => {
|
|
457
|
-
const response = await request.get('/api/users')
|
|
458
|
-
expect(response.ok()).toBeTruthy()
|
|
459
|
-
|
|
460
|
-
const users = await response.json()
|
|
461
|
-
expect(users).toHaveLength(10)
|
|
462
|
-
expect(users[0]).toHaveProperty('id')
|
|
463
|
-
expect(users[0]).toHaveProperty('name')
|
|
464
|
-
})
|
|
465
|
-
|
|
466
|
-
test('POST /api/users', async ({ request }) => {
|
|
467
|
-
const response = await request.post('/api/users', {
|
|
468
|
-
data: {
|
|
469
|
-
name: 'New User',
|
|
470
|
-
email: 'new@example.com',
|
|
471
|
-
},
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
expect(response.status()).toBe(201)
|
|
475
|
-
const user = await response.json()
|
|
476
|
-
expect(user.name).toBe('New User')
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
test('handles authentication', async ({ request }) => {
|
|
480
|
-
const response = await request.get('/api/protected', {
|
|
481
|
-
headers: {
|
|
482
|
-
Authorization: 'Bearer token123',
|
|
483
|
-
},
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
expect(response.ok()).toBeTruthy()
|
|
487
|
-
})
|
|
488
|
-
})
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
### 视觉回归测试
|
|
492
|
-
|
|
493
|
-
```typescript
|
|
494
|
-
// e2e/visual.spec.ts
|
|
495
|
-
import { test, expect } from '@playwright/test'
|
|
496
|
-
|
|
497
|
-
test.describe('Visual Regression', () => {
|
|
498
|
-
test('homepage screenshot', async ({ page }) => {
|
|
499
|
-
await page.goto('/')
|
|
500
|
-
await expect(page).toHaveScreenshot('homepage.png')
|
|
501
|
-
})
|
|
502
|
-
|
|
503
|
-
test('button states', async ({ page }) => {
|
|
504
|
-
await page.goto('/components')
|
|
505
|
-
|
|
506
|
-
const button = page.locator('button.primary')
|
|
507
|
-
await expect(button).toHaveScreenshot('button-default.png')
|
|
508
|
-
|
|
509
|
-
await button.hover()
|
|
510
|
-
await expect(button).toHaveScreenshot('button-hover.png')
|
|
511
|
-
|
|
512
|
-
await button.focus()
|
|
513
|
-
await expect(button).toHaveScreenshot('button-focus.png')
|
|
514
|
-
})
|
|
515
|
-
|
|
516
|
-
test('responsive layout', async ({ page }) => {
|
|
517
|
-
await page.goto('/')
|
|
518
|
-
|
|
519
|
-
// Desktop
|
|
520
|
-
await page.setViewportSize({ width: 1920, height: 1080 })
|
|
521
|
-
await expect(page).toHaveScreenshot('desktop.png')
|
|
522
|
-
|
|
523
|
-
// Tablet
|
|
524
|
-
await page.setViewportSize({ width: 768, height: 1024 })
|
|
525
|
-
await expect(page).toHaveScreenshot('tablet.png')
|
|
526
|
-
|
|
527
|
-
// Mobile
|
|
528
|
-
await page.setViewportSize({ width: 375, height: 667 })
|
|
529
|
-
await expect(page).toHaveScreenshot('mobile.png')
|
|
530
|
-
})
|
|
531
|
-
})
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
## 测试最佳实践
|
|
535
|
-
|
|
536
|
-
### AAA 模式
|
|
537
|
-
|
|
538
|
-
```typescript
|
|
539
|
-
test('user can add item to cart', async ({ page }) => {
|
|
540
|
-
// Arrange - 准备测试环境
|
|
541
|
-
await page.goto('/products')
|
|
542
|
-
const product = page.locator('[data-testid="product-1"]')
|
|
543
|
-
|
|
544
|
-
// Act - 执行操作
|
|
545
|
-
await product.locator('button.add-to-cart').click()
|
|
546
|
-
|
|
547
|
-
// Assert - 验证结果
|
|
548
|
-
await expect(page.locator('.cart-count')).toHaveText('1')
|
|
549
|
-
await expect(page.locator('.notification')).toContainText('Added to cart')
|
|
550
|
-
})
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
### 测试隔离
|
|
554
|
-
|
|
555
|
-
```typescript
|
|
556
|
-
import { test } from '@playwright/test'
|
|
557
|
-
|
|
558
|
-
test.describe('Todo App', () => {
|
|
559
|
-
test.beforeEach(async ({ page }) => {
|
|
560
|
-
// 每个测试前重置状态
|
|
561
|
-
await page.goto('/')
|
|
562
|
-
await page.evaluate(() => localStorage.clear())
|
|
563
|
-
})
|
|
564
|
-
|
|
565
|
-
test('add todo', async ({ page }) => {
|
|
566
|
-
await page.fill('input[name="todo"]', 'Buy milk')
|
|
567
|
-
await page.click('button[type="submit"]')
|
|
568
|
-
await expect(page.locator('.todo-item')).toHaveText('Buy milk')
|
|
569
|
-
})
|
|
570
|
-
|
|
571
|
-
test('delete todo', async ({ page }) => {
|
|
572
|
-
// 独立的测试,不依赖前一个测试
|
|
573
|
-
await page.fill('input[name="todo"]', 'Buy milk')
|
|
574
|
-
await page.click('button[type="submit"]')
|
|
575
|
-
await page.click('.todo-item button.delete')
|
|
576
|
-
await expect(page.locator('.todo-item')).toHaveCount(0)
|
|
577
|
-
})
|
|
578
|
-
})
|
|
579
|
-
```
|
|
580
|
-
|
|
581
|
-
### 数据驱动测试
|
|
582
|
-
|
|
583
|
-
```typescript
|
|
584
|
-
const testCases = [
|
|
585
|
-
{ input: 'hello', expected: 'HELLO' },
|
|
586
|
-
{ input: 'world', expected: 'WORLD' },
|
|
587
|
-
{ input: '123', expected: '123' },
|
|
588
|
-
]
|
|
589
|
-
|
|
590
|
-
testCases.forEach(({ input, expected }) => {
|
|
591
|
-
test(`converts "${input}" to "${expected}"`, () => {
|
|
592
|
-
expect(toUpperCase(input)).toBe(expected)
|
|
593
|
-
})
|
|
594
|
-
})
|
|
595
|
-
|
|
596
|
-
// Playwright 参数化
|
|
597
|
-
const browsers = ['chromium', 'firefox', 'webkit']
|
|
598
|
-
|
|
599
|
-
browsers.forEach((browserName) => {
|
|
600
|
-
test(`works on ${browserName}`, async ({ browser }) => {
|
|
601
|
-
const context = await browser.newContext()
|
|
602
|
-
const page = await context.newPage()
|
|
603
|
-
await page.goto('/')
|
|
604
|
-
// 测试逻辑
|
|
605
|
-
})
|
|
606
|
-
})
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
## 覆盖率配置
|
|
610
|
-
|
|
611
|
-
```typescript
|
|
612
|
-
// vitest.config.ts
|
|
613
|
-
export default defineConfig({
|
|
614
|
-
test: {
|
|
615
|
-
coverage: {
|
|
616
|
-
provider: 'v8',
|
|
617
|
-
reporter: ['text', 'json', 'html', 'lcov'],
|
|
618
|
-
include: ['src/**/*.{ts,tsx}'],
|
|
619
|
-
exclude: [
|
|
620
|
-
'src/**/*.test.{ts,tsx}',
|
|
621
|
-
'src/**/*.spec.{ts,tsx}',
|
|
622
|
-
'src/test/**',
|
|
623
|
-
'src/**/*.d.ts',
|
|
624
|
-
],
|
|
625
|
-
thresholds: {
|
|
626
|
-
lines: 80,
|
|
627
|
-
functions: 80,
|
|
628
|
-
branches: 75,
|
|
629
|
-
statements: 80,
|
|
630
|
-
},
|
|
631
|
-
},
|
|
632
|
-
},
|
|
633
|
-
})
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
## CI/CD 集成
|
|
637
|
-
|
|
638
|
-
```yaml
|
|
639
|
-
# .github/workflows/test.yml
|
|
640
|
-
name: Test
|
|
641
|
-
|
|
642
|
-
on: [push, pull_request]
|
|
643
|
-
|
|
644
|
-
jobs:
|
|
645
|
-
unit-tests:
|
|
646
|
-
runs-on: ubuntu-latest
|
|
647
|
-
steps:
|
|
648
|
-
- uses: actions/checkout@v3
|
|
649
|
-
- uses: actions/setup-node@v3
|
|
650
|
-
with:
|
|
651
|
-
node-version: 18
|
|
652
|
-
- run: npm ci
|
|
653
|
-
- run: npm run test:unit
|
|
654
|
-
- run: npm run test:coverage
|
|
655
|
-
|
|
656
|
-
e2e-tests:
|
|
657
|
-
runs-on: ubuntu-latest
|
|
658
|
-
steps:
|
|
659
|
-
- uses: actions/checkout@v3
|
|
660
|
-
- uses: actions/setup-node@v3
|
|
661
|
-
with:
|
|
662
|
-
node-version: 18
|
|
663
|
-
- run: npm ci
|
|
664
|
-
- run: npx playwright install --with-deps
|
|
665
|
-
- run: npm run test:e2e
|
|
666
|
-
- uses: actions/upload-artifact@v3
|
|
667
|
-
if: always()
|
|
668
|
-
with:
|
|
669
|
-
name: playwright-report
|
|
670
|
-
path: playwright-report/
|
|
671
|
-
```
|
|
672
|
-
|
|
673
|
-
## 最佳实践清单
|
|
674
|
-
|
|
675
|
-
- ✅ 遵循测试金字塔:70% 单元 + 20% 集成 + 10% E2E
|
|
676
|
-
- ✅ 使用 AAA 模式组织测试
|
|
677
|
-
- ✅ 测试行为而非实现细节
|
|
678
|
-
- ✅ 保持测试独立和隔离
|
|
679
|
-
- ✅ 使用有意义的测试描述
|
|
680
|
-
- ✅ Mock 外部依赖(API、时间、随机数)
|
|
681
|
-
- ✅ 测试边界条件和错误情况
|
|
682
|
-
- ✅ 维护合理的覆盖率(80%+)
|
|
683
|
-
- ✅ 在 CI/CD 中自动运行测试
|
|
684
|
-
- ✅ 使用 Page Object 模式组织 E2E 测试
|
|
685
|
-
|
|
686
|
-
## 工具清单
|
|
687
|
-
|
|
688
|
-
| 工具 | 用途 |
|
|
689
|
-
|------|------|
|
|
690
|
-
| Vitest | 单元测试框架 |
|
|
691
|
-
| Playwright | E2E 测试框架 |
|
|
692
|
-
| Testing Library | React 组件测试 |
|
|
693
|
-
| MSW | API Mock |
|
|
694
|
-
| Cypress | E2E 测试(备选) |
|
|
695
|
-
| Chromatic | 视觉回归测试 |
|
|
696
|
-
| Storybook | 组件开发和测试 |
|
|
697
|
-
| Istanbul | 覆盖率报告 |
|
|
698
|
-
|
|
699
|
-
---
|