|
- import type { 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: '200',
- code: 'COMMAND',
- name: '指挥系',
- parentId: null,
- children: [],
- },
- ],
- } as const
-
- const loginIdentities = {
- administrative: [
- { id: '301', displayName: '秦主任', departmentId: '100', departmentName: '工程装备系' },
- ],
- teacher: [
- { id: '401', displayName: '许教员', departmentId: '100', departmentName: '工程装备系' },
- { id: '402', displayName: '杜晨', departmentId: '110', departmentName: '维修教研室' },
- ],
- student: [
- { id: '501', displayName: '学生甲', departmentId: '110', departmentName: '维修教研室' },
- ],
- } as const
-
- const envelope = (data: unknown, requestId: string) => JSON.stringify({
- code: 200,
- message: '成功',
- data,
- timestamp: '2026-08-17T00:00:00+08:00',
- requestId,
- })
-
- interface RuntimeIssues {
- consoleErrors: string[]
- pageErrors: string[]
- }
-
- const watchRuntimeIssues = (page: Page): RuntimeIssues => {
- const issues: RuntimeIssues = { consoleErrors: [], pageErrors: [] }
- page.on('console', (message) => {
- if (message.type() === 'error') issues.consoleErrors.push(message.text())
- })
- page.on('pageerror', (error) => issues.pageErrors.push(error.message))
- return issues
- }
-
- const mockLoginPage = async (page: Page) => {
- await page.route('**/api/auth/v1/system-config/public', (route) => route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: envelope({
- systemName: '某类装备维修实训数字车间',
- logoConfigured: false,
- logoUrl: null,
- faviconConfigured: false,
- faviconUrl: null,
- }, 'login-visual-public-config'),
- }))
- await page.route('**/api/auth/v1/auth/login-context', (route) => route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: envelope(loginContext, 'login-visual-context'),
- }))
- await page.route('**/api/auth/v1/auth/login-identities**', (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 identities = source.filter((identity) => !departmentId || identity.departmentId === departmentId)
- return route.fulfill({
- status: 200,
- contentType: 'application/json',
- body: envelope(identities, 'login-visual-identities'),
- })
- })
- }
-
- const openLoginPage = async (page: Page) => {
- await mockLoginPage(page)
- await page.goto('/login')
- const contextState = page.locator('.login-context-state')
- await expect(contextState).toHaveText('身份认证服务正常')
- await expect(contextState).toHaveAttribute('aria-live', 'polite')
- await expect(contextState).toHaveCSS('position', 'absolute')
- await expect(contextState).toHaveCSS('clip-path', 'inset(50%)')
- }
-
- const expectNoHorizontalOverflow = async (page: 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)
- }
-
- const expectNoRuntimeIssues = (issues: RuntimeIssues) => {
- expect(issues.consoleErrors, `console.error: ${issues.consoleErrors.join('\n')}`).toEqual([])
- expect(issues.pageErrors, `pageerror: ${issues.pageErrors.join('\n')}`).toEqual([])
- }
-
- test.describe('新原型登录页视觉与安全交互回归', () => {
- test('1920x945 展示新背景、校标、450x500 卡片且 SSO 只提示未接入', async ({ page }, testInfo) => {
- await page.setViewportSize({ width: 1920, height: 945 })
- const issues = watchRuntimeIssues(page)
- let loginRequestCount = 0
- page.on('request', (request) => {
- const url = new URL(request.url())
- if (request.method() === 'POST' && url.pathname === '/api/auth/v1/auth/login') loginRequestCount += 1
- })
- await openLoginPage(page)
-
- await expect(page.locator('.login-page')).toBeVisible()
- await expect(page.locator('.login-visual')).toBeVisible()
- await expect(page.locator('.login-copy')).toContainText('装备数字车间')
- await expect(page.locator('.login-copy')).toContainText('统一承载内容制作、虚实训练、训练考核与资源管理。')
- await expect(page.locator('.login-flow')).toContainText('01身份选择')
- await expect(page.locator('.login-flow')).toContainText('02账号认证')
- await expect(page.locator('.login-flow')).toContainText('03进入系统')
-
- const pageBackground = await page.locator('.login-page').evaluate((element) => getComputedStyle(element).backgroundImage)
- expect(pageBackground).toContain('login_bg.jpg')
- const backgroundResponse = await page.request.get(new URL('/login_bg.jpg', page.url()).toString())
- expect(backgroundResponse.status()).toBe(200)
- expect(backgroundResponse.headers()['content-type']).toContain('image/jpeg')
- expect((await backgroundResponse.body()).byteLength).toBeGreaterThan(500_000)
-
- const logo = page.locator('.login-logo')
- await expect(logo).toHaveAttribute('src', '/logo-title.png')
- await expect(logo).toBeVisible()
- const logoGeometry = await logo.evaluate((element: HTMLImageElement) => ({
- complete: element.complete,
- naturalWidth: element.naturalWidth,
- naturalHeight: element.naturalHeight,
- }))
- expect(logoGeometry).toEqual({ complete: true, naturalWidth: 342, naturalHeight: 65 })
- const logoResponse = await page.request.get(new URL('/logo-title.png', page.url()).toString())
- expect(logoResponse.status()).toBe(200)
- expect(logoResponse.headers()['content-type']).toContain('image/png')
- expect((await logoResponse.body()).byteLength).toBeGreaterThan(30_000)
-
- const cardGeometry = await page.locator('.login-card').evaluate((element) => {
- const box = element.getBoundingClientRect()
- return { width: box.width, height: box.height }
- })
- expect(Math.abs(cardGeometry.width - 450)).toBeLessThanOrEqual(1)
- expect(Math.abs(cardGeometry.height - 500)).toBeLessThanOrEqual(1)
- await expect(page.locator('[data-role-code]')).toHaveCount(4)
- await expect(page.getByText('其他登录方式', { exact: true })).toBeVisible()
- const ssoButton = page.getByRole('button', { name: '统一身份认证(SSO)' })
- await expect(ssoButton).toBeVisible()
-
- const username = page.locator('input[name="username"]')
- const password = page.locator('input[name="password"]')
- const remember = page.locator('input[name="rememberMe"]')
- await username.fill('admin.visual-contract')
- await password.fill('VisualOnly-NotSubmitted!')
- await remember.check()
- const urlBeforeSso = page.url()
- const pageCountBeforeSso = page.context().pages().length
-
- await ssoButton.click()
- await expect(page.getByText('统一身份认证暂未接入,请先使用账号密码登录', { exact: true })).toBeVisible()
- expect(page.url()).toBe(urlBeforeSso)
- expect(page.context().pages()).toHaveLength(pageCountBeforeSso)
- expect(loginRequestCount).toBe(0)
- await expect(page.locator('[data-role-code="admin"]')).toHaveAttribute('aria-pressed', 'true')
- await expect(username).toHaveValue('admin.visual-contract')
- await expect(password).toHaveValue('VisualOnly-NotSubmitted!')
- await expect(remember).toBeChecked()
-
- await captureScreenshot(page, testInfo, 'login-prototype-desktop-1920x945')
- expectNoRuntimeIssues(issues)
- })
-
- test('390x844 无横向溢出且四身份、组织字段与全部登录控件可滚动操作', async ({ page }, testInfo) => {
- await page.setViewportSize({ width: 390, height: 844 })
- const issues = watchRuntimeIssues(page)
- await openLoginPage(page)
-
- await expect(page.locator('.login-visual')).toBeHidden()
- await expectNoHorizontalOverflow(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 page.locator('[data-role-code="administrative"]').scrollIntoViewIfNeeded()
- await page.locator('[data-role-code="administrative"]').click()
- await expect(page.getByTestId('login-department')).toBeVisible()
- await expect(page.getByTestId('login-department')).toBeEnabled()
- await expect(page.getByTestId('login-organization')).toHaveCount(0)
- await expect(page.getByTestId('login-identity')).toBeVisible()
- await expect(page.getByTestId('login-identity')).toBeDisabled()
-
- await page.locator('[data-role-code="teacher"]').click()
- await expect(page.getByTestId('login-organization')).toBeVisible()
- await expect(page.getByTestId('login-organization')).toBeEnabled()
- await expect(page.getByTestId('login-department')).toBeVisible()
- await expect(page.getByTestId('login-department')).toBeDisabled()
- await expect(page.getByTestId('login-identity')).toBeVisible()
- await expect(page.getByTestId('login-identity')).toBeDisabled()
-
- await page.locator('[data-role-code="student"]').click()
- await expect(page.getByTestId('login-organization')).toHaveCount(0)
- await expect(page.getByTestId('login-department')).toHaveCount(0)
- await expect(page.getByTestId('login-identity')).toBeVisible()
- await expect(page.getByTestId('login-identity')).toBeEnabled()
-
- await page.locator('[data-role-code="admin"]').click()
- await expect(page.getByTestId('login-identity')).toHaveCount(0)
- await expect(page.getByTestId('login-organization')).toHaveCount(0)
- await expect(page.getByTestId('login-department')).toHaveCount(0)
-
- const username = page.locator('input[name="username"]')
- const password = page.locator('input[name="password"]')
- const passwordToggle = page.locator('.login-password-toggle')
- const remember = page.locator('input[name="rememberMe"]')
- const submit = page.getByRole('button', { name: '登录系统' })
- const sso = page.getByRole('button', { name: '统一身份认证(SSO)' })
- for (const control of [username, password, passwordToggle, remember, submit, sso]) {
- await control.scrollIntoViewIfNeeded()
- await expect(control).toBeVisible()
- }
-
- await username.fill('admin.mobile-contract')
- await password.fill('MobileOnly-NotSubmitted!')
- await passwordToggle.click()
- await expect(password).toHaveAttribute('type', 'text')
- await remember.check()
- await expect(submit).toBeEnabled()
- await expectNoHorizontalOverflow(page)
-
- const visibleControlBounds = await page.locator('.login-card button:visible, .login-card input:visible').evaluateAll((controls) => controls.map((control) => {
- const box = control.getBoundingClientRect()
- return { left: box.left, right: box.right }
- }))
- for (const bounds of visibleControlBounds) {
- expect(bounds.left).toBeGreaterThanOrEqual(0)
- expect(bounds.right).toBeLessThanOrEqual(391)
- }
-
- await sso.scrollIntoViewIfNeeded()
- await captureScreenshot(page, testInfo, 'login-prototype-mobile-390x844')
- expectNoRuntimeIssues(issues)
- })
- })
|