import { test, expect, type Page } from '@playwright/test' async function fixture(page: Page, role = 'super_admin') { const calls: { path: string; body: Record; 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; 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) } })