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 小时') })