Why: The login recovery contract requires a real-browser acceptance spec covering admin issue -> key-only redeem -> forced set-password -> relogin, and the changepw spec needed the same deterministic gate synchronization. Evidence: Both specs pass against an isolated auth-on instance (serve --auth on, binary backend=467718a+frontend=9fc7da2); receipts be7ae9a2/b3a9e691 bound to Dev HEAD 268ef29 (SELF). changepw-form also passes (5.9s) alongside login-key (12.2s) on a reused instance, proving idempotency. Impact: New e2e/login-key.playwright.ts (skip-guarded like existing suites); changepw login() waits for the gate heading and success page before returning to login, fixing a stale-fill race that submitted empty gate fields. Refs: Dev docs/svc-deploy/2026-10-13-login-recovery-contract.md; Dev PR #504.
65 lines
4.0 KiB
TypeScript
65 lines
4.0 KiB
TypeScript
// 增量 3 验收(2026-10-12 诊断报告):改密弹窗表单语义——<form onSubmit> 承载提交
|
||
//(Enter 可提交)、autocomplete 三段标注(username/current-password/new-password)、
|
||
// 只读用户名关联、错误文案复用 humanChangePasswordError(401 不再一刀切)。
|
||
// 驱动:scripts/e2e.sh 第 4 阶段(E2E_PLAIN_BASE=1)——表单语义与命令面无关,
|
||
// 任意 auth-on 服务可验,普通基座段顺路覆盖;RDM 组合主跑(第 3 阶段)本文件自跳过。
|
||
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 && process.env.E2E_PLAIN_BASE),
|
||
'set PLAYWRIGHT_BASE_URL + E2E_PLAIN_BASE (scripts/e2e.sh phase 4 drives this suite)')
|
||
|
||
async function login(page: Page) {
|
||
await page.goto('/')
|
||
await page.getByPlaceholder('由管理员分配').fill(OWNER)
|
||
await page.getByPlaceholder('初始密码或常用密码').fill(PASSWORD)
|
||
await page.getByRole('button', { name: '登录', exact: true }).click()
|
||
// 受限设密门(2026-10-13 合同):heading 唯一定位(标题与按钮同名),
|
||
// 门挂载后登录卡已卸载,fill 不再与登录卡竞态。
|
||
const gate = page.getByRole('heading', { name: '设置新密码' })
|
||
const gated = await gate.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(PASSWORD2)
|
||
await pwInputs.nth(1).fill(PASSWORD2)
|
||
await page.getByRole('button', { name: '设置新密码', exact: true }).click()
|
||
// 设密成功后服务端销毁会话:等成功页再回登录(在途点取消会被会话复验拒绝)。
|
||
await expect(page.getByRole('heading', { name: '密码已更新' })).toBeVisible({ timeout: 10_000 })
|
||
await page.getByRole('button', { name: '返回登录', exact: true }).click()
|
||
await page.getByPlaceholder('由管理员分配').fill(OWNER)
|
||
await page.getByPlaceholder('初始密码或常用密码').fill(PASSWORD2)
|
||
await page.getByRole('button', { name: '登录', exact: true }).click()
|
||
} else {
|
||
await page.getByPlaceholder('初始密码或常用密码').fill(PASSWORD2)
|
||
await page.getByRole('button', { name: '登录', exact: true }).click()
|
||
}
|
||
await expect(page.locator('.tsb-tool', { hasText: '代理' })).toBeVisible({ timeout: 15_000 })
|
||
}
|
||
|
||
test('改密弹窗:form 提交、autocomplete/用户名关联、错误分类文案', async ({ page }) => {
|
||
await login(page)
|
||
await page.locator('.tsb-user').click()
|
||
await page.getByRole('button', { name: '修改密码…' }).click()
|
||
const dlg = page.getByRole('dialog', { name: '修改密码' })
|
||
await expect(dlg).toBeVisible()
|
||
|
||
// 用户名只读关联 + 三段 autocomplete。
|
||
const username = dlg.locator('input[autocomplete="username"]')
|
||
await expect(username).toHaveValue(OWNER)
|
||
// 本仓 @playwright/test 无 toBeReadOnly:以 readonly 属性断言只读关联。
|
||
await expect(username).toHaveAttribute('readonly', '')
|
||
await expect(dlg.locator('input[autocomplete="current-password"]')).toHaveCount(1)
|
||
await expect(dlg.locator('input[autocomplete="new-password"]')).toHaveCount(2)
|
||
|
||
// 表单语义:填错当前密码后按 Enter(不点按钮)提交——证明提交走 <form onSubmit>。
|
||
await dlg.locator('input[autocomplete="current-password"]').fill('definitely-wrong-pass')
|
||
await dlg.locator('input[autocomplete="new-password"]').first().fill('ANewPass#2026x')
|
||
await dlg.locator('input[autocomplete="new-password"]').nth(1).fill('ANewPass#2026x')
|
||
await dlg.locator('input[autocomplete="current-password"]').press('Enter')
|
||
// 后端 401 "current password incorrect" → humanChangePasswordError 的分类文案。
|
||
await expect(dlg.getByText('当前密码不正确')).toBeVisible({ timeout: 10_000 })
|
||
})
|