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

143 lines
12 KiB
TypeScript
Raw Permalink 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'
async function fixture(page: Page, role = 'super_admin', active = false) {
const calls: { path: string; method: string; body: Record<string, unknown> }[] = []
const terminals = [
{ id: 't1', sn: 'TEST-BOX-001', name: '设计工位', group: '设计部', enabled: true, status: 'online', agentVersion: 'qt-test', lastSeen: '2026-11-10T02:00:00Z', sessionDesktopId: active ? 'd1' : null, sessionState: active ? 'connected' : 'idle', configVersion: 1, fps: null, rtt: null },
{ id: 't2', sn: 'TEST-BOX-002', name: '备用终端', group: '备用组', enabled: false, status: 'disabled', agentVersion: '', lastSeen: null, sessionDesktopId: null, sessionState: 'idle', configVersion: 1, fps: null, rtt: null },
]
const users = [
{ id: 'u1', username: 'designer', displayName: '设计人员', enabled: true, terminalId: 't1', desktopIds: ['d1'] },
{ id: 'u2', username: 'office', displayName: '办公人员', enabled: true, terminalId: null, desktopIds: ['d1'] },
]
const desktops = [{ id: 'd1', name: '设计桌面', os: 'Windows', powerState: 'running' }, { id: 'd2', name: '办公桌面', os: 'Ubuntu', powerState: 'running' }]
await page.addInitScript(() => sessionStorage.setItem('vdi.accessToken', 'lifecycle-fixture'))
await page.route('**/api/v1/**', async route => {
const r = route.request(), path = new URL(r.url()).pathname.replace('/api/v1', ''), method = r.method()
const list = (items: unknown[]) => route.fulfill({ json: { items, page: 1, pageSize: 100, total: items.length, requestId: 'lifecycle-fixture' } })
if (method !== 'GET') {
const body = r.postData() ? r.postDataJSON() as Record<string, unknown> : {}; calls.push({ path, method, body })
if (path === '/terminals/t1' && method === 'PUT') { Object.assign(terminals[0]!, body); await route.fulfill({ json: { accepted: true } }); return }
if (path.endsWith('/disable')) { Object.assign(terminals[0]!, { enabled: false, status: 'disabled', lastSeen: null }); await route.fulfill({ json: { accepted: true } }); return }
if (path.endsWith('/reactivate')) { const t = terminals.find(t => path.includes(t.id))!; Object.assign(t, { enabled: true, status: 'unregistered' }); await route.fulfill({ json: { id: t.id, activationSecret: 'activate_fixture_one_time' } }); return }
if (path === '/users/u1') { Object.assign(users[0]!, body); await route.fulfill({ json: users[0] }); return }
if (path === '/users/batch-bindings') {
for (const u of users.filter(u => (body.userIds as string[]).includes(u.id))) u.desktopIds = body.action === 'grant' ? [...new Set([...u.desktopIds, ...body.desktopIds as string[]])] : u.desktopIds.filter(id => !(body.desktopIds as string[]).includes(id))
await route.fulfill({ json: { accepted: true, userCount: (body.userIds as string[]).length } }); return
}
}
if (path === '/auth/me') { await route.fulfill({ json: { id: 'admin', username: 'root', displayName: '平台管理员', role } }); return }
if (path === '/settings') { await route.fulfill({ json: { mode: 'production', pveConfigured: true, capabilities: [] } }); return }
if (path === '/overview') { await route.fulfill({ json: { terminalCount: 2, onlineTerminals: 1, desktopCount: 2, runningDesktops: 2, activeSessions: active ? 1 : 0, failedTasks: 0, mode: 'production' } }); return }
if (path === '/terminals') { await list(terminals); return }
if (path === '/users') { await list(users); return }
if (path === '/desktops') { await list(desktops); return }
if (['/tasks', '/audit-logs', '/resources'].includes(path)) { await list([]); return }
await route.fulfill({ status: 404, json: { code: 'fixture_missing', message: '测试接口未定义', requestId: 'fixture' } })
})
return calls
}
test('终端资料保存名称和分组,深浅主题与窄屏可用', async ({ page }, testInfo) => {
const calls = await fixture(page)
await page.setViewportSize({ width: 1440, height: 1000 }); await page.goto('/terminals')
const row = page.getByRole('row').filter({ hasText: 'TEST-BOX-001' })
await row.getByRole('button', { name: '编辑资料' }).click()
const dialog = page.getByRole('dialog', { name: '编辑终端资料' })
await dialog.getByLabel('编辑终端名称').fill('设计工位 A')
await dialog.getByLabel('编辑终端分组').fill('研发一组')
await dialog.getByRole('button', { name: '保存终端资料' }).click()
await expect(dialog).toBeHidden(); await expect(row).toContainText('研发一组')
expect(calls[0]).toEqual({ path: '/terminals/t1', method: 'PUT', body: { name: '设计工位 A', group: '研发一组' } })
await page.screenshot({ path: testInfo.outputPath('terminals-dark.png'), fullPage: true, animations: 'disabled' })
await page.getByRole('button', { name: '切换浅色主题' }).click()
await page.screenshot({ path: testInfo.outputPath('terminals-light.png'), fullPage: true, animations: 'disabled' })
await row.getByRole('button', { name: '详情', exact: true }).click()
await page.screenshot({ path: testInfo.outputPath('terminal-detail-light.png'), fullPage: true, animations: 'disabled' })
await page.setViewportSize({ width: 390, height: 844 })
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(391)
await page.screenshot({ path: testInfo.outputPath('terminal-detail-mobile.png'), fullPage: true, animations: 'disabled' })
})
test('停用与重新激活需要确认,旧凭据撤销提示和一次性密钥可见', async ({ page }) => {
const calls = await fixture(page); await page.goto('/terminals')
await page.getByRole('row').filter({ hasText: 'TEST-BOX-001' }).getByRole('button', { name: '详情', exact: true }).click()
await page.getByRole('button', { name: '停用设备', exact: true }).click()
const box = page.locator('.el-message-box')
await box.getByRole('button', { name: '取消', exact: true }).click(); expect(calls).toHaveLength(0)
await page.getByRole('button', { name: '停用设备', exact: true }).click()
await box.getByRole('button', { name: '确认提交' }).click()
await expect(page.getByRole('button', { name: '停用设备', exact: true })).toHaveCount(0)
await expect(page.getByRole('button', { name: '收集日志', exact: true })).toHaveCount(0)
await page.getByRole('button', { name: '重新激活', exact: true }).click()
await box.getByRole('button', { name: '取消', exact: true }).click(); expect(calls).toHaveLength(1)
await page.getByRole('button', { name: '重新激活', exact: true }).click()
await box.getByRole('button', { name: '确认提交' }).click()
const activation = page.getByRole('dialog', { name: '终端激活信息', exact: true })
await expect(activation.getByTestId('activation-secret')).toHaveText('activate_fixture_one_time')
await activation.getByRole('button', { name: '已保存,关闭' }).click()
await expect(activation).toBeHidden(); await expect(page.getByText('activate_fixture_one_time')).toHaveCount(0)
expect(calls.map(c => c.path)).toEqual(['/terminals/t1/disable', '/terminals/t1/reactivate'])
})
test('活动会话保护设备身份,运维和只读人员无设备维护入口', async ({ page }) => {
const calls = await fixture(page, 'super_admin', true); await page.goto('/terminals')
await page.getByRole('row').filter({ hasText: 'TEST-BOX-001' }).getByRole('button', { name: '详情', exact: true }).click()
await expect(page.getByRole('button', { name: '停用设备', exact: true })).toBeDisabled()
await expect(page.getByRole('button', { name: '重新激活', exact: true })).toBeDisabled(); expect(calls).toHaveLength(0)
await page.unroute('**/api/v1/**'); await fixture(page, 'operator'); await page.reload()
await page.getByRole('row').filter({ hasText: 'TEST-BOX-001' }).getByRole('button', { name: '详情', exact: true }).click()
await expect(page.getByRole('button', { name: '停用设备', exact: true })).toHaveCount(0)
await expect(page.getByRole('button', { name: '编辑资料', exact: true })).toHaveCount(0)
await page.unroute('**/api/v1/**'); await fixture(page, 'viewer'); await page.goto('/users')
await expect(page.getByRole('button', { name: '批量授权', exact: true })).toHaveCount(0)
await expect(page.getByRole('button', { name: '编辑资料', exact: true })).toHaveCount(0)
})
test('用户改名与停用保持授权,取消状态变更不提交', async ({ page }) => {
const calls = await fixture(page); await page.goto('/users')
const row = page.getByRole('row').filter({ hasText: 'designer' })
await row.getByRole('button', { name: '编辑资料', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '编辑用户资料', exact: true })
await dialog.getByLabel('编辑用户姓名').fill('新设计人员')
await dialog.locator('.el-switch').click()
await dialog.getByRole('button', { name: '保存用户资料' }).click()
await page.locator('.el-message-box').getByRole('button', { name: '取消', exact: true }).click(); expect(calls).toHaveLength(0)
await dialog.getByRole('button', { name: '保存用户资料' }).click()
await page.locator('.el-message-box').getByRole('button', { name: '确认提交' }).click()
await expect(dialog).toBeHidden(); await expect(row).toContainText('新设计人员'); await expect(row).toContainText('设计桌面')
expect(calls[0]).toEqual({ path: '/users/u1', method: 'PUT', body: { displayName: '新设计人员', enabled: false } })
})
test('批量授权明确目标和追加撤销语义,取消不提交', async ({ page }, testInfo) => {
const calls = await fixture(page); await page.setViewportSize({ width: 1440, height: 1000 }); await page.goto('/users')
for (const username of ['designer', 'office']) await page.getByRole('row').filter({ hasText: username }).locator('label.el-checkbox').click()
await page.getByRole('button', { name: /批量授权/ }).click()
const dialog = page.getByRole('dialog', { name: '批量桌面授权', exact: true })
await expect(dialog).toContainText('目标用户(2 位)')
await expect(dialog.getByRole('button', { name: '提交批量授权' })).toBeDisabled()
await dialog.locator('label.el-checkbox').filter({ hasText: '办公桌面 · Ubuntu' }).click()
await dialog.getByRole('button', { name: '提交批量授权' }).click()
await page.locator('.el-message-box').getByRole('button', { name: '取消', exact: true }).click(); expect(calls).toHaveLength(0)
await dialog.getByRole('button', { name: '提交批量授权' }).click()
await page.locator('.el-message-box').getByRole('button', { name: '确认提交' }).click()
await expect(dialog).toBeHidden()
const row = page.getByRole('row').filter({ hasText: 'designer' })
await expect(row).toContainText('设计桌面'); await expect(row).toContainText('办公桌面')
await page.screenshot({ path: testInfo.outputPath('users-dark.png'), fullPage: true, animations: 'disabled' })
await page.getByRole('button', { name: '切换浅色主题' }).click()
await page.screenshot({ path: testInfo.outputPath('users-light.png'), fullPage: true, animations: 'disabled' })
// Refresh intentionally clears table selection; choose targets again if needed.
if (await page.getByRole('button', { name: /批量授权/ }).isDisabled()) for (const username of ['designer', 'office']) await page.getByRole('row').filter({ hasText: username }).locator('label.el-checkbox').click()
await page.getByRole('button', { name: /批量授权/ }).click()
await dialog.locator('label.el-radio-button').filter({ hasText: '撤销指定' }).click()
await dialog.locator('label.el-checkbox').filter({ hasText: '办公桌面 · Ubuntu' }).click()
await page.setViewportSize({ width: 390, height: 844 })
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(391)
await page.screenshot({ path: testInfo.outputPath('batch-users-mobile.png'), fullPage: true, animations: 'disabled' })
await dialog.getByRole('button', { name: '提交批量授权' }).click()
await page.locator('.el-message-box').getByRole('button', { name: '确认提交' }).click()
await expect(dialog).toBeHidden(); await expect(row).toContainText('设计桌面'); await expect(row).not.toContainText('办公桌面')
expect(calls.map(c => c.body)).toEqual([{ userIds: ['u1', 'u2'], desktopIds: ['d2'], action: 'grant' }, { userIds: ['u1', 'u2'], desktopIds: ['d2'], action: 'revoke' }])
})