index.spec.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import { describe, it, expect, vi, beforeEach } from 'vitest'
  2. import { mount, flushPromises } from '@vue/test-utils'
  3. import { createPinia, setActivePinia } from 'pinia'
  4. import { createI18n } from 'vue-i18n'
  5. import LoginView from '@/views/login/index.vue'
  6. import { useUserStore } from '@/store/user'
  7. import { wrapResponse, wrapErrorResponse, mockLoginResponse } from '../../../fixtures'
  8. // Mock vue-router
  9. const mockPush = vi.fn()
  10. const mockRoute = { query: {} }
  11. vi.mock('vue-router', () => ({
  12. useRouter: () => ({ push: mockPush }),
  13. useRoute: () => mockRoute
  14. }))
  15. // Mock element-plus
  16. vi.mock('element-plus', () => ({
  17. ElMessage: {
  18. success: vi.fn(),
  19. error: vi.fn(),
  20. info: vi.fn()
  21. }
  22. }))
  23. // Mock __APP_VERSION__
  24. vi.stubGlobal('__APP_VERSION__', '1.0.0')
  25. // Create i18n instance for tests
  26. const i18n = createI18n({
  27. legacy: false,
  28. locale: 'zh-CN',
  29. messages: {
  30. 'zh-CN': {
  31. 用户名: '用户名',
  32. 密码: '密码',
  33. 登录: '登录',
  34. 记住我: '记住我',
  35. 请输入用户名: '请输入用户名',
  36. 请输入密码: '请输入密码',
  37. 登录成功: '登录成功',
  38. 登录失败: '登录失败',
  39. '登录失败,请检查网络': '登录失败,请检查网络',
  40. 摄像头管理系统: '摄像头管理系统',
  41. 欢迎回来: '欢迎回来',
  42. '登录您的管理后台,开始管理您的业务': '登录您的管理后台,开始管理您的业务',
  43. 用户: '用户',
  44. 稳定性: '稳定性',
  45. 技术支持: '技术支持',
  46. '忘记密码?': '忘记密码?',
  47. 请联系管理员重置密码: '请联系管理员重置密码'
  48. }
  49. }
  50. })
  51. describe('Login View', () => {
  52. beforeEach(() => {
  53. setActivePinia(createPinia())
  54. vi.clearAllMocks()
  55. mockPush.mockClear()
  56. mockRoute.query = {}
  57. localStorage.clear()
  58. })
  59. const mountLogin = () => {
  60. return mount(LoginView, {
  61. global: {
  62. plugins: [createPinia(), i18n],
  63. stubs: {
  64. LangDropdown: true
  65. }
  66. }
  67. })
  68. }
  69. describe('表单渲染', () => {
  70. it('应该正确渲染登录表单', () => {
  71. const wrapper = mountLogin()
  72. expect(wrapper.find('input[placeholder="用户名"]').exists()).toBe(true)
  73. expect(wrapper.find('input[placeholder="密码"]').exists()).toBe(true)
  74. expect(wrapper.find('button[type="submit"]').exists()).toBe(true)
  75. })
  76. it('密码输入框默认为密码类型', () => {
  77. const wrapper = mountLogin()
  78. const passwordInput = wrapper.find('input[placeholder="密码"]')
  79. expect(passwordInput.attributes('type')).toBe('password')
  80. })
  81. it('点击显示密码按钮切换密码可见性', async () => {
  82. const wrapper = mountLogin()
  83. const passwordInput = wrapper.find('input[placeholder="密码"]')
  84. const toggleBtn = wrapper.find('.login__password-toggle')
  85. expect(passwordInput.attributes('type')).toBe('password')
  86. await toggleBtn.trigger('click')
  87. expect(passwordInput.attributes('type')).toBe('text')
  88. await toggleBtn.trigger('click')
  89. expect(passwordInput.attributes('type')).toBe('password')
  90. })
  91. })
  92. describe('表单验证', () => {
  93. it('空用户名提交显示错误', async () => {
  94. const wrapper = mountLogin()
  95. await wrapper.find('input[placeholder="密码"]').setValue('password')
  96. await wrapper.find('form').trigger('submit')
  97. expect(wrapper.text()).toContain('请输入用户名')
  98. })
  99. it('空密码提交显示错误', async () => {
  100. const wrapper = mountLogin()
  101. await wrapper.find('input[placeholder="用户名"]').setValue('admin')
  102. await wrapper.find('form').trigger('submit')
  103. expect(wrapper.text()).toContain('请输入密码')
  104. })
  105. it('用户名和密码都为空时显示两个错误', async () => {
  106. const wrapper = mountLogin()
  107. await wrapper.find('form').trigger('submit')
  108. expect(wrapper.text()).toContain('请输入用户名')
  109. expect(wrapper.text()).toContain('请输入密码')
  110. })
  111. })
  112. describe('登录功能', () => {
  113. it('登录成功后跳转到首页', async () => {
  114. const wrapper = mountLogin()
  115. const userStore = useUserStore()
  116. // Mock loginAction
  117. vi.spyOn(userStore, 'loginAction').mockResolvedValue(wrapResponse(mockLoginResponse))
  118. await wrapper.find('input[placeholder="用户名"]').setValue('admin')
  119. await wrapper.find('input[placeholder="密码"]').setValue('123456')
  120. await wrapper.find('form').trigger('submit')
  121. await flushPromises()
  122. expect(userStore.loginAction).toHaveBeenCalledWith({
  123. username: 'admin',
  124. password: '123456'
  125. })
  126. expect(mockPush).toHaveBeenCalledWith('/')
  127. })
  128. it('登录成功后跳转到 redirect 参数指定的页面', async () => {
  129. mockRoute.query = { redirect: '/dashboard' }
  130. const wrapper = mountLogin()
  131. const userStore = useUserStore()
  132. vi.spyOn(userStore, 'loginAction').mockResolvedValue(wrapResponse(mockLoginResponse))
  133. await wrapper.find('input[placeholder="用户名"]').setValue('admin')
  134. await wrapper.find('input[placeholder="密码"]').setValue('123456')
  135. await wrapper.find('form').trigger('submit')
  136. await flushPromises()
  137. expect(mockPush).toHaveBeenCalledWith('/dashboard')
  138. })
  139. it('登录失败显示错误消息', async () => {
  140. const { ElMessage } = await import('element-plus')
  141. const wrapper = mountLogin()
  142. const userStore = useUserStore()
  143. vi.spyOn(userStore, 'loginAction').mockResolvedValue(wrapErrorResponse('用户名或密码错误', 401) as any)
  144. await wrapper.find('input[placeholder="用户名"]').setValue('admin')
  145. await wrapper.find('input[placeholder="密码"]').setValue('wrongpassword')
  146. await wrapper.find('form').trigger('submit')
  147. await flushPromises()
  148. expect(ElMessage.error).toHaveBeenCalledWith('用户名或密码错误')
  149. expect(mockPush).not.toHaveBeenCalled()
  150. })
  151. it('登录请求异常显示网络错误', async () => {
  152. const { ElMessage } = await import('element-plus')
  153. const wrapper = mountLogin()
  154. const userStore = useUserStore()
  155. vi.spyOn(userStore, 'loginAction').mockRejectedValue(new Error('Network Error'))
  156. await wrapper.find('input[placeholder="用户名"]').setValue('admin')
  157. await wrapper.find('input[placeholder="密码"]').setValue('123456')
  158. await wrapper.find('form').trigger('submit')
  159. await flushPromises()
  160. expect(ElMessage.error).toHaveBeenCalled()
  161. expect(mockPush).not.toHaveBeenCalled()
  162. })
  163. it('登录时显示加载状态', async () => {
  164. const wrapper = mountLogin()
  165. const userStore = useUserStore()
  166. // Mock a slow login
  167. vi.spyOn(userStore, 'loginAction').mockImplementation(
  168. () => new Promise((resolve) => setTimeout(() => resolve(wrapResponse(mockLoginResponse)), 100))
  169. )
  170. await wrapper.find('input[placeholder="用户名"]').setValue('admin')
  171. await wrapper.find('input[placeholder="密码"]').setValue('123456')
  172. await wrapper.find('form').trigger('submit')
  173. // Button should be disabled during loading
  174. const submitBtn = wrapper.find('button[type="submit"]')
  175. expect(submitBtn.attributes('disabled')).toBeDefined()
  176. await flushPromises()
  177. })
  178. })
  179. describe('记住我功能', () => {
  180. it('记住我选中时保存用户名到 localStorage', async () => {
  181. const wrapper = mountLogin()
  182. const userStore = useUserStore()
  183. vi.spyOn(userStore, 'loginAction').mockResolvedValue(wrapResponse(mockLoginResponse))
  184. const checkbox = wrapper.find('.login__checkbox')
  185. await checkbox.setValue(true)
  186. await wrapper.find('input[placeholder="用户名"]').setValue('admin')
  187. await wrapper.find('input[placeholder="密码"]').setValue('123456')
  188. await wrapper.find('form').trigger('submit')
  189. await flushPromises()
  190. expect(localStorage.getItem('login_remember')).toBe('admin')
  191. })
  192. it('记住我未选中时不保存用户名', async () => {
  193. const wrapper = mountLogin()
  194. const userStore = useUserStore()
  195. vi.spyOn(userStore, 'loginAction').mockResolvedValue(wrapResponse(mockLoginResponse))
  196. const checkbox = wrapper.find('.login__checkbox')
  197. await checkbox.setValue(false)
  198. await wrapper.find('input[placeholder="用户名"]').setValue('admin')
  199. await wrapper.find('input[placeholder="密码"]').setValue('123456')
  200. await wrapper.find('form').trigger('submit')
  201. await flushPromises()
  202. expect(localStorage.getItem('login_remember')).toBeNull()
  203. })
  204. it('页面加载时自动填充保存的用户名', async () => {
  205. localStorage.setItem('login_remember', 'saveduser')
  206. const wrapper = mountLogin()
  207. await flushPromises()
  208. const usernameInput = wrapper.find('input[placeholder="用户名"]')
  209. expect((usernameInput.element as HTMLInputElement).value).toBe('saveduser')
  210. })
  211. })
  212. })