Files
ai-proj-helper/skills-dev/dev-test-plugin/skills/dev-test/frontend-testing.md
John Qiu 712063071c refactor: 通用技能按类别拆分为独立目录
skills/ → skills-dev(9), skills-req(10), skills-ops(4),
skills-integration(8), skills-biz(4), skills-workflow(7)

generate-marketplace.py 改为自动扫描所有 skills-* 目录。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 11:31:58 +10:30

175 lines
3.7 KiB
Markdown

# 前端测试 (Vue + React)
## Vue 前端测试
### 测试框架
- **Vitest**: 测试运行器
- **Vue Test Utils**: 组件测试
- **MSW**: API Mock
### 运行测试
```bash
npm run test
npm run test:watch
npm run test:coverage
```
### 组件测试
```typescript
// UserList.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import UserList from './UserList.vue'
describe('UserList', () => {
it('renders user list', () => {
const wrapper = mount(UserList, {
props: {
users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
}
})
expect(wrapper.findAll('.user-item')).toHaveLength(2)
expect(wrapper.text()).toContain('Alice')
})
it('emits select event', async () => {
const wrapper = mount(UserList, {
props: { users: [{ id: 1, name: 'Alice' }] }
})
await wrapper.find('.user-item').trigger('click')
expect(wrapper.emitted('select')).toBeTruthy()
expect(wrapper.emitted('select')[0]).toEqual([1])
})
it('shows empty state', () => {
const wrapper = mount(UserList, {
props: { users: [] }
})
expect(wrapper.find('.empty-state').exists()).toBe(true)
})
})
```
### API Mock (MSW)
```typescript
// mocks/handlers.ts
import { rest } from 'msw'
export const handlers = [
rest.get('/api/v1/users', (req, res, ctx) => {
return res(ctx.json({
code: 0,
data: {
total: 2,
list: [{ id: 1, name: 'Alice' }]
}
}))
}),
rest.post('/api/v1/users', async (req, res, ctx) => {
const body = await req.json()
return res(ctx.json({
code: 0,
data: { id: 3, ...body }
}))
})
]
```
---
## React 前端测试
### 测试框架
- **Jest**: 测试运行器
- **React Testing Library**: 组件测试
- **Playwright**: E2E 测试
### 运行测试
```bash
npm test
npm run test:e2e
npm run test:e2e:headed
```
### 组件测试
```typescript
// TaskCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import TaskCard from './TaskCard'
describe('TaskCard', () => {
const mockTask = {
id: 1,
title: 'Test Task',
status: 'todo',
priority: 'high'
}
it('renders task title', () => {
render(<TaskCard task={mockTask} />)
expect(screen.getByText('Test Task')).toBeInTheDocument()
})
it('displays priority', () => {
render(<TaskCard task={mockTask} />)
expect(screen.getByText('high')).toHaveClass('priority-high')
})
it('calls onClick', () => {
const handleClick = jest.fn()
render(<TaskCard task={mockTask} onClick={handleClick} />)
fireEvent.click(screen.getByRole('article'))
expect(handleClick).toHaveBeenCalledWith(mockTask)
})
})
```
### Hook 测试
```typescript
// useTimer.test.ts
import { renderHook, act } from '@testing-library/react-hooks'
import { useTimer } from './useTimer'
describe('useTimer', () => {
beforeEach(() => jest.useFakeTimers())
afterEach(() => jest.useRealTimers())
it('starts timer', () => {
const { result } = renderHook(() => useTimer())
act(() => result.current.start())
expect(result.current.isRunning).toBe(true)
})
it('increments time', () => {
const { result } = renderHook(() => useTimer())
act(() => {
result.current.start()
jest.advanceTimersByTime(3000)
})
expect(result.current.seconds).toBe(3)
})
})
```