vdi-cloud/console/e2e/operations.spec.ts

103 lines
9.9 KiB
TypeScript

import { test, expect, type Page } from '@playwright/test'
async function fixture(page: Page, role = 'super_admin') {
const calls: { path: string; body: Record<string, unknown> }[] = []
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<string, unknown> : {}; 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)
})