import { test, expect, type Page } from '@playwright/test' // Every endpoint is intercepted; these tests cannot mutate a live PVE desktop. async function fixture(page: Page, options: { running?: boolean; role?: string; fail?: boolean } = {}) { const requests: { path: string; body: Record; key?: string }[] = [], now = new Date().toISOString() const desktop = { id: 'desk-hw', name: '规格验收桌面', vmid: 9000, node: 'test', os: 'Windows', pool: '', poolId: null, provider: 'proxmox', powerState: options.running ? 'running' : 'stopped', serviceState: 'unknown', setupRequired: false, endpoint: '192.0.2.90', app: 'Desktop', width: 1920, height: 1080, fps: 30, updatedAt: now } await page.addInitScript(() => sessionStorage.setItem('vdi.accessToken', 'configuration-fixture')) await page.route('**/api/v1/**', async route => { const r = route.request(), path = new URL(r.url()).pathname.replace('/api/v1', '') const list = (items: unknown[]) => route.fulfill({ json: { items, total: items.length, page: 1, pageSize: 100, requestId: 'config-ui' } }) if (r.method() !== 'GET') { requests.push({ path, body: r.postData() ? r.postDataJSON() : {}, key: r.headers()['idempotency-key'] }) if (path === '/settings/pve/check') { await route.fulfill(options.fail ? { status: 502, json: { code: 'pve_connection_failed', message: '连接检查失败,请检查证书与权限', requestId: 'config-ui' } } : { json: { available: true, nodes: 1, vms: 3, message: 'TLS、身份与资源读取检查通过' } }); return } if (path === '/desktops/desk-hw/hardware/check') { await route.fulfill({ json: { available: true, message: '规格预检通过' } }); return } if (path === '/desktops/desk-hw/hardware') { await route.fulfill({ status: 202, json: { id: 'task-hw', kind: 'hardware', action: 'configure-hardware', targetId: desktop.id, targetName: desktop.name, actor: 'root', status: 'queued', message: '等待执行', providerRef: '', createdAt: now, updatedAt: now } }); return } if (path === '/settings/pve' || path.endsWith('/hardware-reconcile')) { await route.fulfill({ json: { accepted: true } }); return } } if (path === '/auth/me') { await route.fulfill({ json: { id: 'admin-hw', username: 'root', displayName: '验收管理员', role: options.role || 'super_admin' } }); return } if (path === '/settings/pve') { await route.fulfill({ json: { url: 'https://pve.example:8006', tokenId: 'service@pve!vdi', tokenConfigured: true, caPem: '', revision: 'config-v1', source: 'environment' } }); return } if (path === '/desktops/desk-hw/hardware') { await route.fulfill({ json: { cores: 2, sockets: 1, memoryMiB: 2048, digest: 'hardware-v1', disks: [{ key: 'scsi0', storage: 'local', sizeGiB: 60 }], powerState: desktop.powerState, blockers: options.running ? ['请先正常关闭桌面,再修改规格'] : [] } }); return } if (path === '/settings') { await route.fulfill({ json: { mode: 'production', computeProvider: 'proxmox', pveConfigured: true, terminalTransport: 'https-poll', capabilities: [] } }); return } if (path === '/overview') { await route.fulfill({ json: { terminalCount: 0, onlineTerminals: 0, desktopCount: 1, runningDesktops: 0, activeSessions: 0, failedTasks: 0, mode: 'production' } }); return } if (path === '/desktops') { await list([desktop]); return } if (path === '/tasks') { await list([{ id: 'hw-timeout', kind: 'hardware', action: 'expand-disk', targetId: desktop.id, targetName: desktop.name, actor: 'root', status: 'timeout', message: '规格变更结果未确认,不会自动重发', providerRef: 'UPID:old', createdAt: now, updatedAt: now }]); return } if (['/terminals', '/users', '/resources', '/audit-logs'].includes(path)) { await list([]); return } await route.fulfill({ status: 404, json: { code: 'missing_fixture', message: '未定义测试接口', requestId: 'config-ui' } }) }) return requests } test('接入配置不回显密钥,取消不写入,检查后保留原密钥保存', async ({ page }, info) => { const requests = await fixture(page) await page.goto('/settings') await page.getByRole('button', { name: '配置 Proxmox 接入' }).click() const dialog = page.getByRole('dialog', { name: 'Proxmox 接入配置' }) await expect(dialog.getByRole('textbox', { name: 'PVE Token 密钥' })).toHaveValue('') await dialog.getByRole('button', { name: '检查连接' }).click() await expect(dialog.getByText(/TLS、身份与资源读取检查通过/)).toBeVisible() await page.screenshot({ path: info.outputPath('pve-connection-dark.png'), fullPage: true }) await dialog.getByRole('button', { name: '保存并生效' }).click() await page.getByRole('button', { name: '取消', exact: true }).last().click() expect(requests.filter(r => r.path === '/settings/pve')).toHaveLength(0) await dialog.getByRole('button', { name: '保存并生效' }).click() await page.getByRole('button', { name: '确认保存', exact: true }).click() await expect(dialog).not.toBeVisible() expect(requests.find(r => r.path === '/settings/pve')?.body).toMatchObject({ tokenSecret: '', revision: 'config-v1' }) }) test('接入检查失败保留输入并支持窄屏', async ({ page }, info) => { const requests = await fixture(page, { fail: true }) await page.setViewportSize({ width: 390, height: 844 }) await page.goto('/settings') await page.getByRole('button', { name: '配置 Proxmox 接入' }).click() const dialog = page.getByRole('dialog', { name: 'Proxmox 接入配置' }) await dialog.getByRole('button', { name: '检查连接' }).click() await expect(dialog.getByText(/连接检查失败,请检查证书与权限/)).toBeVisible() expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(391) await page.screenshot({ path: info.outputPath('pve-connection-mobile.png'), fullPage: true }) await dialog.getByRole('button', { name: '取消', exact: true }).click() expect(requests.filter(r => r.path === '/settings/pve')).toHaveLength(0) }) test('CPU 内存调整先预检,修改参数使预检失效,确认后提交一次', async ({ page }, info) => { const requests = await fixture(page) await page.goto('/desktops') await page.getByRole('button', { name: '查看详情' }).click() await page.getByRole('button', { name: '规格管理' }).click() const dialog = page.getByRole('dialog', { name: '桌面规格管理' }) await expect(dialog.getByText(/当前:1 插槽 × 2 核/)).toBeVisible() await dialog.getByRole('spinbutton', { name: '每插槽核心数' }).fill('4') await dialog.getByRole('button', { name: '预检规格' }).click() await expect(dialog.getByRole('button', { name: '提交变更' })).toBeEnabled() await dialog.getByRole('spinbutton', { name: '目标内存' }).fill('4096') await expect(dialog.getByRole('button', { name: '提交变更' })).toBeDisabled() await dialog.getByRole('button', { name: '预检规格' }).click() await page.screenshot({ path: info.outputPath('hardware-compute-dark.png'), fullPage: true }) await dialog.getByRole('button', { name: '提交变更' }).click() await page.getByRole('button', { name: '提交规格任务', exact: true }).click() await expect(dialog).not.toBeVisible() const writes = requests.filter(r => r.path === '/desktops/desk-hw/hardware') expect(writes).toHaveLength(1) expect(writes[0]?.body).toEqual({ kind: 'compute', cores: 4, sockets: 1, memoryMiB: 4096, digest: 'hardware-v1' }) expect(writes[0]?.key).toBeTruthy() }) test('磁盘输入总容量并提示客体扩展,取消确认不提交', async ({ page }, info) => { const requests = await fixture(page) await page.goto('/desktops') await page.getByRole('button', { name: '切换浅色主题' }).click() await page.getByRole('button', { name: '查看详情' }).click() await page.getByRole('button', { name: '规格管理' }).click() const dialog = page.getByRole('dialog', { name: '桌面规格管理' }) await dialog.getByRole('tab', { name: '磁盘扩容' }).click() await dialog.getByRole('spinbutton', { name: '磁盘目标容量' }).fill('80') await dialog.getByRole('button', { name: '预检规格' }).click() await page.screenshot({ path: info.outputPath('hardware-disk-light.png'), fullPage: true }) await dialog.getByRole('button', { name: '提交变更' }).click() await expect(page.getByText(/scsi0 扩容至 80 GiB;完成后需在客体内扩展/)).toBeVisible() await page.getByRole('button', { name: '取消', exact: true }).last().click() expect(requests.find(r => r.path.endsWith('/hardware/check'))?.body).toMatchObject({ kind: 'disk', disk: 'scsi0', sizeGiB: 80 }) expect(requests.filter(r => r.path === '/desktops/desk-hw/hardware')).toHaveLength(0) }) test('运行中的桌面阻止修改,结果未知任务只读核对', async ({ page }) => { const requests = await fixture(page, { running: true }) await page.goto('/desktops') await page.getByRole('button', { name: '查看详情' }).click() await page.getByRole('button', { name: '规格管理' }).click() const dialog = page.getByRole('dialog', { name: '桌面规格管理' }) await expect(dialog.getByText('请先正常关闭桌面,再修改规格')).toBeVisible() await expect(dialog.getByRole('button', { name: '预检规格' })).toBeDisabled() await dialog.getByRole('button', { name: '关闭', exact: true }).click() await page.goto('/tasks') await page.getByRole('button', { name: '规格验收桌面', exact: true }).click() await page.getByRole('button', { name: '核对规格结果' }).click() await expect.poll(() => requests.filter(r => r.path.endsWith('/hardware-reconcile')).length).toBe(1) expect(requests.filter(r => r.path === '/desktops/desk-hw/hardware')).toHaveLength(0) }) test('只读角色不显示接入与规格管理入口', async ({ page }) => { await fixture(page, { role: 'viewer' }) await page.goto('/settings') await expect(page.getByRole('button', { name: '配置 Proxmox 接入' })).toHaveCount(0) await page.goto('/desktops') await page.getByRole('button', { name: '查看详情' }).click() await expect(page.getByRole('button', { name: '规格管理' })).toHaveCount(0) })