后端 PublicRegisterDto phone 字段添加 @NotBlank + @Pattern 校验; PublicAuthService 添加手机号唯一性检查(公众租户范围内); 前端 Login.vue 注册表单添加手机号输入框、验证规则、提交参数; 新增 10 条 E2E 测试用例覆盖前端校验、API 参数传递、完整注册流程。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
268 lines
9.7 KiB
TypeScript
268 lines
9.7 KiB
TypeScript
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/)
|
||
})
|
||
})
|