/** * 个人设置页面 E2E 测试 * * 覆盖:设置页面加载、个人信息展示、修改昵称/头像、修改密码、 * 账号安全区域、退出登录按钮、未登录重定向 * * 每个测试独立,先注册登录获取 auth token。 */ import { expect, test, type APIRequestContext, type Page, } from "@playwright/test"; const PASSWORD = "Test123456!"; const apiBase = process.env.E2E_API_BASE || "/api/v1"; const apiOrigin = apiBase.endsWith("/api/v1") ? apiBase.slice(0, -"/api/v1".length) : ""; const routeBrowserApiToTestApi = async (page: Page) => { if (!apiOrigin) return; await page.route("**/api/v1/**", async (route) => { const sourceUrl = new URL(route.request().url()); const response = await route.fetch({ url: `${apiOrigin}${sourceUrl.pathname}${sourceUrl.search}`, }); await route.fulfill({ response }); }); }; 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 限流自动等待重试 */ 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 }, }); } /** 注册并登录,返回 { headers, email, username, userId, accessToken } */ async function createAuthedUser(request: APIRequestContext, label: string) { const email = uniqueEmail(label); const username = uniqueUsername(label); const reg = await request.post(`${apiBase}/auth/register`, { data: { email, password: PASSWORD, username, display_name: `E2E ${label}` }, }); expect(reg.ok(), `注册应成功: ${await reg.text()}`).toBeTruthy(); const regData = await reg.json(); const login = await loginWithRetry(request, email, PASSWORD); expect(login.ok(), `登录应成功: ${await login.text()}`).toBeTruthy(); const loginData = await login.json(); return { headers: { Authorization: `Bearer ${loginData.access_token}` }, email, username, userId: regData.user_id, accessToken: loginData.access_token, }; } /** 设置页面认证状态(localStorage) */ async function setupAuth( page: Page, token: string, user: { id: string; email: string; username: string; display_name: string }, ) { await page.addInitScript( ({ token, user }) => { localStorage.setItem("access_token", token); localStorage.setItem( "auth-storage", JSON.stringify({ state: { user, isAuthenticated: true }, version: 0, }), ); }, { token, user: { id: user.id, user_id: user.id, email: user.email, username: user.username, display_name: user.display_name, is_email_verified: true, email_verified: true, }, }, ); } test.describe("个人设置页面 - 未登录重定向", () => { test("未登录访问重定向到登录页", async ({ page }) => { await page.goto("/app/profile"); await expect(page).toHaveURL(/\/login/); }); }); test.describe("个人设置页面 - 页面加载", () => { test.describe.configure({ timeout: 120_000 }); test("设置页面加载成功", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "profile-load", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E profile-load", }); await page.goto("/app/profile"); await expect(page.locator(".xx-settings-page")).toBeVisible({ timeout: 20_000, }); }); test("页面标题存在", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "profile-title", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E profile-title", }); await page.goto("/app/profile"); await expect(page.locator(".xx-settings-page")).toBeVisible({ timeout: 20_000, }); // 验证页面包含"个人设置"标题 const heading = page.getByRole("heading", { name: /个人设置/ }); await expect(heading.first()).toBeVisible({ timeout: 10_000 }); }); }); test.describe("个人设置 - 个人信息展示", () => { test.describe.configure({ timeout: 120_000 }); test("个人信息卡片展示", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "profile-info", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E profile-info", }); await page.goto("/app/profile"); await expect(page.locator(".xx-settings-page")).toBeVisible({ timeout: 20_000, }); // 验证设置卡片存在 await expect(page.locator(".xx-settings-card")).toBeVisible(); }); test("用户名、邮箱字段展示", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "profile-fields", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E profile-fields", }); await page.goto("/app/profile"); await expect(page.locator(".xx-settings-page")).toBeVisible({ timeout: 20_000, }); // 验证表单字段存在 const fields = page.locator(".xx-settings-field"); await expect(fields.first()).toBeVisible(); const fieldCount = await fields.count(); expect(fieldCount).toBeGreaterThanOrEqual(2); }); test("用户名标签和输入框存在", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "profile-username", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E profile-username", }); await page.goto("/app/profile"); await expect(page.locator(".xx-settings-page")).toBeVisible({ timeout: 20_000, }); // 验证用户名标签 const usernameLabel = page.locator(".xx-settings-label").filter({ hasText: "用户名", }); await expect(usernameLabel).toBeVisible(); // 验证邮箱标签 const emailLabel = page.locator(".xx-settings-label").filter({ hasText: "邮箱", }); await expect(emailLabel).toBeVisible(); }); test("显示名称字段可编辑", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "profile-dispname", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E profile-dispname", }); await page.goto("/app/profile"); await expect(page.locator(".xx-settings-page")).toBeVisible({ timeout: 20_000, }); // 查找显示名称输入框 const displayNameField = page.locator(".xx-settings-field").filter({ has: page.locator(".xx-settings-label", { hasText: "显示名称" }), }); if (await displayNameField.isVisible()) { const input = displayNameField.locator("input"); if (await input.isVisible()) { // 验证输入框存在且可输入 await expect(input).toBeVisible(); const initialValue = await input.inputValue(); await input.fill("新的显示名称"); await expect(input).toHaveValue("新的显示名称"); // 恢复原值 await input.fill(initialValue); } } }); }); test.describe("个人设置 - 修改密码", () => { test.describe.configure({ timeout: 120_000 }); test("修改密码 API - 正向", async ({ request }) => { const { headers, email } = await createAuthedUser(request, "profile-chpwd"); const newPassword = "NewPass123456!"; const response = await request.post(`${apiBase}/auth/change-password`, { headers, data: { old_password: PASSWORD, new_password: newPassword, }, }); // 修改密码可能成功或接口不存在 expect( response.status() < 500, `修改密码应返回 2xx 或 4xx,实际: ${response.status()}`, ).toBeTruthy(); // 如果成功,用新密码登录验证 if (response.ok()) { const loginResp = await loginWithRetry(request, email, newPassword); expect(loginResp.ok(), "新密码应能登录").toBeTruthy(); } }); test("修改密码 - 旧密码错误反向", async ({ request }) => { const { headers } = await createAuthedUser(request, "profile-badpwd"); const response = await request.post(`${apiBase}/auth/change-password`, { headers, data: { old_password: "WrongOldPass123!", new_password: "NewPass123456!", }, }); // 如果接口存在,应该返回 400/401 if (response.status() < 500 && response.status() >= 400) { expect([400, 401]).toContain(response.status()); } // 接口不存在(404)也正常 }); test("修改密码 - 新密码太弱反向", async ({ request }) => { const { headers } = await createAuthedUser(request, "profile-weakpwd"); const response = await request.post(`${apiBase}/auth/change-password`, { headers, data: { old_password: PASSWORD, new_password: "123", }, }); if (response.status() < 500 && response.status() >= 400) { expect([400, 422]).toContain(response.status()); } }); test("未登录修改密码 - 反向", async ({ request }) => { const response = await request.post(`${apiBase}/auth/change-password`, { data: { old_password: "old", new_password: "new", }, }); expect([401, 403, 404]).toContain(response.status()); }); }); test.describe("个人设置 - 账号安全", () => { test.describe.configure({ timeout: 120_000 }); test("获取当前用户信息 - 正向", async ({ request }) => { const { headers, email, username } = await createAuthedUser( request, "profile-me", ); const response = await request.get(`${apiBase}/auth/me`, { headers }); expect( response.ok(), `获取用户信息应返回 2xx,实际: ${response.status()}`, ).toBeTruthy(); const data = await response.json(); expect(data.email).toBe(email); expect(data.username).toBe(username); }); test("账号安全区域提示信息存在", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "profile-security", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E profile-security", }); await page.goto("/app/profile"); await expect(page.locator(".xx-settings-page")).toBeVisible({ timeout: 20_000, }); // 验证通知区域存在 const notice = page.locator(".xx-settings-notice"); await expect(notice).toBeVisible(); }); }); test.describe("个人设置 - 退出登录", () => { test.describe.configure({ timeout: 120_000 }); test("登出 API - 正向", async ({ request }) => { const { headers } = await createAuthedUser( request, "profile-logout", ); const response = await request.post(`${apiBase}/auth/logout`, { headers, }); expect( response.ok(), `登出应返回 2xx,实际: ${response.status()}`, ).toBeTruthy(); // 登出后 token 应失效 const meResp = await request.get(`${apiBase}/auth/me`, { headers }); expect([401, 403]).toContain(meResp.status()); }); test("登出后页面跳转登录页", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "profile-logout-ui", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E profile-logout-ui", }); await page.goto("/app/profile"); await expect(page.locator(".xx-settings-page")).toBeVisible({ timeout: 20_000, }); // 清除 localStorage 模拟登出 await page.evaluate(() => { localStorage.removeItem("access_token"); localStorage.removeItem("auth-storage"); }); // 刷新页面应该重定向到登录页 await page.reload(); await expect(page).toHaveURL(/\/login/, { timeout: 10_000 }); }); }); test.describe("个人设置 - 保存按钮", () => { test.describe.configure({ timeout: 120_000 }); test("保存按钮存在", async ({ page, request }) => { await routeBrowserApiToTestApi(page); const { accessToken, userId, email, username } = await createAuthedUser( request, "profile-save", ); await setupAuth(page, accessToken, { id: userId, email, username, display_name: "E2E profile-save", }); await page.goto("/app/profile"); await expect(page.locator(".xx-settings-page")).toBeVisible({ timeout: 20_000, }); // 验证按钮存在 const button = page.getByRole("button", { name: /保存|暂未开放/ }); await expect(button.first()).toBeVisible({ timeout: 5_000 }); }); });