fix frontend phase0 bugs

This commit is contained in:
2026-03-18 16:34:27 +08:00
parent 2d063870fe
commit b6abf43075
3 changed files with 50 additions and 52 deletions
+20 -48
View File
@@ -85,6 +85,19 @@ describe('Login Page', () => {
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 () => { it('calls /api/v1/login, sets token/user and redirects to /', async () => {
const { api } = await import('@/utils/request') const { api } = await import('@/utils/request')
const mockResponse = { const mockResponse = {
@@ -96,17 +109,7 @@ describe('Login Page', () => {
await mountPage() await mountPage()
const store = useUserStore() const store = useUserStore()
await submitLogin({ username: 'admin', password: 'password' })
// 直接调用 handleSubmit 来绕过表单验证
// 通过组件 vm 访问 setup 暴露的方法
const vm = wrapper.getCurrentComponent().setupState
vm.formState.username = 'admin'
vm.formState.password = 'password'
vm.formRef = { value: { validate: vi.fn().mockResolvedValue(true) } }
// mock formRef.validate
vm.formRef = { validate: vi.fn().mockResolvedValue(true) }
await vm.handleSubmit()
await flushPromises()
expect(api.post).toHaveBeenCalledWith('/api/v1/login', { expect(api.post).toHaveBeenCalledWith('/api/v1/login', {
username: 'admin', username: 'admin',
@@ -128,13 +131,7 @@ describe('Login Page', () => {
}) })
await mountPage() await mountPage()
const vm = wrapper.getCurrentComponent().setupState await submitLogin({ username: 'user', password: 'pass', remember: false })
vm.formState.username = 'user'
vm.formState.password = 'pass'
vm.formState.remember = false
vm.formRef = { validate: vi.fn().mockResolvedValue(true) }
await vm.handleSubmit()
await flushPromises()
expect(localStorage.getItem('access_token')).toBeNull() expect(localStorage.getItem('access_token')).toBeNull()
expect(localStorage.getItem('user')).toBeNull() expect(localStorage.getItem('user')).toBeNull()
@@ -150,12 +147,7 @@ describe('Login Page', () => {
mockRoute.query = { redirect: '/dashboard' } mockRoute.query = { redirect: '/dashboard' }
await mountPage() await mountPage()
const vm = wrapper.getCurrentComponent().setupState await submitLogin({ username: 'user', password: 'pass' })
vm.formState.username = 'user'
vm.formState.password = 'pass'
vm.formRef = { validate: vi.fn().mockResolvedValue(true) }
await vm.handleSubmit()
await flushPromises()
vi.advanceTimersByTime(500) vi.advanceTimersByTime(500)
expect(mockPush).toHaveBeenCalledWith('/dashboard') expect(mockPush).toHaveBeenCalledWith('/dashboard')
@@ -171,12 +163,7 @@ describe('Login Page', () => {
mockRoute.query = { redirect: '//evil.com' } mockRoute.query = { redirect: '//evil.com' }
await mountPage() await mountPage()
const vm = wrapper.getCurrentComponent().setupState await submitLogin({ username: 'user', password: 'pass' })
vm.formState.username = 'user'
vm.formState.password = 'pass'
vm.formRef = { validate: vi.fn().mockResolvedValue(true) }
await vm.handleSubmit()
await flushPromises()
vi.advanceTimersByTime(500) vi.advanceTimersByTime(500)
expect(mockPush).toHaveBeenCalledWith('/') expect(mockPush).toHaveBeenCalledWith('/')
@@ -192,12 +179,7 @@ describe('Login Page', () => {
mockRoute.query = { redirect: 'http://evil.com' } mockRoute.query = { redirect: 'http://evil.com' }
await mountPage() await mountPage()
const vm = wrapper.getCurrentComponent().setupState await submitLogin({ username: 'user', password: 'pass' })
vm.formState.username = 'user'
vm.formState.password = 'pass'
vm.formRef = { validate: vi.fn().mockResolvedValue(true) }
await vm.handleSubmit()
await flushPromises()
vi.advanceTimersByTime(500) vi.advanceTimersByTime(500)
expect(mockPush).toHaveBeenCalledWith('/') expect(mockPush).toHaveBeenCalledWith('/')
@@ -209,12 +191,7 @@ describe('Login Page', () => {
const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as any) const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as any)
await mountPage() await mountPage()
const vm = wrapper.getCurrentComponent().setupState await submitLogin({ username: 'user', password: 'wrong' })
vm.formState.username = 'user'
vm.formState.password = 'wrong'
vm.formRef = { validate: vi.fn().mockResolvedValue(true) }
await vm.handleSubmit()
await flushPromises()
expect(errorSpy).toHaveBeenCalledWith('用户名或密码错误') expect(errorSpy).toHaveBeenCalledWith('用户名或密码错误')
errorSpy.mockRestore() errorSpy.mockRestore()
@@ -226,12 +203,7 @@ describe('Login Page', () => {
const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as any) const errorSpy = vi.spyOn(message, 'error').mockImplementation(() => ({}) as any)
await mountPage() await mountPage()
const vm = wrapper.getCurrentComponent().setupState await submitLogin({ username: 'user', password: 'pass' })
vm.formState.username = 'user'
vm.formState.password = 'pass'
vm.formRef = { validate: vi.fn().mockResolvedValue(true) }
await vm.handleSubmit()
await flushPromises()
expect(errorSpy).toHaveBeenCalledWith('登录失败,请检查网络连接') expect(errorSpy).toHaveBeenCalledWith('登录失败,请检查网络连接')
errorSpy.mockRestore() errorSpy.mockRestore()
+26 -2
View File
@@ -96,8 +96,9 @@ describe('useUserStore', () => {
}) })
describe('setUser', () => { describe('setUser', () => {
it('sets user info in store and localStorage', () => { it('persists to localStorage when remember=true', () => {
const store = useUserStore() const store = useUserStore()
store.setToken('h.p.s', 'r.p.s', true)
const userInfo = { id: 1, username: 'test', email: 't@t.com', role: 'user', status: 1 } const userInfo = { id: 1, username: 'test', email: 't@t.com', role: 'user', status: 1 }
store.setUser(userInfo) store.setUser(userInfo)
@@ -108,7 +109,17 @@ describe('useUserStore', () => {
expect(JSON.parse(localStorage.getItem('user')!)).toEqual(userInfo) expect(JSON.parse(localStorage.getItem('user')!)).toEqual(userInfo)
}) })
it('does not write to localStorage when remember=false', () => { it('does not persist to localStorage by default (remember=false)', () => {
const store = useUserStore()
const userInfo = { id: 1, username: 'test', email: 't@t.com', role: 'user', status: 1 }
store.setUser(userInfo)
expect(store.user).toEqual(userInfo)
expect(localStorage.getItem('user')).toBeNull()
})
it('does not write to localStorage after setToken(remember=false)', () => {
const store = useUserStore() const store = useUserStore()
store.setToken('h.p.s', 'r.p.s', false) store.setToken('h.p.s', 'r.p.s', false)
@@ -136,6 +147,19 @@ describe('useUserStore', () => {
expect(localStorage.getItem('refresh_token')).toBeNull() expect(localStorage.getItem('refresh_token')).toBeNull()
expect(localStorage.getItem('user')).toBeNull() expect(localStorage.getItem('user')).toBeNull()
}) })
it('resets _remember so subsequent setUser does not persist', () => {
const store = useUserStore()
store.setToken('h.p.s', 'r.p.s', true)
store.setUser({ id: 1, username: 'a', email: 'a@a.com', role: 'admin', status: 1 })
expect(localStorage.getItem('user')).not.toBeNull()
store.logout()
store.setUser({ id: 2, username: 'b', email: 'b@b.com', role: 'user', status: 1 })
expect(store.user?.username).toBe('b')
expect(localStorage.getItem('user')).toBeNull()
})
}) })
describe('fetchCurrentUser', () => { describe('fetchCurrentUser', () => {
+4 -2
View File
@@ -12,7 +12,8 @@ export const useUserStore = defineStore('user', () => {
const token = ref<string | null>(localStorage.getItem('access_token')) const token = ref<string | null>(localStorage.getItem('access_token'))
const refreshToken = ref<string | null>(localStorage.getItem('refresh_token')) const refreshToken = ref<string | null>(localStorage.getItem('refresh_token'))
const user = ref<UserInfo | null>(null) const user = ref<UserInfo | null>(null)
const _remember = ref(true) // 默认不持久化;从 localStorage 推断:有 token 说明上次选了"记住我"
const _remember = ref(!!localStorage.getItem('access_token'))
// 基本 JWT 格式校验(三段式),防止垃圾值绕过路由守卫 // 基本 JWT 格式校验(三段式),防止垃圾值绕过路由守卫
const isLoggedIn = computed(() => { const isLoggedIn = computed(() => {
@@ -30,7 +31,7 @@ export const useUserStore = defineStore('user', () => {
localStorage.setItem('access_token', accessToken) localStorage.setItem('access_token', accessToken)
localStorage.setItem('refresh_token', newRefreshToken) localStorage.setItem('refresh_token', newRefreshToken)
} else { } else {
// 不记住:清除持久化,token 仅存于内存,关闭标签页即失效 // 不记住:清除全部持久化数据,token 仅存于内存,关闭标签页即失效
localStorage.removeItem('access_token') localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token') localStorage.removeItem('refresh_token')
localStorage.removeItem('user') localStorage.removeItem('user')
@@ -54,6 +55,7 @@ export const useUserStore = defineStore('user', () => {
token.value = null token.value = null
refreshToken.value = null refreshToken.value = null
user.value = null user.value = null
_remember.value = false
localStorage.removeItem('access_token') localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token') localStorage.removeItem('refresh_token')
localStorage.removeItem('user') localStorage.removeItem('user')