Преглед на файлове

test: 补充禅道登录与用户管理回归

main
leiyun преди 1 месец
родител
ревизия
92a9419f01
променени са 2 файла, в които са добавени 233 реда и са изтрити 0 реда
  1. +154
    -0
      tests/zentao-auth-859-861-864.spec.ts
  2. +79
    -0
      tests/zentao-user-regression.spec.ts

+ 154
- 0
tests/zentao-auth-859-861-864.spec.ts Целия файл

@@ -0,0 +1,154 @@
import type { Locator, 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: 'ape2e-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 } = {},
) {
let refreshRequests = 0
let initialPasswordRequests = 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('/system-config/public')) return fulfill(route, {})
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,
}
}

async function selectOption(page: Page, select: 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('ai-person:web:access-token:v1', 'zentao-access-token')
if (refresh) sessionStorage.setItem('ai-person: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()
const organizationSelects = page.locator('.login-role-fields .login-picker:not(.login-identity-picker)')
await selectOption(page, organizationSelects.nth(0), '测试教学系')
await selectOption(page, organizationSelects.nth(1), '空账号教研室')

const identity = page.locator('.login-identity-picker')
await expect(page.getByText('当前条件下暂无可登录教员账号,请联系管理员。')).toBeVisible()
await expect(identity.getByRole('combobox')).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('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', [inputs])
})

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('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('#login-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('/overview')

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)
})

+ 79
- 0
tests/zentao-user-regression.spec.ts Целия файл

@@ -0,0 +1,79 @@
import type { Page, Route } from '@playwright/test'

import { captureScreenshot, expect, test } from './fixtures'

const json = (route: Route, data: unknown) => route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ code: 0, message: '成功', data, timestamp: Date.now(), requestId: 'zentao-user-regression' }),
})

const roles = [
{ id: '1', code: 'admin', name: '管理员', shortName: '管', enabled: true, builtIn: true, superAdmin: true, dataScope: 'ALL', dataVersion: 1 },
{ id: '2', code: 'teacher', name: '教员', shortName: '教', enabled: true, builtIn: true, superAdmin: false, dataScope: 'SYSTEM', dataVersion: 1 },
]

async function mockUsers(page: Page) {
let userRequests = 0
let statsRequests = 0
await page.route(/\/api\/(?:auth\/v1|v1)\//, async (route) => {
const path = new URL(route.request().url()).pathname
if (path.endsWith('/auth/me')) return json(route, {
userId: '1', username: 'admin', displayName: '系统管理员', departmentId: '100', departmentName: '数字人平台',
activeRoleId: '1', roles, authorizationMode: 'SINGLE_ACTIVE', mustChangePassword: false,
permissions: ['system.users', 'system.users.update', 'system.users.reset-password'],
})
if (path.endsWith('/departments/tree')) return json(route, [{
id: '100', code: 'ORG-100', name: '数字人平台', parentId: null, enabled: true, isRoot: true,
sortOrder: 10, directUserCount: 1, directChildCount: 0, dataVersion: 1, children: [],
}])
if (path.endsWith('/roles/all')) return json(route, roles)
if (path.endsWith('/users/stats')) {
statsRequests += 1
const pending = statsRequests === 1 ? 2 : 1
return json(route, { total: 12, enabled: 12, disabled: 0, passwordPending: pending, mustChangePasswordCount: pending })
}
if (path.endsWith('/users')) {
userRequests += 1
return json(route, {
records: [{
id: '9', username: 'teacher09', displayName: '测试教员', departmentId: '100', departmentName: '数字人平台',
roleIds: ['2', '1'], defaultRoleId: '1', enabled: true, isProtected: false,
credentialConfigured: true, mustChangePassword: userRequests === 1, lastLoginAt: null, dataVersion: 1,
}],
total: 1, page: 1, size: 20,
})
}
if (path.endsWith('/menus/navigation')) return json(route, [])
if (path.endsWith('/system-config/public')) return json(route, { systemName: '数字人平台' })
return json(route, {})
})
await page.addInitScript(() => {
window.sessionStorage.setItem('ai-person:web:access-token:v1', 'zentao-user-regression-token')
})
return {
statsRequests: () => statsRequests,
}
}

test('Bug 860、862:默认角色置顶且搜索同步刷新全局待改密统计', async ({ page }, testInfo) => {
const requests = await mockUsers(page)
await page.goto('/organization/users')

const row = page.locator('.users-table .el-table__row').filter({ hasText: '测试教员' })
const roleTags = row.locator('.role-tags .el-tag')
await expect(roleTags).toHaveCount(2)
await expect(roleTags.first()).toContainText('管理员')
await expect(roleTags.first()).toContainText('默认')
await expect(page.getByTestId('user-inline-statistics')).toContainText('全局待改密 2')

const searchInput = page.getByPlaceholder('请输入姓名、账号、部门或角色')
await searchInput.fill('teacher09')
await searchInput.press('Enter')
await expect.poll(requests.statsRequests).toBe(2)
await expect(page.getByTestId('user-inline-statistics')).toContainText('全局待改密 1')
await expect(row.getByText('待修改密码', { exact: true })).toHaveCount(0)
await expect(page).toHaveURL(/\/organization\/users(?:[?#].*)?$/)
await captureScreenshot(page, testInfo, 'bugs-860-862-user-role-and-pending-statistics')
await expect(page).toHaveURL(/\/organization\/users(?:[?#].*)?$/)
})

Зареждане…
Отказ
Запис