# 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} 响应 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: '

测试内容

', 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 | □ | | 全部测试通过 | □ |