Files
xiaoxia-saas/apps/web/e2e/register.spec.ts
T
xiaoxia 0fcb77b991
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 1m6s
CI Build & Deploy Pipeline / Build Staging Web Image (push) Successful in 1m49s
CI Build & Deploy Pipeline / Build Production API Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (push) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (push) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (push) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 3m1s
CI/CD Pipeline / Unit Tests (push) Successful in 3m20s
CI/CD Pipeline / Integration Tests (push) Successful in 1m29s
CI Build & Deploy Pipeline / Build Staging API Image (push) Successful in 7m4s
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Successful in 7m54s
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
CI Build & Deploy Pipeline / Staging E2E Tests (push) Failing after 2m50s
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Successful in 3m28s
ci: Prettier纳入两层防御体系 (#520)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-07-18 18:08:19 +08:00

297 lines
10 KiB
TypeScript

/**
* 注册页面 E2E 测试
*
* 覆盖:页面渲染、表单验证、成功注册、跳转链接
* 每个测试独立,使用随机邮箱避免冲突。
*/
import { expect, test, type APIRequestContext } from "@playwright/test"
const PASSWORD = "Test123456!"
const apiBase = process.env.E2E_API_BASE || "/api/v1"
function uniqueEmail(prefix: string): string {
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}@example.com`
}
function uniqueUsername(prefix: string): string {
return `${prefix}_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`
}
/** 登录操作,遇到 429 限流自动等待重试(最多等 65s) */
async function loginWithRetry(
request: APIRequestContext,
email: string,
password: string,
maxRetries = 2,
) {
for (let i = 0; i <= maxRetries; i++) {
const response = await request.post(`${apiBase}/auth/login`, {
data: { email, password },
})
if (response.status() !== 429) return response
console.log(`[login] 触发限流,等待 65s 后重试 (${i + 1}/${maxRetries})`)
await new Promise((r) => setTimeout(r, 65000))
}
return request.post(`${apiBase}/auth/login`, {
data: { email, password },
})
}
test.describe("注册页面", () => {
test.describe.configure({ timeout: 120_000 })
// ─── 页面渲染 ──────────────────────────────────────
test("页面正常渲染 - 标题、表单元素、提交按钮", async ({ page }) => {
await page.goto("/register")
// 品牌标识
await expect(page.locator(".xx-auth-brand-name")).toHaveText("小虾智剪")
// 标题/描述
await expect(page.getByText("创建账户,开启智能视频创作之旅")).toBeVisible()
// 表单字段
await expect(page.getByLabel("邮箱")).toBeVisible()
await expect(page.getByLabel("用户名")).toBeVisible()
await expect(page.getByLabel("密码")).toBeVisible()
await expect(page.getByLabel("确认密码")).toBeVisible()
// 提交按钮
await expect(page.locator("button[type='submit']").filter({ hasText: "注册" })).toBeVisible()
})
// ─── 表单验证 ──────────────────────────────────────
test("空提交 - 显示必填错误", async ({ page }) => {
await page.goto("/register")
// 直接点击注册按钮
await page.locator("button[type='submit']").filter({ hasText: "注册" }).click()
// 应显示必填错误
await expect(page.getByText("请输入邮箱")).toBeVisible()
await expect(page.getByText("请输入用户名")).toBeVisible()
await expect(page.getByText("请输入密码")).toBeVisible()
await expect(page.getByText("请确认密码")).toBeVisible()
})
test("无效邮箱格式 - 显示格式错误", async ({ page }) => {
await page.goto("/register")
await page.getByLabel("邮箱").fill("not-an-email")
await page.getByLabel("用户名").fill("testuser")
await page.getByLabel("密码").fill(PASSWORD)
await page.getByLabel("确认密码").fill(PASSWORD)
await page.locator("button[type='submit']").filter({ hasText: "注册" }).click()
// 应显示邮箱格式错误
await expect(page.getByText("请输入有效的邮箱地址")).toBeVisible()
})
test("密码太短 - 显示长度错误", async ({ page }) => {
await page.goto("/register")
await page.getByLabel("邮箱").fill(uniqueEmail("short-pwd"))
await page.getByLabel("用户名").fill("testuser")
await page.getByLabel("密码").fill("123")
await page.getByLabel("确认密码").fill("123")
await page.locator("button[type='submit']").filter({ hasText: "注册" }).click()
// 应显示密码长度错误
await expect(page.getByText("密码至少 8 个字符")).toBeVisible()
})
test("确认密码不一致 - 显示不一致错误", async ({ page }) => {
await page.goto("/register")
await page.getByLabel("邮箱").fill(uniqueEmail("pwd-mismatch"))
await page.getByLabel("用户名").fill("testuser")
await page.getByLabel("密码").fill(PASSWORD)
await page.getByLabel("确认密码").fill("Different123!")
await page.locator("button[type='submit']").filter({ hasText: "注册" }).click()
// 应显示密码不一致错误
await expect(page.getByText("两次输入的密码不一致")).toBeVisible()
})
test("用户名为空 - 显示必填错误", async ({ page }) => {
await page.goto("/register")
await page.getByLabel("邮箱").fill(uniqueEmail("empty-user"))
await page.getByLabel("用户名").fill("")
await page.getByLabel("密码").fill(PASSWORD)
await page.getByLabel("确认密码").fill(PASSWORD)
await page.locator("button[type='submit']").filter({ hasText: "注册" }).click()
await expect(page.getByText("请输入用户名")).toBeVisible()
})
// ─── 成功注册 ──────────────────────────────────────
test("成功注册 - 提交有效表单", async ({ page, _request }) => {
const email = uniqueEmail("reg-ui-ok")
const username = uniqueUsername("reguiok")
await page.goto("/register")
await page.getByLabel("邮箱").fill(email)
await page.getByLabel("用户名").fill(username)
await page.getByLabel("密码").fill(PASSWORD)
await page.getByLabel("确认密码").fill(PASSWORD)
// 监听注册请求
const registerResponse = page.waitForResponse(
(resp) => resp.url().includes("/auth/register") && resp.request().method() === "POST",
{ timeout: 15_000 },
)
await page.locator("button[type='submit']").filter({ hasText: "注册" }).click()
const resp = await registerResponse
expect(resp.ok(), `注册请求应返回 2xx,实际: ${resp.status()}`).toBeTruthy()
// 注册成功后应跳转到登录页或显示成功消息
// 页面应停留在可识别的状态(成功提示或跳转)
await expect
.poll(
async () => {
const url = page.url()
// 可能跳转到 login,也可能在当前页显示成功消息
if (url.includes("/login")) return "redirected"
const hasSuccess = await page.getByText(/注册成功/).isVisible()
return hasSuccess ? "success_msg" : url
},
{ timeout: 10_000 },
)
.toMatch(/redirected|success_msg/)
})
test("注册已存在邮箱 - UI 显示错误", async ({ page, request }) => {
const email = uniqueEmail("reg-ui-dup")
const username1 = uniqueUsername("reguidup1")
const username2 = uniqueUsername("reguidup2")
// 先通过 API 注册一个账号
const firstReg = await request.post(`${apiBase}/auth/register`, {
data: {
email,
password: PASSWORD,
username: username1,
display_name: "User 1",
},
})
expect(firstReg.ok(), "第一次注册应成功").toBeTruthy()
// 再在 UI 上用相同邮箱注册
await page.goto("/register")
await page.getByLabel("邮箱").fill(email)
await page.getByLabel("用户名").fill(username2)
await page.getByLabel("密码").fill(PASSWORD)
await page.getByLabel("确认密码").fill(PASSWORD)
await page.locator("button[type='submit']").filter({ hasText: "注册" }).click()
// 应显示错误提示(通过 antd message 或表单错误)
await expect
.poll(
async () => {
// 检查是否有错误消息
const hasError = await page.getByText(/注册失败|已注册|已存在|exists/).isVisible()
return hasError ? "error_shown" : "waiting"
},
{ timeout: 10_000 },
)
.toBe("error_shown")
})
// ─── 跳转链接 ──────────────────────────────────────
test("跳转到登录页的链接", async ({ page }) => {
await page.goto("/register")
await page.getByRole("link", { name: "立即登录" }).click()
await expect(page).toHaveURL(/\/login/)
await expect(page.getByLabel("邮箱")).toBeVisible()
})
test("登录页有跳转到注册页的链接(反向验证)", async ({ page }) => {
await page.goto("/login")
await page.getByRole("link", { name: "立即注册" }).click()
await expect(page).toHaveURL(/\/register/)
})
test("登录页有忘记密码链接", async ({ page }) => {
await page.goto("/login")
await expect(page.getByRole("link", { name: /忘记密码/ })).toBeVisible()
await page.getByRole("link", { name: /忘记密码/ }).click()
await expect(page).toHaveURL(/\/forgot-password/)
})
// ─── 路由守卫 - 已登录用户访问注册页 ──────────────
test("已登录用户访问注册页 - 可正常访问(注册页无守卫)", async ({ page, request }) => {
const email = uniqueEmail("reg-auth")
const username = uniqueUsername("regauth")
// 注册
await request.post(`${apiBase}/auth/register`, {
data: {
email,
password: PASSWORD,
username,
display_name: "Reg Auth Test",
},
})
// 登录
const login = await loginWithRetry(request, email, PASSWORD)
expect(login.ok(), "登录应成功").toBeTruthy()
const loginData = await login.json()
// 设置登录态
await page.addInitScript(
({ token, user }) => {
localStorage.setItem("access_token", token)
localStorage.setItem(
"auth-storage",
JSON.stringify({
state: { user, isAuthenticated: true },
version: 0,
}),
)
},
{
token: loginData.access_token,
user: {
id: loginData.user_id,
user_id: loginData.user_id,
email,
username,
display_name: username,
is_email_verified: true,
email_verified: true,
},
},
)
await page.goto("/register")
// 注册页对已登录用户也可访问(注册页是公开页面)
// 验证页面正常渲染
await expect(page.getByLabel("邮箱")).toBeVisible()
await expect(page.locator("button[type='submit']").filter({ hasText: "注册" })).toBeVisible()
})
})