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

167 lines
17 KiB
TypeScript

import { test, expect, type Page } from '@playwright/test'
async function fixture(page: Page, options: { role?: string; empty?: boolean; ambiguous?: boolean; fail?: string; id?: string; saved?: string; timeout?: boolean } = {}) {
const writes: { path: string; body: Record<string, unknown> }[] = [], reads: string[] = []
let users = options.empty ? [] : [{ id: 'u1', username: 'zhang', displayName: '张三', enabled: true, terminalId: 't1', desktopIds: ['d1'] }, { id: 'u2', username: 'li', displayName: '李四', enabled: false, terminalId: null, desktopIds: [] as string[] }]
const desktops = options.empty ? [] : ['d1', 'd2', ...(options.ambiguous ? ['d3'] : [])].map((id, i) => ({ id, name: options.ambiguous && i === 2 ? '备用桌面' : i ? '备用桌面' : '办公桌面', os: 'Windows', node: 'pve', vmid: 100 + i, powerState: i ? 'stopped' : 'running', serviceState: i ? 'unknown' : 'ready', setupRequired: false, updatedAt: new Date().toISOString() }))
const terminals = options.empty ? [] : [{ id: 't1', sn: 'SN-01', name: '前台盒子', enabled: true, status: 'offline', sessionDesktopId: null, lastSeen: new Date().toISOString() }, { id: 't2', sn: 'SN-02', name: '新盒子', enabled: true, status: 'unregistered', sessionDesktopId: null, lastSeen: null }]
const task = { id: 'k1', targetId: 'd2', targetName: '备用桌面', kind: 'desktop', action: 'start', status: options.timeout ? 'timeout' : 'success', message: '等待核对', actor: 'admin', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }
await page.addInitScript(({ id, saved }) => { sessionStorage.setItem('vdi.accessToken', 'editions-fixture'); if (saved) localStorage.setItem(`vdi.console-mode:${id}`, saved) }, { id: options.id || 'admin', saved: options.saved })
await page.route('**/api/v1/**', async route => {
const request = route.request(), url = new URL(request.url()), path = url.pathname.replace('/api/v1', ''), method = request.method(), body = request.postData() ? request.postDataJSON() : {}
const list = (items: unknown[], total = items.length) => route.fulfill({ json: { items, total, page: Number(url.searchParams.get('page') || 1), pageSize: 10, requestId: 'edition-ui' } })
if (method !== 'GET') writes.push({ path, body }); else reads.push(path + url.search)
if (options.fail === path && method !== 'GET' || options.fail === 'reads' && path === '/desktops' || options.fail === 'alerts' && path === '/monitor/alerts') { await route.fulfill({ status: 503, json: { code: 'unavailable', message: '当前服务不可用', requestId: 'edition-ui' } }); return }
if (path === '/auth/me') { await route.fulfill({ json: { id: options.id || 'admin', username: 'admin', displayName: '管理员', role: options.role || 'super_admin' } }); return }
if (path === '/settings') { await route.fulfill({ json: { mode: 'production', pveConfigured: true, capabilities: [] } }); return }
if (path === '/overview') { await route.fulfill({ json: { activeSessions: 1, desktopCount: desktops.length, onlineTerminals: 0, terminalCount: terminals.length, runningDesktops: 1, failedTasks: 0 } }); return }
if (path === '/desktops') { await list(desktops); return }
if (path === '/users' && method === 'POST') { const user = { ...body, id: 'new-user', terminalId: null, desktopIds: [], enabled: true }; users.push(user); await route.fulfill({ status: 201, json: user }); return }
if (path === '/users') { await list(users); return }
if (path === '/terminals' && method === 'POST') { await route.fulfill({ status: 201, json: { id: 'new-terminal', activationSecret: 'one-time-test-secret' } }); return }
if (path === '/terminals') { await list(terminals); return }
if (path === '/users/batch-bindings') {
users = users.map(u => body.userIds.includes(u.id) ? { ...u, desktopIds: body.action === 'grant' ? [...new Set([...u.desktopIds, ...body.desktopIds])] : u.desktopIds.filter(id => !body.desktopIds.includes(id)) } : u)
await route.fulfill({ json: { affectedUsers: 1 } }); return
}
if (path.startsWith('/desktops/') && path.endsWith('/actions')) { await route.fulfill({ status: 202, json: task }); return }
if (path === '/tasks') { await list(url.searchParams.get('active') ? options.timeout ? [task] : [] : [task]); return }
if (path === '/audit-logs') { await list([{ id: 'a1', target: '张三', action: 'batch_grant_bindings', result: 'success', actor: 'admin', createdAt: new Date().toISOString() }], 21); return }
if (path === '/monitor/alerts') { await route.fulfill({ json: { items: [], total: 0, firingCount: 0, unacknowledgedCount: 0 } }); return }
if (path === '/resources') { await list([]); return }
await route.fulfill({ status: 404, json: { message: '未定义的测试接口' } })
})
return { writes, reads }
}
test('默认通用版五个入口,切换高级版保留专业深链和账号偏好', async ({ page }) => {
const calls = await fixture(page); await page.goto('/')
await expect(page).toHaveURL(/\/workbench$/); await expect(page.getByRole('heading', { name: '工作台', exact: true })).toBeVisible(); await expect(page.locator('.simple-stats .panel').first()).toContainText('运行中的桌面1 / 2 台桌面')
const nav = page.getByRole('navigation', { name: '主导航' }); await expect(nav.getByRole('link')).toHaveCount(5)
await expect(nav.getByRole('link', { name: 'GPU 资源' })).toHaveCount(0)
await page.getByRole('button', { name: '高级版', exact: true }).click(); await expect(page).toHaveURL(/\/overview$/)
await expect(nav.getByRole('link', { name: 'GPU 资源' })).toBeVisible(); await expect(nav.getByRole('link', { name: '镜像模板' })).toBeVisible()
await page.goto('/'); await expect(page).toHaveURL(/\/overview$/)
await page.goto('/people'); await expect(page.getByRole('button', { name: '通用版', exact: true })).toHaveAttribute('aria-pressed', 'true')
await page.goto('/users'); await expect(page.getByRole('heading', { name: '用户与授权' })).toBeVisible()
expect(calls.writes).toHaveLength(0)
})
test('另一个账号默认通用版,存储不可用时仍可切换', async ({ page }) => {
await fixture(page, { id: 'reader' }); await page.addInitScript(() => { localStorage.setItem('vdi.console-mode:another-account', 'advanced'); Storage.prototype.setItem = () => { throw new Error('storage disabled') } })
await page.goto('/'); await expect(page).toHaveURL(/\/workbench$/)
await page.getByRole('button', { name: '高级版', exact: true }).click(); await expect(page).toHaveURL(/\/overview$/)
})
test('授权先展示固定对象方案,确认才提交,追加和撤销保留其余授权', async ({ page }) => {
const calls = await fixture(page); await page.goto('/people')
const row = page.getByRole('row').filter({ hasText: '张三' }).first()
await row.getByRole('button', { name: '分配桌面', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '分配已有桌面' }); await dialog.locator('.el-checkbox').filter({ hasText: '备用桌面' }).click()
await dialog.getByRole('button', { name: '查看方案' }).click(); await expect(dialog.getByText('确认变更方案')).toBeVisible(); expect(calls.writes).toHaveLength(0)
await dialog.getByRole('button', { name: '确认并保存授权' }).dblclick(); await expect(dialog).not.toBeVisible()
expect(calls.writes).toEqual([{ path: '/users/batch-bindings', body: { userIds: ['u1'], desktopIds: ['d2'], action: 'grant' } }])
await expect(row).toContainText('备用桌面'); await expect(row).toContainText('办公桌面'); await expect(row).toContainText('前台盒子')
await row.getByRole('button', { name: '撤销授权' }).click(); const revoke = page.getByRole('dialog', { name: '撤销指定授权' })
await revoke.locator('.el-checkbox').filter({ hasText: '备用桌面' }).click(); await revoke.getByRole('button', { name: '查看方案' }).click(); await revoke.getByRole('button', { name: '确认并保存授权' }).click()
await expect(revoke).not.toBeVisible(); await expect(row).not.toContainText('备用桌面'); await expect(row).toContainText('办公桌面')
expect(calls.writes[1]?.body).toEqual({ userIds: ['u1'], desktopIds: ['d2'], action: 'revoke' })
})
test('授权失败保留方案,不显示成功或自动重发', async ({ page }) => {
const calls = await fixture(page, { fail: '/users/batch-bindings' }); await page.goto('/people')
await page.getByRole('row').filter({ hasText: '张三' }).first().getByRole('button', { name: '分配桌面' }).click()
const dialog = page.getByRole('dialog', { name: '分配已有桌面' }); await dialog.locator('.el-checkbox').filter({ hasText: '备用桌面' }).click(); await dialog.getByRole('button', { name: '查看方案' }).click(); await dialog.getByRole('button', { name: '确认并保存授权' }).click()
await expect(page.getByText(/当前服务不可用/)).toBeVisible(); await expect(dialog).toBeVisible(); expect(calls.writes).toHaveLength(1)
})
for (const role of ['viewer', 'operator']) test(`${role}切换版本不提升权限`, async ({ page }) => {
const calls = await fixture(page, { role }); await page.goto('/people')
await expect(page.getByRole('button', { name: '添加员工' })).toHaveCount(0); await expect(page.getByRole('button', { name: '分配桌面', exact: true })).toHaveCount(0)
if (role === 'viewer') await expect(page.getByRole('button', { name: '正常关机', exact: true })).toHaveCount(0)
await page.getByRole('button', { name: '高级版', exact: true }).click(); await page.goto('/settings'); await expect(page).toHaveURL(/\/forbidden$/)
expect(calls.writes).toHaveLength(0)
})
test('设备诊断只读,区分离线、等待激活与缺少关联', async ({ page }) => {
const calls = await fixture(page); await page.goto('/help?terminal=t1')
await expect(page.getByText('未收到在线心跳,请检查盒子电源、网络和平台地址。')).toBeVisible(); await expect(page.getByText('使用人:张三')).toBeVisible(); await expect(page.getByText('桌面运行且连接服务就绪', { exact: false })).toBeVisible()
await page.goto('/help?terminal=t2'); await expect(page.getByText('设备尚未激活', { exact: false })).toBeVisible(); await expect(page.getByText('请在员工与桌面中关联此设备。')).toBeVisible()
expect(calls.writes).toHaveLength(0)
})
test('设备登记返回真实接口激活信息,关闭后移除且不持久化', async ({ page }) => {
const calls = await fixture(page); await page.goto('/devices'); await page.getByRole('button', { name: '登记新设备' }).click()
await page.getByLabel('新设备名称').fill('新设备'); await page.getByLabel('新设备 SN').fill('SN-NEW'); await page.getByRole('button', { name: '登记并获取激活信息' }).click()
const dialog = page.getByRole('dialog', { name: '请保存设备激活信息' }); await expect(dialog.getByLabel('设备激活密钥')).toHaveValue('one-time-test-secret')
await dialog.getByRole('button', { name: '已保存,关闭' }).click(); await expect(dialog).not.toBeVisible()
expect(await page.evaluate(() => JSON.stringify({ ...localStorage, ...sessionStorage }))).not.toContain('one-time-test-secret')
expect(calls.writes[0]).toEqual({ path: '/terminals', body: { name: '新设备', sn: 'SN-NEW', group: '' } })
})
test('电源操作取消不提交,结果未知时阻止重复操作', async ({ page }) => {
const calls = await fixture(page, { timeout: true }); await page.goto('/people')
const backup = page.getByRole('row').filter({ hasText: '备用桌面' }).last(); await expect(backup.getByRole('button', { name: '开机', exact: true })).toBeDisabled()
await page.getByRole('button', { name: '重启桌面', exact: true }).click(); const dialog = page.locator('.el-message-box')
await expect(dialog).toContainText('保存文件'); await dialog.getByRole('button', { name: '取消', exact: true }).click(); expect(calls.writes).toHaveLength(0)
await page.getByRole('button', { name: '重启桌面', exact: true }).click(); await dialog.getByRole('button', { name: '确认提交', exact: true }).click()
await expect.poll(() => calls.writes.length).toBe(1); expect(calls.writes[0]).toEqual({ path: '/desktops/d1/actions', body: { action: 'reboot' } })
})
test('通用记录查历史审计和任务,服务端分页', async ({ page }) => {
const calls = await fixture(page); await page.goto('/records'); await expect(page.getByRole('cell', { name: '批量分配桌面' })).toBeVisible()
await page.locator('.el-pagination .btn-next').click(); await expect.poll(() => calls.reads.some(p => p.includes('/audit-logs?') && p.includes('page=2'))).toBe(true)
await page.getByRole('tab', { name: '设备与桌面任务' }).click(); await expect(page.getByRole('cell', { name: '等待核对' })).toBeVisible()
expect(calls.writes).toHaveLength(0)
})
test('空态、告警失败和平台数据失败有明确提示', async ({ page }) => {
await fixture(page, { empty: true, fail: 'alerts' }); await page.goto('/people'); await expect(page.getByText('暂无员工,请先添加员工')).toBeVisible()
await page.goto('/help'); await expect(page.getByText(/告警读取失败,不能判断是否正常/)).toBeVisible(); await expect(page.getByText('当前没有触发中的告警')).toHaveCount(0)
await page.unroute('**/api/v1/**'); await fixture(page, { fail: 'reads' }); await page.goto('/workbench'); await expect(page.getByText('数据刷新失败,当前信息可能已过期')).toBeVisible()
})
test('通用工作台深浅主题与窄屏导航无溢出', async ({ page }, info) => {
await fixture(page); await page.goto('/workbench'); await expect(page.getByRole('heading', { name: '你想完成什么?' })).toBeVisible()
await page.screenshot({ path: info.outputPath('workbench-dark.png'), fullPage: true })
await page.getByRole('button', { name: /切换浅色主题/ }).click(); await page.screenshot({ path: info.outputPath('workbench-light.png'), fullPage: true })
await page.setViewportSize({ width: 390, height: 844 }); await page.getByRole('button', { name: '打开导航', exact: true }).click(); await expect(page.getByRole('navigation', { name: '主导航' }).getByRole('link', { name: '员工与桌面' })).toBeVisible()
await page.getByRole('button', { name: '关闭导航', exact: true }).click(); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
await page.screenshot({ path: info.outputPath('workbench-mobile.png'), fullPage: true })
})
test('通用工作台固定场景助手生成方案,确认后复用真实授权接口', async ({ page }) => {
const calls = await fixture(page); await page.goto('/workbench')
await page.getByRole('textbox', { name: '描述需求', exact: true }).fill('给张三分配备用桌面')
await page.getByRole('button', { name: '生成方案', exact: true }).click()
await expect(page.getByText('可执行方案', { exact: true })).toBeVisible()
await expect(page.getByRole('heading', { name: '分配已有桌面', exact: true })).toBeVisible()
await expect(page.locator('.assistant-plan').getByText('备用桌面', { exact: false }).first()).toBeVisible()
await page.getByRole('button', { name: '执行方案', exact: true }).click()
await page.getByRole('button', { name: '确认提交', exact: true }).click()
await expect(page.getByRole('button', { name: '已执行', exact: true })).toBeVisible()
expect(calls.writes.filter(w => w.path === '/users/batch-bindings')).toHaveLength(1)
expect(calls.writes.find(w => w.path === '/users/batch-bindings')?.body).toEqual({ userIds: ['u1'], desktopIds: ['d2'], action: 'grant' })
})
test('通用工作台助手对未知或重复授权给出阻止方案,不发写请求', async ({ page }) => {
const calls = await fixture(page); await page.goto('/workbench')
await page.getByRole('textbox', { name: '描述需求', exact: true }).fill('给不存在的人分配一台桌面')
await page.getByRole('button', { name: '生成方案', exact: true }).click()
await expect(page.getByText('需要补充信息', { exact: true })).toBeVisible()
await expect(page.getByText('没有匹配到员工', { exact: false })).toBeVisible()
await page.getByRole('textbox', { name: '描述需求', exact: true }).fill('给张三分配办公桌面')
await page.getByRole('button', { name: '生成方案', exact: true }).click()
await expect(page.getByText('授权已存在', { exact: false })).toBeVisible()
expect(calls.writes).toHaveLength(0)
})
test('通用工作台助手不会在多个同名桌面之间自动选择', async ({ page }) => {
const calls = await fixture(page, { ambiguous: true }); await page.goto('/workbench')
await page.getByRole('textbox', { name: '描述需求', exact: true }).fill('给张三分配备用桌面')
await page.getByRole('button', { name: '生成方案', exact: true }).click()
await expect(page.getByText('需要补充信息', { exact: true })).toBeVisible()
await expect(page.getByText('匹配到多个桌面', { exact: false })).toBeVisible()
expect(calls.writes).toHaveLength(0)
})