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

86 lines
9.0 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: { role?: string; partial?: boolean; fail?: boolean; empty?: boolean; configured?: boolean; stale?: boolean } = {}) {
const writes: string[] = [], reads: string[] = [], now = new Date().toISOString()
let fail = !!options.fail
const reference = { vmid: 9000, vmName: 'win-reference', vmStatus: 'stopped', template: false, desktopId: 'desk-gpu', desktopName: '测试 Windows', slot: 'hostpci0', phase: 'current', source: 'direct', mapping: '', mediated: false, pendingRemoval: true }
const inventory = () => ({ node: 'node-gpu', observedAt: new Date(Date.now() - (options.stale ? 120000 : 0)).toISOString(), complete: !options.partial, visibleVmCount: 2, scannedVmCount: options.partial ? 1 : 2, issues: options.partial ? [{ code: 'vm_config_unavailable', vmid: 9001, message: '无法读取虚拟机当前及待应用配置' }] : [], items: options.empty ? [] : [
{ id: 'node-gpu:0000:00:02.0', address: '0000:00:02.0', name: 'Intel Integrated Graphics', vendor: 'Intel', vendorId: '0x8086', deviceId: '0x0416', class: '0x030000', iommuGroup: 0, state: options.partial ? 'unknown' : 'unreferenced', references: [] },
{ id: 'node-gpu:0000:01:00.0', address: '0000:01:00.0', name: 'GeForce GTX Test', vendor: 'NVIDIA', vendorId: '0x10de', deviceId: '0x1234', class: '0x030200', iommuGroup: 2, state: 'multiple', references: [reference, { ...reference, vmid: 9001, vmName: 'ubuntu-unmanaged', desktopId: '', desktopName: '', phase: 'pending', source: 'mapping', mapping: 'gpu-shared-map', mediated: true, template: true, pendingRemoval: false }] },
] })
await page.addInitScript(() => sessionStorage.setItem('vdi.accessToken', 'gpu-fixture'))
await page.route('**/api/v1/**', async route => {
const r = route.request(), path = new URL(r.url()).pathname.replace('/api/v1', '')
if (r.method() !== 'GET') { writes.push(path); await route.fulfill({ status: 403, json: { code: 'readonly_fixture', message: 'GPU 查看不应发送写请求', requestId: 'gpu-ui' } }); return }
const list = (items: unknown[]) => route.fulfill({ json: { items, total: items.length, page: 1, pageSize: 100, requestId: 'gpu-ui' } })
if (path === '/auth/me') { await route.fulfill({ json: { id: 'gpu-admin', username: 'gpu-reader', displayName: 'GPU 查看人员', role: options.role || 'viewer' } }); return }
if (path === '/settings') { await route.fulfill({ json: { mode: 'production', computeProvider: 'proxmox', pveConfigured: options.configured !== false, terminalTransport: 'https-poll', capabilities: ['gpu-inventory'] } }); 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(options.configured === false ? [] : [{ id: 'node-gpu', name: 'node-gpu', status: 'online', cpu: 0.2, memory: 0.5, storage: 0.3, provider: 'proxmox', updatedAt: now }]); return }
if (path === '/resources/gpus') {
reads.push(new URL(r.url()).searchParams.get('node') || '')
if (fail) { fail = false; await route.fulfill({ status: 502, json: { code: 'pci_unavailable', message: '无法读取节点 PCI 清单,请检查 Sys.Audit 权限', requestId: 'gpu-ui' } }); return }
await route.fulfill({ json: inventory() }); return
}
if (['/desktops', '/terminals', '/users', '/tasks', '/audit-logs'].includes(path)) { await list([]); return }
await route.fulfill({ status: 404, json: { code: 'fixture_missing', message: '测试接口未定义', requestId: 'gpu-ui' } })
})
return { writes, reads }
}
test('只读角色可查显卡并筛选配置引用,IOMMU 0 保留', async ({ page }, info) => {
const requests = await fixture(page); await page.goto('/gpus')
await expect(page.getByRole('heading', { name: 'Intel Integrated Graphics', exact: true })).toBeVisible()
const intel = page.locator('.gpu-card').filter({ has: page.getByRole('heading', { name: 'Intel Integrated Graphics', exact: true }) })
await expect(intel.getByText('未发现配置引用', { exact: true })).toBeVisible(); await expect(intel.locator('dd').filter({ hasText: /^0$/ })).toBeVisible()
await expect(page.locator('.gpu-card').getByText('多 VM 引用', { exact: true })).toBeVisible()
await page.getByRole('textbox', { name: '搜索 GPU' }).fill('win-reference'); await expect(page.locator('.gpu-card')).toHaveCount(1)
await page.getByRole('textbox', { name: '搜索 GPU' }).clear()
await page.screenshot({ path: info.outputPath('gpu-inventory-dark.png'), fullPage: true, animations: 'disabled' })
await expect(page.getByText('GPU 清单按需读取,请手动刷新', { exact: true })).toBeVisible()
expect(requests.reads).toEqual(['node-gpu']); expect(requests.writes).toEqual([])
await expect(page.getByRole('button', { name: '绑定 GPU', exact: true })).toHaveCount(0)
})
test('详情区分当前与待应用、待移除、模板及未纳管的映射引用', async ({ page }, info) => {
const requests = await fixture(page); await page.goto('/gpus')
await page.locator('.gpu-card').filter({ has: page.getByRole('heading', { name: 'GeForce GTX Test', exact: true }) }).getByRole('button', { name: '查看 GPU 详情' }).click()
const drawer = page.getByRole('dialog', { name: 'GPU 资源详情', exact: true })
for (const text of ['当前配置', '待应用配置', '待移除', '模板', '介导设备', '尚未纳管为平台桌面']) await expect(drawer.getByText(text, { exact: true })).toBeVisible()
await expect(drawer.getByText('hostpci0 · PCI 映射 gpu-shared-map', { exact: true })).toBeVisible()
await expect(drawer.getByText('已纳管桌面:测试 Windows', { exact: true })).toBeVisible()
await page.screenshot({ path: info.outputPath('gpu-reference-dark.png'), fullPage: true, animations: 'disabled' })
expect(requests.writes).toEqual([])
})
test('配置读取不完整显示待核查,不宣称显卡可用', async ({ page }) => {
const requests = await fixture(page, { partial: true }); await page.goto('/gpus')
await expect(page.getByRole('heading', { name: '检查未完整' })).toBeVisible()
await expect(page.getByText('VM 9001:无法读取虚拟机当前及待应用配置', { exact: true })).toBeVisible()
const intel = page.locator('.gpu-card').filter({ hasText: 'Intel Integrated Graphics' }); await expect(intel.getByText('待核查', { exact: true })).toBeVisible()
await intel.getByRole('button', { name: '查看 GPU 详情' }).click(); await expect(page.getByText('检查范围不完整,引用情况待核查', { exact: true })).toBeVisible()
expect(requests.writes).toEqual([])
})
test('PCI 权限错误提供重试,数据过期有明确提示', async ({ page }) => {
const requests = await fixture(page, { fail: true, stale: true }); await page.goto('/gpus')
await expect(page.getByText(/无法读取节点 PCI 清单,请检查 Sys.Audit 权限/)).toBeVisible()
await page.getByRole('button', { name: '重试读取', exact: true }).click()
await expect(page.getByText('数据已过期,请刷新', { exact: true })).toBeVisible()
await expect(page.locator('.gpu-card')).toHaveCount(2); expect(requests.reads).toHaveLength(2); expect(requests.writes).toEqual([])
})
test('浅色窄屏清单和详情可读且页面不横向溢出', async ({ page }, info) => {
await fixture(page); await page.addInitScript(() => localStorage.setItem('vdi.theme', 'light')); await page.setViewportSize({ width: 390, height: 844 }); await page.goto('/gpus')
await expect(page.locator('.gpu-card')).toHaveCount(2)
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(391)
await page.screenshot({ path: info.outputPath('gpu-inventory-light-mobile.png'), fullPage: true, animations: 'disabled' })
await page.getByRole('button', { name: '查看 GPU 详情' }).last().click()
const drawer = page.getByRole('dialog', { name: 'GPU 资源详情', exact: true }); await expect(drawer.getByRole('heading', { name: 'GeForce GTX Test', exact: true })).toBeVisible()
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(391)
await expect.poll(async () => { const box = await drawer.boundingBox(); return box ? box.x + box.width : 999 }).toBeLessThanOrEqual(391)
await page.screenshot({ path: info.outputPath('gpu-detail-light-mobile.png'), animations: 'disabled' })
})
test('空节点清单与尚未接入显示真实空态', async ({ page }) => {
let requests = await fixture(page, { empty: true }); await page.goto('/gpus')
await expect(page.getByText('当前节点未发现显示控制器类 PCI 设备', { exact: true })).toBeVisible(); expect(requests.writes).toEqual([])
await page.unroute('**/api/v1/**'); requests = await fixture(page, { configured: false }); await page.reload()
await expect(page.getByText('尚未接入 Proxmox', { exact: true })).toBeVisible(); await expect(page.getByRole('button', { name: '刷新 GPU 清单', exact: true })).toBeDisabled(); expect(requests.reads).toEqual([])
})