import { test, expect, type Page } from '@playwright/test' async function fixture(page: Page, role = 'super_admin') { const calls: { path: string; body: Record }[] = [] const now = new Date(), earlier = new Date(now.getTime() - 180000).toISOString() let acknowledged = false const rules = [{ id: 'r1', name: '节点 CPU 高水位', metric: 'node_cpu', threshold: 90, holdSeconds: 120, enabled: true }] await page.addInitScript(() => sessionStorage.setItem('vdi.accessToken', 'operations-fixture')) await page.route('**/api/v1/**', async route => { const request = route.request(), path = new URL(request.url()).pathname.replace('/api/v1', '') const list = (items: unknown[], extra = {}) => route.fulfill({ json: { items, total: items.length, page: 1, pageSize: 20, ...extra } }) if (request.method() !== 'GET') { const body = request.postData() ? request.postDataJSON() as Record : {}; calls.push({ path, body }) if (path === '/imports/preview') { const invalid = String(body.csv).includes('bad'), terminal = body.kind === 'terminals' await route.fulfill({ json: { kind: body.kind, totalRows: 1, validRows: invalid ? 0 : 1, errorRows: invalid ? 1 : 0, previewToken: invalid ? '' : 'preview-token', rows: [{ line: 2, key: terminal ? 'BULK-001' : 'imported', values: terminal ? { sn: 'BULK-001', name: '批量设备', group: '研发' } : { username: 'imported', displayName: '导入用户' }, errors: invalid ? ['该账号已存在'] : [] }] } }); return } if (path === '/imports/commit') { await route.fulfill({ json: { id: 'i1', kind: body.kind, count: 1, replayed: false, items: [{ line: 2, key: 'imported', id: 'new-id' }], activationSecrets: body.kind === 'terminals' ? [{ sn: 'BULK-001', activationSecret: 'activate_test_only' }] : [] } }); return } if (path.endsWith('/acknowledge')) { acknowledged = true; await route.fulfill({ json: { accepted: true } }); return } if (path === '/monitor/rules' || path === '/monitor/rules/r1') { Object.assign(rules[0]!, body); await route.fulfill({ json: rules[0] }); return } } if (path === '/auth/me') { await route.fulfill({ json: { id: 'admin', username: 'root', displayName: '平台管理员', role } }); return } if (path === '/settings') { await route.fulfill({ json: { mode: 'production', pveConfigured: true, capabilities: [] } }); return } if (path === '/overview') { await route.fulfill({ json: { terminalCount: 0, onlineTerminals: 0, desktopCount: 0, runningDesktops: 0, activeSessions: 0, failedTasks: 0, mode: 'production' } }); return } if (path === '/resources') { await list([{ id: 'node1', name: '计算节点', provider: 'proxmox', status: 'online', cpu: .4, memory: .6, storage: .5 }]); return } if (path === '/sessions/history') { await list([{ id: 'h1', sessionId: 's1', terminalId: 't1', terminalName: '历史终端', desktopId: 'd1', desktopName: 'Windows 桌面', username: 'imported', displayName: '研发人员', state: 'interrupted', source: 'ticket', reservedAt: earlier, connectedAt: earlier, lastReportedAt: earlier, endedAt: now.toISOString(), endReason: 'heartbeat_timeout' }]); return } if (path === '/monitor/rules') { await list(rules); return } if (path === '/monitor/series') { await route.fulfill({ json: { bucketSeconds: 300, retentionDays: 30, items: [{ time: earlier, cpu: 94, memory: 60, storage: null, samples: 1 }, { time: now.toISOString(), cpu: 95, memory: 61, storage: null, samples: 1 }] } }); return } if (path === '/monitor/alerts') { await list([{ id: 'a1', ruleId: 'r1', ruleName: '节点 CPU 高水位', metric: 'node_cpu', targetId: 'node1', targetName: '计算节点', value: 95, threshold: 90, status: 'firing', startedAt: earlier, openedAt: earlier, resolvedAt: null, resolveReason: '', acknowledgedAt: acknowledged ? now.toISOString() : null, acknowledgedBy: acknowledged ? 'root' : '' }], { firingCount: 1, unacknowledgedCount: acknowledged ? 0 : 1 }); return } if (['/tasks', '/audit-logs', '/terminals', '/users', '/desktops'].includes(path)) { await list([]); return } await route.fulfill({ status: 404, json: { code: 'fixture_missing', message: path } }) }) return calls } test('CSV 预检有错误不能提交,修改后重新预检并整批导入', async ({ page }) => { const calls = await fixture(page); await page.goto('/users') await page.getByRole('button', { name: '导入用户', exact: true }).click() const dialog = page.getByRole('dialog', { name: '批量新增用户' }) await dialog.getByLabel('CSV 内容').fill('username,displayName\nbad,重复\n') await dialog.getByRole('button', { name: '预检文件' }).click() await expect(dialog.getByText('该账号已存在', { exact: true })).toBeVisible() await expect(dialog.getByRole('button', { name: '提交导入' })).toBeDisabled() const download = page.waitForEvent('download'); await dialog.getByRole('button', { name: '下载错误行' }).click(); expect((await download).suggestedFilename()).toBe('import-errors.csv') await dialog.getByLabel('CSV 内容').fill('username,displayName\nimported,导入用户\n') await dialog.getByRole('button', { name: '预检文件' }).click() await expect(dialog.getByRole('button', { name: '提交导入' })).toBeEnabled() await dialog.getByRole('button', { name: '提交导入' }).click() await page.locator('.el-message-box').getByRole('button', { name: '确认导入' }).click() await expect(dialog.getByText('导入完成', { exact: true })).toBeVisible() expect(calls.filter(c => c.path.endsWith('/commit'))).toHaveLength(1) }) test('批量终端激活信息支持下载,未下载关闭需要确认且密钥不残留', async ({ page }) => { await fixture(page); await page.goto('/terminals') await page.getByRole('button', { name: '批量导入', exact: true }).click() const dialog = page.getByRole('dialog', { name: '批量登记终端' }) await dialog.getByLabel('CSV 内容').fill('sn,name,group\nBULK-001,批量设备,研发\n') await dialog.getByRole('button', { name: '预检文件' }).click(); await dialog.getByRole('button', { name: '提交导入' }).click() await page.locator('.el-message-box').getByRole('button', { name: '确认导入' }).click() await expect(dialog.getByText('导入完成', { exact: true })).toBeVisible() await dialog.getByRole('button', { name: '关闭', exact: true }).last().click() await page.locator('.el-message-box').getByRole('button', { name: '返回保存' }).click() const downloaded = page.waitForEvent('download'); await dialog.getByRole('button', { name: '下载终端激活信息' }).click(); expect((await downloaded).suggestedFilename()).toBe('terminal-activation.csv') await dialog.getByRole('button', { name: '关闭', exact: true }).last().click(); await expect(dialog).toBeHidden() await page.getByRole('button', { name: '批量导入', exact: true }).click(); await expect(dialog.getByRole('button', { name: '下载终端激活信息' })).toHaveCount(0) }) test('会话历史按需查询,展示心跳超时和实际连接时长', async ({ page }, testInfo) => { await fixture(page); await page.goto('/sessions'); await page.getByRole('tab', { name: '历史记录' }).click() await expect(page.getByText('历史终端', { exact: true })).toBeVisible(); await expect(page.getByText('心跳超时', { exact: true })).toBeVisible() await expect(page.getByText('3 分 0 秒')).toBeVisible() await page.getByLabel('搜索历史会话').fill('历史终端'); await page.getByRole('button', { name: '查询历史' }).click() await page.screenshot({ path: testInfo.outputPath('session-history.png'), fullPage: true, animations: 'disabled' }) }) test('告警确认保留正在告警状态,规则编辑和深浅窄屏可用', async ({ page }, testInfo) => { const calls = await fixture(page); await page.setViewportSize({ width: 1440, height: 1050 }); await page.goto('/monitor') await expect(page.getByRole('img', { name: 'CPU 使用率历史趋势' })).toBeVisible() await expect(page.getByText('此范围暂无有效采样')).toBeVisible() await page.getByRole('button', { name: '确认告警' }).click(); await expect(page.getByRole('button', { name: '确认告警' })).toHaveCount(0) await expect(page.getByRole('cell', { name: '正在告警', exact: true })).toBeVisible() await page.screenshot({ path: testInfo.outputPath('monitor-dark.png'), fullPage: true, animations: 'disabled' }) await page.getByRole('button', { name: '切换浅色主题' }).click() await page.screenshot({ path: testInfo.outputPath('monitor-light.png'), fullPage: true, animations: 'disabled' }) await page.getByRole('tab', { name: '告警规则' }).click(); await page.getByRole('button', { name: '编辑规则' }).click() const dialog = page.getByRole('dialog', { name: '编辑告警规则' }) await dialog.getByLabel('规则名称', { exact: true }).fill('CPU 持续高水位') await dialog.getByRole('button', { name: '保存规则' }).click(); await expect(dialog).toBeHidden() expect(calls.find(c => c.path === '/monitor/rules/r1')?.body.name).toBe('CPU 持续高水位') await page.setViewportSize({ width: 390, height: 844 }); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(391) await page.screenshot({ path: testInfo.outputPath('monitor-mobile.png'), fullPage: true, animations: 'disabled' }) }) test('只读人员可查监控及历史,无导入、确认或规则编辑入口', async ({ page }) => { await fixture(page, 'viewer'); await page.goto('/monitor') await expect(page.getByText('节点 CPU 高水位', { exact: true })).toBeVisible() await expect(page.getByRole('button', { name: '确认告警' })).toHaveCount(0) await page.getByRole('tab', { name: '告警规则' }).click(); await expect(page.getByRole('button', { name: '编辑规则' })).toHaveCount(0) await expect(page.getByRole('button', { name: '新增规则' })).toHaveCount(0) await page.goto('/users'); await expect(page.getByRole('button', { name: '导入用户', exact: true })).toHaveCount(0) })