import { test, expect, type Page } from '@playwright/test' async function fixture(page: Page, options: { role?: string; fail?: boolean; status?: string } = {}) { const writes: string[] = [] const task = { id: 'gpu-task', kind: 'gpu-prepare', action: 'prepare-desktop', targetId: 'desk', targetName: '显卡办公桌面', actor: 'device:box', status: options.status || 'timeout', message: '等待核对', providerRef: '{}', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() } await page.addInitScript(() => sessionStorage.setItem('vdi.accessToken', 'gpu-fixture')) await page.route('**/api/v1/**', async route => { const request = route.request(), url = new URL(request.url()), path = url.pathname.replace('/api/v1', '') const list = (items: unknown[]) => route.fulfill({ json: { items, total: items.length, page: 1, pageSize: 20 } }) if (request.method() !== 'GET') { writes.push(path) if (path !== '/tasks/gpu-task/gpu-reconcile') { await route.abort(); return } if (options.fail) { await route.fulfill({ status: 409, json: { code: 'unknown_submission', message: '未记录原 PVE 任务编号,需要在 PVE 手工核对,不能自动重发', requestId: 'gpu-ui' } }); return } task.status = 'failed'; task.message = '管理员已核对原 PVE 任务及实际电源状态,可重新准备桌面' await route.fulfill({ json: { reconciled: true, vmid: '100' } }); return } if (path === '/auth/me') { await route.fulfill({ json: { id: 'admin', username: 'admin', displayName: '管理员', role: options.role || 'operator' } }); return } if (path === '/settings') { await route.fulfill({ json: { mode: 'production', pveConfigured: true, capabilities: [] } }); return } if (path === '/overview') { await route.fulfill({ json: {} }); return } if (path === '/tasks') { await list(url.searchParams.get('active') && task.status !== 'timeout' ? [] : [task]); return } if (['/desktops', '/users', '/terminals', '/resources'].includes(path)) { await list([]); return } await route.fulfill({ status: 404, json: { message: '未定义接口' } }) }) return writes } test('高级版运维先确认再核对,取消无变更,成功后不再提供核对按钮', async ({ page }) => { const writes = await fixture(page); await page.goto('/tasks'); await page.getByRole('button', { name: '显卡办公桌面', exact: true }).click() const drawer = page.getByRole('dialog', { name: '任务详情' }); await drawer.getByRole('button', { name: '核对显卡准备结果', exact: true }).click() const confirm = page.locator('.el-message-box'); await expect(confirm).toContainText('不重发电源操作'); await expect(confirm).toContainText('原任务标记为失败') await confirm.getByRole('button', { name: '取消', exact: true }).click(); expect(writes).toHaveLength(0) await drawer.getByRole('button', { name: '核对显卡准备结果', exact: true }).click(); await confirm.getByRole('button', { name: '确认提交' }).click() await expect(drawer).not.toBeVisible(); expect(writes).toEqual(['/tasks/gpu-task/gpu-reconcile']) await page.getByRole('button', { name: '显卡办公桌面', exact: true }).click(); await expect(drawer.getByRole('button', { name: '核对显卡准备结果' })).toHaveCount(0) }) test('缺少原任务编号显示后台拒绝,保留未知状态且不自动重发', async ({ page }) => { const writes = await fixture(page, { fail: true }); await page.goto('/tasks'); await page.getByRole('button', { name: '显卡办公桌面', exact: true }).click() const drawer = page.getByRole('dialog', { name: '任务详情' }); await drawer.getByRole('button', { name: '核对显卡准备结果' }).click(); await page.locator('.el-message-box').getByRole('button', { name: '确认提交' }).click() await expect(page.getByText(/未记录原 PVE 任务编号/)).toBeVisible(); await expect(drawer).toBeVisible(); expect(writes).toEqual(['/tasks/gpu-task/gpu-reconcile']) }) for (const options of [{ role: 'viewer' }, { role: 'operator', status: 'success' }]) test(`角色${options.role}与状态${options.status || 'timeout'}限制核对`, async ({ page }) => { const writes = await fixture(page, options); await page.goto('/tasks'); await page.getByRole('button', { name: '显卡办公桌面', exact: true }).click() await expect(page.getByRole('dialog', { name: '任务详情' }).getByRole('button', { name: '核对显卡准备结果' })).toHaveCount(0); expect(writes).toHaveLength(0) })