|
- import type { Locator, Page } from '@playwright/test'
-
- import { captureScreenshot, expect, test } from './fixtures'
-
- const loginContext = {
- defaultRoleCode: 'admin',
- roles: [
- {
- code: 'administrative',
- label: '行政',
- accountLabel: '教学系领导账号',
- organizationMode: 'SINGLE',
- organizationLabels: ['教学系'],
- },
- {
- code: 'teacher',
- label: '教员',
- accountLabel: '教员账号',
- organizationMode: 'CASCADE',
- organizationLabels: ['教学系', '教研室'],
- },
- {
- code: 'student',
- label: '学员',
- accountLabel: '学号或账号',
- organizationMode: 'NONE',
- organizationLabels: [],
- },
- {
- code: 'admin',
- label: '管理员',
- accountLabel: '管理员账号',
- organizationMode: 'NONE',
- organizationLabels: [],
- },
- ],
- departments: [
- {
- id: '100',
- code: 'EQUIPMENT',
- name: '工程装备系',
- parentId: null,
- children: [
- {
- id: '110',
- code: 'MAINTENANCE',
- name: '维修教研室',
- parentId: '100',
- children: [
- {
- id: '111',
- code: 'MAINTENANCE-GROUP-1',
- name: '维修一组',
- parentId: '110',
- children: [
- {
- id: '112',
- code: 'HYDROPOWER-A',
- name: '水电组A',
- parentId: '111',
- children: [],
- },
- ],
- },
- ],
- },
- {
- id: '120',
- code: 'SUPPORT',
- name: '智能保障教研室',
- parentId: '100',
- children: [],
- },
- ],
- },
- {
- id: '200',
- code: 'COMMAND',
- name: '指挥系',
- parentId: null,
- children: [
- {
- id: '210',
- code: 'COMMAND-TEACHING',
- name: '指挥教研室',
- parentId: '200',
- children: [],
- },
- ],
- },
- ],
- } as const
-
- const loginIdentities = {
- administrative: [
- { id: '301', displayName: '秦主任', departmentId: '100', departmentName: '工程装备系' },
- { id: '302', displayName: '周主任', departmentId: '200', departmentName: '指挥系' },
- ],
- teacher: [
- { id: '401', displayName: '许教员', departmentId: '100', departmentName: '工程装备系' },
- { id: '402', displayName: '杜晴', departmentId: '110', departmentName: '维修教研室' },
- { id: '403', displayName: '马雅柔', departmentId: '112', departmentName: '水电组A' },
- { id: '404', displayName: '韩教员', departmentId: '120', departmentName: '智能保障教研室' },
- ],
- student: [
- { id: '501', displayName: '学生甲', departmentId: '112', departmentName: '水电组A' },
- ],
- } as const
-
- const descendantDepartmentIds: Record<string, ReadonlySet<string>> = {
- '100': new Set(['100', '110', '111', '112', '120']),
- '110': new Set(['110', '111', '112']),
- '120': new Set(['120']),
- '200': new Set(['200', '210']),
- '210': new Set(['210']),
- }
-
- const mockLoginContext = async (page: Page) => {
- await page.route('**/api/auth/v1/auth/login-context', async (route) => {
- await route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify({
- code: 200,
- message: '成功',
- data: loginContext,
- timestamp: '2026-08-11T00:00:00+08:00',
- requestId: 'ute2e-login-context-success',
- }),
- })
- })
- await page.route('**/api/auth/v1/auth/login-identities**', async (route) => {
- const url = new URL(route.request().url())
- const roleCode = url.searchParams.get('roleCode')
- const departmentId = url.searchParams.get('departmentId')
- const source = roleCode && roleCode in loginIdentities
- ? loginIdentities[roleCode as keyof typeof loginIdentities]
- : []
- const allowedDepartmentIds = departmentId ? descendantDepartmentIds[departmentId] : null
- const identities = source.filter((identity) => !allowedDepartmentIds || allowedDepartmentIds.has(identity.departmentId))
- await route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify({
- code: 200,
- message: '成功',
- data: identities,
- timestamp: '2026-08-11T00:00:00+08:00',
- requestId: 'ute2e-login-identities-success',
- }),
- })
- })
- }
-
- const mockLoginContextFailure = async (page: Page) => {
- await page.route('**/api/auth/v1/auth/login-context', async (route) => {
- await route.fulfill({
- status: 503,
- contentType: 'application/json',
- body: JSON.stringify({
- code: 50300,
- message: '认证配置暂不可用',
- data: null,
- timestamp: '2026-08-11T00:00:00+08:00',
- requestId: 'ute2e-login-context-failure',
- }),
- })
- })
- }
-
- const openLoginWithContext = async (page: Page) => {
- await mockLoginContext(page)
- await page.goto('/login')
- const contextState = page.getByText('身份认证服务正常', { exact: true })
- await expect(contextState).toBeAttached()
- await expect(contextState).toHaveAttribute('aria-live', 'polite')
- await expect(contextState).toHaveCSS('position', 'absolute')
- await expect(contextState).toHaveCSS('clip-path', 'inset(50%)')
- }
-
- const adminUsernameInput = (page: Page) => page.locator('input[name="username"]')
- const passwordInput = (page: Page) => page.locator('input[name="password"]')
- const submitButton = (page: Page) => page.getByRole('button', { name: '登录系统' })
- const identitySelect = (page: Page) => page.getByTestId('login-identity')
-
- const openElementSelect = async (page: Page, select: Locator) => {
- const combobox = select.getByRole('combobox')
- await select.locator('.el-select__wrapper').click()
- await expect(combobox).toHaveAttribute('aria-expanded', 'true')
- const listboxId = await combobox.getAttribute('aria-controls')
- expect(listboxId).toBeTruthy()
- const listbox = page.locator(`[id="${listboxId}"]`)
- await expect(listbox).toBeVisible()
- return listbox
- }
-
- const selectElementOption = async (page: Page, select: Locator, name: string | RegExp) => {
- const listbox = await openElementSelect(page, select)
- await listbox.getByRole('option', { name, exact: typeof name === 'string' }).click()
- }
-
- const expectElementSelectOptions = async (page: Page, select: Locator, expected: string[]) => {
- const listbox = await openElementSelect(page, select)
- const labels = (await listbox.getByRole('option').allTextContents())
- .map((label) => label.replace(/\s+/g, ' ').trim())
- expect(labels).toEqual(expected)
- await page.keyboard.press('Escape')
- }
-
- const expectElementSelectPlaceholder = async (select: Locator, placeholder: string) => {
- await expect(select.locator('.el-select__placeholder').first()).toContainText(placeholder)
- }
-
- test.describe('登录上下文 UI 契约', () => {
- test('恢复上次成功登录的部门和账号但不保存密码', async ({ page }, testInfo) => {
- await page.addInitScript(() => {
- window.localStorage.setItem('unreal-tran:web:login-identity-preference:v1', JSON.stringify({
- lastRoleCode: 'teacher',
- selections: {
- teacher: { departmentId: '110', userId: '402', username: '' },
- },
- }))
- })
-
- await openLoginWithContext(page)
-
- await expect(page.locator('[data-role-code="teacher"]')).toHaveAttribute('aria-pressed', 'true')
- await expect(page.getByTestId('login-organization')).toContainText('工程装备系')
- await expect(page.getByTestId('login-department')).toContainText('维修教研室')
- await expect(identitySelect(page)).toContainText('杜晴')
- await expect(passwordInput(page)).toHaveValue('')
- await expect(submitButton(page)).toBeDisabled()
- await captureScreenshot(page, testInfo, 'restored-login-identity-without-password')
- })
-
- test('按登录身份分别恢复记住的账号和密码', async ({ page }) => {
- await page.addInitScript(() => {
- window.localStorage.setItem('unreal-tran:web:login-identity-preference:v1', JSON.stringify({
- lastRoleCode: 'teacher',
- selections: {
- teacher: {
- departmentId: '110', userId: '402', username: '',
- password: 'Teacher-Remembered-1', remember: true,
- },
- admin: {
- departmentId: '', userId: '', username: 'admin.remembered',
- password: 'Admin-Remembered-2', remember: true,
- },
- },
- }))
- })
-
- await openLoginWithContext(page)
-
- await expect(identitySelect(page)).toContainText('杜晴')
- await expect(passwordInput(page)).toHaveValue('Teacher-Remembered-1')
- await expect(page.locator('input[name="rememberMe"]')).toBeChecked()
-
- await page.locator('[data-role-code="admin"]').click()
- await expect(adminUsernameInput(page)).toHaveValue('admin.remembered')
- await expect(passwordInput(page)).toHaveValue('Admin-Remembered-2')
- await expect(page.locator('input[name="rememberMe"]')).toBeChecked()
-
- await page.locator('[data-role-code="teacher"]').click()
- await expect(identitySelect(page)).toContainText('杜晴')
- await expect(passwordInput(page)).toHaveValue('Teacher-Remembered-1')
- })
-
- test('成功登录后按勾选状态保存账号密码', async ({ page }) => {
- await page.route('**/api/auth/v1/auth/login', (route) => route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify({
- code: 200,
- message: '成功',
- data: { accessToken: 'remember-access-token', refreshToken: 'remember-refresh-token', expiresIn: 3600 },
- }),
- }))
- await page.route('**/api/auth/v1/auth/me', (route) => route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: JSON.stringify({
- code: 200,
- message: '成功',
- data: {
- user: { id: '1', username: 'admin.saved', displayName: '系统管理员', mustChangePassword: true },
- activeRoleId: '1', roles: [{ id: '1', code: 'admin', name: '管理员', enabled: true }],
- permissions: [], authorizationMode: 'SINGLE_ACTIVE',
- },
- }),
- }))
- await openLoginWithContext(page)
-
- await adminUsernameInput(page).fill('admin.saved')
- await passwordInput(page).fill('Saved-Password-3')
- await page.locator('input[name="rememberMe"]').check()
- await submitButton(page).click()
-
- await expect.poll(() => page.evaluate(() => {
- const raw = window.localStorage.getItem('unreal-tran:web:login-identity-preference:v1') || '{}'
- return JSON.parse(raw).selections?.admin
- })).toEqual({
- departmentId: '', userId: '', username: 'admin.saved',
- password: 'Saved-Password-3', remember: true,
- })
- })
-
- test('已删除的部门或账号不会被本地偏好错误回填', async ({ page }) => {
- await page.addInitScript(() => {
- window.localStorage.setItem('unreal-tran:web:login-identity-preference:v1', JSON.stringify({
- lastRoleCode: 'teacher',
- selections: {
- teacher: { departmentId: '999', userId: '999', username: '' },
- },
- }))
- })
-
- await openLoginWithContext(page)
-
- await expect(page.locator('[data-role-code="teacher"]')).toHaveAttribute('aria-pressed', 'true')
- await expectElementSelectPlaceholder(page.getByTestId('login-organization'), '请选择教学系')
- await expect(page.getByTestId('login-department')).toBeDisabled()
- await expect(identitySelect(page)).toBeDisabled()
- await expect(passwordInput(page)).toHaveValue('')
- })
-
- test('默认管理员且四个身份展示各自约定字段', async ({ page }, testInfo) => {
- await openLoginWithContext(page)
-
- const roleButtons = page.locator('[data-role-code]')
- await expect(roleButtons).toHaveCount(4)
- expect(await roleButtons.evaluateAll((buttons) => buttons.map((button) => ({
- code: button.getAttribute('data-role-code'),
- label: button.textContent?.trim(),
- })))).toEqual([
- { code: 'administrative', label: '行政' },
- { code: 'teacher', label: '教员' },
- { code: 'student', label: '学员' },
- { code: 'admin', label: '管理员' },
- ])
-
- await expect(page.locator('[data-role-code="admin"]')).toHaveAttribute('aria-pressed', 'true')
- await expect(adminUsernameInput(page)).toHaveAttribute('placeholder', '请输入管理员账号')
- await expect(identitySelect(page)).toHaveCount(0)
- await expect(page.getByTestId('login-organization')).toHaveCount(0)
- await expect(page.getByTestId('login-department')).toHaveCount(0)
-
- await page.locator('[data-role-code="administrative"]').click()
- await expect(adminUsernameInput(page)).toHaveCount(0)
- await expect(identitySelect(page)).toBeVisible()
- await expectElementSelectPlaceholder(identitySelect(page), '请选择行政账号')
- await expect(identitySelect(page)).toBeDisabled()
- await expect(page.getByTestId('login-organization')).toHaveCount(0)
- await expect(page.getByTestId('login-department')).toBeVisible()
-
- await page.locator('[data-role-code="teacher"]').click()
- await expect(adminUsernameInput(page)).toHaveCount(0)
- await expect(identitySelect(page)).toBeVisible()
- await expectElementSelectPlaceholder(identitySelect(page), '请选择教员账号')
- await expect(identitySelect(page)).toBeDisabled()
- await expect(page.getByTestId('login-organization')).toBeVisible()
- await expect(page.getByTestId('login-department')).toBeVisible()
-
- await page.locator('[data-role-code="student"]').click()
- await expect(adminUsernameInput(page)).toHaveCount(0)
- await expect(identitySelect(page)).toBeVisible()
- await expectElementSelectPlaceholder(identitySelect(page), '请选择学员账号')
- await expect(identitySelect(page)).toBeEnabled()
- await expect(page.getByTestId('login-organization')).toHaveCount(0)
- await expect(page.getByTestId('login-department')).toHaveCount(0)
- await captureScreenshot(page, testInfo, 'four-role-fields')
- })
-
- test('行政身份按 SINGLE 模式只选择一个教学系', async ({ page }, testInfo) => {
- await openLoginWithContext(page)
- await page.locator('[data-role-code="administrative"]').click()
-
- const department = page.getByTestId('login-department')
- await expect(department).toBeEnabled()
- await expectElementSelectPlaceholder(department, '请选择教学系')
- await expectElementSelectOptions(page, department, [
- '工程装备系',
- '指挥系',
- ])
- await expect(submitButton(page)).toBeDisabled()
- await selectElementOption(page, department, '工程装备系')
- await expect(identitySelect(page)).toBeEnabled()
- await expectElementSelectPlaceholder(identitySelect(page), '请选择行政账号')
- await selectElementOption(page, identitySelect(page), /秦主任.*工程装备系/)
- await passwordInput(page).fill('ContractOnly-NotSubmitted')
- await expect(submitButton(page)).toBeEnabled()
- await captureScreenshot(page, testInfo, 'administrative-single')
- })
-
- test('教员身份按 CASCADE 模式支持教学系本级和直属教研室', async ({ page }, testInfo) => {
- await openLoginWithContext(page)
- await page.locator('[data-role-code="teacher"]').click()
-
- const organization = page.getByTestId('login-organization')
- const department = page.getByTestId('login-department')
- await expect(organization).toBeEnabled()
- await expect(department).toBeDisabled()
- await expect(identitySelect(page)).toBeDisabled()
- await expect(submitButton(page)).toBeDisabled()
-
- await selectElementOption(page, organization, '工程装备系')
- await expect(department).toBeEnabled()
- await expectElementSelectPlaceholder(department, '请选择教研室')
- await expectElementSelectOptions(page, department, [
- '工程装备系(本级)',
- '维修教研室',
- '智能保障教研室',
- ])
-
- await selectElementOption(page, department, '工程装备系(本级)')
- await expect(department).toContainText('工程装备系(本级)')
- await expect(identitySelect(page)).toBeEnabled()
- await selectElementOption(page, identitySelect(page), /许教员.*工程装备系/)
- await passwordInput(page).fill('ContractOnly-NotSubmitted')
- await expect(submitButton(page)).toBeEnabled()
-
- await selectElementOption(page, department, '维修教研室')
- await expect(department).toContainText('维修教研室')
- await expect(passwordInput(page)).toHaveValue('')
- await expect(identitySelect(page)).toBeEnabled()
- const identityListbox = await openElementSelect(page, identitySelect(page))
- await expect(identityListbox.getByRole('option', { name: /马雅柔.*水电组A/ })).toBeVisible()
- await identityListbox.getByRole('option', { name: /马雅柔.*水电组A/ }).click()
- await passwordInput(page).fill('ContractOnly-NotSubmitted')
- await expect(submitButton(page)).toBeEnabled()
- await captureScreenshot(page, testInfo, 'teacher-cascade-current-and-child')
- })
-
- test('切换身份会清空账号密码和已选组织', async ({ page }, testInfo) => {
- await openLoginWithContext(page)
- await page.locator('[data-role-code="teacher"]').click()
- await selectElementOption(page, page.getByTestId('login-organization'), '工程装备系')
- await selectElementOption(page, page.getByTestId('login-department'), '维修教研室')
- await selectElementOption(page, identitySelect(page), /马雅柔.*水电组A/)
- await passwordInput(page).fill('ContractOnly-NotSubmitted')
-
- await page.locator('[data-role-code="administrative"]').click()
- await expect(adminUsernameInput(page)).toHaveCount(0)
- await expectElementSelectPlaceholder(identitySelect(page), '请选择行政账号')
- await expect(passwordInput(page)).toHaveValue('')
- await expect(page.getByTestId('login-organization')).toHaveCount(0)
- await expectElementSelectPlaceholder(page.getByTestId('login-department'), '请选择教学系')
-
- await selectElementOption(page, page.getByTestId('login-department'), '指挥系')
- await selectElementOption(page, identitySelect(page), /周主任.*指挥系/)
- await passwordInput(page).fill('ContractOnly-NotSubmitted')
- await page.locator('[data-role-code="teacher"]').click()
- await expect(adminUsernameInput(page)).toHaveCount(0)
- await expectElementSelectPlaceholder(identitySelect(page), '请选择教员账号')
- await expect(passwordInput(page)).toHaveValue('')
- await expectElementSelectPlaceholder(page.getByTestId('login-organization'), '请选择教学系')
- await expectElementSelectPlaceholder(page.getByTestId('login-department'), '请选择教研室')
- await expect(page.getByTestId('login-department')).toBeDisabled()
- await captureScreenshot(page, testInfo, 'role-switch-clears-sensitive-fields')
- })
-
- test('登录页只按当前身份和组织加载安全账号候选且不会预填密码', async ({ page }) => {
- await openLoginWithContext(page)
-
- await expect(adminUsernameInput(page)).toHaveValue('')
- await expect(passwordInput(page)).toHaveValue('')
- await expect(adminUsernameInput(page)).toHaveAttribute('autocomplete', 'username')
- await expect(passwordInput(page)).toHaveAttribute('autocomplete', 'current-password')
- await expect(page.locator('.login-password-toggle')).toBeVisible()
- await expect(page.locator('.login-password-toggle svg')).toHaveCount(1)
- await expect(page.locator('.login-password-toggle')).toHaveAttribute('aria-label', '显示密码')
- await expect(page.locator('[data-password-visibility-icon="hidden"]')).toBeVisible()
- await page.locator('.login-password-toggle').click()
- await expect(passwordInput(page)).toHaveAttribute('type', 'text')
- await expect(page.locator('.login-password-toggle')).toHaveAttribute('aria-label', '隐藏密码')
- await expect(page.locator('[data-password-visibility-icon="visible"]')).toBeVisible()
- await page.locator('.login-password-toggle').click()
- await expect(passwordInput(page)).toHaveAttribute('type', 'password')
- await expect(page.locator('[data-password-visibility-icon="hidden"]')).toBeVisible()
- expect(await adminUsernameInput(page).getAttribute('list')).toBeNull()
- await expect(page.locator('datalist')).toHaveCount(0)
- await expect(page.locator('select[name*="user" i], select[name*="account" i]')).toHaveCount(0)
-
- await page.locator('[data-role-code="administrative"]').click()
- await expect(adminUsernameInput(page)).toHaveCount(0)
- await expect(identitySelect(page)).toBeDisabled()
- await expect(passwordInput(page)).toHaveValue('')
-
- await page.locator('[data-role-code="teacher"]').click()
- await expect(adminUsernameInput(page)).toHaveCount(0)
- await expect(identitySelect(page)).toBeDisabled()
- await expect(passwordInput(page)).toHaveValue('')
-
- await page.locator('[data-role-code="student"]').click()
- await expect(adminUsernameInput(page)).toHaveCount(0)
- await expect(identitySelect(page)).toBeEnabled()
- const studentListbox = await openElementSelect(page, identitySelect(page))
- await expect(studentListbox.getByRole('option', { name: /学生甲.*水电组A/ })).toBeVisible()
- await page.keyboard.press('Escape')
- await expect(passwordInput(page)).toHaveValue('')
-
- await page.locator('[data-role-code="admin"]').click()
- await expect(identitySelect(page)).toHaveCount(0)
- await expect(adminUsernameInput(page)).toHaveValue('')
- await expect(passwordInput(page)).toHaveValue('')
- })
-
- test('登录上下文失败时管理员账号密码入口仍可使用', async ({ page }, testInfo) => {
- await mockLoginContextFailure(page)
- await page.goto('/login')
- const contextState = page.getByText('认证配置连接失败', { exact: true })
- await expect(contextState).toBeAttached()
- await expect(contextState).toHaveAttribute('aria-live', 'polite')
- await expect(contextState).toBeVisible()
-
- await expect(page.locator('[data-role-code="admin"]')).toHaveAttribute('aria-pressed', 'true')
- await expect(page.getByTestId('login-organization')).toHaveCount(0)
- await expect(page.getByTestId('login-department')).toHaveCount(0)
- await adminUsernameInput(page).fill('admin.contract')
- await passwordInput(page).fill('ContractOnly-NotSubmitted')
- await expect(submitButton(page)).toBeEnabled()
- await captureScreenshot(page, testInfo, 'context-failure-admin-available')
- })
- })
-
- for (const viewport of [
- { width: 375, height: 812 },
- { width: 1024, height: 768 },
- { width: 1440, height: 900 },
- ]) {
- test(`登录页在 ${viewport.width}px 宽度无横向滚动`, async ({ page }, testInfo) => {
- await page.setViewportSize(viewport)
- await openLoginWithContext(page)
-
- const geometry = await page.evaluate(() => {
- const card = document.querySelector<HTMLElement>('.login-card')?.getBoundingClientRect()
- return {
- documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
- bodyOverflow: document.body.scrollWidth - document.body.clientWidth,
- cardLeft: card?.left ?? -1,
- cardRight: card?.right ?? Number.POSITIVE_INFINITY,
- viewportWidth: window.innerWidth,
- }
- })
- expect(geometry.documentOverflow).toBeLessThanOrEqual(1)
- expect(geometry.bodyOverflow).toBeLessThanOrEqual(1)
- expect(geometry.cardLeft).toBeGreaterThanOrEqual(0)
- expect(geometry.cardRight).toBeLessThanOrEqual(geometry.viewportWidth + 1)
- await captureScreenshot(page, testInfo, `login-responsive-${viewport.width}`)
- })
- }
|