测试与部署
约 753 字大约 3 分钟
布欧-Lewyon
2026-05-15
组件测试
看板卡片组件测试
<!-- KanbanCard.vue -->
<script setup lang="ts">
import type { Task } from '@/types/board'
const props = defineProps<{ task: Task }>()
</script>
<template>
<div class="kanban-card" data-testid="kanban-card">
<h4>{{ task.title }}</h4>
<p v-if="task.description">{{ task.description }}</p>
<div class="tags">
<span v-for="tag in task.tags" :key="tag" class="tag">
{{ tag }}
</span>
</div>
</div>
</template>// __tests__/KanbanCard.spec.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import KanbanCard from '@/components/kanban/KanbanCard.vue'
const mockTask = {
id: 1,
title: '实现登录功能',
description: '需要完成 OAuth2 登录',
tags: ['frontend', 'auth'],
priority: 'high' as const,
assignee: 'Alice'
}
describe('KanbanCard.vue', () => {
it('渲染任务标题', () => {
const wrapper = mount(KanbanCard, { props: { task: mockTask } })
expect(wrapper.text()).toContain('实现登录功能')
})
it('渲染标签', () => {
const wrapper = mount(KanbanCard, { props: { task: mockTask } })
const tags = wrapper.findAll('.tag')
expect(tags).toHaveLength(2)
expect(tags[0].text()).toBe('frontend')
})
it('没有 description 时不显示', () => {
const taskWithoutDesc = { ...mockTask, description: undefined }
const wrapper = mount(KanbanCard, {
props: { task: taskWithoutDesc }
})
expect(wrapper.find('p').exists()).toBe(false)
})
})权限组件测试
import { mount } from '@vue/test-utils'
import { setActivePinia, createPinia } from 'pinia'
import Permission from '@/components/common/Permission.vue'
import { useAuthStore } from '@/stores/auth'
describe('Permission.vue', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('有权限时渲染内容', () => {
const auth = useAuthStore()
// 设置权限
auth.permissions = ['board:create']
auth.user = { role: 'editor' }
const wrapper = mount(Permission, {
props: { code: 'board:create' },
slots: { default: '<button>新建</button>' }
})
expect(wrapper.find('button').exists()).toBe(true)
})
it('无权限时渲染 fallback', () => {
const auth = useAuthStore()
auth.permissions = []
auth.user = { role: 'viewer' }
const wrapper = mount(Permission, {
props: { code: 'board:create' },
slots: {
default: '<button>新建</button>',
fallback: '<button disabled>无权限</button>'
}
})
expect(wrapper.find('button:disabled').exists()).toBe(true)
})
})E2E 测试(Playwright)
// e2e/board.spec.ts
import { test, expect } from '@playwright/test'
test.describe('看板 E2E', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:5173/login')
// 登录
await page.fill('[data-testid="username"]', 'admin')
await page.fill('[data-testid="password"]', 'admin123')
await page.click('[data-testid="login-btn"]')
await page.waitForURL('**/dashboard')
})
test('创建看板', async ({ page }) => {
await page.click('text=新建看板')
await page.fill('[data-testid="board-title-input"]', '测试看板')
await page.click('[data-testid="confirm-create"]')
await expect(page.getByText('测试看板')).toBeVisible()
})
test('拖拽任务', async ({ page }) => {
await page.goto('http://localhost:5173/board/1')
const task = page.locator('.kanban-card').first()
const targetColumn = page.locator('.kanban-column').nth(1)
// 拖拽
await task.dragTo(targetColumn)
await expect(targetColumn.locator('.kanban-card')).toHaveCount(1)
})
})Package.json scripts
{
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui",
"test:e2e": "playwright test",
"lint": "eslint src/**/*.{ts,vue}",
"format": "prettier --write src/**/*.{ts,vue,css}"
}
}Docker 部署
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install
COPY . .
RUN pnpm build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]# nginx.conf
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html; # SPA fallback
}
location /api/ {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}# docker-compose.yml
services:
frontend:
build: .
ports:
- "80:80"
depends_on:
- backend
backend:
image: your-backend:latest
ports:
- "8080:8080"
environment:
- DB_URL=postgres://db:5432/kanban
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: kanban
POSTGRES_PASSWORD: secretCI/CD
# .github/workflows/deploy.yml
name: Deploy Kanban App
on:
push:
branches: [main]
jobs:
test-build-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: corepack enable && pnpm install
- run: pnpm lint
- run: pnpm test
- run: pnpm build
- name: Docker build & push
run: |
docker build -t your-repo/kanban-frontend:${{ github.sha }} .
docker push your-repo/kanban-frontend:${{ github.sha }}
- name: Deploy
run: |
ssh deploy@server "docker pull your-repo/kanban-frontend:${{ github.sha }} && docker compose up -d"小结
| 内容 | 工具/方式 |
|---|---|
| 单元测试 | Vitest + Vue Test Utils |
| 组件测试 | mount / shallowMount + Props/Slots |
| Store 测试 | setActivePinia + createPinia |
| E2E 测试 | Playwright |
| Docker 部署 | Dockerfile + nginx + docker-compose |
| CI/CD | GitHub Actions |
上一节:路由与权限控制
