/** * 注册页面 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(); }); });