vdi-cloud/console/e2e/terminal-metrics.spec.ts

99 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect, type Page } from '@playwright/test'
async function fixture(page: Page, options: { stale?: boolean; legacy?: boolean; missing?: boolean; fail?: boolean; delayFirst?: boolean } = {}) {
const now = new Date().toISOString(), writes: string[] = [], reads: URL[] = []
let fail = options.fail, firstDelivered = false
await page.addInitScript(() => sessionStorage.setItem('vdi.accessToken', 'metrics-fixture'))
await page.route('**/api/v1/**', async route => {
const request = route.request(), url = new URL(request.url()), path = url.pathname.replace('/api/v1', '')
if (request.method() !== 'GET') { writes.push(path); await route.fulfill({ status: 403, json: { message: '只读页面出现写请求' } }); return }
const list = (items: unknown[]) => route.fulfill({ json: { items, total: items.length, page: 1, pageSize: 100, requestId: 'metrics-fixture' } })
if (path === '/auth/me') { await route.fulfill({ json: { id: 'metrics-viewer', username: 'viewer', displayName: '只读人员', role: 'viewer' } }); return }
if (path === '/settings') { await route.fulfill({ json: { mode: 'production', capabilities: [], pveConfigured: true, computeProvider: 'proxmox' } }); return }
if (path === '/overview') { await route.fulfill({ json: { terminalCount: 1, onlineTerminals: 1, desktopCount: 0, runningDesktops: 0, activeSessions: 0, failedTasks: 0, mode: 'production' } }); return }
if (path === '/terminals') { await list([{ id: 'metrics-a', name: '指标测试盒子', sn: 'METRICS-001', group: '测试', status: options.stale ? 'offline' : 'online', agentVersion: 'qt-platform/0.4.0', lastSeen: now, sessionDesktopId: null, configVersion: 1, fps: null, rtt: null }]); return }
if (path === '/terminals/metrics-a/metrics') {
reads.push(url)
const requestNumber = reads.length
if (options.delayFirst && reads.length === 1) await new Promise(resolve => setTimeout(resolve, 2000))
if (fail) { fail = false; await route.fulfill({ status: 503, json: { code: 'unavailable', message: '指标服务暂不可用', requestId: 'metrics-fixture' } }); return }
const from = url.searchParams.get('from')!, to = url.searchParams.get('to')!, start = Date.parse(from)
await route.fulfill({ json: { latest: { cpuPercent: options.missing ? null : options.delayFirst && requestNumber > 1 ? 25 : 0, memoryPercent: 30, socTemperatureC: 44.37, uptimeSeconds: 90120, sampledAt: now, supported: !options.legacy, fresh: !options.stale && !options.legacy, online: !options.stale }, from, to, bucketSeconds: 60, retentionDays: 30, items: [
{ time: new Date(start + 60000).toISOString(), cpuPercent: 10, memoryPercent: 30, socTemperatureC: 44, uptimeSeconds: 90100, samples: 2 },
{ time: new Date(start + 120000).toISOString(), cpuPercent: null, memoryPercent: null, socTemperatureC: null, uptimeSeconds: null, samples: 0 },
{ time: new Date(start + 180000).toISOString(), cpuPercent: 20, memoryPercent: 30, socTemperatureC: 44.37, uptimeSeconds: 90120, samples: 2 },
] } }); if (requestNumber === 1) firstDelivered = true; return
}
if (['/desktops', '/users', '/tasks', '/audit-logs', '/resources'].includes(path)) { await list([]); return }
await route.fulfill({ status: 404, json: { message: '测试接口未定义' } })
})
return { writes, reads, firstDelivered: () => firstDelivered }
}
async function open(page: Page) {
await page.goto('/terminals')
await page.getByRole('button', { name: '详情', exact: true }).click()
await page.getByRole('button', { name: '查看运行指标', exact: true }).click()
return page.getByRole('region', { name: '终端运行指标' })
}
test('真实指标独立按需加载,零值、SoC 温度、运行时长与缺失断线显示', async ({ page }, info) => {
const requests = await fixture(page)
await page.goto('/terminals'); await page.getByRole('button', { name: '详情', exact: true }).click()
expect(requests.reads).toHaveLength(0)
await page.getByRole('button', { name: '查看运行指标', exact: true }).click()
const panel = page.getByRole('region', { name: '终端运行指标' })
await expect(panel.getByText('0.0 %', { exact: true })).toBeVisible()
await expect(panel.getByText('44.4 °C', { exact: true })).toBeVisible()
await expect(panel.getByText('1 天 1 小时 2 分钟', { exact: true })).toBeVisible()
await expect(panel.getByRole('img', { name: /历史趋势/ })).toHaveCount(3)
const path = await panel.getByRole('img', { name: 'CPU 使用率历史趋势' }).locator('path').getAttribute('d')
expect(path?.match(/M/g)).toHaveLength(2); expect(path).not.toContain('L')
await expect(panel.getByText(/串流 FPS\/RTT 尚未采集/)).toBeVisible()
await page.screenshot({ path: info.outputPath('terminal-metrics-dark.png'), fullPage: true })
expect(requests.writes).toEqual([])
})
test('离线隐藏当前数值并保留历史', async ({ page }) => {
const requests = await fixture(page, { stale: true }); const panel = await open(page)
await expect(panel.getByText(/设备离线或采样已过期/)).toBeVisible()
await expect(panel.locator('.metric-values strong')).toHaveText(['未采集', '未采集', '未采集', '未采集'])
await expect(panel.getByRole('img', { name: /历史趋势/ })).toHaveCount(3); expect(requests.writes).toEqual([])
})
test('旧 Agent 给出升级提示,不冒充零值', async ({ page }) => {
await fixture(page, { legacy: true }); const panel = await open(page)
await expect(panel.getByText('设备尚未上报系统指标,请更新 Agent', { exact: true })).toBeVisible()
await expect(panel.locator('.metric-values strong')).toHaveText(['未采集', '未采集', '未采集', '未采集'])
})
test('单项缺失保持未采集,读取失败可重试', async ({ page }) => {
await fixture(page, { missing: true, fail: true }); const panel = await open(page)
await expect(panel.getByText('指标服务暂不可用', { exact: true })).toBeVisible()
await expect(panel.locator('.metric-values')).toHaveCount(0)
await panel.getByRole('button', { name: '刷新指标' }).click()
await expect(panel.locator('.metric-values strong').first()).toHaveText('未采集')
await expect(panel.getByText('30.0 %', { exact: true })).toBeVisible()
})
test('时间范围切换与窄屏浅色展示', async ({ page }, info) => {
const requests = await fixture(page)
await page.addInitScript(() => localStorage.setItem('vdi.theme', 'light'))
await page.setViewportSize({ width: 390, height: 844 })
const panel = await open(page)
await expect(panel.getByText('44.4 °C', { exact: true })).toBeVisible()
await panel.locator('.el-select').click()
await page.getByRole('option', { name: '最近 7 天' }).click()
await expect.poll(() => requests.reads.length).toBe(2)
const url = requests.reads[1]!
expect(Date.parse(url.searchParams.get('to')!) - Date.parse(url.searchParams.get('from')!)).toBe(7 * 86400000)
await expect(panel.getByText('44.4 °C', { exact: true })).toBeVisible()
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(391)
await page.screenshot({ path: info.outputPath('terminal-metrics-mobile-light.png'), fullPage: true })
expect(requests.writes).toEqual([])
})
test('切换时间范围时旧响应不能覆盖新响应', async ({ page }) => {
const requests = await fixture(page, { delayFirst: true }); const panel = await open(page)
await expect.poll(() => requests.reads.length).toBe(1)
await panel.locator('.el-select').click(); await page.getByRole('option', { name: '最近 24 小时' }).click()
await expect.poll(() => requests.reads.length).toBe(2)
await expect(panel.getByText('44.4 °C', { exact: true })).toBeVisible()
await expect.poll(requests.firstDelivered).toBe(true)
await expect(panel.getByText('25.0 %', { exact: true })).toBeVisible()
await expect(panel.locator('.el-select')).toHaveText('最近 24 小时')
})