import { test, expect, type Page } from '@playwright/test' // Isolated browser fixtures; no live administrator passwords or roles are modified. async function fixture(page: Page, role = 'super_admin') { const calls: { method: string; path: string; body: Record }[] = [] const me = { id: 'self-admin', username: 'root', displayName: '平台管理员', role, enabled: true } const accounts = [me, { id: 'other-admin', username: 'operations', displayName: '运维人员', role: 'operator', enabled: true }] await page.addInitScript(() => sessionStorage.setItem('vdi.accessToken', 'admin-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: 'admin-fixture' } }) if (method !== 'GET') { const body = r.postData() ? r.postDataJSON() as Record : {}; calls.push({ method, path, body }) if (path === '/administrators') { if (accounts.some(a => a.username === body.username)) { await route.fulfill({ status: 409, json: { code: 'account_conflict', message: '管理员账号已存在', requestId: 'fixture' } }); return } const a = { id: 'new-admin', username: String(body.username), displayName: String(body.displayName), role: String(body.role), enabled: true };accounts.push(a) await route.fulfill({ status: 201, json: a }); return } if (path.startsWith('/administrators/') && method === 'PUT') { const a = accounts.find(a => path.endsWith(a.id)); if (a) Object.assign(a, body);await route.fulfill({ json: a }); return } if (path.endsWith('/password') && path.startsWith('/administrators/')) { await route.fulfill({ status: 204 }); return } if (path === '/auth/password') { if (body.currentPassword !== 'Correct-old-password') { await route.fulfill({ status: 400, json: { code: 'current_password_invalid', message: '原密码不正确', requestId: 'fixture' } }); return } await route.fulfill({ status: 204 }); return } } if (path === '/auth/me') { await route.fulfill({ json: me }); return } if (path === '/settings') { await route.fulfill({ json: { mode: 'production', pveConfigured: true, computeProvider: 'proxmox', terminalTransport: 'https-poll', capabilities: ['administrators'] } }); return } if (path === '/overview') { await route.fulfill({ json: { terminalCount: 0, onlineTerminals: 0, desktopCount: 0, runningDesktops: 0, activeSessions: 0, failedTasks: 0, mode: 'production' } }); return } if (path === '/administrators') { await list(accounts); return } if (['/desktops', '/terminals', '/users', '/resources', '/tasks', '/audit-logs'].includes(path)) { await list([]); return } await route.fulfill({ status: 404, json: { code: 'fixture_missing', message: '测试接口未定义', requestId: 'fixture' } }) }) return calls } async function openAccounts(page: Page) { await page.goto('/settings'); await page.getByRole('button', { name: '管理员账号', exact: true }).click(); await expect(page.getByRole('heading', { name: '管理员账号', exact: true })).toBeVisible() } test('新增管理员校验密码确认,账号持久化字段正确', async ({ page }, testInfo) => { const calls = await fixture(page) await page.setViewportSize({ width: 1440, height: 1000 }) await openAccounts(page) await page.screenshot({ path: testInfo.outputPath('administrators-dark.png'), fullPage: true }) await page.getByRole('button', { name: '新增管理员', exact: true }).click() const dialog = page.getByRole('dialog', { name: '新增管理员' }) await dialog.getByRole('textbox', { name: '管理员账号', exact: true }).fill('finance') await dialog.getByRole('textbox', { name: '管理员显示名称' }).fill('财务只读人员') await dialog.getByLabel('管理员初始密码', { exact: true }).fill('Finance-test-2026') await expect(dialog.getByRole('button', { name: '保存管理员' })).toBeDisabled() await dialog.getByLabel('确认管理员初始密码', { exact: true }).fill('Finance-test-2026') await dialog.getByRole('button', { name: '保存管理员' }).click() await expect(dialog).not.toBeVisible() await expect(page.getByText('finance', { exact: true })).toBeVisible() expect(calls.find(c => c.path === '/administrators')?.body).toEqual({ username: 'finance', displayName: '财务只读人员', role: 'viewer', password: 'Finance-test-2026' }) await page.getByRole('button', { name: '切换浅色主题' }).click() await page.screenshot({ path: testInfo.outputPath('administrators-light.png'), fullPage: true }) await page.setViewportSize({ width: 390, height: 844 }) expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(391) await page.screenshot({ path: testInfo.outputPath('administrators-mobile.png'), fullPage: true }) }) test('编辑本人禁止角色和停用,取消其他账号停用不提交', async ({ page }) => { const calls = await fixture(page) await openAccounts(page) await page.getByRole('row').filter({ hasText: 'root' }).getByRole('button', { name: '编辑', exact: true }).click() let dialog = page.getByRole('dialog', { name: '编辑管理员' }) await expect(dialog.getByRole('combobox')).toBeDisabled() await expect(dialog.getByRole('switch')).toBeDisabled() await dialog.getByRole('button', { name: '取消', exact: true }).click() await page.getByRole('row').filter({ hasText: 'operations' }).getByRole('button', { name: '编辑', exact: true }).click() dialog = page.getByRole('dialog', { name: '编辑管理员' }) 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.filter(c => c.method === 'PUT')).toHaveLength(0) await dialog.getByRole('button', { name: '保存管理员' }).click() await page.locator('.el-message-box').getByRole('button', { name: '确认提交' }).click() await expect(dialog).not.toBeVisible() expect(calls.find(c => c.method === 'PUT')?.body.enabled).toBe(false) }) test('重置其他账号密码需要确认,取消不提交', async ({ page }) => { const calls = await fixture(page) await openAccounts(page) await page.getByRole('button', { name: '重置密码', exact: true }).click() const dialog = page.getByRole('dialog', { name: '重置密码 · operations' }) await dialog.getByLabel('重置后的新密码', { exact: true }).fill('Reset-test-2026') await dialog.getByLabel('确认重置后的新密码', { exact: true }).fill('Reset-test-2026') await dialog.getByRole('button', { name: '重置密码', exact: true }).click() await page.locator('.el-message-box').getByRole('button', { name: '取消', exact: true }).click() expect(calls.filter(c => c.path.endsWith('/password'))).toHaveLength(0) await dialog.getByRole('button', { name: '重置密码', exact: true }).click() await page.locator('.el-message-box').getByRole('button', { name: '确认提交' }).click() await expect(dialog).not.toBeVisible() expect(calls.find(c => c.path === '/administrators/other-admin/password')?.body).toEqual({ password: 'Reset-test-2026' }) }) test('只读人员可修改本人密码,错误原密码不退出,成功后清理登录', async ({ page }) => { await fixture(page, 'viewer') await page.goto('/settings') await expect(page.getByText('无访问权限', { exact: true })).toBeVisible() await page.getByRole('button', { name: '修改密码', exact: true }).click() const dialog = page.getByRole('dialog', { name: '修改本人密码' }) await dialog.getByLabel('原密码', { exact: true }).fill('Incorrect-old-password') await dialog.getByLabel('新密码', { exact: true }).fill('Updated-test-2026') await dialog.getByLabel('确认新密码', { exact: true }).fill('Updated-test-2026') await dialog.getByRole('button', { name: '保存并重新登录' }).click() await expect(dialog.getByText(/原密码不正确/)).toBeVisible() await dialog.getByLabel('原密码', { exact: true }).fill('Correct-old-password') await dialog.getByRole('button', { name: '保存并重新登录' }).click() await expect(page).toHaveURL(/\/login$/) await expect(page.getByRole('heading', { name: '登录管理控制台' })).toBeVisible() expect(await page.evaluate(() => sessionStorage.getItem('vdi.accessToken'))).toBeNull() })