library-picturebook-activity/frontend/e2e/public/register.spec.ts
En 67de13c29a feat: 公众端注册添加手机号必填——支持 AI 创作功能绑定手机号
后端 PublicRegisterDto phone 字段添加 @NotBlank + @Pattern 校验;
PublicAuthService 添加手机号唯一性检查(公众租户范围内);
前端 Login.vue 注册表单添加手机号输入框、验证规则、提交参数;
新增 10 条 E2E 测试用例覆盖前端校验、API 参数传递、完整注册流程。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 15:36:52 +08:00

268 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect } from '@playwright/test'
/**
* 公众端注册流程测试
* 验证手机号必填功能
*/
/** 公众端登录/注册页面路径 */
const REGISTER_PATH = '/p/login'
/** Mock 注册 API 的响应 */
async function setupRegisterApiMocks(page: import('@playwright/test').Page) {
// 拦截注册 API
await page.route('**/api/public/auth/register', async (route) => {
const request = route.request()
const postData = request.postDataJSON()
// 模拟后端校验逻辑
if (!postData?.phone) {
await route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({ code: 400, message: '手机号不能为空' }),
})
return
}
if (!/^1[3-9]\d{9}$/.test(postData.phone)) {
await route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({ code: 400, message: '手机号格式不正确' }),
})
return
}
if (postData.phone === '13800000000') {
await route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({ code: 400, message: '该手机号已注册' }),
})
return
}
// 注册成功
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
code: 200,
message: 'success',
data: {
token: 'mock-jwt-token-for-test',
user: {
id: 999,
username: postData.username,
nickname: postData.nickname,
phone: postData.phone,
avatar: null,
city: null,
gender: null,
userType: 'adult',
roles: ['public_user'],
permissions: [],
},
},
}),
})
})
// 拦截登录 API切换到登录模式时需要
await page.route('**/api/public/auth/login', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
code: 200,
message: 'success',
data: {
token: 'mock-jwt-token-for-test',
user: {
id: 1,
username: 'demo',
nickname: '测试用户',
phone: '13800138000',
avatar: null,
city: null,
gender: null,
userType: 'adult',
roles: ['public_user'],
permissions: [],
},
},
}),
})
})
}
test.describe('公众端注册 - 手机号必填', () => {
test.beforeEach(async ({ page }) => {
await setupRegisterApiMocks(page)
await page.goto(REGISTER_PATH)
// 点击"立即注册"切换到注册模式
await page.locator('text=立即注册').click()
// 等待注册表单渲染
await expect(page.locator('text=创建账号')).toBeVisible()
})
test('PR-01 注册表单包含手机号输入框', async ({ page }) => {
// 验证手机号标签可见
await expect(page.locator('label:has-text("手机号")')).toBeVisible()
// 验证手机号输入框可见
await expect(page.locator('input[placeholder="请输入手机号"]')).toBeVisible()
})
test('PR-02 手机号输入框限制最大长度11位', async ({ page }) => {
const phoneInput = page.locator('input[placeholder="请输入手机号"]')
await expect(phoneInput).toHaveAttribute('maxlength', '11')
})
test('PR-03 登录模式不显示手机号输入框', async ({ page }) => {
// 点击"去登录"切换回登录模式
await page.locator('text=去登录').click()
await expect(page.locator('text=登录')).toBeVisible()
// 验证手机号输入框不存在
await expect(page.locator('input[placeholder="请输入手机号"]')).not.toBeVisible()
})
test('PR-04 手机号为空时显示必填校验错误', async ({ page }) => {
// 清空手机号(开发环境可能预填了值)
const phoneInput = page.locator('input[placeholder="请输入手机号"]')
await phoneInput.clear()
// 触发 blur 校验
await phoneInput.blur()
// 验证校验错误信息
await expect(page.locator('.ant-form-item-explain-error')).toContainText('请输入手机号')
})
test('PR-05 手机号格式不正确时显示校验错误', async ({ page }) => {
const phoneInput = page.locator('input[placeholder="请输入手机号"]')
await phoneInput.fill('12345678901')
await phoneInput.blur()
// 验证格式校验错误
await expect(page.locator('.ant-form-item-explain-error')).toContainText('手机号格式不正确')
})
test('PR-06 输入正确的手机号格式不显示错误', async ({ page }) => {
const phoneInput = page.locator('input[placeholder="请输入手机号"]')
await phoneInput.fill('13912345678')
await phoneInput.blur()
// 验证无校验错误
await expect(page.locator('.ant-form-item-explain-error')).not.toBeVisible()
})
test('PR-07 不传手机号后端返回校验错误', async ({ page }) => {
// 修改 route 来模拟不传手机号
let capturedBody: any = null
await page.route('**/api/public/auth/register', async (route) => {
capturedBody = route.request().postDataJSON()
// 模拟后端 @NotBlank 校验
await route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({ code: 400, message: '手机号不能为空' }),
})
})
// 清空手机号并提交(绕过前端校验直接测试 API
const phoneInput = page.locator('input[placeholder="请输入手机号"]')
await phoneInput.clear()
// 填写其他必填项
await page.locator('input[placeholder="给自己取个名字吧"]').fill('测试昵称')
await page.locator('input[placeholder="请输入用户名"]').fill('testuser' + Date.now())
await page.locator('input[type="password"]').first().fill('test123456')
await page.locator('input[placeholder="再次输入密码"]').fill('test123456')
// 点击注册按钮
await page.locator('button:has-text("注册并登录")').click()
// 验证 Ant Design 前端表单校验拦截了提交(手机号必填)
await expect(page.locator('.ant-form-item-explain-error')).toContainText('请输入手机号')
})
test('PR-08 注册时传递手机号参数到后端', async ({ page }) => {
let capturedBody: any = null
// 重新注册路由来捕获请求体(覆盖 beforeEach 的 setupRegisterApiMocks
await page.unroute('**/api/public/auth/register')
await page.route('**/api/public/auth/register', async (route) => {
capturedBody = route.request().postDataJSON()
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
code: 200,
message: 'success',
data: {
token: 'mock-token',
user: { id: 999, username: 'test', nickname: '测试', phone: '13900001111', avatar: null, city: null, gender: null, userType: 'adult', roles: ['public_user'], permissions: [] },
},
}),
})
})
// 填写完整注册表单
await page.locator('input[placeholder="给自己取个名字吧"]').fill('测试昵称')
await page.locator('input[placeholder="请输入手机号"]').fill('13900001111')
const uniqueUsername = 'testuser' + Date.now()
await page.locator('input[placeholder="请输入用户名"]').fill(uniqueUsername)
await page.locator('input[type="password"]').first().fill('test123456')
await page.locator('input[placeholder="再次输入密码"]').fill('test123456')
// 点击注册
await page.locator('button:has-text("注册并登录")').click()
// 等待成功提示(说明 API 已被调用)
await expect(page.locator('.ant-message')).toContainText('注册成功', { timeout: 5000 })
// 验证请求体包含 phone 字段
expect(capturedBody).toBeTruthy()
expect(capturedBody.phone).toBe('13900001111')
})
test('PR-09 已注册手机号后端返回错误提示', async ({ page }) => {
// 使用预定义的"已注册"手机号
await page.locator('input[placeholder="给自己取个名字吧"]').fill('测试昵称')
await page.locator('input[placeholder="请输入手机号"]').fill('13800000000')
await page.locator('input[placeholder="请输入用户名"]').fill('testuser' + Date.now())
await page.locator('input[type="password"]').first().fill('test123456')
await page.locator('input[placeholder="再次输入密码"]').fill('test123456')
// 点击注册
await page.locator('button:has-text("注册并登录")').click()
// 验证错误提示(由 mock 返回"该手机号已注册"
await expect(page.locator('.ant-message')).toContainText('该手机号已注册', { timeout: 5000 })
})
test('PR-10 完整注册流程成功', async ({ page }) => {
const testSuffix = Date.now()
// 填写完整注册表单
await page.locator('input[placeholder="给自己取个名字吧"]').fill('测试昵称')
await page.locator('input[placeholder="请输入手机号"]').fill('139' + String(testSuffix).slice(-8))
await page.locator('input[placeholder="请输入用户名"]').fill('e2e' + testSuffix)
await page.locator('input[type="password"]').first().fill('test123456')
await page.locator('input[placeholder="再次输入密码"]').fill('test123456')
// 点击注册
await page.locator('button:has-text("注册并登录")').click()
// 验证成功提示
await expect(page.locator('.ant-message')).toContainText('注册成功', { timeout: 5000 })
// 验证跳转离开登录页
await page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 10_000 })
// 验证跳转到了活动列表页(/p/activities
await expect(page).toHaveURL(/\/p\/activities/)
})
})