| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192 |
- /**
- * API 级 e2e 共享助手
- * ================================================================
- * 背景:远程环境 http://cfc.iwintrue.com 仅部署后端 API(Spring Boot),
- * 未托管前端 SPA(访问任意路径均返回 Spring Boot 白标 404),
- * 因此基于 DOM 的 e2e(page.goto + 选择器等待)在远程不可运行。
- * 本助手为「API 级 e2e」提供统一入口:
- * - registerRole:测试专用直注接口 /api/auth/direct-register 自注册,
- * 不依赖种子数据库、不依赖前端表单登录、不打印任何敏感信息;
- * - apiPost:统一 POST 封装(项目约定接口一律使用 POST)。
- *
- * 安全约定:本文件及调用方一律不打印 token / 手机号等敏感信息。
- */
- const { expect } = require('@playwright/test');
- /** 生成合法手机号(严格 11 位,匹配后端 ^1[3-9]\d{9}$ 校验,否则返回「手机号格式不正确」) */
- function randPhone() {
- return '13' + String(Math.floor(Math.random() * 900000000) + 100000000);
- }
- /**
- * 自注册账号并返回会话上下文
- * @param {import('@playwright/test').APIRequestContext} request
- * @param {string} role 角色:parent / teacher / admin ...
- * @returns {Promise<{token:string,userId:*,familyId:*,role:string,roles:*}>}
- */
- async function registerRole(request, role, nickname) {
- const res = await request.post('/api/auth/direct-register', {
- data: { phone: randPhone(), role, nickname: nickname || 'QA-' + role }, timeout: API_TIMEOUT_MS,
- });
- const body = await res.json().catch(() => ({}));
- expect(body.code, 'direct-register 应当成功(code=200)').toBe(200);
- const d = body.data || {};
- expect(typeof d.token, '响应应包含 token').toBe('string');
- return {
- token: d.token,
- userId: d.userId,
- familyId: d.familyId,
- role: d.role,
- roles: d.roles,
- };
- }
- /**
- * 统一 POST 调用,返回 { status, code, message, data }
- * 若响应非 JSON(如未捕获异常导致的 Spring 500),回退以 HTTP 状态码作为 code,
- * 避免断言拿到 undefined 造成误判。
- *
- * 超时说明:远程 /api/family/member/add 冷启动首调实测约 16s(后续约 1.5s),
- * 超过 Playwright 默认 actionTimeout(15s) 会误报超时,故显式放宽到 60s。
- */
- const API_TIMEOUT_MS = 120000; // 冷启动容忍:远端 member/add 冷启动实测 16s+,降级窗口可能超过 60s
- async function apiPost(request, path, body, token) {
- const headers = {};
- if (token) headers.Authorization = 'Bearer ' + token;
- const res = await request.post(path, { data: body || {}, headers, timeout: API_TIMEOUT_MS });
- const raw = await res.text();
- let json = null;
- try {
- json = raw ? JSON.parse(raw) : null;
- } catch (e) {
- json = null;
- }
- if (!json || typeof json !== 'object') {
- return {
- status: res.status(),
- code: res.status(),
- message: 'NON_JSON_RESPONSE',
- data: null,
- };
- }
- return {
- status: res.status(),
- code: json.code,
- message: json.message,
- data: json.data,
- };
- }
- /** 从成员列表中找出对应登录用户的成员记录(用于「切换到自己」等用例) */
- function pickMemberByUserId(members, userId) {
- if (!Array.isArray(members)) return null;
- return members.find((m) => String(m.userId) === String(userId)) || null;
- }
- /** 统一时间戳后缀,避免同名字段唯一约束冲突 */
- function uniqueSuffix() {
- return String(Date.now()).slice(-8);
- }
- module.exports = { randPhone, registerRole, apiPost, pickMemberByUserId, uniqueSuffix };
|