128 lines
10 KiB
TypeScript
128 lines
10 KiB
TypeScript
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)
|
||
})
|