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

128 lines
10 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'
// 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<string, unknown>; 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)
})