Files
zsp-project/docs/superpowers/plans/2026-04-25-frontend-api-test-infra.md
2026-06-03 20:59:39 +08:00

1314 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 前端 API 测试基础设施 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为 Vue3+TypeScript 前端建立 Vitest 测试框架Mock Axios 测试 11 个 API 模块
**Architecture:** Vitest + vi.mock 模式,测试 API 层请求/响应类型验证,不依赖真实后端
**Tech Stack:** Vitest, @vitest/coverage-v8, jsdom, TypeScript
---
## 文件结构
| 文件 | 责任 |
|------|------|
| `vitest.config.ts` | Vitest 配置,环境、覆盖率、别名设置 |
| `package.json` | 新增 vitest 依赖和测试脚本 |
| `src/api/test-utils.ts` | Mock 工具函数封装 |
| `src/api/__tests__/contract.spec.ts` | 合同 API 测试 (15-20 cases) |
| `src/api/__tests__/zspContract.spec.ts` | 中尚鹏合同测试 |
| `src/api/__tests__/zspFinances.spec.ts` | 财务模块测试 |
| `src/api/__tests__/applyDelivery.spec.ts` | 提货申请测试 |
| `src/api/__tests__/deliveryDetails.spec.ts` | 出库明细测试 |
| `src/api/__tests__/inventory.spec.ts` | 库存管理测试 |
| `src/api/__tests__/ownershipTransfer.spec.ts` | 货权转移测试 |
| `src/api/__tests__/company.spec.ts` | 公司信息测试 |
| `src/api/__tests__/user.spec.ts` | 用户认证测试 |
| `src/api/__tests__/invoice.spec.ts` | 发票测试 |
| `src/api/__tests__/settlement.spec.ts` | 结算测试 |
| `src/api/__tests__/reconciliation.spec.ts` | 对账单测试 |
---
### Task 1: 基础设施配置
**Files:**
- Create: `frontend/vitest.config.ts`
- Modify: `frontend/package.json`
- [ ] **Step 1: 安装 Vitest 依赖**
Run in `frontend/`:
```bash
pnpm add -D vitest @vitest/coverage-v8 jsdom @vitest/ui
```
Expected: 依赖安装成功
- [ ] **Step 2: 创建 vitest.config.ts**
```typescript
// frontend/vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
include: ['src/api/__tests__/**/*.spec.ts'],
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'json'],
include: ['src/api/**/*.ts'],
exclude: ['src/api/__tests__/**', 'src/api/test-utils.ts', 'src/api/base.ts', 'src/api/utils.ts']
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})
```
- [ ] **Step 3: 更新 package.json 添加测试脚本**
`frontend/package.json``scripts` 中添加:
```json
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}
```
- [ ] **Step 4: 验证 Vitest 配置**
Run:
```bash
cd frontend && pnpm test
```
Expected: Vitest 运行,报告 "No test files found" (正常,尚未创建测试)
- [ ] **Step 5: 创建测试目录结构**
```bash
mkdir -p frontend/src/api/__tests__
```
---
### Task 2: Mock 工具函数
**Files:**
- Create: `frontend/src/api/test-utils.ts`
- [ ] **Step 1: 创建 test-utils.ts**
```typescript
// frontend/src/api/test-utils.ts
import { vi } from 'vitest'
import type { BaseResponse } from './base'
/**
* Mock http.request 成功响应
*/
export function mockHttpSuccess<T>(data: T): BaseResponse<T> {
return {
success: true,
data,
message: 'ok'
}
}
/**
* Mock http.request 错误响应
*/
export function mockHttpError(message: string, code = 500): BaseResponse<null> {
return {
success: false,
data: null,
message
}
}
/**
* 创建 http.request 的 spy mock
* 注意:需要在每个测试文件中 import http 后使用
*/
export function createHttpMock() {
return {
spyOnRequest: (response: any) => {
const http = require('@/utils/http')
return vi.spyOn(http.http, 'request').mockResolvedValue(response)
},
spyOnError: (error: any) => {
const http = require('@/utils/http')
return vi.spyOn(http.http, 'request').mockRejectedValue(error)
}
}
}
```
- [ ] **Step 2: 验证 test-utils 导入**
创建临时测试验证导入:
```typescript
// frontend/src/api/__tests__/test-utils.spec.ts
import { describe, it, expect } from 'vitest'
import { mockHttpSuccess, mockHttpError } from '../test-utils'
describe('test-utils', () => {
it('mockHttpSuccess 应返回正确结构', () => {
const result = mockHttpSuccess({ id: 1 })
expect(result.success).toBe(true)
expect(result.data).toEqual({ id: 1 })
})
it('mockHttpError 应返回错误结构', () => {
const result = mockHttpError('错误', 500)
expect(result.success).toBe(false)
expect(result.message).toBe('错误')
})
})
```
Run:
```bash
cd frontend && pnpm test
```
Expected: 2 tests pass
- [ ] **Step 3: 删除临时测试,提交基础设施**
```bash
rm frontend/src/api/__tests__/test-utils.spec.ts
cd frontend && git add vitest.config.ts package.json pnpm-lock.yaml src/api/test-utils.ts
git commit -m "feat(frontend): add vitest test infrastructure"
```
---
### Task 3: Contract API 测试
**Files:**
- Create: `frontend/src/api/__tests__/contract.spec.ts`
- [ ] **Step 1: 创建 contract.spec.ts 基础结构**
```typescript
// frontend/src/api/__tests__/contract.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { http } from '@/utils/http'
import type { BaseResponse } from '../base'
import {
getContractList,
getContract,
submitContract,
batchUploadContracts,
updateContract,
deleteContract,
getContractFolders,
createContractFolder,
deleteContractFolder,
getContractBatches,
createContractBatch,
deleteContractBatch
} from '../contract'
// Mock http.request
vi.mock('@/utils/http', () => ({
http: {
request: vi.fn()
}
}))
describe('Contract API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getContractList', () => {
it('应返回合同列表', async () => {
const mockData = [
{ id: 1, name: '合同1', contractCode: 'C001' },
{ id: 2, name: '合同2', contractCode: 'C002' }
]
vi.mocked(http.request).mockResolvedValue({
success: true,
data: mockData
})
const result = await getContractList()
expect(http.request).toHaveBeenCalledWith('get', '/api/contract', { params: undefined })
expect(result.success).toBe(true)
expect(result.data).toHaveLength(2)
})
it('带 contractType 参数应正确传递', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await getContractList({ contractType: 'business' })
expect(http.request).toHaveBeenCalledWith('get', '/api/contract', {
params: { contractType: 'business' }
})
})
})
describe('getContract', () => {
it('应返回单个合同详情', async () => {
const mockData = { id: 1, name: '合同详情', contractCode: 'C001' }
vi.mocked(http.request).mockResolvedValue({ success: true, data: mockData })
const result = await getContract(1)
expect(http.request).toHaveBeenCalledWith('get', '/api/contract/1')
expect(result.data.id).toBe(1)
})
})
describe('submitContract', () => {
it('应提交 FormData 格式数据', async () => {
const formData = new FormData()
formData.append('name', '新合同')
vi.mocked(http.request).mockResolvedValue({ success: true, message: 'ok' })
await submitContract(formData)
expect(http.request).toHaveBeenCalledWith('post', '/api/contract/upload', {
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
})
})
describe('batchUploadContracts', () => {
it('应批量上传合同', async () => {
const formData = new FormData()
vi.mocked(http.request).mockResolvedValue({
success: true,
data: { successCount: 5, failedItems: [] }
})
const result = await batchUploadContracts(formData)
expect(http.request).toHaveBeenCalledWith('post', '/api/contract/batch-upload', {
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
expect(result.data.successCount).toBe(5)
})
})
describe('updateContract', () => {
it('应 PUT 更新合同', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await updateContract(1, { name: '更新名称' })
expect(http.request).toHaveBeenCalledWith('put', '/api/contract/1', {
data: { name: '更新名称' }
})
})
})
describe('deleteContract', () => {
it('应 DELETE 删除合同', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await deleteContract(1)
expect(http.request).toHaveBeenCalledWith('delete', '/api/contract/1')
})
})
describe('Folder operations', () => {
it('getContractFolders 应返回文件夹列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await getContractFolders()
expect(http.request).toHaveBeenCalledWith('get', '/api/contract/folders')
})
it('createContractFolder 应 POST 创建文件夹', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: { id: 1 } })
await createContractFolder({ name: '新文件夹' })
expect(http.request).toHaveBeenCalledWith('post', '/api/contract/folders', { data: { name: '新文件夹' } })
})
it('deleteContractFolder 应 DELETE 文件夹', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await deleteContractFolder(1)
expect(http.request).toHaveBeenCalledWith('delete', '/api/contract/folders/1')
})
})
describe('Batch operations', () => {
it('getContractBatches 应返回批次列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await getContractBatches()
expect(http.request).toHaveBeenCalledWith('get', '/api/contract/batches', { params: undefined })
})
it('getContractBatches 带 folderId 参数', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await getContractBatches({ folderId: 1 })
expect(http.request).toHaveBeenCalledWith('get', '/api/contract/batches', { params: { folderId: 1 } })
})
})
})
```
- [ ] **Step 2: 运行测试验证**
Run:
```bash
cd frontend && pnpm test src/api/__tests__/contract.spec.ts
```
Expected: 所有测试通过
- [ ] **Step 3: 提交**
```bash
cd frontend && git add src/api/__tests__/contract.spec.ts
git commit -m "test(frontend): add contract API tests"
```
---
### Task 4: User API 测试
**Files:**
- Create: `frontend/src/api/__tests__/user.spec.ts`
- [ ] **Step 1: 读取 user.ts 了解 API 结构**
```typescript
// frontend/src/api/user.ts (参考)
export const login = (data: { username: string; password: string }) => {
return http.request<BaseResponse>("post", "/login", { data });
};
export const register = (data: RegisterData) => {
return http.request<BaseResponse>("post", "/auth/register", { data });
};
```
- [ ] **Step 2: 创建 user.spec.ts**
```typescript
// frontend/src/api/__tests__/user.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { http } from '@/utils/http'
vi.mock('@/utils/http', () => ({
http: {
request: vi.fn()
}
}))
// 动态导入以应用 mock
const { login, register, getUser, refreshToken } = await import('../user')
describe('User API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('login', () => {
it('应 POST 登录请求', async () => {
vi.mocked(http.request).mockResolvedValue({
success: true,
data: { accessToken: 'token123', refreshToken: 'refresh123' }
})
const result = await login({ username: 'test', password: '123456' })
expect(http.request).toHaveBeenCalledWith('post', '/login', {
data: { username: 'test', password: '123456' }
})
expect(result.success).toBe(true)
})
})
describe('register', () => {
it('应 POST 注册请求', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await register({ username: 'newuser', email: 'test@example.com', password: '123456' })
expect(http.request).toHaveBeenCalledWith('post', '/auth/register', {
data: { username: 'newuser', email: 'test@example.com', password: '123456' }
})
})
})
describe('getUser', () => {
it('应 GET 用户信息', async () => {
vi.mocked(http.request).mockResolvedValue({
success: true,
data: { id: 1, username: 'test', email: 'test@example.com' }
})
const result = await getUser(1)
expect(http.request).toHaveBeenCalledWith('get', '/api/users/1')
expect(result.data.username).toBe('test')
})
})
describe('refreshToken', () => {
it('应 POST 刷新 token', async () => {
vi.mocked(http.request).mockResolvedValue({
success: true,
data: { accessToken: 'newtoken', refreshToken: 'newrefresh' }
})
await refreshToken({ refreshToken: 'oldrefresh' })
expect(http.request).toHaveBeenCalledWith('post', '/refresh-token', {
data: { refreshToken: 'oldrefresh' }
})
})
})
})
```
- [ ] **Step 3: 运行测试**
```bash
cd frontend && pnpm test src/api/__tests__/user.spec.ts
```
- [ ] **Step 4: 提交**
```bash
git add src/api/__tests__/user.spec.ts
git commit -m "test(frontend): add user API tests"
```
---
### Task 5: ZSP Contract API 测试
**Files:**
- Create: `frontend/src/api/__tests__/zspContract.spec.ts`
- [ ] **Step 1: 创建 zspContract.spec.ts**
```typescript
// frontend/src/api/__tests__/zspContract.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { http } from '@/utils/http'
vi.mock('@/utils/http', () => ({
http: {
request: vi.fn()
}
}))
const api = await import('../zspContract')
describe('ZSP Contract API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Folder operations', () => {
it('getFolders 应返回文件夹列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.getFolders()
expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/folders')
})
it('createFolder 应创建文件夹', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.createFolder({ name: '新文件夹', parentId: null })
expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/folders', {
data: { name: '新文件夹', parentId: null }
})
})
})
describe('Contract operations', () => {
it('getContracts 应返回合同列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.getContracts()
expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts')
})
it('getContractDetail 应返回合同详情', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: { id: 1 } })
await api.getContractDetail(1)
expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts/1')
})
it('uploadContract 应上传合同文件', async () => {
const formData = new FormData()
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.uploadContract(formData)
expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/contracts', {
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
})
})
describe('Detail operations', () => {
it('getContractDetails 应返回明细列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.getContractDetails(1)
expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts/1/details')
})
it('importDetailsFromExcel 应导入 Excel', async () => {
const formData = new FormData()
vi.mocked(http.request).mockResolvedValue({ success: true, data: { count: 10 } })
await api.importDetailsFromExcel(1, formData)
expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-contract/contracts/1/details/import', {
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
})
it('exportDetailsToExcel 应导出 Excel', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.exportDetailsToExcel(1)
expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-contract/contracts/1/details/export')
})
})
})
```
- [ ] **Step 2: 运行测试**
```bash
cd frontend && pnpm test src/api/__tests__/zspContract.spec.ts
```
- [ ] **Step 3: 提交**
```bash
git add src/api/__tests__/zspContract.spec.ts
git commit -m "test(frontend): add zspContract API tests"
```
---
### Task 6: ZSP Finances API 测试
**Files:**
- Create: `frontend/src/api/__tests__/zspFinances.spec.ts`
- [ ] **Step 1: 创建 zspFinances.spec.ts**
```typescript
// frontend/src/api/__tests__/zspFinances.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { http } from '@/utils/http'
vi.mock('@/utils/http', () => ({
http: {
request: vi.fn()
}
}))
const api = await import('../zspFinances')
describe('ZSP Finances API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// 货代费用
describe('Freight Charges', () => {
it('listFreightCharges 应返回列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.listFreightCharges()
expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-finances/freight-charges')
})
it('createFreightCharge 应创建记录', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.createFreightCharge({ blNumber: 'BL001', amount: 1000 })
expect(http.request).toHaveBeenCalledWith('post', '/api/zsp-finances/freight-charges', {
data: { blNumber: 'BL001', amount: 1000 }
})
})
})
// 杂费
describe('Misc Charges', () => {
it('listMiscCharges 应返回列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.listMiscCharges()
expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-finances/misc-charges')
})
})
// 服务费
describe('Service Fees', () => {
it('listServiceFees 应返回列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.listServiceFees()
expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-finances/service-fees')
})
})
// 成本构成
describe('Cost Breakdowns', () => {
it('listCostBreakdowns 应返回列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.listCostBreakdowns()
expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-finances/cost-breakdowns')
})
})
// 利润记录
describe('Profit Records', () => {
it('listProfitRecords 应返回列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.listProfitRecords()
expect(http.request).toHaveBeenCalledWith('get', '/api/zsp-finances/profit-records')
})
})
})
```
- [ ] **Step 2: 运行测试**
```bash
cd frontend && pnpm test src/api/__tests__/zspFinances.spec.ts
```
- [ ] **Step 3: 提交**
```bash
git add src/api/__tests__/zspFinances.spec.ts
git commit -m "test(frontend): add zspFinances API tests"
```
---
### Task 7: Apply Delivery API 测试
**Files:**
- Create: `frontend/src/api/__tests__/applyDelivery.spec.ts`
- [ ] **Step 1: 创建 applyDelivery.spec.ts**
```typescript
// frontend/src/api/__tests__/applyDelivery.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { http } from '@/utils/http'
vi.mock('@/utils/http', () => ({
http: {
request: vi.fn()
}
}))
const api = await import('../applyDelivery')
describe('Apply Delivery API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('listApplyDelivery 应返回提货申请列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: { items: [], total: 0 } })
await api.listApplyDelivery()
expect(http.request).toHaveBeenCalledWith('get', '/api/apply-delivery/list')
})
it('getApplyDelivery 应返回详情', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: { id: 1 } })
await api.getApplyDelivery(1)
expect(http.request).toHaveBeenCalledWith('get', '/api/apply-delivery/detail/1')
})
it('createApplyDelivery 应创建申请', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: { id: 1 } })
await api.createApplyDelivery({ blNumber: 'BL001' })
expect(http.request).toHaveBeenCalledWith('post', '/api/apply-delivery/create', {
data: { blNumber: 'BL001' }
})
})
it('updateApplyDelivery 应更新申请', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.updateApplyDelivery(1, { status: 'approved' })
expect(http.request).toHaveBeenCalledWith('put', '/api/apply-delivery/update/1', {
data: { status: 'approved' }
})
})
it('cancelApplyDelivery 应取消申请', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.cancelApplyDelivery(1)
expect(http.request).toHaveBeenCalledWith('put', '/api/apply-delivery/cancel/1')
})
it('deleteApplyDelivery 应删除申请', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.deleteApplyDelivery(1)
expect(http.request).toHaveBeenCalledWith('delete', '/api/apply-delivery/1')
})
})
```
- [ ] **Step 2: 运行测试**
```bash
cd frontend && pnpm test src/api/__tests__/applyDelivery.spec.ts
```
- [ ] **Step 3: 提交**
```bash
git add src/api/__tests__/applyDelivery.spec.ts
git commit -m "test(frontend): add applyDelivery API tests"
```
---
### Task 8: Delivery Details API 测试
**Files:**
- Create: `frontend/src/api/__tests__/deliveryDetails.spec.ts`
- [ ] **Step 1: 创建 deliveryDetails.spec.ts**
```typescript
// frontend/src/api/__tests__/deliveryDetails.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { http } from '@/utils/http'
vi.mock('@/utils/http', () => ({
http: {
request: vi.fn()
}
}))
const api = await import('../deliveryDetails')
describe('Delivery Details API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('listDeliveryDetails 应返回出库明细列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: { items: [], total: 0 } })
await api.listDeliveryDetails()
expect(http.request).toHaveBeenCalledWith('get', '/api/delivery-details/list')
})
it('getDeliveryDetail 应返回详情', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: { id: 1 } })
await api.getDeliveryDetail(1)
expect(http.request).toHaveBeenCalledWith('get', '/api/delivery-details/detail/1')
})
it('createDeliveryDetail 应创建出库单', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.createDeliveryDetail({ quantity: 100 })
expect(http.request).toHaveBeenCalledWith('post', '/api/delivery-details/create', {
data: { quantity: 100 }
})
})
it('updateDeliveryDetail 应更新出库单', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.updateDeliveryDetail(1, { quantity: 200 })
expect(http.request).toHaveBeenCalledWith('put', '/api/delivery-details/update/1', {
data: { quantity: 200 }
})
})
it('deleteDeliveryDetail 应删除出库单', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.deleteDeliveryDetail(1)
expect(http.request).toHaveBeenCalledWith('delete', '/api/delivery-details/1')
})
})
```
- [ ] **Step 2: 运行测试**
```bash
cd frontend && pnpm test src/api/__tests__/deliveryDetails.spec.ts
```
- [ ] **Step 3: 提交**
```bash
git add src/api/__tests__/deliveryDetails.spec.ts
git commit -m "test(frontend): add deliveryDetails API tests"
```
---
### Task 9: Inventory API 测试
**Files:**
- Create: `frontend/src/api/__tests__/inventory.spec.ts`
- [ ] **Step 1: 创建 inventory.spec.ts**
```typescript
// frontend/src/api/__tests__/inventory.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { http } from '@/utils/http'
vi.mock('@/utils/http', () => ({
http: {
request: vi.fn()
}
}))
const api = await import('../inventory')
describe('Inventory API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Warehouse', () => {
it('listWarehouses 应返回仓库列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.listWarehouses()
expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/warehouses')
})
it('createWarehouse 应创建仓库', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.createWarehouse({ name: '新仓库' })
expect(http.request).toHaveBeenCalledWith('post', '/api/inventory/warehouses/create', {
data: { name: '新仓库' }
})
})
})
describe('Inbound', () => {
it('listInbound 应返回入库列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: { items: [], total: 0 } })
await api.listInbound()
expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/inbound/list')
})
it('createInbound 应创建入库记录', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.createInbound({ warehouse: '仓库A', commodity: '大豆' })
expect(http.request).toHaveBeenCalledWith('post', '/api/inventory/inbound/create', {
data: { warehouse: '仓库A', commodity: '大豆' }
})
})
})
describe('Summary', () => {
it('inventorySummary 应返回库存汇总', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.inventorySummary()
expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/summary')
})
it('inventorySummary 带筛选参数', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.inventorySummary({ warehouse: '仓库A' })
expect(http.request).toHaveBeenCalledWith('get', '/api/inventory/summary', {
params: { warehouse: '仓库A' }
})
})
})
})
```
- [ ] **Step 2: 运行测试**
```bash
cd frontend && pnpm test src/api/__tests__/inventory.spec.ts
```
- [ ] **Step 3: 提交**
```bash
git add src/api/__tests__/inventory.spec.ts
git commit -m "test(frontend): add inventory API tests"
```
---
### Task 10: Ownership Transfer API 测试
**Files:**
- Create: `frontend/src/api/__tests__/ownershipTransfer.spec.ts`
- [ ] **Step 1: 创建 ownershipTransfer.spec.ts**
```typescript
// frontend/src/api/__tests__/ownershipTransfer.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { http } from '@/utils/http'
vi.mock('@/utils/http', () => ({
http: {
request: vi.fn()
}
}))
const api = await import('../ownershipTransfer')
describe('Ownership Transfer API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('listOwnershipTransfers 应返回列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: { items: [], total: 0 } })
await api.listOwnershipTransfers()
expect(http.request).toHaveBeenCalledWith('get', '/api/ownership-transfer/list')
})
it('getOwnershipTransfer 应返回详情', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: { id: 1 } })
await api.getOwnershipTransfer(1)
expect(http.request).toHaveBeenCalledWith('get', '/api/ownership-transfer/detail/1')
})
it('createOwnershipTransfer 应创建货权转移', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.createOwnershipTransfer({ transferor: '公司A', transferee: '公司B' })
expect(http.request).toHaveBeenCalledWith('post', '/api/ownership-transfer/create', {
data: { transferor: '公司A', transferee: '公司B' }
})
})
it('createOwnershipTransferWithFiles 应带文件创建', async () => {
const formData = new FormData()
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.createOwnershipTransferWithFiles(formData)
expect(http.request).toHaveBeenCalledWith('post', '/api/ownership-transfer/create-with-files', {
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
})
it('deleteOwnershipTransfer 应删除', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.deleteOwnershipTransfer(1)
expect(http.request).toHaveBeenCalledWith('delete', '/api/ownership-transfer/1')
})
})
```
- [ ] **Step 2: 运行测试**
```bash
cd frontend && pnpm test src/api/__tests__/ownershipTransfer.spec.ts
```
- [ ] **Step 3: 提交**
```bash
git add src/api/__tests__/ownershipTransfer.spec.ts
git commit -m "test(frontend): add ownershipTransfer API tests"
```
---
### Task 11: Company API 测试
**Files:**
- Create: `frontend/src/api/__tests__/company.spec.ts`
- [ ] **Step 1: 创建 company.spec.ts**
```typescript
// frontend/src/api/__tests__/company.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { http } from '@/utils/http'
vi.mock('@/utils/http', () => ({
http: {
request: vi.fn()
}
}))
const api = await import('../company')
describe('Company API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Folders', () => {
it('listFolders 应返回文件夹列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.listFolders()
expect(http.request).toHaveBeenCalledWith('get', '/api/company/folders')
})
it('createFolder 应创建文件夹', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.createFolder({ name: '新文件夹' })
expect(http.request).toHaveBeenCalledWith('post', '/api/company/folders', {
data: { name: '新文件夹' }
})
})
})
describe('Files', () => {
it('listFiles 应返回文件列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.listFiles()
expect(http.request).toHaveBeenCalledWith('get', '/api/company/files')
})
it('uploadFiles 应上传文件', async () => {
const formData = new FormData()
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.uploadFiles(formData)
expect(http.request).toHaveBeenCalledWith('post', '/api/company/files/upload', {
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
})
})
})
```
- [ ] **Step 2: 运行测试**
```bash
cd frontend && pnpm test src/api/__tests__/company.spec.ts
```
- [ ] **Step 3: 提交**
```bash
git add src/api/__tests__/company.spec.ts
git commit -m "test(frontend): add company API tests"
```
---
### Task 12: Invoice API 测试
**Files:**
- Create: `frontend/src/api/__tests__/invoice.spec.ts`
- [ ] **Step 1: 创建 invoice.spec.ts**
```typescript
// frontend/src/api/__tests__/invoice.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { http } from '@/utils/http'
vi.mock('@/utils/http', () => ({
http: {
request: vi.fn()
}
}))
const api = await import('../invoice')
describe('Invoice API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('listInvoices 应返回发票列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.listInvoices()
expect(http.request).toHaveBeenCalled()
})
it('createInvoice 应创建发票', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.createInvoice({ invoiceNumber: 'INV001', amount: 10000 })
expect(http.request).toHaveBeenCalled()
})
it('updateInvoice 应更新发票', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.updateInvoice(1, { amount: 20000 })
expect(http.request).toHaveBeenCalled()
})
it('deleteInvoice 应删除发票', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.deleteInvoice(1)
expect(http.request).toHaveBeenCalled()
})
})
```
- [ ] **Step 2: 运行测试**
```bash
cd frontend && pnpm test src/api/__tests__/invoice.spec.ts
```
- [ ] **Step 3: 提交**
```bash
git add src/api/__tests__/invoice.spec.ts
git commit -m "test(frontend): add invoice API tests"
```
---
### Task 13: Settlement & Reconciliation API 测试
**Files:**
- Create: `frontend/src/api/__tests__/settlement.spec.ts`
- Create: `frontend/src/api/__tests__/reconciliation.spec.ts`
- [ ] **Step 1: 创建 settlement.spec.ts**
```typescript
// frontend/src/api/__tests__/settlement.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { http } from '@/utils/http'
vi.mock('@/utils/http', () => ({
http: {
request: vi.fn()
}
}))
const api = await import('../settlement')
describe('Settlement API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('listSettlements 应返回结算单列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.listSettlements()
expect(http.request).toHaveBeenCalled()
})
it('createSettlement 应创建结算单', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.createSettlement({ contractCode: 'C001' })
expect(http.request).toHaveBeenCalled()
})
})
```
- [ ] **Step 2: 创建 reconciliation.spec.ts**
```typescript
// frontend/src/api/__tests__/reconciliation.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { http } from '@/utils/http'
vi.mock('@/utils/http', () => ({
http: {
request: vi.fn()
}
}))
const api = await import('../reconciliation')
describe('Reconciliation API', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('listReconciliations 应返回对账单列表', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true, data: [] })
await api.listReconciliations()
expect(http.request).toHaveBeenCalled()
})
it('createReconciliation 应创建对账单', async () => {
vi.mocked(http.request).mockResolvedValue({ success: true })
await api.createReconciliation({ companyName: '公司A' })
expect(http.request).toHaveBeenCalled()
})
})
```
- [ ] **Step 3: 运行测试**
```bash
cd frontend && pnpm test src/api/__tests__/settlement.spec.ts src/api/__tests__/reconciliation.spec.ts
```
- [ ] **Step 4: 提交**
```bash
git add src/api/__tests__/settlement.spec.ts src/api/__tests__/reconciliation.spec.ts
git commit -m "test(frontend): add settlement and reconciliation API tests"
```
---
### Task 14: 运行完整测试套件并验证覆盖率
**Files:**
- Run tests
- [ ] **Step 1: 运行所有 API 测试**
```bash
cd frontend && pnpm test
```
Expected: 所有测试通过
- [ ] **Step 2: 运行覆盖率报告**
```bash
cd frontend && pnpm test:coverage
```
Expected: 覆盖率 > 80%
- [ ] **Step 3: 查看覆盖率 HTML 报告**
```bash
open frontend/coverage/index.html
```
- [ ] **Step 4: 最终提交**
```bash
git add -A
git commit -m "feat(frontend): complete API test infrastructure with vitest"
```
---
## Self-Review
### Spec Coverage Check
| Spec Requirement | Task |
|------------------|------|
| Vitest 配置 | Task 1 |
| package.json 脚本 | Task 1 |
| Mock 工具函数 | Task 2 |
| 11 API 模块测试 | Tasks 3-13 |
| 覆盖率报告 | Task 14 |
### Placeholder Scan
- 无 TBD/TODO 占位符
- 所有代码步骤包含完整实现
- 所有命令包含预期输出
### Type Consistency
- `http.request` mock 在所有测试文件中一致使用 `vi.mock('@/utils/http')`
- 响应结构统一为 `{ success: boolean, data?: any, message?: string }`
---
Plan complete and saved to `docs/superpowers/plans/2026-04-25-frontend-api-test-infra.md`. Two execution options:
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
**Which approach?**