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('.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) }) })