|
|
@@ -0,0 +1,270 @@
|
|
|
+import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
+import { mount, flushPromises } from '@vue/test-utils'
|
|
|
+import { createPinia, setActivePinia } from 'pinia'
|
|
|
+import { createI18n } from 'vue-i18n'
|
|
|
+import LoginView from '@/views/login/index.vue'
|
|
|
+import { useUserStore } from '@/store/user'
|
|
|
+import { wrapResponse, wrapErrorResponse, mockLoginResponse } from '../../../fixtures'
|
|
|
+
|
|
|
+// Mock vue-router
|
|
|
+const mockPush = vi.fn()
|
|
|
+const mockRoute = { query: {} }
|
|
|
+vi.mock('vue-router', () => ({
|
|
|
+ useRouter: () => ({ push: mockPush }),
|
|
|
+ useRoute: () => mockRoute
|
|
|
+}))
|
|
|
+
|
|
|
+// Mock element-plus
|
|
|
+vi.mock('element-plus', () => ({
|
|
|
+ ElMessage: {
|
|
|
+ success: vi.fn(),
|
|
|
+ error: vi.fn(),
|
|
|
+ info: vi.fn()
|
|
|
+ }
|
|
|
+}))
|
|
|
+
|
|
|
+// Mock __APP_VERSION__
|
|
|
+vi.stubGlobal('__APP_VERSION__', '1.0.0')
|
|
|
+
|
|
|
+// Create i18n instance for tests
|
|
|
+const i18n = createI18n({
|
|
|
+ legacy: false,
|
|
|
+ locale: 'zh-CN',
|
|
|
+ messages: {
|
|
|
+ 'zh-CN': {
|
|
|
+ 用户名: '用户名',
|
|
|
+ 密码: '密码',
|
|
|
+ 登录: '登录',
|
|
|
+ 记住我: '记住我',
|
|
|
+ 请输入用户名: '请输入用户名',
|
|
|
+ 请输入密码: '请输入密码',
|
|
|
+ 登录成功: '登录成功',
|
|
|
+ 登录失败: '登录失败',
|
|
|
+ '登录失败,请检查网络': '登录失败,请检查网络',
|
|
|
+ 摄像头管理系统: '摄像头管理系统',
|
|
|
+ 欢迎回来: '欢迎回来',
|
|
|
+ '登录您的管理后台,开始管理您的业务': '登录您的管理后台,开始管理您的业务',
|
|
|
+ 用户: '用户',
|
|
|
+ 稳定性: '稳定性',
|
|
|
+ 技术支持: '技术支持',
|
|
|
+ '忘记密码?': '忘记密码?',
|
|
|
+ 请联系管理员重置密码: '请联系管理员重置密码'
|
|
|
+ }
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+describe('Login View', () => {
|
|
|
+ beforeEach(() => {
|
|
|
+ setActivePinia(createPinia())
|
|
|
+ vi.clearAllMocks()
|
|
|
+ mockPush.mockClear()
|
|
|
+ mockRoute.query = {}
|
|
|
+ localStorage.clear()
|
|
|
+ })
|
|
|
+
|
|
|
+ const mountLogin = () => {
|
|
|
+ return mount(LoginView, {
|
|
|
+ global: {
|
|
|
+ plugins: [createPinia(), i18n],
|
|
|
+ stubs: {
|
|
|
+ LangDropdown: true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ describe('表单渲染', () => {
|
|
|
+ it('应该正确渲染登录表单', () => {
|
|
|
+ const wrapper = mountLogin()
|
|
|
+
|
|
|
+ expect(wrapper.find('input[placeholder="用户名"]').exists()).toBe(true)
|
|
|
+ expect(wrapper.find('input[placeholder="密码"]').exists()).toBe(true)
|
|
|
+ expect(wrapper.find('button[type="submit"]').exists()).toBe(true)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('密码输入框默认为密码类型', () => {
|
|
|
+ const wrapper = mountLogin()
|
|
|
+ const passwordInput = wrapper.find('input[placeholder="密码"]')
|
|
|
+
|
|
|
+ expect(passwordInput.attributes('type')).toBe('password')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('点击显示密码按钮切换密码可见性', async () => {
|
|
|
+ const wrapper = mountLogin()
|
|
|
+ const passwordInput = wrapper.find('input[placeholder="密码"]')
|
|
|
+ const toggleBtn = wrapper.find('.login__password-toggle')
|
|
|
+
|
|
|
+ expect(passwordInput.attributes('type')).toBe('password')
|
|
|
+
|
|
|
+ await toggleBtn.trigger('click')
|
|
|
+ expect(passwordInput.attributes('type')).toBe('text')
|
|
|
+
|
|
|
+ await toggleBtn.trigger('click')
|
|
|
+ expect(passwordInput.attributes('type')).toBe('password')
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ describe('表单验证', () => {
|
|
|
+ it('空用户名提交显示错误', async () => {
|
|
|
+ const wrapper = mountLogin()
|
|
|
+
|
|
|
+ await wrapper.find('input[placeholder="密码"]').setValue('password')
|
|
|
+ await wrapper.find('form').trigger('submit')
|
|
|
+
|
|
|
+ expect(wrapper.text()).toContain('请输入用户名')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('空密码提交显示错误', async () => {
|
|
|
+ const wrapper = mountLogin()
|
|
|
+
|
|
|
+ await wrapper.find('input[placeholder="用户名"]').setValue('admin')
|
|
|
+ await wrapper.find('form').trigger('submit')
|
|
|
+
|
|
|
+ expect(wrapper.text()).toContain('请输入密码')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('用户名和密码都为空时显示两个错误', async () => {
|
|
|
+ const wrapper = mountLogin()
|
|
|
+
|
|
|
+ await wrapper.find('form').trigger('submit')
|
|
|
+
|
|
|
+ expect(wrapper.text()).toContain('请输入用户名')
|
|
|
+ expect(wrapper.text()).toContain('请输入密码')
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ describe('登录功能', () => {
|
|
|
+ it('登录成功后跳转到首页', async () => {
|
|
|
+ const wrapper = mountLogin()
|
|
|
+ const userStore = useUserStore()
|
|
|
+
|
|
|
+ // Mock loginAction
|
|
|
+ vi.spyOn(userStore, 'loginAction').mockResolvedValue(wrapResponse(mockLoginResponse))
|
|
|
+
|
|
|
+ await wrapper.find('input[placeholder="用户名"]').setValue('admin')
|
|
|
+ await wrapper.find('input[placeholder="密码"]').setValue('123456')
|
|
|
+ await wrapper.find('form').trigger('submit')
|
|
|
+ await flushPromises()
|
|
|
+
|
|
|
+ expect(userStore.loginAction).toHaveBeenCalledWith({
|
|
|
+ username: 'admin',
|
|
|
+ password: '123456'
|
|
|
+ })
|
|
|
+ expect(mockPush).toHaveBeenCalledWith('/')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('登录成功后跳转到 redirect 参数指定的页面', async () => {
|
|
|
+ mockRoute.query = { redirect: '/dashboard' }
|
|
|
+
|
|
|
+ const wrapper = mountLogin()
|
|
|
+ const userStore = useUserStore()
|
|
|
+
|
|
|
+ vi.spyOn(userStore, 'loginAction').mockResolvedValue(wrapResponse(mockLoginResponse))
|
|
|
+
|
|
|
+ await wrapper.find('input[placeholder="用户名"]').setValue('admin')
|
|
|
+ await wrapper.find('input[placeholder="密码"]').setValue('123456')
|
|
|
+ await wrapper.find('form').trigger('submit')
|
|
|
+ await flushPromises()
|
|
|
+
|
|
|
+ expect(mockPush).toHaveBeenCalledWith('/dashboard')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('登录失败显示错误消息', async () => {
|
|
|
+ const { ElMessage } = await import('element-plus')
|
|
|
+ const wrapper = mountLogin()
|
|
|
+ const userStore = useUserStore()
|
|
|
+
|
|
|
+ vi.spyOn(userStore, 'loginAction').mockResolvedValue(wrapErrorResponse('用户名或密码错误', 401) as any)
|
|
|
+
|
|
|
+ await wrapper.find('input[placeholder="用户名"]').setValue('admin')
|
|
|
+ await wrapper.find('input[placeholder="密码"]').setValue('wrongpassword')
|
|
|
+ await wrapper.find('form').trigger('submit')
|
|
|
+ await flushPromises()
|
|
|
+
|
|
|
+ expect(ElMessage.error).toHaveBeenCalledWith('用户名或密码错误')
|
|
|
+ expect(mockPush).not.toHaveBeenCalled()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('登录请求异常显示网络错误', async () => {
|
|
|
+ const { ElMessage } = await import('element-plus')
|
|
|
+ const wrapper = mountLogin()
|
|
|
+ const userStore = useUserStore()
|
|
|
+
|
|
|
+ vi.spyOn(userStore, 'loginAction').mockRejectedValue(new Error('Network Error'))
|
|
|
+
|
|
|
+ await wrapper.find('input[placeholder="用户名"]').setValue('admin')
|
|
|
+ await wrapper.find('input[placeholder="密码"]').setValue('123456')
|
|
|
+ await wrapper.find('form').trigger('submit')
|
|
|
+ await flushPromises()
|
|
|
+
|
|
|
+ expect(ElMessage.error).toHaveBeenCalled()
|
|
|
+ expect(mockPush).not.toHaveBeenCalled()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('登录时显示加载状态', async () => {
|
|
|
+ const wrapper = mountLogin()
|
|
|
+ const userStore = useUserStore()
|
|
|
+
|
|
|
+ // Mock a slow login
|
|
|
+ vi.spyOn(userStore, 'loginAction').mockImplementation(
|
|
|
+ () => new Promise((resolve) => setTimeout(() => resolve(wrapResponse(mockLoginResponse)), 100))
|
|
|
+ )
|
|
|
+
|
|
|
+ await wrapper.find('input[placeholder="用户名"]').setValue('admin')
|
|
|
+ await wrapper.find('input[placeholder="密码"]').setValue('123456')
|
|
|
+ await wrapper.find('form').trigger('submit')
|
|
|
+
|
|
|
+ // Button should be disabled during loading
|
|
|
+ const submitBtn = wrapper.find('button[type="submit"]')
|
|
|
+ expect(submitBtn.attributes('disabled')).toBeDefined()
|
|
|
+
|
|
|
+ await flushPromises()
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ describe('记住我功能', () => {
|
|
|
+ it('记住我选中时保存用户名到 localStorage', async () => {
|
|
|
+ const wrapper = mountLogin()
|
|
|
+ const userStore = useUserStore()
|
|
|
+
|
|
|
+ vi.spyOn(userStore, 'loginAction').mockResolvedValue(wrapResponse(mockLoginResponse))
|
|
|
+
|
|
|
+ const checkbox = wrapper.find('.login__checkbox')
|
|
|
+ await checkbox.setValue(true)
|
|
|
+
|
|
|
+ await wrapper.find('input[placeholder="用户名"]').setValue('admin')
|
|
|
+ await wrapper.find('input[placeholder="密码"]').setValue('123456')
|
|
|
+ await wrapper.find('form').trigger('submit')
|
|
|
+ await flushPromises()
|
|
|
+
|
|
|
+ expect(localStorage.getItem('login_remember')).toBe('admin')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('记住我未选中时不保存用户名', async () => {
|
|
|
+ const wrapper = mountLogin()
|
|
|
+ const userStore = useUserStore()
|
|
|
+
|
|
|
+ vi.spyOn(userStore, 'loginAction').mockResolvedValue(wrapResponse(mockLoginResponse))
|
|
|
+
|
|
|
+ const checkbox = wrapper.find('.login__checkbox')
|
|
|
+ await checkbox.setValue(false)
|
|
|
+
|
|
|
+ await wrapper.find('input[placeholder="用户名"]').setValue('admin')
|
|
|
+ await wrapper.find('input[placeholder="密码"]').setValue('123456')
|
|
|
+ await wrapper.find('form').trigger('submit')
|
|
|
+ await flushPromises()
|
|
|
+
|
|
|
+ expect(localStorage.getItem('login_remember')).toBeNull()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('页面加载时自动填充保存的用户名', async () => {
|
|
|
+ localStorage.setItem('login_remember', 'saveduser')
|
|
|
+
|
|
|
+ const wrapper = mountLogin()
|
|
|
+ await flushPromises()
|
|
|
+
|
|
|
+ const usernameInput = wrapper.find('input[placeholder="用户名"]')
|
|
|
+ expect((usernameInput.element as HTMLInputElement).value).toBe('saveduser')
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|