86 lines
9.0 KiB
TypeScript
86 lines
9.0 KiB
TypeScript
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([])
|
||
})
|