|
- import type { Page, Route } from '@playwright/test'
-
- import { captureScreenshot, expect, test } from './fixtures'
-
- test.use({ trace: 'off', video: 'off' })
-
- const loginContext = {
- defaultRoleCode: 'admin',
- roles: [
- { code: 'teacher', label: '教员', accountLabel: '教员账号', organizationMode: 'CASCADE', organizationLabels: ['教学系', '教研室'] },
- { code: 'admin', label: '管理员', accountLabel: '管理员账号', organizationMode: 'NONE', organizationLabels: [] },
- ],
- departments: [
- {
- id: '100', code: 'TEST', name: '测试教学系', parentId: null,
- children: [{ id: '110', code: 'EMPTY', name: '空账号教研室', parentId: '100', children: [] }],
- },
- ],
- }
-
- const forcedPasswordProfile = {
- user: {
- id: '7', username: 'teacher.test', displayName: '测试教员',
- departmentId: '110', departmentName: '空账号教研室', mustChangePassword: true,
- },
- activeRoleId: '20',
- activeRole: { id: '20', code: 'teacher', name: '教员' },
- roles: [{ id: '20', code: 'teacher', name: '教员', status: 1 }],
- permissions: [],
- authorizationMode: 'SINGLE_ACTIVE',
- mustChangePassword: true,
- }
-
- const envelope = (data: unknown, message = '成功', code = 200) => JSON.stringify({
- code, message, data, timestamp: '2026-08-17T12:00:00+08:00', requestId: 'ute2e-zentao-auth',
- })
-
- const fulfill = (route: Route, data: unknown, status = 200, message = '成功', code = 200) => route.fulfill({
- status,
- contentType: 'application/json',
- body: envelope(data, message, code),
- })
-
- async function mockAuth(
- page: Page,
- options: { me?: 'forced' | 'expired'; initialPassword401?: boolean; loginFailure?: boolean } = {},
- ) {
- let refreshRequests = 0
- let initialPasswordRequests = 0
- let loginRequests = 0
- await page.route('**/api/auth/v1/**', async (route) => {
- const path = new URL(route.request().url()).pathname
- if (path.endsWith('/auth/login-context')) return fulfill(route, loginContext)
- if (path.endsWith('/auth/login-identities')) return fulfill(route, [])
- if (path.endsWith('/auth/login') && route.request().method() === 'POST') {
- loginRequests += 1
- if (options.loginFailure) return fulfill(route, null, 401, '账号、密码或登录身份不匹配', 40102)
- }
- if (path.endsWith('/menus/navigation')) return fulfill(route, [])
- if (path.endsWith('/auth/me')) {
- if (options.me === 'expired') {
- return fulfill(route, null, 401, '内部会话详情不应展示', 40101)
- }
- return fulfill(route, forcedPasswordProfile)
- }
- if (path.endsWith('/auth/initial-password')) {
- initialPasswordRequests += 1
- if (options.initialPassword401) {
- return fulfill(route, null, 401, '账号已停用:internal-user-id=7', 40102)
- }
- return fulfill(route, {})
- }
- if (path.endsWith('/auth/refresh')) {
- refreshRequests += 1
- return fulfill(route, null, 401, 'refresh-token-internal-detail', 40101)
- }
- return fulfill(route, {})
- })
- return {
- refreshRequests: () => refreshRequests,
- initialPasswordRequests: () => initialPasswordRequests,
- loginRequests: () => loginRequests,
- }
- }
-
- async function selectOption(page: Page, select: ReturnType<Page['locator']>, text: string) {
- await select.locator('.el-select__wrapper').click()
- await page.locator('.el-select-dropdown:visible').getByRole('option', { name: text, exact: true }).click()
- }
-
- async function seedSession(page: Page, includeRefresh = false) {
- await page.addInitScript(({ refresh }) => {
- if (sessionStorage.getItem('zentao-auth-seeded') === '1') return
- sessionStorage.setItem('zentao-auth-seeded', '1')
- sessionStorage.setItem('unreal-tran:web:access-token:v1', 'zentao-access-token')
- if (refresh) sessionStorage.setItem('unreal-tran:web:refresh-token:v1', 'zentao-refresh-token')
- }, { refresh: includeRefresh })
- }
-
- test('859:部门没有可登录教员时账号选择器明确禁用且不可展开', async ({ page }, testInfo) => {
- await mockAuth(page)
- await page.goto('/login')
- await page.locator('[data-role-code="teacher"]').click()
- await selectOption(page, page.getByTestId('login-organization'), '测试教学系')
- await selectOption(page, page.getByTestId('login-department'), '空账号教研室')
-
- const identity = page.getByTestId('login-identity')
- await expect(page.getByText('当前条件下暂无可登录教员账号,请联系管理员。')).toBeVisible()
- await expect(identity).toBeDisabled()
- await identity.locator('.el-select__wrapper').click({ force: true })
- await expect(page.locator('.login-identity-popper:visible')).toHaveCount(0)
- await captureScreenshot(page, testInfo, '859-empty-teacher-account-disabled')
- })
-
- test('861:首次改密两次输入不一致时立即显示字段提示且不提交', async ({ page }, testInfo) => {
- await seedSession(page)
- const mocked = await mockAuth(page, { me: 'forced' })
- await page.goto('/change-password')
- const inputs = page.locator('.password-card input[autocomplete="new-password"]')
- await inputs.nth(0).fill('Strong#Password8')
- await inputs.nth(1).fill('Different#Pass9')
-
- await expect(page.getByText('两次输入的新密码不一致', { exact: true })).toBeVisible()
- await expect(page.getByRole('button', { name: '确认修改并登录' })).toBeDisabled()
- expect(mocked.initialPasswordRequests()).toBe(0)
- await captureScreenshot(page, testInfo, '861-initial-password-mismatch')
- })
-
- test('864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌', async ({ page }, testInfo) => {
- await seedSession(page, true)
- const mocked = await mockAuth(page, { me: 'forced', initialPassword401: true })
- await page.goto('/change-password')
- const inputs = page.locator('.password-card input[autocomplete="new-password"]')
- await inputs.nth(0).fill('Strong#Password8')
- await inputs.nth(1).fill('Strong#Password8')
- await page.getByRole('button', { name: '确认修改并登录' }).click()
-
- await expect(page).toHaveURL(/\/login\?redirect=/)
- await expect(page.locator('.el-message--error')).toHaveText('账号、密码或登录身份不匹配')
- await expect(page.getByText(/internal-user-id|refresh-token-internal-detail/)).toHaveCount(0)
- expect(mocked.initialPasswordRequests()).toBe(1)
- expect(mocked.refreshRequests()).toBe(0)
- await captureScreenshot(page, testInfo, '864-disabled-account-one-time-notice')
-
- await page.reload()
- await expect(page.locator('#login-error')).toHaveText('')
- })
-
- test('普通会话过期只跳转登录,不展示后端错误详情或停用账号提示', async ({ page }) => {
- await seedSession(page, true)
- const mocked = await mockAuth(page, { me: 'expired' })
- await page.goto('/dashboard')
-
- await expect(page).toHaveURL(/\/login\?redirect=/)
- await expect(page.locator('#login-error')).toHaveText('')
- await expect(page.getByText(/内部会话详情|refresh-token-internal-detail|账号、密码或登录身份不匹配/)).toHaveCount(0)
- expect(mocked.refreshRequests()).toBe(1)
- })
-
- test('当前登录接口失败只显示临时 Message,自动消失且不留下登录错误条', async ({ page }, testInfo) => {
- const mocked = await mockAuth(page, { loginFailure: true })
- await page.goto('/login')
- await page.locator('[data-role-code="admin"]').click()
- await page.locator('input[name="username"]').fill('__ute2e_message_mock__')
- await page.locator('input[name="password"]').fill('Invalid#Only8')
- await page.getByRole('button', { name: '登录系统', exact: true }).click()
-
- const message = page.locator('.el-message--error')
- await expect(message).toHaveCount(1)
- await expect(message).toHaveText('账号、密码或登录身份不匹配')
- await expect(page.locator('#login-error')).toHaveText('')
- expect(mocked.loginRequests()).toBe(1)
- expect(mocked.refreshRequests()).toBe(0)
- await captureScreenshot(page, testInfo, 'login-api-error-message')
-
- await expect(message).toHaveCount(0, { timeout: 7_000 })
- await expect(page.locator('#login-error')).toHaveText('')
- await expect(page).toHaveURL(/\/login(?:\?|$)/)
- await captureScreenshot(page, testInfo, 'login-api-error-message-dismissed')
- })
|