# FMS Vue 前端测试规范(AI 全权测试版) 本文档是 fms-vue 前端测试的**唯一权威约定**。测试工作(编写、运行、补覆盖)默认全权交给 AI 执行,本文档保证 AI 在任何时候都能: - 知道**测什么**(每个对象的测试清单) - 知道**怎么写**(mock 模板与装配方式) - 知道**怎么跑**(按文件/页面/全部/覆盖率) - 知道**什么时候算完**(覆盖率门槛与验收标准) ## 1. 测试命令速查 | 场景 | 命令 | | --- | --- | | 跑全部测试(CI 风格,一次性) | `pnpm test`(同 `pnpm test:all`) | | 只测一个文件 | `pnpm test:file tests/views/login.spec.js` | | 只测一个页面/功能对应的测试文件 | `pnpm test:file tests/views/<页面>.spec.js` | | 只测一个目录(如全部 store) | `pnpm test:file tests/stores` | | 带覆盖率跑(检查缺口) | `pnpm test:coverage` | | 监听模式(改代码自动重跑) | `pnpm test:watch` | 注意:`pnpm test` 与 `pnpm test:coverage` 的区别——**只有 `test:coverage` 会检查 60% 阈值**。普通 `pnpm test` 只看用例红绿,不看覆盖率。 ## 2. 目录与命名约定 ``` tests/ setup.js # 全局环境:jsdom polyfill、nprogress mock(已就绪,勿删) helpers/ app.js # 统一装配:createTestPinia / createTestRouter / mountPage(已就绪) unit/ # 纯函数/工具测试(无 DOM 或极轻 DOM) utils-*.spec.js # 对应 src/utils/* theme.spec.js # 对应 src/theme/* ui-utils-*.spec.js # 对应 src/components/ui/utils/* stores/ # pinia store 测试 auth.spec.js app.spec.js preferences.spec.js components/ # UI 组件测试(冒烟 + 核心交互) input.spec.js select.spec.js ... 每个组件一个文件 views/ # 页面测试 login.spec.js # 已存在 dashboard.spec.js ... services/ # http 拦截器/请求层测试 http.spec.js router-auth.spec.js # 路由守卫集成测试(已存在,留在根目录) permissions.spec.js # store 测试(已存在,留在根目录,不强制迁移) ``` 规则: - 测试文件一律 `*.spec.js`,与源码文件同名(`login/index.vue` → `tests/views/login.spec.js`)。 - **每个被测源码文件最多对应一个 spec**,禁止把多个对象的测试堆进一个文件。 - `describe` 第一层用中文描述被测对象(如 `describe('登录页')`);用例名用中文描述业务场景。 - 新测试按上表归类;**已存在的测试文件(permissions.spec.js、router-auth.spec.js、views/login.spec.js)不必迁移**,保持原位即可,新增的按新约定放。 ## 3. 编写模板 ### 3.1 页面测试(用 mountPage) 所有请求都收敛在 `@/services/api`,页面测试只需 mock 这一个模块: ```js import { describe, it, expect, beforeEach, vi } from 'vitest' import { nextTick } from 'vue' import { flushPromises } from '@vue/test-utils' import LoginPage from '@/views/login/index.vue' import { useAuthStore } from '@/stores/auth' import { mountPage } from '../helpers/app.js' // —— 单一接缝:mock 业务 API 层,确定性驱动页面状态 —— vi.mock('@/services/api', () => ({ loginApi: vi.fn(), loadDataApi: vi.fn(), })) // 命令式组件(Message 等)需要 mock,避免 jsdom 下创建 DOM 容器 vi.mock('@/components/ui/message/message-manager.js', () => { const m = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() } return { Message: m, default: m } }) import { loginApi } from '@/services/api' // 子组件用 shallowMount 自动打桩;需要触发子组件事件时给一个稳定 name const formStub = { name: 'Form', template: '
' } beforeEach(() => { localStorage.clear() sessionStorage.clear() loginApi.mockReset() }) describe('登录页', () => { it('登录成功:调用 loginApi、写入会话、跳转 redirect', async () => { loginApi.mockResolvedValue({ data: { token: 'TK', user: { id: 1, account: 'admin' }, orgid: 'ORG1' }, code: 0, success: true, }) const { wrapper, router } = await mountPage(LoginPage, { location: '/login?redirect=/dashboard', stubs: { Form: formStub }, }) await wrapper.findComponent({ name: 'Form' }).vm.$emit('submit', { orgId: 'ORG1', account: 'admin', password: 'secret' }) await flushPromises() await nextTick() expect(loginApi).toHaveBeenCalledWith('ORG1', 'admin', 'secret') expect(useAuthStore().isAuthenticated).toBe(true) expect(router.currentRoute.value.fullPath).toBe('/dashboard') }) }) ``` 要点: - `beforeEach` 里必须清理 `localStorage` / `sessionStorage`(persist 插件会写它们,跨用例泄漏)。 - 用 `flushPromises()` + `nextTick()` 冲刷异步,不要用固定 `setTimeout`。 - 需要路由参数时用 `mountPage(Comp, { location: '/detail/123' })`。 ### 3.2 store 测试 ```js import { describe, it, expect, beforeEach, vi } from 'vitest' import { useAuthStore } from '@/stores/auth' import { createTestPinia } from '../helpers/app.js' vi.mock('@/services/api', () => ({ loadDataApi: vi.fn() })) beforeEach(() => { localStorage.clear() }) describe('auth store', () => { it('setSession 写入会话', () => { createTestPinia() // 内部已 install,persist 插件真实注册(见 helpers/app.js 注释) const auth = useAuthStore() auth.setSession({ token: 't', user: { id: 1 } }, 'ORG') expect(auth.isAuthenticated).toBe(true) }) }) ``` **必须用 `createTestPinia()`,不要手动 `createPinia()`**: - pinia 4 中 `pinia.use()` 在未安装 app 前只把插件放入队列,直接 `setActivePinia(createPinia())` 会让 persist 插件静默失效,store 上缺 `$hydrate`/`$persist`。 - `createTestPinia()` 内部用假 app 触发 `install()`,行为与页面测试(经 @vue/test-utils 挂载)一致。 ### 3.3 纯函数测试(unit) ```js import { describe, it, expect } from 'vitest' import { buildTree, sortTree } from '@/utils/tree' describe('utils/tree', () => { it('buildTree 按 parentKey 构建树', () => { const list = [ { b_id: 1, b_parent_id: null }, { b_id: 2, b_parent_id: 1 }, ] const tree = buildTree(list) expect(tree).toHaveLength(1) expect(tree[0].children).toHaveLength(1) }) }) ``` 纯函数测试**不需要** mountPage / pinia / mock,直接 import 断言即可。 ### 3.4 UI 组件测试(components) ```js import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import Input from '@/components/ui/input/input.vue' describe('Input 组件', () => { it('渲染输入框并转发 update:modelValue', async () => { const wrapper = mount(Input, { props: { modelValue: '' } }) const input = wrapper.find('[data-slot="input"]') await input.setValue('abc') expect(wrapper.emitted('update:modelValue')[0]).toEqual(['abc']) }) }) ``` 组件测试用 `mount`(组件自身逻辑需要真实渲染);页面测试用 `shallowMount`(经 mountPage)。定位元素优先用组件已有的 `data-slot`、`aria-label`、`role` 等稳定属性,**不要依赖 CSS class 或文本**。 ## 4. 每个对象的测试清单(AI 补测试时逐项对照) ### 4.1 services(最高优先级,当前 0%) `tests/services/http.spec.js` 必须覆盖: - [ ] 请求拦截器:有 token 时注入 `Authorization: Bearer