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 }[] = [] 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 : {}; 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' }]) })