143 lines
12 KiB
TypeScript
143 lines
12 KiB
TypeScript
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' }])
|
||
})
|