import type { Page, Route } from '@playwright/test' import { captureScreenshot, expect, test } from './fixtures' const envelope = (data: unknown) => ({ code: 0, message: 'ok', data, timestamp: Date.now(), requestId: 'sidebar-bootstrap-regression', }) const profile = { user: { id: 'sidebar-admin', username: 'sidebar-admin', displayName: '系统管理员', departmentId: 'system', departmentName: '系统管理', enabled: true, mustChangePassword: false, version: 1, }, roles: [{ id: 'admin-role', code: 'ADMIN', name: '系统管理员', shortName: '系统', enabled: true, builtIn: true, isSuperAdmin: true, dataScope: 'ALL', }], activeRoleId: 'admin-role', permissions: ['*'], authorizationMode: 'SINGLE_ACTIVE', loginTime: new Date().toISOString(), } const navigation = [ { id: 'agents', code: 'agents', name: '数字人智能体', type: 'GROUP', sortOrder: 10, enabled: true, path: '/agents/manage', icon: 'Platform', permissionCodes: [], children: [ { id: 'agent-management', code: 'agent-management', name: '虚拟教员智能体', type: 'PAGE', sortOrder: 10, enabled: true, path: '/agents/manage', icon: 'Management', permissionCodes: [], children: [] }, { id: 'agent-projects', code: 'agent-projects', name: '智能体组别管理', type: 'PAGE', sortOrder: 20, enabled: true, path: '/agents/projects', icon: 'Files', permissionCodes: [], children: [] }, ], }, { id: 'assets', code: 'assets', name: '资源中心', type: 'GROUP', sortOrder: 20, enabled: true, path: '/assets/avatars', icon: 'Collection', permissionCodes: [], children: [ { id: 'avatars', code: 'avatars', name: '数字形象', type: 'PAGE', sortOrder: 10, enabled: true, path: '/assets/avatars', icon: 'Avatar', permissionCodes: [], children: [] }, { id: 'ai-models', code: 'ai-models', name: 'AI模型', type: 'PAGE', sortOrder: 20, enabled: true, path: '/assets/ai-models', icon: 'MagicStick', permissionCodes: [], children: [] }, ], }, { id: 'settings', code: 'settings', name: '系统设置', type: 'GROUP', sortOrder: 30, enabled: true, path: '/settings/general', icon: 'Setting', permissionCodes: [], children: [ { id: 'settings-general', code: 'settings-general', name: '基础设置', type: 'PAGE', sortOrder: 10, enabled: true, path: '/settings/general', icon: 'Setting', permissionCodes: [], children: [] }, { id: 'settings-menus', code: 'settings-menus', name: '菜单管理', type: 'PAGE', sortOrder: 20, enabled: true, path: '/settings/menus', icon: 'Menu', permissionCodes: [], children: [] }, ], }, ] async function installShellMocks(page: Page, holdBootstrap = false) { let releaseBootstrap = () => {} const bootstrapGate = holdBootstrap ? new Promise((resolve) => { releaseBootstrap = resolve }) : Promise.resolve() await page.addInitScript(() => { window.localStorage.setItem('ai-person:web:access-token:v1', 'sidebar-bootstrap-token') window.localStorage.setItem('ai-person:web:remember:v1', '1') }) const fulfillApi = async (route: Route) => { const pathname = new URL(route.request().url()).pathname let data: unknown = {} if (pathname.endsWith('/system-config/public') || pathname.endsWith('/auth/me')) { await bootstrapGate } if (pathname.endsWith('/auth/me')) data = profile else if (pathname.endsWith('/menus/navigation')) data = navigation else if (pathname.endsWith('/overview')) { data = { stats: {}, agents: [], videos: [], jobs: [] } } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(envelope(data)) }) } await page.route('**/api/auth/v1/**', fulfillApi) await page.route('**/api/v1/**', fulfillApi) return releaseBootstrap } test('App 模块尚未加载时由 HTML 直接展示启动状态', async ({ page }) => { let releaseApp = () => {} const appGate = new Promise((resolve) => { releaseApp = resolve }) await page.route('**/src/App.vue*', async (route) => { await appGate await route.continue() }) try { await page.goto('/overview', { waitUntil: 'commit' }) await expect(page.locator('.static-bootstrap-screen')).toBeVisible() await expect(page.getByText('正在启动系统')).toBeVisible() await expect(page.getByText('正在加载应用资源,请稍候…')).toBeVisible() } finally { releaseApp() } await expect(page.locator('.login-shell')).toBeVisible() await expect(page.locator('.static-bootstrap-screen')).toHaveCount(0) }) test('认证与公开配置请求未完成时展示品牌启动状态', async ({ page }, testInfo) => { const releaseBootstrap = await installShellMocks(page, true) await page.goto('/overview') await expect(page.locator('.app-bootstrap-screen')).toBeVisible() await expect(page.getByText('正在进入系统')).toBeVisible() await expect(page.getByText('正在验证登录状态并加载工作台,请稍候…')).toBeVisible() await captureScreenshot(page, testInfo, 'bootstrap-loading-not-blank') releaseBootstrap() await expect(page.locator('.app-shell')).toBeVisible() await expect(page.locator('.app-bootstrap-screen')).toHaveCount(0) }) test('侧栏使用 Element Plus 菜单并保持手风琴展开', async ({ page }, testInfo) => { await installShellMocks(page) await page.goto('/overview') await expect(page.locator('.app-shell')).toBeVisible() const menu = page.locator('#main-sidebar .sidebar-menu') const groups = menu.locator(':scope > .el-sub-menu') await expect(menu).toHaveClass(/el-menu/) await expect(groups).toHaveCount(3) const first = groups.nth(0) const second = groups.nth(1) const third = groups.nth(2) await first.locator(':scope > .el-sub-menu__title').click() await expect(first).toHaveClass(/is-opened/) await second.locator(':scope > .el-sub-menu__title').click() await expect(second).toHaveClass(/is-opened/) await expect(first).not.toHaveClass(/is-opened/) await third.locator(':scope > .el-sub-menu__title').click() await expect(third).toHaveClass(/is-opened/) await expect(second).not.toHaveClass(/is-opened/) await expect(menu.locator(':scope > .el-sub-menu.is-opened')).toHaveCount(1) await captureScreenshot(page, testInfo, 'element-menu-accordion') })