Files
datahub/frontend/src/pages/__tests__/login.spec.ts
T
2026-03-18 16:34:27 +08:00

220 lines
6.7 KiB
TypeScript
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.
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { mount, flushPromises } from '@vue/test-utils'
import { nextTick } from 'vue'
import { message } from 'ant-design-vue'
import { ApiError } from '@/types/api'
import { useUserStore } from '@/stores/user'
// jsdom 不支持 matchMediaAnt Design Vue 响应式布局需要
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}),
})
vi.mock('@/utils/request', () => ({
api: {
get: vi.fn(),
post: vi.fn(),
},
}))
const mockPush = vi.fn()
const mockRoute = { query: {} }
vi.mock('vue-router', () => ({
useRouter: () => ({ push: mockPush }),
useRoute: () => mockRoute,
}))
// Stub Brand component to avoid asset loading
vi.mock('@/components/Brand.vue', () => ({
default: { template: '<div class="brand-stub" />' },
}))
describe('Login Page', () => {
let wrapper: ReturnType<typeof mount>
beforeEach(() => {
setActivePinia(createPinia())
localStorage.clear()
vi.restoreAllMocks()
mockPush.mockReset()
mockRoute.query = {}
vi.useFakeTimers()
})
afterEach(() => {
wrapper?.unmount()
vi.useRealTimers()
})
async function mountPage() {
const { default: LoginPage } = await import('../login.vue')
wrapper = mount(LoginPage, {
attachTo: document.body,
global: {
stubs: {
UserOutlined: { template: '<span />' },
LockOutlined: { template: '<span />' },
},
},
})
await flushPromises()
await nextTick()
return wrapper
}
it('renders login form with username, password inputs and submit button', async () => {
await mountPage()
const html = wrapper.html()
// 验证输入框存在
expect(html).toContain('请输入用户名')
expect(html).toContain('请输入密码')
// 验证提交按钮存在
expect(html).toContain('登录')
})
// 辅助:填写表单并提交,绕过 Ant Design 表单验证
async function submitLogin(
fields: { username: string; password: string; remember?: boolean },
) {
const vm = wrapper.getCurrentComponent().setupState
vm.formState.username = fields.username
vm.formState.password = fields.password
if (fields.remember !== undefined) vm.formState.remember = fields.remember
vm.formRef = { validate: vi.fn().mockResolvedValue(true) }
await vm.handleSubmit()
await flushPromises()
}
it('calls /api/v1/login, sets token/user and redirects to /', async () => {
const { api } = await import('@/utils/request')
const mockResponse = {
access_token: 'a.b.c',
refresh_token: 'r.r.r',
user: { id: 1, username: 'admin', email: 'a@b.com', role: 'admin', status: 1 },
}
vi.mocked(api.post).mockResolvedValueOnce(mockResponse)
await mountPage()
const store = useUserStore()
await submitLogin({ username: 'admin', password: 'password' })
expect(api.post).toHaveBeenCalledWith('/api/v1/login', {
username: 'admin',
password: 'password',
})
expect(store.token).toBe('a.b.c')
expect(store.user).toEqual(mockResponse.user)
vi.advanceTimersByTime(500)
expect(mockPush).toHaveBeenCalledWith('/')
})
it('passes remember=false to setToken when unchecked', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.post).mockResolvedValueOnce({
access_token: 'a.b.c',
refresh_token: 'r.r.r',
user: { id: 1, username: 'u', email: 'u@u.com', role: 'user', status: 1 },
})
await mountPage()
await submitLogin({ username: 'user', password: 'pass', remember: false })
expect(localStorage.getItem('access_token')).toBeNull()
expect(localStorage.getItem('user')).toBeNull()
})
it('redirects to safe path from query param', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.post).mockResolvedValueOnce({
access_token: 'a.b.c',
refresh_token: 'r.r.r',
user: { id: 1, username: 'u', email: 'u@u.com', role: 'user', status: 1 },
})
mockRoute.query = { redirect: '/dashboard' }
await mountPage()
await submitLogin({ username: 'user', password: 'pass' })
vi.advanceTimersByTime(500)
expect(mockPush).toHaveBeenCalledWith('/dashboard')
})
it('blocks //evil redirect and falls back to /', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.post).mockResolvedValueOnce({
access_token: 'a.b.c',
refresh_token: 'r.r.r',
user: { id: 1, username: 'u', email: 'u@u.com', role: 'user', status: 1 },
})
mockRoute.query = { redirect: '//evil.com' }
await mountPage()
await submitLogin({ username: 'user', password: 'pass' })
vi.advanceTimersByTime(500)
expect(mockPush).toHaveBeenCalledWith('/')
})
it('blocks http:// redirect and falls back to /', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.post).mockResolvedValueOnce({
access_token: 'a.b.c',
refresh_token: 'r.r.r',
user: { id: 1, username: 'u', email: 'u@u.com', role: 'user', status: 1 },
})
mockRoute.query = { redirect: 'http://evil.com' }
await mountPage()
await submitLogin({ username: 'user', password: 'pass' })
vi.advanceTimersByTime(500)
expect(mockPush).toHaveBeenCalledWith('/')
})
it('shows error message on ApiError', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.post).mockRejectedValueOnce(new ApiError('用户名或密码错误', 401))
const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as any)
await mountPage()
await submitLogin({ username: 'user', password: 'wrong' })
expect(errorSpy).toHaveBeenCalledWith('用户名或密码错误')
errorSpy.mockRestore()
})
it('shows network error message on generic error', async () => {
const { api } = await import('@/utils/request')
vi.mocked(api.post).mockRejectedValueOnce(new Error('Network failure'))
const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as any)
await mountPage()
await submitLogin({ username: 'user', password: 'pass' })
expect(errorSpy).toHaveBeenCalledWith('登录失败,请检查网络连接')
errorSpy.mockRestore()
})
it('navigates to register page', async () => {
await mountPage()
const vm = wrapper.getCurrentComponent().setupState
vm.goToRegister()
expect(mockPush).toHaveBeenCalledWith('/register')
})
})