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

100 lines
9.2 KiB
TypeScript

import { test, expect, type Page } from '@playwright/test'
async function fixture(page: Page, role = 'super_admin') {
const calls: { path: string; body: Record<string, unknown>; key: string | undefined }[] = []
const templates = [{ id: 'p1', name: '标准办公', version: '1.0', heartbeatInterval: 30, commandPollInterval: 3, desktopRefreshInterval: 15, createdAt: '2026-11-10T03:00:00Z' }]
const states = [
{ id: '', terminalId: 't1', terminalName: '设计工位', group: '设计组', protocol: 1, deploymentId: '', templateName: '', templateVersion: '', configVersion: 1, status: 'unconfigured', errorCode: '', acknowledgedAt: null, online: true, enabled: true, registered: true },
{ id: 'r0', terminalId: 't2', terminalName: '备用终端', group: '设计组', protocol: 1, deploymentId: 'd0', templateName: '标准办公', templateVersion: '1.0', configVersion: 2, status: 'failed', errorCode: 'persist_failed', acknowledgedAt: '2026-11-10T03:10:00Z', online: false, enabled: true, registered: true },
{ id: '', terminalId: 't3', terminalName: '旧客户端', group: '旧设备组', protocol: 0, deploymentId: '', templateName: '', templateVersion: '', configVersion: 1, status: 'unconfigured', errorCode: '', acknowledgedAt: null, online: true, enabled: true, registered: true },
]
const deployments = [{ id: 'd0', templateId: 'p1', templateName: '标准办公', templateVersion: '1.0', actor: 'root', targetCount: 1, createdAt: '2026-11-10T03:00:00Z' }]
await page.addInitScript(() => sessionStorage.setItem('vdi.accessToken', 'policies-fixture'))
await page.route('**/api/v1/**', async route => {
const req = route.request(), path = new URL(req.url()).pathname.replace('/api/v1', '')
const list = (items: unknown[]) => route.fulfill({ json: { items, page: 1, pageSize: 100, total: items.length, requestId: 'policy-fixture' } })
if (req.method() === 'POST') {
const body = req.postDataJSON() as Record<string, unknown>; calls.push({ path, body, key: req.headers()['idempotency-key'] })
if (path === '/terminal-policy-templates') { const t = { ...templates[0]!, ...body, id: 'p2' }; templates.unshift(t); await route.fulfill({ status: 201, json: t }); return }
if (path === '/terminal-policy-deployments') {
const chosen = states.filter(s => body.targetMode === 'group' ? s.group === body.group : (body.terminalIds as string[]).includes(s.terminalId))
const d = { ...deployments[0]!, id: 'd1', targetCount: chosen.length }; deployments.unshift(d)
for (const s of chosen) Object.assign(s, { status: 'pending', deploymentId: 'd1', templateName: '标准办公', templateVersion: '1.0', configVersion: s.configVersion + 1, errorCode: '', acknowledgedAt: null })
await route.fulfill({ status: 202, json: d }); return
}
}
if (path === '/auth/me') { await route.fulfill({ json: { id: 'a', username: 'root', displayName: '管理员', role } }); return }
if (path === '/overview') { await route.fulfill({ json: { terminalCount: 3, onlineTerminals: 2, desktopCount: 0, runningDesktops: 0, activeSessions: 0, failedTasks: 0, mode: 'production' } }); return }
if (path === '/settings') { await route.fulfill({ json: { mode: 'production', pveConfigured: true, capabilities: ['terminal-policies'] } }); return }
if (path === '/terminal-policy-templates') { await list(templates); return }
if (path === '/terminal-policy-states') { await list(states); return }
if (path === '/terminal-policy-deployments') { await list(deployments); return }
if (/\/terminal-policy-deployments\/.*\/results/.test(path)) { await list(states.filter(s => s.deploymentId === path.split('/')[2])); return }
if (['/terminals', '/users', '/resources', '/tasks', '/desktops', '/audit-logs'].includes(path)) { await list([]); return }
await route.fulfill({ status: 404, json: { code: 'fixture_missing', message: path, requestId: 'fixture' } })
})
return calls
}
test('策略版本可保存和复制,深浅主题及手机表单可用', async ({ page }, info) => {
const calls = await fixture(page); await page.setViewportSize({ width: 1440, height: 1000 }); await page.goto('/terminal-policies')
await expect(page.getByRole('heading', { name: '终端策略', exact: true })).toBeVisible()
await page.getByRole('button', { name: '新建策略', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '新建终端策略版本' })
await dialog.getByLabel('策略名称', { exact: true }).fill('研发组')
await dialog.getByLabel('策略版本', { exact: true }).fill('2.0')
await dialog.getByLabel('命令检查间隔', { exact: true }).fill('5')
await dialog.getByRole('button', { name: '保存策略版本' }).click(); await expect(dialog).toBeHidden()
expect(calls[0]?.body).toEqual({ name: '研发组', version: '2.0', heartbeatInterval: 30, commandPollInterval: 5, desktopRefreshInterval: 15 })
await page.screenshot({ path: info.outputPath('policies-dark.png'), fullPage: true, animations: 'disabled' })
await page.getByRole('button', { name: '切换浅色主题' }).click()
await page.screenshot({ path: info.outputPath('policies-light.png'), fullPage: true, animations: 'disabled' })
await page.getByRole('row').filter({ hasText: '研发组' }).getByRole('button', { name: '复制新版本' }).click()
await expect(dialog.getByLabel('策略版本', { exact: true })).toHaveValue('')
await page.setViewportSize({ width: 390, height: 844 })
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(391)
await page.screenshot({ path: info.outputPath('policy-create-mobile.png'), fullPage: true, animations: 'disabled' })
await dialog.getByRole('button', { name: '取消', exact: true }).click(); expect(calls).toHaveLength(1)
})
test('按分组下发先确认,提交后等待设备回执,不显示已应用', async ({ page }, info) => {
const calls = await fixture(page); await page.goto('/terminal-policies')
await page.getByRole('row').filter({ hasText: '标准办公' }).getByRole('button', { name: '下发', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '下发终端策略', exact: true })
await dialog.getByText('指定分组', { exact: true }).click()
await dialog.locator('.el-select').filter({ has: page.getByLabel('选择策略分组') }).click(); await page.getByRole('option', { name: '设计组', exact: true }).click()
await expect(dialog).toContainText('本次 2 台')
await dialog.getByRole('button', { name: '提交下发' }).click()
const box = page.locator('.el-message-box'); await box.getByRole('button', { name: '取消', exact: true }).click(); expect(calls).toHaveLength(0)
await dialog.getByRole('button', { name: '提交下发' }).click(); await box.getByRole('button', { name: '确认下发', exact: true }).click()
const drawer = page.getByRole('dialog', { name: '策略下发结果', exact: true })
await expect(drawer.getByText('等待设备应用', { exact: true })).toHaveCount(2)
await expect(drawer.getByText('已应用', { exact: true })).toHaveCount(0)
await expect(drawer).toContainText('离线等待')
expect(calls[0]?.body).toEqual({ templateId: 'p1', targetMode: 'group', group: '设计组' }); expect(calls[0]?.key).toBeTruthy()
await page.screenshot({ path: info.outputPath('policy-results-dark.png'), fullPage: true, animations: 'disabled' })
})
test('旧客户端分组禁止下发,失败原因和重试入口可查看', async ({ page }) => {
const calls = await fixture(page); await page.goto('/terminal-policies')
await page.getByRole('tab', { name: '设备结果', exact: true }).click()
await expect(page.getByRole('row').filter({ hasText: '旧客户端' })).toContainText('需更新客户端')
const failed = page.getByRole('row').filter({ hasText: '备用终端' }); await expect(failed).toContainText('设备无法安全保存配置')
await failed.getByRole('button', { name: '重新下发' }).click()
const dialog = page.getByRole('dialog', { name: '下发终端策略', exact: true }); await expect(dialog).toContainText('本次 1 台')
await dialog.getByText('指定分组', { exact: true }).click(); await dialog.locator('.el-select').filter({ has: page.getByLabel('选择策略分组') }).click(); await page.getByRole('option', { name: '旧设备组', exact: true }).click()
await expect(dialog.getByRole('button', { name: '提交下发' })).toBeDisabled(); await expect(dialog).toContainText('旧客户端设备')
await dialog.getByRole('button', { name: '取消', exact: true }).click(); expect(calls).toHaveLength(0)
})
test('只读和运维角色只能查看策略与结果', async ({ page }) => {
for (const role of ['viewer', 'operator']) {
await page.unroute('**/api/v1/**'); const calls = await fixture(page, role); await page.goto('/terminal-policies'); await page.reload()
await expect(page.getByRole('heading', { name: '终端策略', exact: true })).toBeVisible()
await expect(page.getByRole('button', { name: '新建策略', exact: true })).toHaveCount(0)
await expect(page.getByRole('button', { name: '下发', exact: true })).toHaveCount(0)
await page.getByRole('tab', { name: '设备结果', exact: true }).click()
await expect(page.getByRole('button', { name: '重新下发', exact: true })).toHaveCount(0); expect(calls).toHaveLength(0)
}
})