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(), })) // —— mock 命令式 Message(避免 jsdom 下创建/操作 DOM 容器)—— // 该模块同时有具名导出与默认导出(页面用默认导入),两者都要提供。 // 注意:vi.mock 工厂会被提升,不能引用顶层变量,需内联。 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' import { Message } from '@/components/ui/message/message-manager.js' // 把 Form 打桩为可定位、可 emit 的组件:页面测试只关心「自己逻辑」, // 子组件(Form/Input/Button)由 shallowMount 自动打桩,这里仅给 Form 一个稳定 name 以便触发 @submit。 const formStub = { name: 'Form', template: '
', } const validValues = { orgId: 'ORG1', account: 'admin', password: 'secret' } beforeEach(() => { // pinia persist 插件会把状态写入 localStorage,跨测试会泄漏,需清零 localStorage.clear() sessionStorage.clear() loginApi.mockReset() Message.success.mockReset() Message.error.mockReset() Message.warning.mockReset() }) describe('登录页', () => { it('渲染标题与品牌文案', async () => { const { wrapper } = await mountPage(LoginPage) expect(wrapper.text()).toContain('登录 FMS') expect(wrapper.text()).toContain('企业管理系统') }) it('无 reason 时不展示会话提示条', async () => { const { wrapper } = await mountPage(LoginPage) expect(wrapper.find('[role="alert"]').exists()).toBe(false) }) it('reason=expired:展示「登录已过期」提示', async () => { const { wrapper } = await mountPage(LoginPage, { location: '/login?reason=expired' }) await nextTick() const hint = wrapper.find('[role="alert"]') expect(hint.exists()).toBe(true) expect(hint.text()).toContain('登录已过期') }) it('reason=session-replaced:展示「已在其他设备登录」提示', async () => { const { wrapper } = await mountPage(LoginPage, { location: '/login?reason=session-replaced' }) await nextTick() expect(wrapper.find('[role="alert"]').text()).toContain('已在其他设备登录') }) it('表单校验失败:触发 onFailed,弹出 warning', async () => { const { wrapper } = await mountPage(LoginPage, { stubs: { Form: formStub } }) await wrapper.findComponent({ name: 'Form' }).vm.$emit('submit-failed') await nextTick() expect(Message.warning).toHaveBeenCalledWith('请填写完整的登录信息') }) 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', validValues) await flushPromises() await nextTick() await flushPromises() // 请求按页面入参发出 expect(loginApi).toHaveBeenCalledWith('ORG1', 'admin', 'secret') // 会话已写入 const auth = useAuthStore() expect(auth.isAuthenticated).toBe(true) expect(auth.loginInfo.token).toBe('TK') expect(auth.userInfo.orgId).toBe('ORG1') // 跳转目标 expect(router.currentRoute.value.fullPath).toBe('/dashboard') // 成功提示 expect(Message.success).toHaveBeenCalledWith('登录成功') }) it('登录失败:弹 error,且不写入会话', async () => { loginApi.mockRejectedValue(new Error('账号或密码错误')) const { wrapper } = await mountPage(LoginPage, { stubs: { Form: formStub } }) await wrapper.findComponent({ name: 'Form' }).vm.$emit('submit', validValues) await flushPromises() await nextTick() await flushPromises() expect(Message.error).toHaveBeenCalledWith('账号或密码错误') expect(useAuthStore().isAuthenticated).toBe(false) }) })