Files
ai-proj-helper/plugins/dev-test-plugin/skills/dev-test/frontend-testing.md

3.7 KiB

前端测试 (Vue + React)

Vue 前端测试

测试框架

  • Vitest: 测试运行器
  • Vue Test Utils: 组件测试
  • MSW: API Mock

运行测试

npm run test
npm run test:watch
npm run test:coverage

组件测试

// 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)

// 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 测试

运行测试

npm test
npm run test:e2e
npm run test:e2e:headed

组件测试

// 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 测试

// 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)
    })
})