瀏覽代碼

feat: enhance login functionality and improve test coverage

- Updated login tests to reflect changes in placeholder text and button names for better clarity.
- Added new test cases for error handling during login, including incorrect password scenarios and the "remember me" functionality.
- Refactored existing tests to ensure consistency in element selectors and improved localization support in the login view.
- Cleaned up the Jenkinsfile to include workspace cleanup steps before builds, ensuring a fresh environment for each run.
yb 2 周之前
父節點
當前提交
260ba4c4af
共有 3 個文件被更改,包括 347 次插入23 次删除
  1. 6 1
      Jenkinsfile
  2. 71 22
      tests/e2e/auth.spec.ts
  3. 270 0
      tests/unit/views/login/index.spec.ts

+ 6 - 1
Jenkinsfile

@@ -193,6 +193,10 @@ pnpm 安装失败,请手动安装: npm install -g pnpm"
                         source ${WORKSPACE}/jenkins/lib/telegram.sh
                         source ${WORKSPACE}/build_vars.env
 
+                        log_message "清理 Git 工作区(保留配置文件和 jenkins 目录)"
+                        git checkout -- . || true
+                        git clean -fd -e .env* -e jenkins/ -e node_modules/ || true
+
                         log_message "检查当前 Git 状态"
                         log_message "当前分支: $(git rev-parse --abbrev-ref HEAD)"
                         log_message "最新提交: $(git log -1 --oneline)"
@@ -261,7 +265,8 @@ pnpm 安装失败,请手动安装: npm install -g pnpm"
                         log_message "Node 版本: $(node -v)"
                         log_message "pnpm 版本: $(${PNPM_PATH} -v)"
 
-                        log_message "清理旧的 node_modules"
+                        log_message "清理旧的构建产物和 node_modules"
+                        rm -rf dist
                         rm -rf node_modules
 
                         log_message "开始安装依赖"

+ 71 - 22
tests/e2e/auth.spec.ts

@@ -18,51 +18,84 @@ test.describe('登录登出测试', () => {
   })
 
   test('登录页面正确显示', async ({ page }) => {
-    await expect(page.getByPlaceholder('请输入用户名')).toBeVisible()
-    await expect(page.getByPlaceholder('请输入密码')).toBeVisible()
-    await expect(page.getByRole('button', { name: '登 录' })).toBeVisible()
+    await expect(page.getByPlaceholder('用户名')).toBeVisible()
+    await expect(page.getByPlaceholder('密码')).toBeVisible()
+    await expect(page.getByRole('button', { name: '登录' })).toBeVisible()
     await expect(page.getByText('记住我')).toBeVisible()
   })
 
   test('空表单提交显示验证错误', async ({ page }) => {
-    await page.getByRole('button', { name: '登 录' }).click()
+    await page.getByRole('button', { name: '登录' }).click()
     await expect(page.getByText('请输入用户名')).toBeVisible()
     await expect(page.getByText('请输入密码')).toBeVisible()
   })
 
+  test('错误密码登录失败', async ({ page }) => {
+    await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
+    await page.getByPlaceholder('密码').fill('wrongpassword')
+
+    const loginBtn = page.getByRole('button', { name: '登录' })
+    await loginBtn.click()
+
+    // 等待一段时间后验证仍在登录页
+    await page.waitForTimeout(3000)
+    await expect(page).toHaveURL(/\/login/)
+
+    // 验证登录按钮恢复可用状态(加载结束)
+    await expect(loginBtn).toBeEnabled()
+  })
+
   test('登录成功并显示用户名 admin', async ({ page }) => {
     // 输入登录信息
-    await page.getByPlaceholder('请输入用户名').fill(TEST_USERNAME)
-    await page.getByPlaceholder('请输入密码').fill(TEST_PASSWORD)
+    await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
+    await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
 
     // 等待登录按钮可用并点击
-    const loginBtn = page.getByRole('button', { name: '登 录' })
+    const loginBtn = page.getByRole('button', { name: '登录' })
     await expect(loginBtn).toBeEnabled()
     await loginBtn.click()
 
     // 等待登录 API 响应并跳转
     await page.waitForURL(/^(?!.*\/login).*$/, { timeout: 15000 })
 
-    // 验证用户名显示为 admin
-    await expect(page.locator('.username')).toBeVisible({ timeout: 10000 })
-    await expect(page.locator('.username')).toContainText('admin')
+    // 验证用户名显示为 admin (layout__username 类)
+    await expect(page.locator('.layout__username')).toBeVisible({ timeout: 10000 })
+    await expect(page.locator('.layout__username')).toContainText('admin')
+  })
+
+  test('记住我功能保存用户名', async ({ page }) => {
+    // 确保记住我选中
+    const rememberCheckbox = page.locator('.login__checkbox')
+    await rememberCheckbox.check()
+
+    // 输入用户名并登录
+    await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
+    await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
+    await page.getByRole('button', { name: '登录' }).click()
+
+    // 等待登录成功
+    await page.waitForURL(/^(?!.*\/login).*$/, { timeout: 15000 })
+
+    // 验证 localStorage 保存了用户名
+    const savedUsername = await page.evaluate(() => localStorage.getItem('login_remember'))
+    expect(savedUsername).toBe(TEST_USERNAME)
   })
 
   test('登录后可以正常登出', async ({ page }) => {
     // 先登录
-    await page.getByPlaceholder('请输入用户名').fill(TEST_USERNAME)
-    await page.getByPlaceholder('请输入密码').fill(TEST_PASSWORD)
-    const loginBtn = page.getByRole('button', { name: '登 录' })
+    await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
+    await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
+    const loginBtn = page.getByRole('button', { name: '登录' })
     await expect(loginBtn).toBeEnabled()
     await loginBtn.click()
     await page.waitForURL(/^(?!.*\/login).*$/, { timeout: 15000 })
 
-    // 点击用户下拉菜单
-    await page.locator('.user-info').click()
+    // 点击用户下拉菜单 (layout__user 类)
+    await page.locator('.layout__user').click()
     await page.waitForTimeout(500)
 
     // 点击退出登录
-    await page.getByText('退出登录').click()
+    await page.getByText('退出登录').first().click()
 
     // 验证跳转到登录页
     await expect(page).toHaveURL(/\/login/, { timeout: 10000 })
@@ -70,22 +103,38 @@ test.describe('登录登出测试', () => {
 
   test('修改密码弹窗可以打开', async ({ page }) => {
     // 先登录
-    await page.getByPlaceholder('请输入用户名').fill(TEST_USERNAME)
-    await page.getByPlaceholder('请输入密码').fill(TEST_PASSWORD)
-    const loginBtn = page.getByRole('button', { name: '登 录' })
+    await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
+    await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
+    const loginBtn = page.getByRole('button', { name: '登录' })
     await expect(loginBtn).toBeEnabled()
     await loginBtn.click()
     await page.waitForURL(/^(?!.*\/login).*$/, { timeout: 15000 })
 
     // 点击用户下拉菜单
-    await page.locator('.user-info').click()
+    await page.locator('.layout__user').click()
     await page.waitForTimeout(500)
 
     // 点击修改密码
-    await page.getByText('修改密码').click()
+    await page.locator('.layout__dropdown-item').filter({ hasText: '修改密码' }).click()
 
     // 验证弹窗显示
     await expect(page.getByRole('dialog')).toBeVisible()
-    await expect(page.locator('.el-dialog').getByText('修改密码')).toBeVisible()
+    await expect(page.locator('.el-dialog__title')).toContainText('修改密码')
+  })
+
+  test('密码显示/隐藏切换', async ({ page }) => {
+    const passwordInput = page.getByPlaceholder('密码')
+    const toggleBtn = page.locator('.login__password-toggle')
+
+    // 默认是密码模式
+    await expect(passwordInput).toHaveAttribute('type', 'password')
+
+    // 点击切换按钮显示密码
+    await toggleBtn.click()
+    await expect(passwordInput).toHaveAttribute('type', 'text')
+
+    // 再次点击隐藏密码
+    await toggleBtn.click()
+    await expect(passwordInput).toHaveAttribute('type', 'password')
   })
 })

+ 270 - 0
tests/unit/views/login/index.spec.ts

@@ -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')
+    })
+  })
+})