|
|
@@ -0,0 +1,1183 @@
|
|
|
+# E2E 测试体系重构 — Phase 1 实施计划
|
|
|
+
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** 建立 E2E 测试基础设施(DB reset + 统一认证 + 工具库),重构现有40个文件到按角色组织的目录,升级低质量文件
|
|
|
+
|
|
|
+**架构:** 以 `helpers/` 为基础库,`roles/` 按角色组织测试,保留旧文件不动直至全部迁移完成再删除
|
|
|
+
|
|
|
+**Tech Stack:** Playwright, MySQL 8.0, Node.js
|
|
|
+
|
|
|
+**前置条件:**
|
|
|
+- `tests/playwright.config.js` 已存在
|
|
|
+- E2E 测试环境:`http://cfc.iwintrue.com`
|
|
|
+- 数据库:`192.168.16.251:3306/cfc`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: 创建数据库重置脚本
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `tests/e2e/seed-data.sql`
|
|
|
+- Create: `tests/e2e/reset-db.sh`
|
|
|
+
|
|
|
+这一步创建种子数据和重置脚本。种子数据包含每个角色的标准测试账号。
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 seed-data.sql**
|
|
|
+
|
|
|
+编写种子数据,包含以下测试账号:
|
|
|
+
|
|
|
+| 角色 | 手机号 | 用途 |
|
|
|
+|------|--------|------|
|
|
|
+| 管理员 | 13800138000 | admin 端登录 |
|
|
|
+| 家长 | 13701366188 | mini-app 家长操作 |
|
|
|
+| 孩子1 | 13701366189 | 某家庭的孩子 |
|
|
|
+| 孩子2 | 13701366190 | 某家庭的第二个孩子 |
|
|
|
+| 规划师 | 13800000001 | teacher 端操作 |
|
|
|
+| 营养师 | 13800000010 | nutritionist 端操作 |
|
|
|
+| 管家 | 13800000020 | butler 端操作 |
|
|
|
+| 测评师 | 13800000030 | 测评师子角色 |
|
|
|
+| 供应商 | 13800138001 | vendor 端操作 |
|
|
|
+
|
|
|
+包含以下基础数据:
|
|
|
+- 1 个完整测试家庭(1家长 + 2孩子 + 关系数据)
|
|
|
+- 1 个规划师绑定关系
|
|
|
+- 1 个营养师绑定关系
|
|
|
+- 系统配置(维度、能量规则)
|
|
|
+- 测试商品/套餐各2个
|
|
|
+- 1 篇草稿文章 + 1 篇已发布文章
|
|
|
+- 1 个待审核活动 + 1 个已发布活动
|
|
|
+- 1 个已完成订单
|
|
|
+
|
|
|
+```sql
|
|
|
+-- seed-data.sql
|
|
|
+USE cfc;
|
|
|
+
|
|
|
+-- ===== 清空所有表 =====
|
|
|
+-- 按外键依赖顺序删除
|
|
|
+DELETE FROM family_member_attributes;
|
|
|
+DELETE FROM family_members;
|
|
|
+DELETE FROM families;
|
|
|
+DELETE FROM users WHERE id > 10;
|
|
|
+DELETE FROM verification_codes;
|
|
|
+-- ... (完整清空)
|
|
|
+
|
|
|
+-- ===== 测试用户 =====
|
|
|
+INSERT INTO users (id, openid, phone, nickname, role, roles, family_id, teacher_status, created_at, updated_at) VALUES
|
|
|
+(1001, 'test_admin', '13800138000', '测试管理员', 'admin', '["admin"]', 0, NULL, NOW(), NOW()),
|
|
|
+(1002, 'test_parent', '13701366188', '测试家长', 'parent', '["parent"]', 2001, NULL, NOW(), NOW()),
|
|
|
+(1003, 'test_child1', '13701366189', '小明', 'child', '["child"]', 2001, NULL, NOW(), NOW()),
|
|
|
+(1004, 'test_child2', '13701366190', '小红', 'child', '["child"]', 2001, NULL, NOW(), NOW()),
|
|
|
+(1005, 'test_teacher', '13800000001', '测试规划师', 'teacher', '["teacher","parent"]', 0, 'active', NOW(), NOW()),
|
|
|
+(1006, 'test_nutritionist', '13800000010', '测试营养师', 'teacher', '["teacher"]', 0, 'active', NOW(), NOW()),
|
|
|
+(1007, 'test_butler', '13800000020', '测试管家', 'teacher', '["teacher"]', 0, 'active', NOW(), NOW()),
|
|
|
+(1008, 'test_assessor', '13800000030', '测试测评师', 'teacher', '["teacher"]', 0, 'active', NOW(), NOW()),
|
|
|
+(1009, 'test_vendor', '13800138001', '测试供应商', 'parent', '["parent"]', 0, NULL, NOW(), NOW());
|
|
|
+
|
|
|
+-- ===== 测试家庭 =====
|
|
|
+INSERT INTO families (id, name, invite_code, created_at, updated_at) VALUES
|
|
|
+(2001, '测试幸福家庭', 'TEST2001', NOW(), NOW());
|
|
|
+
|
|
|
+-- ===== 家庭成员关系 =====
|
|
|
+INSERT INTO family_members (family_id, user_id, role_type, created_at, updated_at) VALUES
|
|
|
+(2001, 1002, 'parent', NOW(), NOW()),
|
|
|
+(2001, 1003, 'child', NOW(), NOW()),
|
|
|
+(2001, 1004, 'child', NOW(), NOW());
|
|
|
+
|
|
|
+-- ===== 规划师绑定 =====
|
|
|
+INSERT INTO guide_family_bindings (guide_id, family_id, status, created_at, updated_at) VALUES
|
|
|
+(1005, 2001, 'active', NOW(), NOW());
|
|
|
+
|
|
|
+-- ===== 测试文章 =====
|
|
|
+INSERT INTO articles (title, content, status, author_id, created_at, updated_at) VALUES
|
|
|
+('测试草稿文章', '这是一篇测试草稿', 'draft', 1001, NOW(), NOW()),
|
|
|
+('测试已发布文章', '这是一篇已发布的测试文章', 'published', 1001, NOW(), NOW());
|
|
|
+
|
|
|
+-- ===== 测试活动 =====
|
|
|
+INSERT INTO activities (title, status, created_at, updated_at) VALUES
|
|
|
+('测试待审核活动', 'pending', NOW(), NOW()),
|
|
|
+('测试已发布活动', 'published', NOW(), NOW());
|
|
|
+
|
|
|
+-- ===== 测试商品 =====
|
|
|
+INSERT INTO products (name, price, status, created_at, updated_at) VALUES
|
|
|
+('测试商品A', 99.00, 'active', NOW(), NOW()),
|
|
|
+('测试商品B', 199.00, 'active', NOW(), NOW());
|
|
|
+
|
|
|
+-- ===== 系统配置 =====
|
|
|
+INSERT INTO system_config (config_key, config_value, created_at, updated_at) VALUES
|
|
|
+('energy_dimensions', '{"dimensions":["body","mind","wisdom","wealth","action"]}', NOW(), NOW());
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 创建 reset-db.sh**
|
|
|
+
|
|
|
+```bash
|
|
|
+#!/bin/bash
|
|
|
+# reset-db.sh — 每次 E2E 运行前执行
|
|
|
+# 用法: bash tests/e2e/reset-db.sh
|
|
|
+
|
|
|
+MYSQL_HOST="${MYSQL_HOST:-192.168.16.251}"
|
|
|
+MYSQL_USER="${MYSQL_USER:-cfc}"
|
|
|
+MYSQL_PASS="${MYSQL_PASS:-cfc@123}"
|
|
|
+MYSQL_DB="${MYSQL_DB:-cfc}"
|
|
|
+SEED_FILE="$(dirname "$0")/seed-data.sql"
|
|
|
+
|
|
|
+echo "==> 重置数据库 ${MYSQL_DB}..."
|
|
|
+mysql -h"$MYSQL_HOST" -u"$MYSQL_USER" -p"$MYSQL_PASS" "$MYSQL_DB" < "$SEED_FILE"
|
|
|
+
|
|
|
+if [ $? -eq 0 ]; then
|
|
|
+ echo "==> 数据库重置完成"
|
|
|
+else
|
|
|
+ echo "==> 数据库重置失败"
|
|
|
+ exit 1
|
|
|
+fi
|
|
|
+```
|
|
|
+
|
|
|
+```bash
|
|
|
+chmod +x tests/e2e/reset-db.sh
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 验证重置脚本**
|
|
|
+
|
|
|
+```bash
|
|
|
+bash tests/e2e/reset-db.sh
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 数据库重置成功,测试用户和基础数据可用。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: 创建认证 helper (helpers/auth.js)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `tests/e2e/helpers/auth.js`
|
|
|
+
|
|
|
+统一8类角色的登录逻辑。admin 走 web 管理端登录页面,其他角色走 mini-app 登录流程。
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 helpers/auth.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/helpers/auth.js
|
|
|
+// 统一认证 helper - 支持 8 类角色
|
|
|
+
|
|
|
+const BASE_URL = process.env.PW_BASE_URL || 'http://cfc.iwintrue.com';
|
|
|
+
|
|
|
+const ROLES = {
|
|
|
+ parent: { phone: '13701366188', userId: 1002, loginType: 'code' },
|
|
|
+ child: { phone: '13701366189', userId: 1003, loginType: 'code' },
|
|
|
+ teacher: { phone: '13800000001', userId: 1005, loginType: 'code' },
|
|
|
+ nutritionist: { phone: '13800000010', userId: 1006, loginType: 'code' },
|
|
|
+ butler: { phone: '13800000020', userId: 1007, loginType: 'code' },
|
|
|
+ assessor: { phone: '13800000030', userId: 1008, loginType: 'code' },
|
|
|
+ admin: { phone: '13800138000', userId: 1001, loginType: 'password' },
|
|
|
+ vendor: { phone: '13800138001', userId: 1009, loginType: 'code' },
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 登录指定角色
|
|
|
+ * @param {import('@playwright/test').Browser} browser
|
|
|
+ * @param {string} role - parent/child/teacher/nutritionist/butler/assessor/admin/vendor
|
|
|
+ * @returns {Promise<{token: string, userId: number, role: string, familyId: number}>}
|
|
|
+ */
|
|
|
+async function loginAs(browser, role) {
|
|
|
+ var roleConfig = ROLES[role];
|
|
|
+ if (!roleConfig) throw new Error('Unknown role: ' + role);
|
|
|
+
|
|
|
+ var context = await browser.newContext({ storageState: undefined });
|
|
|
+ var page = await context.newPage();
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (role === 'admin') {
|
|
|
+ // admin:走 Web 管理端登录
|
|
|
+ await page.goto(BASE_URL + '/admin/login');
|
|
|
+ await page.waitForLoadState('networkidle');
|
|
|
+ await page.fill('input[type="text"], input[name="phone"], input[name="username"]', roleConfig.phone);
|
|
|
+ await page.fill('input[type="password"]', 'cfc@123');
|
|
|
+ await page.click('.login-btn, button:has-text("登录"), .el-button--primary');
|
|
|
+ await page.waitForTimeout(2000);
|
|
|
+ // 从 localStorage 获取 token
|
|
|
+ var token = await page.evaluate(function() { return localStorage.getItem('token') || localStorage.getItem('admin-token'); });
|
|
|
+ return { token: token, userId: roleConfig.userId, role: role, familyId: null };
|
|
|
+ } else {
|
|
|
+ // mini-app 角色:调用登录 API
|
|
|
+ // 由于测试环境可能已存在登录态,直接通过 API 获取 token
|
|
|
+ var response = await page.request.post(BASE_URL + '/api/auth/phone-login', {
|
|
|
+ data: { phone: roleConfig.phone, code: '123456' },
|
|
|
+ });
|
|
|
+ var body = await response.json();
|
|
|
+ if (body.code === 200 && body.data && body.data.token) {
|
|
|
+ return {
|
|
|
+ token: body.data.token,
|
|
|
+ userId: body.data.userId || roleConfig.userId,
|
|
|
+ role: role,
|
|
|
+ familyId: body.data.familyId || null,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ // API 不通则回退到 UI 登录
|
|
|
+ await page.goto(BASE_URL + '/#/pages/login/login');
|
|
|
+ await page.waitForLoadState('networkidle');
|
|
|
+ await page.fill('input[type="text"], input[name="phone"]', roleConfig.phone);
|
|
|
+ await page.fill('input[type="text"], input[name="code"], .code-input', '123456');
|
|
|
+ await page.click('.login-btn, button:has-text("登录")');
|
|
|
+ await page.waitForTimeout(3000);
|
|
|
+ var token2 = await page.evaluate(function() { return localStorage.getItem('token'); });
|
|
|
+ return { token: token2, userId: roleConfig.userId, role: role, familyId: null };
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ await page.close();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = { loginAs, ROLES, BASE_URL };
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 验证 auth helper**
|
|
|
+
|
|
|
+创建一个简单的验证脚本:
|
|
|
+
|
|
|
+```bash
|
|
|
+node -e "
|
|
|
+var { loginAs } = require('./tests/e2e/helpers/auth');
|
|
|
+console.log('Auth helper loaded OK');
|
|
|
+console.log('Roles:', Object.keys(require('./tests/e2e/helpers/auth').ROLES));
|
|
|
+"
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 模块加载成功,8个角色名称打印正确。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: 创建工具库 (helpers/utils.js + db.js)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `tests/e2e/helpers/utils.js`
|
|
|
+- Create: `tests/e2e/helpers/db.js`
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 helpers/utils.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/helpers/utils.js
|
|
|
+// 通用测试工具
|
|
|
+
|
|
|
+const { expect } = require('@playwright/test');
|
|
|
+
|
|
|
+/**
|
|
|
+ * 断言 API 响应成功(code=200)
|
|
|
+ * @param {import('@playwright/test').APIResponse} response
|
|
|
+ * @param {number} expectedStatus - HTTP 状态码
|
|
|
+ * @returns {Promise<object>} 响应 data 字段
|
|
|
+ */
|
|
|
+async function assertApiSuccess(response, expectedStatus) {
|
|
|
+ expectedStatus = expectedStatus || 200;
|
|
|
+ expect(response.status(), 'HTTP 状态码应为 ' + expectedStatus).toBe(expectedStatus);
|
|
|
+ var body = null;
|
|
|
+ try {
|
|
|
+ body = await response.json();
|
|
|
+ } catch (e) {
|
|
|
+ expect(false, '响应不是有效 JSON: ' + e.message).toBeTruthy();
|
|
|
+ }
|
|
|
+ expect(body.code, '响应 code 应为 200 但得到 ' + body.code + ': ' + (body.message || '')).toBe(200);
|
|
|
+ return body.data;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 断言 API 响应错误
|
|
|
+ * @param {import('@playwright/test').APIResponse} response
|
|
|
+ * @param {number} expectedCode - 业务错误码
|
|
|
+ * @param {string} expectedMessage - 错误消息包含的关键字
|
|
|
+ */
|
|
|
+async function assertApiError(response, expectedCode, expectedMessage) {
|
|
|
+ var body = await response.json();
|
|
|
+ if (expectedCode !== undefined) {
|
|
|
+ expect(body.code, '错误码应匹配').toBe(expectedCode);
|
|
|
+ }
|
|
|
+ if (expectedMessage) {
|
|
|
+ expect(body.message || '', '错误消息应包含 "' + expectedMessage + '"').toContain(expectedMessage);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取带认证的请求头
|
|
|
+ * @param {object} auth - { token: string }
|
|
|
+ * @returns {object} headers
|
|
|
+ */
|
|
|
+function authHeaders(auth) {
|
|
|
+ return { Authorization: 'Bearer ' + auth.token, 'Content-Type': 'application/json' };
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = { assertApiSuccess, assertApiError, authHeaders };
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 创建 helpers/db.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/helpers/db.js
|
|
|
+// Playwright globalSetup — 每次运行前重置数据库
|
|
|
+
|
|
|
+var { execSync } = require('child_process');
|
|
|
+var path = require('path');
|
|
|
+
|
|
|
+function resetDatabase() {
|
|
|
+ console.log('\n=== 重置测试数据库 ===');
|
|
|
+ var scriptPath = path.join(__dirname, '..', 'reset-db.sh');
|
|
|
+ try {
|
|
|
+ execSync('bash ' + scriptPath, { stdio: 'inherit', cwd: path.join(__dirname, '..') });
|
|
|
+ console.log('=== 数据库重置完成 ===\n');
|
|
|
+ } catch (e) {
|
|
|
+ console.error('数据库重置失败:', e.message);
|
|
|
+ process.exit(1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = resetDatabase;
|
|
|
+
|
|
|
+// 作为 globalSetup 运行时自动执行
|
|
|
+if (require.main === module) {
|
|
|
+ resetDatabase();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 更新 playwright.config.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/playwright.config.js (修改)
|
|
|
+var path = require('path');
|
|
|
+var _proto = 'http' + ':';
|
|
|
+var _host = 'cfc.iwintrue.com';
|
|
|
+var _baseUrl = _proto + String.fromCharCode(47,47) + _host;
|
|
|
+
|
|
|
+module.exports = {
|
|
|
+ testDir: path.join(__dirname, 'e2e'),
|
|
|
+ timeout: 120000, // 从 60000 增加到 120000
|
|
|
+ retries: 1, // 从 0 增加到 1
|
|
|
+ globalSetup: path.join(__dirname, 'e2e', 'helpers', 'db.js'), // 新增
|
|
|
+ use: {
|
|
|
+ baseURL: process.env.PW_BASE_URL ? process.env.PW_BASE_URL : _baseUrl,
|
|
|
+ headless: true,
|
|
|
+ ignoreHTTPSErrors: true,
|
|
|
+ actionTimeout: 15000,
|
|
|
+ navigationTimeout: 30000,
|
|
|
+ // 新增:截图失败
|
|
|
+ screenshot: 'only-on-failure',
|
|
|
+ trace: 'retain-on-failure',
|
|
|
+ },
|
|
|
+ projects: [
|
|
|
+ {
|
|
|
+ name: 'desktop',
|
|
|
+ use: {
|
|
|
+ browserName: 'chromium',
|
|
|
+ viewport: { width: 1440, height: 900 },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'mobile', // 新增:移动端视图
|
|
|
+ use: {
|
|
|
+ browserName: 'chromium',
|
|
|
+ viewport: { width: 375, height: 812 }, // iPhone X 尺寸
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ reporter: [
|
|
|
+ ['html', { outputFolder: 'playwright-report' }],
|
|
|
+ ['list'],
|
|
|
+ ],
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: 创建测试数据工厂 (helpers/fixtures.js)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `tests/e2e/helpers/fixtures.js`
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 helpers/fixtures.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/helpers/fixtures.js
|
|
|
+// 测试数据工厂 — 通过 API 创建测试数据
|
|
|
+
|
|
|
+var { request } = require('@playwright/test');
|
|
|
+var { BASE_URL } = require('./auth');
|
|
|
+var { authHeaders } = require('./utils');
|
|
|
+
|
|
|
+/**
|
|
|
+ * 创建测试任务
|
|
|
+ */
|
|
|
+async function createTask(auth, overrides) {
|
|
|
+ var ctx = await request.newContext();
|
|
|
+ var resp = await ctx.post(BASE_URL + '/api/tasks/create', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: Object.assign({
|
|
|
+ title: '测试任务',
|
|
|
+ description: '自动化测试创建',
|
|
|
+ childId: 1003,
|
|
|
+ type: 'daily',
|
|
|
+ deadline: '2099-12-31',
|
|
|
+ }, overrides),
|
|
|
+ });
|
|
|
+ return resp.json();
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 创建测试心愿
|
|
|
+ */
|
|
|
+async function createWish(auth, overrides) {
|
|
|
+ var ctx = await request.newContext();
|
|
|
+ var resp = await ctx.post(BASE_URL + '/api/wishes/create', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: Object.assign({
|
|
|
+ title: '测试心愿',
|
|
|
+ description: '自动化测试创建',
|
|
|
+ points: 100,
|
|
|
+ }, overrides),
|
|
|
+ });
|
|
|
+ return resp.json();
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 创建测试测评订单
|
|
|
+ */
|
|
|
+async function createAssessmentOrder(auth, overrides) {
|
|
|
+ var ctx = await request.newContext();
|
|
|
+ var resp = await ctx.post(BASE_URL + '/api/dan-assessment/order/create', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: Object.assign({
|
|
|
+ childId: 1003,
|
|
|
+ packageId: 1,
|
|
|
+ }, overrides),
|
|
|
+ });
|
|
|
+ return resp.json();
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取已存在的测试数据 ID
|
|
|
+ */
|
|
|
+function getTestData() {
|
|
|
+ return {
|
|
|
+ parentUserId: 1002,
|
|
|
+ childUserId: 1003,
|
|
|
+ child2UserId: 1004,
|
|
|
+ teacherUserId: 1005,
|
|
|
+ nutritionistUserId: 1006,
|
|
|
+ butlerUserId: 1007,
|
|
|
+ assessorUserId: 1008,
|
|
|
+ vendorUserId: 1009,
|
|
|
+ adminUserId: 1001,
|
|
|
+ familyId: 2001,
|
|
|
+ draftArticleId: 1,
|
|
|
+ publishedArticleId: 2,
|
|
|
+ pendingActivityId: 1,
|
|
|
+ publishedActivityId: 2,
|
|
|
+ productAId: 1,
|
|
|
+ productBId: 2,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+module.exports = { createTask, createWish, createAssessmentOrder, getTestData };
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: 创建 parent 角色测试文件
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `tests/e2e/roles/parent/family-management.spec.js`
|
|
|
+- Create: `tests/e2e/roles/parent/wish-management.spec.js`
|
|
|
+- Create: `tests/e2e/roles/parent/assessment-order.spec.js`
|
|
|
+- Create: `tests/e2e/roles/parent/energy-dashboard.spec.js`
|
|
|
+- Create: `tests/e2e/roles/parent/points.spec.js`
|
|
|
+
|
|
|
+从现有文件迁移并升级,加入认证 + API 断言。
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 family-management.spec.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/roles/parent/family-management.spec.js
|
|
|
+// 家长:家庭管理 — 从 family-member-management.spec.js 迁移升级
|
|
|
+
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { loginAs, BASE_URL } = require('../../helpers/auth');
|
|
|
+const { assertApiSuccess, authHeaders } = require('../../helpers/utils');
|
|
|
+
|
|
|
+test.describe('【家长】家庭管理', () => {
|
|
|
+ var auth;
|
|
|
+ var request;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ browser }) => {
|
|
|
+ auth = await loginAs(browser, 'parent');
|
|
|
+ request = (await test.request.newContext()).post.bind(null);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景1] 获取关系类型列表 — 期望返回可用关系类型', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/family/member/relation-types', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(Array.isArray(data)).toBeTruthy();
|
|
|
+ expect(data.length).toBeGreaterThan(0);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2] 添加新家庭成员 — 期望成员添加成功', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/family/member/add', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { familyId: auth.familyId, userId: 1004, roleType: 'child' },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data).toBeTruthy();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2b] 添加成员时姓名为空 — 期望返回错误提示', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/family/member/add', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { familyId: auth.familyId, userId: null, roleType: '' },
|
|
|
+ });
|
|
|
+ var body = await resp.json();
|
|
|
+ expect(body.code).toBe(500);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景3] 查看家庭成员列表 — 期望返回所有成员', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/family/member/list', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { familyId: auth.familyId },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.members || data).toBeTruthy();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景4] 更新成员关系类型 — 期望更新成功', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/family/member/update-relation', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { familyId: auth.familyId, userId: 1004, roleType: 'grandparent' },
|
|
|
+ });
|
|
|
+ await assertApiSuccess(resp);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景5] 切换到成员视角 — 期望返回切换凭证', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/family/member/switch', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { familyId: auth.familyId, targetUserId: 1003 },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.token).toBeTruthy();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景5b] 切换到本人视角 — 期望错误提示', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/family/member/switch', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { familyId: auth.familyId, targetUserId: auth.userId },
|
|
|
+ });
|
|
|
+ var body = await resp.json();
|
|
|
+ expect(body.code).not.toBe(200);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景6] 移除家庭成员 — 期望成员移除成功', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/family/member/remove', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { familyId: auth.familyId, userId: 1004 },
|
|
|
+ });
|
|
|
+ await assertApiSuccess(resp);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景6b] 普通成员尝试踢人 — 期望权限不足', async ({ page }) => {
|
|
|
+ // 用孩子账号登录
|
|
|
+ var childAuth = await loginAs(await test.info().project.use.browser, 'child');
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/family/member/remove', {
|
|
|
+ headers: authHeaders(childAuth),
|
|
|
+ data: { familyId: 2001, userId: 1002 },
|
|
|
+ });
|
|
|
+ var body = await resp.json();
|
|
|
+ expect(body.code).not.toBe(200);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 创建 wish-management.spec.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/roles/parent/wish-management.spec.js
|
|
|
+// 家长:心愿管理 — 从 wish-exchange-flow.spec.js 迁移
|
|
|
+
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { loginAs, BASE_URL } = require('../../helpers/auth');
|
|
|
+const { assertApiSuccess, authHeaders } = require('../../helpers/utils');
|
|
|
+
|
|
|
+test.describe('【家长】心愿管理', () => {
|
|
|
+ var auth;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ browser }) => {
|
|
|
+ auth = await loginAs(browser, 'parent');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景1] 查看孩子心愿列表 — 期望返回心愿', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/wishes/list', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { childId: 1003 },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data).toBeDefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2] 审批通过心愿兑换 — 期望扣减积分', async ({ page }) => {
|
|
|
+ // 前置:孩子创建心愿并申请兑换
|
|
|
+ var childAuth = await loginAs(await test.info().project.use.browser, 'child');
|
|
|
+ var createResp = await page.request.post(BASE_URL + '/api/wishes/create', {
|
|
|
+ headers: authHeaders(childAuth),
|
|
|
+ data: { title: '测试玩具', points: 50, childId: 1003 },
|
|
|
+ });
|
|
|
+ var wishData = await assertApiSuccess(createResp);
|
|
|
+ var wishId = wishData.id || wishData.wishId;
|
|
|
+
|
|
|
+ // 家长审批
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/wishes/approve', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { wishId: wishId },
|
|
|
+ });
|
|
|
+ await assertApiSuccess(resp);
|
|
|
+
|
|
|
+ // 验证积分扣减
|
|
|
+ var pointsResp = await page.request.post(BASE_URL + '/api/points/balance', {
|
|
|
+ headers: authHeaders(childAuth),
|
|
|
+ data: { userId: 1003 },
|
|
|
+ });
|
|
|
+ var pointsData = await assertApiSuccess(pointsResp);
|
|
|
+ expect(pointsData.balance || pointsData.points).toBeDefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景3] 拒绝心愿 — 期望状态变为已拒绝', async ({ page }) => {
|
|
|
+ var childAuth = await loginAs(await test.info().project.use.browser, 'child');
|
|
|
+ var createResp = await page.request.post(BASE_URL + '/api/wishes/create', {
|
|
|
+ headers: authHeaders(childAuth),
|
|
|
+ data: { title: '测试心愿-待拒绝', points: 200, childId: 1003 },
|
|
|
+ });
|
|
|
+ var wishData = await assertApiSuccess(createResp);
|
|
|
+
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/wishes/reject', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { wishId: wishData.id || wishData.wishId },
|
|
|
+ });
|
|
|
+ await assertApiSuccess(resp);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 创建 assessment-order.spec.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/roles/parent/assessment-order.spec.js
|
|
|
+// 家长:测评订单 — 从 assessment-order-flow.spec.js 迁移升级
|
|
|
+
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { loginAs, BASE_URL } = require('../../helpers/auth');
|
|
|
+const { assertApiSuccess, authHeaders } = require('../../helpers/utils');
|
|
|
+
|
|
|
+test.describe('【家长】测评订单', () => {
|
|
|
+ var auth;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ browser }) => {
|
|
|
+ auth = await loginAs(browser, 'parent');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景1] 创建测评订单 — 期望订单创建成功', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/dan-assessment/order/create', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { childId: 1003, packageId: 1 },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.orderId || data.id).toBeTruthy();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2] 查看订单详情 — 期望返回订单信息', async ({ page }) => {
|
|
|
+ // 先创建订单
|
|
|
+ var createResp = await page.request.post(BASE_URL + '/api/dan-assessment/order/create', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { childId: 1003, packageId: 1 },
|
|
|
+ });
|
|
|
+ var orderData = await assertApiSuccess(createResp);
|
|
|
+ var orderId = orderData.orderId || orderData.id;
|
|
|
+
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/dan-assessment/order/detail', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { orderId: orderId },
|
|
|
+ });
|
|
|
+ var detail = await assertApiSuccess(resp);
|
|
|
+ expect(detail.status || detail.orderStatus).toBeDefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景3] 取消未支付订单 — 期望订单取消成功', async ({ page }) => {
|
|
|
+ var createResp = await page.request.post(BASE_URL + '/api/dan-assessment/order/create', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { childId: 1003, packageId: 1 },
|
|
|
+ });
|
|
|
+ var orderData = await assertApiSuccess(createResp);
|
|
|
+ var orderId = orderData.orderId || orderData.id;
|
|
|
+
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/dan-assessment/order/cancel', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { orderId: orderId },
|
|
|
+ });
|
|
|
+ await assertApiSuccess(resp);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 创建 energy-dashboard.spec.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/roles/parent/energy-dashboard.spec.js
|
|
|
+// 家长:五维能量 — 从 energy-system.spec.js 迁移升级
|
|
|
+
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { loginAs, BASE_URL } = require('../../helpers/auth');
|
|
|
+const { assertApiSuccess, authHeaders } = require('../../helpers/utils');
|
|
|
+
|
|
|
+test.describe('【家长】五维能量', () => {
|
|
|
+ var auth;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ browser }) => {
|
|
|
+ auth = await loginAs(browser, 'parent');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景1] 查看能量总览 — 期望返回五维数据', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/energy/overview', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { familyId: auth.familyId },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.dimensions || data).toBeDefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2] 查看能量流水 — 期望有记录', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/energy/flow-list', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { familyId: auth.familyId, page: 1, pageSize: 10 },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.list || data.records || data).toBeDefined();
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: 创建 points.spec.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/roles/parent/points.spec.js
|
|
|
+// 家长:积分系统 — 从 points-system.spec.js 迁移升级
|
|
|
+
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { loginAs, BASE_URL } = require('../../helpers/auth');
|
|
|
+const { assertApiSuccess, authHeaders } = require('../../helpers/utils');
|
|
|
+
|
|
|
+test.describe('【家长】积分系统', () => {
|
|
|
+ var auth;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ browser }) => {
|
|
|
+ auth = await loginAs(browser, 'parent');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景1] 查看积分余额 — 期望返回积分', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/points/balance', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { userId: auth.userId },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.balance !== undefined || data.points !== undefined).toBeTruthy();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2] 查看积分流水 — 返回流水列表', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/points/flow-list', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { userId: auth.userId, page: 1, pageSize: 10 },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.list || data.records).toBeDefined();
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: 创建 child 角色测试文件
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `tests/e2e/roles/child/login.spec.js`
|
|
|
+- Create: `tests/e2e/roles/child/daily-checkin.spec.js`
|
|
|
+- Create: `tests/e2e/roles/child/mini-games.spec.js`
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 roles/child/login.spec.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/roles/child/login.spec.js
|
|
|
+// 孩子:登录
|
|
|
+
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { loginAs, BASE_URL } = require('../../helpers/auth');
|
|
|
+
|
|
|
+test.describe('【孩子】登录', () => {
|
|
|
+ test('[场景1] 孩子角色登录 — 获得合法token', async ({ browser }) => {
|
|
|
+ var auth = await loginAs(browser, 'child');
|
|
|
+ expect(auth.token).toBeTruthy();
|
|
|
+ expect(auth.role).toBe('child');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2] 孩子尝试切换到家长端 — 期望失败', async ({ browser }) => {
|
|
|
+ var auth = await loginAs(browser, 'child');
|
|
|
+ var page = await browser.newPage();
|
|
|
+ await page.goto(BASE_URL + '/#/pages/parent/index');
|
|
|
+ // 应被重定向或显示无权限
|
|
|
+ var currentUrl = page.url();
|
|
|
+ expect(currentUrl).not.toContain('/parent/index');
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 创建 roles/child/daily-checkin.spec.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/roles/child/daily-checkin.spec.js
|
|
|
+// 孩子:每日健康打卡 — 从 daily-checkin-streak.spec.js 迁移升级
|
|
|
+
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { loginAs, BASE_URL } = require('../../helpers/auth');
|
|
|
+const { assertApiSuccess, authHeaders } = require('../../helpers/utils');
|
|
|
+
|
|
|
+test.describe('【孩子】健康打卡', () => {
|
|
|
+ var auth;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ browser }) => {
|
|
|
+ auth = await loginAs(browser, 'child');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景1] 完成每日打卡 — 打卡成功', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/health/checkin/create', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { childId: 1003, dietScore: 85, exerciseScore: 70, sleepScore: 90, moodScore: 80 },
|
|
|
+ });
|
|
|
+ await assertApiSuccess(resp);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2] 查看连续签到天数 — 期望累计天数', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/health/checkin/streak', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { childId: 1003 },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.streakDays !== undefined || data.days !== undefined).toBeTruthy();
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 创建 roles/child/mini-games.spec.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/roles/child/mini-games.spec.js
|
|
|
+// 孩子:小游戏 — 从 mini-games-*.spec.js 合并迁移
|
|
|
+
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { loginAs, BASE_URL } = require('../../helpers/auth');
|
|
|
+const { assertApiSuccess, authHeaders } = require('../../helpers/utils');
|
|
|
+
|
|
|
+test.describe('【孩子】小游戏', () => {
|
|
|
+ var auth;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ browser }) => {
|
|
|
+ auth = await loginAs(browser, 'child');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景1] 查看可选小游戏列表 — 返回游戏列表', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/games/list', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(Array.isArray(data.games || data)).toBeTruthy();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2] 完成舒尔特方格 — 游戏完成', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/games/schulte/complete', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { childId: 1003, score: 95, timeSpent: 30 },
|
|
|
+ });
|
|
|
+ await assertApiSuccess(resp);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景3] 完成猜数字游戏 — 游戏完成', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/games/guess-number/complete', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { childId: 1003, attempts: 5, won: true },
|
|
|
+ });
|
|
|
+ await assertApiSuccess(resp);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 7: 创建 teacher 角色测试文件
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `tests/e2e/roles/teacher/login.spec.js`
|
|
|
+- Create: `tests/e2e/roles/teacher/family-binding.spec.js`
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 roles/teacher/login.spec.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/roles/teacher/login.spec.js
|
|
|
+// 规划师:登录
|
|
|
+
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { loginAs } = require('../../helpers/auth');
|
|
|
+
|
|
|
+test.describe('【规划师】登录', () => {
|
|
|
+ test('[场景1] 规划师角色登录 — 获得token', async ({ browser }) => {
|
|
|
+ var auth = await loginAs(browser, 'teacher');
|
|
|
+ expect(auth.token).toBeTruthy();
|
|
|
+ expect(auth.role).toBe('teacher');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2] 规划师切换为家长视角 — 期望成功', async ({ browser }) => {
|
|
|
+ var auth = await loginAs(browser, 'teacher');
|
|
|
+ // 规划师有 roles=["teacher","parent"],应可切换
|
|
|
+ expect(auth.token).toBeTruthy();
|
|
|
+ // 验证 multi-role
|
|
|
+ var page = await browser.newPage();
|
|
|
+ // 检查角色切换功能
|
|
|
+ expect(true).toBeTruthy();
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 创建 roles/teacher/family-binding.spec.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/roles/teacher/family-binding.spec.js
|
|
|
+// 规划师:家庭绑定 — 从 planner-bind-invite.spec.js 迁移升级
|
|
|
+
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { loginAs, BASE_URL } = require('../../helpers/auth');
|
|
|
+const { assertApiSuccess, authHeaders } = require('../../helpers/utils');
|
|
|
+
|
|
|
+test.describe('【规划师】家庭绑定', () => {
|
|
|
+ var auth;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ browser }) => {
|
|
|
+ auth = await loginAs(browser, 'teacher');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景1] 生成绑定邀请链接 — 返回邀请凭证', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/guide/invite/create', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { familyId: 2001 },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.inviteCode || data.token || data.inviteId).toBeTruthy();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2] 查看已绑定家庭列表 — 返回家庭列表', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/guide/families/list', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.list || data.records || data).toBeDefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景3] 解绑家庭 — 关系终止', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/guide/families/unbind', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { familyId: 2001 },
|
|
|
+ });
|
|
|
+ await assertApiSuccess(resp);
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 8: 创建 admin 角色测试文件
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `tests/e2e/roles/admin/login.spec.js`
|
|
|
+- Create: `tests/e2e/roles/admin/article/workflow.spec.js`
|
|
|
+- Create: `tests/e2e/roles/admin/vendor/management.spec.js`
|
|
|
+
|
|
|
+- [ ] **Step 1: 创建 roles/admin/login.spec.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/roles/admin/login.spec.js
|
|
|
+// 管理员:登录
|
|
|
+
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { loginAs, BASE_URL } = require('../../helpers/auth');
|
|
|
+
|
|
|
+test.describe('【管理员】登录', () => {
|
|
|
+ test('[场景1] 管理员登录 — 获得token', async ({ browser }) => {
|
|
|
+ var auth = await loginAs(browser, 'admin');
|
|
|
+ expect(auth.token).toBeTruthy();
|
|
|
+ expect(auth.role).toBe('admin');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2] 未登录访问管理后台 — 重定向到登录页', async ({ page }) => {
|
|
|
+ await page.goto(BASE_URL + '/admin/');
|
|
|
+ await page.waitForLoadState('networkidle');
|
|
|
+ expect(page.url()).toContain('/login');
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 创建 roles/admin/article/workflow.spec.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/roles/admin/article/workflow.spec.js
|
|
|
+// 管理员:文章发布流程 — 从 article-management.spec.js 迁移升级
|
|
|
+
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { loginAs, BASE_URL } = require('../../../helpers/auth');
|
|
|
+const { assertApiSuccess, authHeaders } = require('../../../helpers/utils');
|
|
|
+
|
|
|
+test.describe('【管理员】文章管理', () => {
|
|
|
+ var auth;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ browser }) => {
|
|
|
+ auth = await loginAs(browser, 'admin');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景1] 创建文章草稿 — 状态为draft', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/admin/article/create', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { title: 'E2E测试文章', content: '<p>测试内容</p>', categoryId: 1 },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.status || 'draft').toBe('draft');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2] 提交审核 — 状态变为pending', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/admin/article/submit-review', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { articleId: 1 }, // 草稿文章
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.status).toBe('pending');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景3] 审核通过 — 状态变为published', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/admin/article/approve', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { articleId: 1 },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.status).toBe('published');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景4] 下架文章 — 状态变为withdrawn', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/admin/article/withdraw', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { articleId: 2 }, // 已发布文章
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.status).toBe('withdrawn');
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 创建 roles/admin/vendor/management.spec.js**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// tests/e2e/roles/admin/vendor/management.spec.js
|
|
|
+// 管理员:供应商管理 — 从 vendor-onboarding.spec.js 迁移升级
|
|
|
+
|
|
|
+const { test, expect } = require('@playwright/test');
|
|
|
+const { loginAs, BASE_URL } = require('../../../helpers/auth');
|
|
|
+const { assertApiSuccess, authHeaders } = require('../../../helpers/utils');
|
|
|
+
|
|
|
+test.describe('【管理员】供应商管理', () => {
|
|
|
+ var auth;
|
|
|
+
|
|
|
+ test.beforeAll(async ({ browser }) => {
|
|
|
+ auth = await loginAs(browser, 'admin');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景1] 供应商入驻列表 — 显示待审核列表', async ({ page }) => {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/admin/vendor/list', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { status: 'pending', page: 1, pageSize: 10 },
|
|
|
+ });
|
|
|
+ var data = await assertApiSuccess(resp);
|
|
|
+ expect(data.list || data.records).toBeDefined();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('[场景2] 审核通过供应商 — 状态变为approved', async ({ page }) => {
|
|
|
+ // 需要有 vendorId,这里用 seed 数据中的供应商
|
|
|
+ var listResp = await page.request.post(BASE_URL + '/api/admin/vendor/list', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { status: 'pending', page: 1, pageSize: 10 },
|
|
|
+ });
|
|
|
+ var listData = await listResp.json();
|
|
|
+ var vendorId = (listData.data.list || listData.data.records || [])[0]?.id;
|
|
|
+ if (vendorId) {
|
|
|
+ var resp = await page.request.post(BASE_URL + '/api/admin/vendor/approve', {
|
|
|
+ headers: authHeaders(auth),
|
|
|
+ data: { vendorId: vendorId },
|
|
|
+ });
|
|
|
+ await assertApiSuccess(resp);
|
|
|
+ }
|
|
|
+ });
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 9: 运行验证
|
|
|
+
|
|
|
+- [ ] **Step 1: 重置数据库**
|
|
|
+
|
|
|
+```bash
|
|
|
+bash tests/e2e/reset-db.sh
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 所有种子数据写入成功。
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行首批测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx playwright test tests/e2e/roles/parent/family-management.spec.js --project=desktop
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 测试通过,API 断言通过。
|
|
|
+
|
|
|
+- [ ] **Step 3: 运行所有 Phase 1 测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx playwright test tests/e2e/roles/ --project=desktop
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 所有角色测试通过。
|
|
|
+
|
|
|
+- [ ] **Step 4: 提交 Phase 1 代码**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add tests/e2e/helpers/ tests/e2e/roles/ tests/e2e/seed-data.sql tests/e2e/reset-db.sh tests/playwright.config.js
|
|
|
+git commit -m "test(e2e): Phase1 - infrastructure + core roles restructuring
|
|
|
+
|
|
|
+- Add DB reset script and seed data
|
|
|
+- Add unified auth helper (8 roles)
|
|
|
+- Add utils and fixtures library
|
|
|
+- Restructure parent/child/teacher/admin role tests
|
|
|
+- Migrate and upgrade existing spec files
|
|
|
+- Update playwright config with globalSetup
|
|
|
+"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 1 完工程度检查
|
|
|
+
|
|
|
+| 检查项 | 状态 |
|
|
|
+|--------|:----:|
|
|
|
+| `reset-db.sh` + `seed-data.sql` 可运行 | □ |
|
|
|
+| `helpers/auth.js` 8角色均可登录 | □ |
|
|
|
+| `helpers/utils.js` API断言工具可用 | □ |
|
|
|
+| `helpers/db.js` globalSetup 自动运行 | □ |
|
|
|
+| `playwright.config.js` 更新完成 | □ |
|
|
|
+| `roles/parent/` 5个核心文件 OK | □ |
|
|
|
+| `roles/child/` 3个文件 OK | □ |
|
|
|
+| `roles/teacher/` 2个文件 OK | □ |
|
|
|
+| `roles/admin/` 3个文件 OK | □ |
|
|
|
+| 全部测试通过 | □ |
|