// 防再发守卫:管理员建号路径(UsersPanel)必须持续符合后端待初始化契约。 // 背景:backend 8dfc223 将 POST /api/admin/users 改为严格 {owner, profile} 解码, // 前端未跟进导致部署后 UI 建号 400(json: unknown field "name")——单测与 mock // 都没有覆盖这条组合路径,本场景以真实浏览器把「填用户名 → 创建账号 → // 确认『待初始化』提示与用户表行」钉进组合验收线。 // 驱动方:Dev 仓 scripts/e2e.sh——构建 RDM 二进制 + frontend dist,起 serve(auth on) // 与 vite preview;种子账号 e2e-auditor 为基座管理员(bootstrap-admin)。 // 环境契约(缺省值与 e2e.sh 一致;PLAYWRIGHT_BASE_URL 未设置时整组跳过): // PLAYWRIGHT_BASE_URL / E2E_OWNER / E2E_PASSWORD / E2E_PASSWORD2 import { test, expect, type Page } from '@playwright/test' const OWNER = process.env.E2E_OWNER || 'e2e-auditor' const PASSWORD = process.env.E2E_PASSWORD || 'E2eAudit#2026' const PASSWORD2 = process.env.E2E_PASSWORD2 || 'E2eAudit#2026b' test.skip(!process.env.PLAYWRIGHT_BASE_URL, 'set PLAYWRIGHT_BASE_URL (scripts/e2e.sh drives this suite)') async function login(page: Page) { await page.goto('/') await expect(page.getByPlaceholder('由管理员分配')).toBeVisible() await page.getByPlaceholder('由管理员分配').fill(OWNER) await page.getByPlaceholder('初始密码或常用密码').fill(PASSWORD) await page.getByRole('button', { name: '登录', exact: true }).click() // 三种落点:直接进工作台(密码已生效)|强制改密门|初始密码已失效(种子二次运行)→ 改用新密码重登。 const entered = await page.locator('.tsb-user').waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false) if (!entered) { const gated = await page.getByText('修改密码').waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false) if (gated) { const pwInputs = page.locator('.lg-card input[type="password"]') await pwInputs.nth(0).fill(PASSWORD) await pwInputs.nth(1).fill(PASSWORD2) await pwInputs.nth(2).fill(PASSWORD2) await page.getByRole('button', { name: '设置新密码并进入' }).click() } else { await page.getByPlaceholder('初始密码或常用密码').fill(PASSWORD2) await page.getByRole('button', { name: '登录', exact: true }).click() } } await expect(page.locator('.tsb-user')).toBeVisible({ timeout: 15_000 }) } test('管理员建号:UI 请求新契约并给出待初始化提示', async ({ page }) => { await login(page) // 账户菜单 → 系统管理…(全屏独立视图)→ 用户管理分区。 await page.locator('.tsb-user').click() const adminEntry = page.getByText('系统管理…', { exact: true }) await expect(adminEntry).toBeVisible() await adminEntry.click() await expect(page.locator('.az')).toBeVisible() const usersSection = page.getByText('用户管理', { exact: true }).first() if (await usersSection.count()) await usersSection.click() // 建号表单:用户名每次运行唯一(重复运行不因 409 假红),显示名可空。 const newOwner = `e2e-ui-${Date.now().toString(36)}` const userInput = page.getByPlaceholder('用户名(登录名)') await expect(userInput).toBeVisible() await userInput.fill(newOwner) await page.getByPlaceholder('显示名(可空)').fill('组合验收') // 创建 → 契约断言一:请求体是严格 {owner, profile:{name}},无 password/isAdmin。 const createBody = page.waitForRequest( req => req.url().endsWith('/api/admin/users') && req.method() === 'POST', ).then(req => req.postDataJSON()) await page.getByRole('button', { name: '创建账号', exact: true }).click() const body = await createBody expect(body).toEqual({ owner: newOwner, profile: { name: '组合验收' } }) // 契约断言二:成功提示逐字含「待初始化」与 CLI 发放指引。 await expect(page.locator('.ad-notice')).toHaveText( `已创建 ${newOwner}(待初始化):请在服务器执行 user initialize-account 发放激活 Key`, ) // 契约断言三:用户表出现该账号,状态为未设密码(HTTP 建号不能设密码/授权限)。 const row = page.locator('.ad-table tbody tr', { hasText: newOwner }) await expect(row).toBeVisible() await expect(row).toContainText('未设密码') await expect(row).not.toContainText('ADMIN') // 界面说明与后端流程一致(激活 Key 由服务器本地 CLI 发放)。 await expect(page.locator('.ad-create .ad-hint')).toContainText('待初始化') await expect(page.locator('.ad-create .ad-hint')).toContainText('user initialize-account') })